当前位置: 首页 > news >正文

从零驱动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有几十个寄存器需要配置。我花了整整一天时间才调通所有参数。初始化流程大致分为以下几个步骤:

  1. 硬件复位:拉低RES引脚至少10ms
  2. 退出睡眠模式
  3. 设置像素格式(我选择16位RGB565)
  4. 配置伽马曲线
  5. 开启显示

最关键的像素格式设置命令是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. 性能优化技巧

经过一段时间的使用,我总结出几个提升显示性能的技巧:

  1. 批量写入:设置好区域后,连续写入多个像素数据,减少命令开销
  2. 双缓冲:在内存中维护一个屏幕缓冲区,修改后再整体刷新
  3. 局部刷新:只更新屏幕上变化的部分,而不是全屏刷新
  4. 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波形,这是最直接的诊断方法。

http://www.jsqmd.com/news/1088676/

相关文章:

  • 软考备考路径选择终极拆解(20年命题组成员内部复盘笔记):3类人群必须报班,2类人自学稳过——你属于哪一类?
  • FOC——SVPWM:从理论到代码实现的工程化解析
  • RA8D1中断控制器(ICU)实战:从架构解析到低功耗唤醒配置
  • SQLmap 2025实战指南:从注入检测到数据提取的完整流程
  • 球坐标系数值模拟与Kerr-Schild坐标系下的电磁场离散化
  • ArcGIS实战:从零绘制专业中国地图(附完整数据与步骤)
  • 大模型MoE架构揭秘:为何98%参数休眠却性能更强
  • 精准捕获DC/DC电源纹波:从原理到实战的测量指南
  • Xilinx Platform Cable USB II 驱动安装疑难解析——从设备识别到ISE链初始化
  • VLC鼠标点击暂停插件:解放双手的终极视频控制方案
  • Tree-GRPO:面向AI Agent的分层策略蒸馏与梯度路由优化框架
  • 从零构建:基于移远展锐5G模组的嵌入式Linux应用开发实战
  • 3步解锁:让Blender成为专业3D打印工作流的核心枢纽
  • 5个关键步骤:让Blender完美支持3MF格式的完整指南
  • SBL(Flash驱动程序)在Bootloader中的三种部署策略与实战解析
  • Gartner Hype Cycle 2023:穿越炒作迷雾,锚定技术投资的真实价值
  • 相关表格介绍
  • 深入解析Web Session机制:从原理到集群部署与安全实战
  • NVIDIA Profile Inspector架构解析:超越官方工具的显卡驱动深度调优方案
  • 影刀RPA新手教程:商品评分与DSR监控完全指南——多店铺数据汇总与异常预警
  • Java国密算法实战:GmSSL-Java集成与SM2/SM3/SM4应用指南
  • Playwright Python API测试实战:从环境搭建到CI/CD集成
  • 从二进制到AI训练:深入解析FP16的精度边界与混合精度实战
  • 089、案例九:DevOps 基础设施即代码——Terraform 和 Ansible 的 AI 辅助
  • Claude Mythos Preview:AI安全能力的范式重置与工程化跃迁
  • OpenPnP相机标定:从‘subject not found’到稳定识别的实战避坑指南
  • 如何通过Excel表格快速掌握AI算法原理:5个简单步骤的完整指南
  • MimeKit邮件安全实战:S/MIME、PGP与DKIM加密签名全解析
  • 实战解析:5种高效绕过WAF的SQL注入技巧与防御策略
  • 3步解锁加密音乐:终极桌面工具让你真正拥有自己的音乐