STM32驱动1.8寸TFT彩屏:从模拟SPI到硬件SPI的实战指南(标准库与HAL库对比)
1. 硬件准备与基础概念
1.8寸TFT彩屏是嵌入式开发中常用的显示设备,采用SPI接口通信。我刚开始接触这类屏幕时,用的也是模拟SPI方式,后来发现硬件SPI能大幅提升性能。先说说硬件准备,市面上常见的1.8寸TFT屏驱动芯片多为ST7735S或ILI9163,引脚定义基本一致:
- VCC:3.3V或5V供电
- GND:地线
- SCL:时钟线
- SDA:数据线
- RES:复位线
- DC:数据/命令选择线
- CS:片选线
- BL:背光控制
我用的是STM32F103C8T6最小系统板,也就是大家常说的"蓝板"。选择它是因为资源丰富,性价比高,而且网上资料多。这里有个坑要注意:有些屏幕的背光需要单独供电,如果接上电源发现屏幕不亮,先检查背光是否接好。
2. 从模拟SPI到硬件SPI的转变
模拟SPI确实容易上手,我用GPIO口模拟时序也能让屏幕正常工作。核心代码就是通过循环移位实现数据发送:
void SPI_WriteData(u8 Data) { for(int i=0; i<8; i++) { if(Data & 0x80) SET_SDA(); else CLR_SDA(); CLR_SCL(); delay_us(1); SET_SCL(); Data <<= 1; } }但实测发现刷新率只有12fps左右,刷个图片明显卡顿。改用硬件SPI后,同样的操作能达到45fps,提升近4倍!硬件SPI利用了STM32内置的硬件加速器,不仅释放了CPU资源,还能通过DMA实现无阻塞传输。
3. 标准库硬件SPI配置
在标准库环境下配置硬件SPI,需要先初始化GPIO和SPI外设。我用的是SPI1,对应引脚PA5(SCK)、PA6(MISO)、PA7(MOSI)。配置步骤:
- 使能GPIO和SPI时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);- 配置GPIO为复用推挽输出:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure);- SPI参数配置:
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);这里有个关键点:SPI时钟分频系数决定了通信速率。我测试发现分频系数设为4时(主频72MHz下SPI时钟18MHz),屏幕工作最稳定。再提高速率会出现雪花点。
4. HAL库硬件SPI实现
HAL库的配置更简单,用CubeMX工具勾选几下就行。不过有些细节需要注意:
- CubeMX中配置SPI为"Transmit Only Master",数据宽度8bit
- 时钟极性选择Low,相位选择1 Edge
- 片选信号(NSS)选择Software
- 预分频系数建议先设为8,后续再调整
生成代码后,发送数据的函数变为:
void SPI_WriteData(uint8_t Data) { HAL_SPI_Transmit(&hspi1, &Data, 1, 100); }HAL库的优势是代码更简洁,但实测发现直接调用HAL_SPI_Transmit效率不如标准库。解决方案是启用DMA传输:
HAL_SPI_Transmit_DMA(&hspi1, buffer, length);使用DMA后,刷屏速度能达到60fps,CPU占用率几乎为零。不过要注意DMA缓冲区的大小设置,太小会导致传输不完整。
5. 性能优化实战技巧
经过多次测试,我总结了几个提升TFT刷新率的技巧:
批量传输优化:不要逐点发送数据,而是整行或整块传输。将显示数据先存入数组,然后一次性发送。
内存布局优化:使用__attribute__((aligned(4)))确保DMA缓冲区4字节对齐,能提升20%传输速度。
时序调整:不同屏幕对SPI时序要求不同。我的经验是:
- 复位信号(RES)保持低电平至少10ms
- 命令和数据之间延迟至少1us
- 初始化序列结束后延迟120ms再发送图像数据
双缓冲技术:在内存中维护两个显示缓冲区,一个用于绘制,一个用于显示,通过DMA交替切换,实现无闪烁动画。
实测数据显示:
- 模拟SPI:12fps @ 72MHz
- 标准库硬件SPI:45fps @ 18MHz
- HAL库+DMA:60fps @ 36MHz
6. 标准库与HAL库深度对比
两种开发方式各有优劣,我做了个详细对比:
| 特性 | 标准库 | HAL库 |
|---|---|---|
| 初始化复杂度 | 需要手动配置所有寄存器 | CubeMX可视化配置 |
| 执行效率 | 高 | 中等,但DMA可弥补 |
| 代码体积 | 小(约5KB) | 大(约15KB) |
| 移植性 | 差,需修改硬件相关代码 | 好,硬件抽象层隔离 |
| 开发效率 | 低 | 高 |
| 中断处理 | 需自行编写中断服务程序 | 提供完整回调机制 |
| 维护成本 | 高 | 低 |
个人建议:如果是学习目的,先用标准库理解底层原理;实际项目开发推荐HAL库,特别是需要快速迭代时。
7. 常见问题与解决方案
在项目开发中遇到过不少坑,这里分享几个典型问题的解决方法:
问题1:屏幕显示花屏
- 检查SPI时钟极性(CPOL)和相位(CPHA)设置
- 确认复位时序是否正确
- 测量电源电压是否稳定(建议加100uF电容)
问题2:DMA传输不完整
- 检查缓冲区是否4字节对齐
- 确认DMA通道优先级设置
- 增加DMA完成中断回调进行验证
问题3:刷新率不达标
- 尝试提高SPI时钟频率
- 改用硬件NSS信号代替软件控制
- 优化显示数据预处理算法
问题4:功耗过高
- 动态调整SPI时钟速率
- 不刷新时关闭背光
- 使用睡眠指令(0x10)降低屏幕功耗
8. 进阶应用:GUI实现基础
掌握了底层驱动后,可以进一步实现简单GUI。我的做法是封装几个基础函数:
// 绘制矩形 void GUI_DrawRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { Lcd_SetRegion(x, y, x+w-1, y+h-1); for(int i=0; i<w*h; i++) { LCD_WriteData_16Bit(color); } } // 显示16x16图标 void GUI_DrawIcon(uint16_t x, uint16_t y, const uint16_t *icon) { Lcd_SetRegion(x, y, x+15, y+15); for(int i=0; i<256; i++) { LCD_WriteData_16Bit(icon[i]); } }更复杂的界面可以考虑移植ucGUI或LVGL等开源库。我在项目中移植过LVGL,需要实现以下几个底层接口:
- 显示缓冲区管理
- 输入设备读取
- 定时器驱动
移植成功后,就能用LVGL丰富的控件库开发专业级界面了。