当前位置: 首页 > 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.gsyq.cn/news/1600626.html

相关文章:

  • RA8D1中断控制器(ICU)实战:从架构解析到低功耗唤醒配置
  • Tree-GRPO:面向AI Agent的分层策略蒸馏与梯度路由优化框架
  • VLC鼠标点击暂停插件:解放双手的终极视频控制方案
  • NVIDIA Profile Inspector架构解析:超越官方工具的显卡驱动深度调优方案
  • 如何让旧款Mac运行最新macOS?OpenCore Legacy Patcher完整指南
  • Frida Hook实战:Android加密API自动捕获与自吐算法实现
  • 089、案例九:DevOps 基础设施即代码——Terraform 和 Ansible 的 AI 辅助
  • Claude Mythos Preview:AI安全能力的范式重置与工程化跃迁
  • 如何通过Excel表格快速掌握AI算法原理:5个简单步骤的完整指南
  • 【操作系统】前趋图与PV操作(结合前趋图解题)
  • 纯JavaScript实现RSA加密库:从大数运算到PKCS#1填充
  • Claude Code Security:AI驱动的代码审计与漏洞挖掘实战指南
  • 终极Wallpaper Engine资源提取解决方案:RePKG完全指南
  • 终极指南:如何使用VMPDump高效破解VMProtect 3.x保护 - 完整动态脱壳教程
  • 如何免费解锁网易云加密音乐:NCMDump终极转换指南
  • 5分钟掌握Ofd2Pdf:轻松解决OFD文件转换难题
  • 路径遍历漏洞攻防实战:从原理到多层次防御体系构建
  • Web安全实战:40个漏洞挖掘清单与零信任攻防思维
  • 2026免费在线抠图工具指南,电脑手机均可使用无水印渠道整理
  • 瑞萨RA MCU LIN总线驱动配置与实战避坑指南
  • 从像素到感知:MSE、PSNR与SSIM在图像质量评估中的演进与实战
  • C语言实现凯撒密码与RSA算法:从古典到现代的加密原理与实践
  • 基于Python与Scapy的DDoS攻击模拟工具:从原理到实践
  • RA8D2 GWCA模块寄存器实战:AXI主控、描述符链与速率限制详解
  • Python与PHP的AES加密互通:从原理到实战解决方案
  • Red Panda Dev-C++:5大核心功能重塑C++开发体验的现代化IDE解决方案
  • 告别限速困扰!9大网盘直链下载助手终极指南
  • 3分钟掌握WELearn网课助手:告别熬夜刷课,拥抱智能学习
  • 【CANdelaStudio-从入门到深入到实战】79 从“查字典”到“自动翻译”:用Python脚本实现多协议配置的批量转换
  • 碧蓝航线Alas自动化脚本:告别重复劳动,享受智能游戏体验