从零驱动1.3寸TFT:基于STM32的SPI屏显实战笔记
1. 硬件准备与连接
第一次接触1.3寸TFT屏时,我被它小巧的尺寸和丰富的显示效果惊艳到了。这块240x240分辨率的屏幕虽然比不上手机屏幕细腻,但对于嵌入式项目来说已经足够强大。我手头的这块屏采用ST7789V驱动芯片,通过精简版SPI接口通信,只需要7根线就能搞定。
屏幕的引脚排列非常标准:
- GND:接地
- VCC:3.3V供电
- SCL:时钟线
- SDA:数据线
- RES:复位线
- DC:数据/命令选择线
- BLK:背光控制
我用的是STM32F103C6T6这款性价比超高的单片机,具体连接方式如下:
#define LCD_BLK_PIN GPIO_Pin_5 // PB5 #define LCD_DC_PIN GPIO_Pin_6 // PB6 #define LCD_RST_PIN GPIO_Pin_7 // PB7 #define LCD_SDA_PIN GPIO_Pin_8 // PB8 #define LCD_SCL_PIN GPIO_Pin_9 // PB9这里有个小技巧:所有GPIO都应配置为推挽输出模式,但初始电平有讲究。SCL和SDA需要初始化为高电平,因为SPI协议规定时钟线在空闲状态要保持高电平。而RES、DC和BLK则初始化为低电平,避免屏幕在上电时出现异常状态。
2. GPIO模拟SPI的实现
由于STM32F103C6T6的硬件SPI可能被其他外设占用,我选择用GPIO模拟SPI协议。这种方式虽然速度稍慢,但胜在灵活可控。ST7789V的SPI时序有个特点:在时钟上升沿采样数据,所以我们的代码要确保数据在时钟线从低变高时保持稳定。
这是我最开始写的传输函数:
void LCD_WriteByte(uint8_t data) { for(uint8_t i=0; i<8; i++) { LCD_SCL_Low(); if(data & 0x80) LCD_SDA_High(); else LCD_SDA_Low(); LCD_SCL_High(); data <<= 1; } }实际测试时发现屏幕偶尔会出现花屏,后来才明白是时序问题。ST7789V对时序要求比较严格,在两个字节传输之间需要加入微小延时。修改后的版本增加了延时:
void LCD_WriteByte(uint8_t data) { for(uint8_t i=0; i<8; i++) { LCD_SCL_Low(); Delay_us(1); // 关键延时 if(data & 0x80) LCD_SDA_High(); else LCD_SDA_Low(); LCD_SCL_High(); Delay_us(1); // 关键延时 data <<= 1; } }3. 屏幕初始化详解
屏幕初始化是个精细活,ST7789V有几十个寄存器需要配置。我花了整整一天时间才调通所有参数。初始化流程大致分为以下几个步骤:
- 硬件复位:拉低RES引脚至少10ms
- 退出睡眠模式
- 设置像素格式(我选择16位RGB565)
- 配置伽马曲线
- 开启显示
最关键的像素格式设置命令是0x3A,参数0x55表示16位色,0x66表示18位色。我推荐使用16位色,因为18位色会显著增加传输数据量,但视觉效果提升不明显。
void LCD_Init(void) { // 硬件复位 LCD_RST_Low(); Delay_ms(20); LCD_RST_High(); Delay_ms(20); // 背光开启 LCD_BLK_High(); // 设置像素格式 LCD_WriteCmd(0x3A); LCD_WriteData(0x55); // 16位RGB // 更多初始化命令... LCD_WriteCmd(0x11); // 退出睡眠 Delay_ms(120); LCD_WriteCmd(0x29); // 开启显示 }有个坑我踩过:初始化后必须等待120ms以上才能发送其他命令,否则屏幕可能无法正常响应。这个延时在数据手册里写得很小,但实际需要更长。
4. 显示区域设置与绘图
要在屏幕上显示内容,首先需要设置操作区域。ST7789V使用4个命令来定义矩形区域:
- 0x2A:设置列地址(X坐标)
- 0x2B:设置行地址(Y坐标)
- 0x2C:开始写入显存
我封装了一个区域设置函数:
void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_WriteCmd(0x2A); LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteData(x2 >> 8); LCD_WriteData(x2 & 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteData(y2 >> 8); LCD_WriteData(y2 & 0xFF); LCD_WriteCmd(0x2C); // 准备写入数据 }清屏函数就是设置全屏区域后填充颜色:
void LCD_Clear(uint16_t color) { LCD_SetWindow(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1); for(uint32_t i=0; i<LCD_WIDTH*LCD_HEIGHT; i++) { LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } }画线函数稍微复杂些,需要考虑水平线、垂直线和斜线的情况。以水平线为例:
void LCD_DrawHLine(uint16_t x, uint16_t y, uint16_t len, uint16_t color) { LCD_SetWindow(x, y, x+len-1, y); for(uint16_t i=0; i<len; i++) { LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } }5. 字符与图形显示实战
显示字符需要先准备好字模库。我使用8x16的点阵字库,每个字符占用16字节。比如显示字符'A':
const uint8_t font8x16[] = { 0x00,0x00,0x18,0x3C,0x66,0x66,0x7E,0x66, 0x66,0x66,0x66,0x66,0x00,0x00,0x00,0x00 // 'A' }; void LCD_DrawChar(uint16_t x, uint16_t y, char c, uint16_t color) { uint8_t i,j; uint8_t pixel; c -= 32; // ASCII码偏移 LCD_SetWindow(x, y, x+7, y+15); for(i=0; i<16; i++) { pixel = font8x16[c*16 + i]; for(j=0; j<8; j++) { if(pixel & (1<<(7-j))) { LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } else { LCD_WriteData(0x00); LCD_WriteData(0x00); } } } }显示字符串就是逐个显示字符:
void LCD_Print(uint16_t x, uint16_t y, char *str, uint16_t color) { while(*str) { LCD_DrawChar(x, y, *str++, color); x += 8; if(x > LCD_WIDTH-8) { x = 0; y += 16; } } }显示数字需要先转换成字符串:
void LCD_PrintNum(uint16_t x, uint16_t y, uint32_t num, uint16_t color) { char buf[10]; sprintf(buf, "%lu", num); LCD_Print(x, y, buf, color); }6. 性能优化技巧
经过一段时间的使用,我总结出几个提升显示性能的技巧:
- 批量写入:设置好区域后,连续写入多个像素数据,减少命令开销
- 双缓冲:在内存中维护一个屏幕缓冲区,修改后再整体刷新
- 局部刷新:只更新屏幕上变化的部分,而不是全屏刷新
- DMA传输:如果使用硬件SPI,可以启用DMA减少CPU占用
比如改进后的清屏函数:
void LCD_FastClear(uint16_t color) { LCD_SetWindow(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1); uint8_t hi = color >> 8; uint8_t lo = color & 0xFF; for(uint32_t i=0; i<LCD_WIDTH*LCD_HEIGHT; i++) { LCD_SDA = hi; LCD_SCL_High(); LCD_SCL_Low(); LCD_SDA = lo; LCD_SCL_High(); LCD_SCL_Low(); } }这个版本比原始版本快约30%,因为它减少了函数调用次数,直接操作GPIO寄存器。
7. 常见问题排查
在调试过程中,我遇到过各种奇怪的问题,这里分享几个典型案例:
问题1:屏幕全白或有条纹
- 检查电源是否稳定,3.3V供电不足会导致异常
- 确认复位时序正确,RES引脚要有足够的低电平时间
- 检查SPI时序,特别是时钟极性是否符合ST7789V要求
问题2:显示内容错位
- 确认屏幕分辨率设置正确(240x240)
- 检查区域设置命令的参数顺序
- 确保像素格式与代码设置一致
问题3:显示颜色异常
- 检查RGB格式设置(5-6-5或6-6-6)
- 确认伽马校正参数是否正确
- 测试基础颜色(红、绿、蓝)是否正常显示
问题4:屏幕闪烁
- 增加电源滤波电容
- 检查背光电路是否稳定
- 降低SPI时钟频率试试
记得每次修改只调整一个参数,这样才能准确定位问题根源。调试时可以用逻辑分析仪抓取SPI波形,这是最直接的诊断方法。
