资讯详情

资讯详情

STM32F407驱动ST7789屏幕:SPI时序、初始化与DMA刷新

最近把一块1.69寸的TFT LCD接到了F407ZET6上主控是ST7789。这块屏在淘宝上很常见价格也不高但真正把它点亮、调稳、刷出流畅画面还是花了我不少时间。其实驱动代码本身不长真正折磨人的是初始化序列的时序配合、显示窗口的坐标设置以及各种“看起来代码没问题但屏幕就是不正常”的玄学现象。这篇就基于我实际做的STM32F407ZET6驱动1.69寸ST7789屏幕的过程把引脚规划、SPI时序、初始化命令、绘图底层、DMA优化和排查经验完整写一遍给准备自己写LCD驱动的人做个参考。如果你正在纠结“到底要不要学自己写驱动”我的建议是非常值得。底层驱动写一次后面换屏、改分辨率、做UI都心里有底不会被别人的库牵着走。这篇文章的内容不需要你有多强的硬件基础只要会STM32的基本GPIO和SPI操作跟着思路走就能复现。1. 为什么1.69寸这颗屏值得自己写驱动1.1 硬件参数与接口初印象1.69寸TFT屏分辨率常见是240x280驱动IC是ST7789V接口有SPI、RGB、MCU并口等几种版本。市面上买到的大多数是四线SPI接口模块引脚不算多只有VCC、GND、CS、DC也叫A0、RST、BLK、MOSI、SCLK这几个。相比老款的ST7735屏幕ST7789支持更大的分辨率、更高的像素格式颜色深度为RGB565时显示效果很细腻用来做小仪表盘、温湿度计、迷你游戏机都非常合适。我手里这块屏幕是240x280的宽高比接近1:1.17实际上这也就意味着它并不是标准的240x320全尺寸。ST7789V这颗芯片原生最大支持240x320但1.69寸屏往往只用到240x280甚至240x240的区域厂商在屏的玻璃上做了裁切剩下的区域要么物理不存在要么被遮蔽。这个特点直接影响了后面的显示窗口初始化后面会展开说。1.2 现成驱动库的适配问题很多人拿到屏幕第一反应是找现成的驱动代码。网上的图形库源码确实很多但这类代码通常绑定某个开发板或特定引脚直接拿来用的时候经常要面对“原作者的引脚和你的板子对不上”“底层是软件模拟SPI还是硬件SPI没写清楚”“不同屏幕版本初始化参数不同”这些麻烦。更重要的是现成库往往把初始化序列封装成了一个函数内部每个命令的作用、为什么需要延时、为什么MADCTL要设置为某个值全都被隐藏起来了。一旦屏幕显示方向不对、颜色发紫、图像偏移你根本不知道从哪下手。自己写驱动并不是为了显得多专业而是为了在出问题的时候有清晰的排查方向。尤其当你换了不同批次、不同分辨率的屏幕只需修改显示窗口参数和扫描方向其他代码完全不动这个灵活性是现成库很难给的。1.3 先搞懂数据是怎么跑到屏幕上的在写代码之前必须理解整个数据链路。MCU通过SPI接口把数据发给屏幕驱动IC驱动IC再控制玻璃面板上的像素点。SPI接口中SCLK提供时钟MOSI负责传输数据CS做片选RST负责复位DC线则是区分“当前传输的是命令还是数据”。DC线的作用可以这样理解MCU和数据手册之间需要一种“这是通知还是包裹”的区分方式。当DC拉低时屏幕把这个字节当作命令来解释比如设置颜色格式、设置扫描方向当DC拉高时屏幕把后续数据当作真正的像素数据或命令参数。这个机制很简单但在实际调试中DC线的时序配合非常关键很多花屏问题都出在DC翻转和SCLK边沿的相对位置上。2. 硬件设计GPIO规划与接线细节2.1 引脚分配与选型理由F407ZET6的资源非常充裕GPIO多、外设多驱动一块SPI屏绰绰有余。我选用了SPI1作为通信接口原因是SPI1挂在APB2总线上最大时钟可以达到42MHz而SPI2和SPI3挂在APB1上最大只有21MHz。后续如果想提升刷新率SPI1的上限明显更有优势。我实际使用的引脚分配如下信号引脚说明SCLKPA5SPI1_SCK复用功能AF5MOSIPA7SPI1_MOSI复用功能AF5CSPB6任意GPIO即可软件控制DCPA0任意GPIO命令/数据选择RSTPE1任意GPIO复位控制BLKPE0任意GPIO背光控制VCC3.3V屏幕模块电源GNDGND共地必须可靠这里有个小建议如果板子上的SPI1引脚被其他外设占用用GPIO模拟SPI也能驱动这块屏只是速度会明显下降高频刷图会卡顿。硬件SPI是首选方案务必别为了省事把所有信号线都用杜邦线飞得很长。2.2 供电与背光设计屏幕模块大多内置了电平转换和稳压电路但VCC依然需要用干净的3.3V供电。我实测屏幕全亮时电流大约在30mA到60mA之间单个稳压芯片完全带得动。需要注意的是背光引脚BLK不能悬空悬空时屏幕可能不亮或者亮度不稳定。把BLK接到高电平是最简单的做法如果要做亮度调节可以将BLK接在PWM输出引脚上通过调节占空比控制背光电流。2.3 接线中的隐患第一个坑是杜邦线过长。SCLK频率高了之后过长的杜邦线会引入信号反射和串扰屏幕上就会出现随机雪花点或边缘闪烁。我一开始用20cm长的杜邦线连接42MHz下刷图偶尔会有杂点后来把线缩短到10cm以内并让SCLK和MOSI尽量分开走线问题就消失了。第二个坑是模块丝印的DC和D0容易看错尤其是一些小厂模块丝印模糊时最好拿万用表对照原理图逐一确认。第三个坑是共地不可靠电源地和信号地必须连到同一块板子的GND上否则SPI电平参考点不一致逻辑分析仪里看波形没问题但实际就是不稳定。3. 时序与初始化序列从波形到命令3.1 SPI模式与DC线配合ST7789的SPI接口通常工作在模式0也就是CPOL0、CPHA0空闲时SCLK为低电平数据在SCLK上升沿被采样下降沿切换。发送命令时DC拉低发送数据时DC拉高。一个容易忽视的点是DC的状态必须在SCLK上升沿到来之前就已经稳定不能和MOSI同时翻转。如果你用逻辑分析仪抓波形会发现有些花屏问题并不是命令错误而是DC翻转比SCLK晚了那么一两百纳秒。低速SPI下这种问题不明显频率高到几十兆的时候就会被放大。所以我在代码里发送命令和数据时都是先写GPIO的BSRR寄存器设置DC状态再等待一小段时间最后才操作SPI数据寄存器。这个顺序看着不起眼但对稳定性帮助很大。3.2 可用的最小初始化序列ST7789的完整初始化序列很长数据手册里会给出Power_Setting、Gamma_Setting等一大堆寄存器配置。实际上在大多数场景下我们只需要一个最小初始化序列就能点亮屏幕其余寄存器保持默认值即可。我使用的初始化代码如下static void ST7789_Init(void) { // 硬件复位 GPIO_ResetBits(GPIOE, GPIO_Pin_1); delay_ms(50); GPIO_SetBits(GPIOE, GPIO_Pin_1); delay_ms(120); // 软件复位 ST7789_WriteCmd(0x01); delay_ms(120); // 退出睡眠模式 ST7789_WriteCmd(0x11); delay_ms(200); // 16位像素格式 RGB565 ST7789_WriteCmd(0x3A); ST7789_WriteData(0x55); // 内存数据访问控制调整扫描方向 ST7789_WriteCmd(0x36); ST7789_WriteData(0x00); // 是否反白不同批次屏幕不同 ST7789_WriteCmd(0x20); // 如果显示颜色异常可改为0x21 delay_ms(50); // 设置显示区域为240x280 ST7789_WriteCmd(0x2A); ST7789_WriteData(0x00); ST7789_WriteData(0x00); ST7789_WriteData(0x00); ST7789_WriteData(0xEF); // X方向结束 239 ST7789_WriteCmd(0x2B); ST7789_WriteData(0x00); ST7789_WriteData(0x00); ST7789_WriteData(0x01); ST7789_WriteData(0x17); // Y方向结束 279 // 打开显示 ST7789_WriteCmd(0x29); delay_ms(20); }这个序列只用了7个命令。SWRESET让屏幕复位到默认状态SLPOUT唤醒屏幕电源COLMOD设置RGB565格式MADCTL控制扫描方向CASET和RASET划定显示窗口最后DISPON打开显示。每一步后面的延时都不能省略尤其是SLPOUT之后屏幕内部电源稳定需要时间如果立刻发后续命令命令很容易被丢弃表现为白屏或显示异常。3.3 初始化后花屏最常见的几个根因第一个根因是延时不足。很多例程把延时全部缩到最小导致屏幕上电时序没走完就发命令。我实测SWRESET之后至少等120msSLPOUT之后至少等200ms才能保证后续命令被正确接收。第二个根因是像素格式不匹配。屏幕被设置为18位色但MCU发的是16位数据画面颜色会明显发灰、发紫。COLMOD必须设置成0x55。第三个根因是MADCTL扫描方向不对。这个值决定了X和Y的增长方向以及RGB的排列顺序设置不对时画面会镜像、旋转或者出现斜向条纹。第四个根因是反白模式。有些屏默认白字黑底有些屏默认黑字白底如果发现颜色像“底片”一样反转把0x20和0x21互换一下即可。4. 绘图核心显示窗口、画点与坐标计算4.1 全帧缓冲的算账拿到屏幕第一反应是开一块全帧缓冲把所有像素都放在内存里再整体刷新到屏幕上。然而240x280分辨率、RGB565格式每个像素占用2字节全帧大小为2402802134400字节约131KB。F407ZET6虽然有多达192KB的SRAM但实际项目里还要放通信缓存、任务栈、运动参数等全部占用并不现实。而且一些编译器默认堆栈配置还吃掉了不少内存强行开全帧缓冲很容易导致编译能过但运行异常。我的做法是不开全帧缓冲改用行缓冲加局部刷新的方案。屏幕一次只刷新一小块区域比如40行或者60行这样内存占用只有几KB刷新效果却几乎看不出差别。这是实际项目中更务实的选择也避免了“内存足够”的错觉。4.2 设置窗口与画点ST7789的绘图核心是“窗口RAMWR”的组合。先通过CASET设置列范围RASET设置行范围然后连续发送数据屏幕上对应窗口内的像素就会被依次填充。窗口越小发送的数据越少刷新速度越快。画一个像素点的实现逻辑如下void ST7789_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x 240 || y 280) return; ST7789_SetWindow(x, y, x 1, y 1); ST7789_WriteCmd(0x2C); ST7789_WriteData(color 8); ST7789_WriteData(color 0xFF); }注意这里把颜色拆成了高字节和低字节因为RGB565颜色是16位的而SPI每个数据单元是8位。发送顺序必须是高字节在前否则颜色通道会互换红色变成蓝色蓝色变成绿色。这一点在小端模式下尤其容易踩坑因为STM32的内存中uint16_t的低字节在低地址不能直接把指针强转成uint8_t发送。4.3 画线、矩形填充与取模有了画点函数直线和矩形就很简单了。画直线可以用Bresenham算法避免浮点运算速度也快。画实心矩形时一条一条扫描线地填充即可。不过要注意逐点调用画点函数效率很低因为每个点都要重新设置窗口白白发送6个额外的窗口命令字节。更快的做法是一次性把整个矩形的窗口设置好然后连续发送像素数据。显示文字或图片时通常用取模软件将位图转换成C数组。取模方向必须与显示窗口的方向一致否则字符看起来是颠倒的。我自己习惯用“逐行取模、高字节在前”的方式配合行缓冲一次性发送整行数据显示16x16中文字符时速度非常理想。5. 提速从阻塞SPI到DMA局部刷新5.1 阻塞SPI的实测瓶颈刚开始我直接用阻塞方式发送数据SPI时钟设为21MHz每发送一个字节需要等待TXE标志置位。看起来频率不低但实际测下来刷一屏还是要上百毫秒。原因有两层第一层是SPI底层本身需要一个字节一个字节地搬运每发一个字节都有等待第二层是绘图函数里夹杂了大量函数调用、窗口设置、边界判断白白消耗了CPU时间。如果只是显示静态画面这些可以忽略但要做动画或者菜单滑动体验就会很差。5.2 用DMA把CPU解放出来DMA能在外设和内存之间直接搬运数据不经过CPU干预。对刷新屏幕来说我们只需要把一块内存中的数据连续搬到SPI的数据寄存器SPI外设就会自动把这些数据发出去。F407ZET6的SPI1_TX对应DMA2硬件请求我使用的是DMA2_Stream3外设通道选择Channel_3。配置思路如下将外设地址设为SPI1的数据寄存器地址内存地址设为待发送的缓冲区首地址内存地址增量使能外设地址不增量方向为内存到外设数据宽度均为半字16位缓冲区大小为待发送的数据量。搬运完成后在DMA传输完成中断里关闭DMA同时关闭SPI的TX DMA请求。这里有一个容易踩的坑使能DMA之前必须先把SPI_I2S_DMACmd配置为ENABLE否则DMA搬运的数据不会到达SPI外设。DMA触发完成后也需要先把SPI的DMA请求关闭再关闭DMA顺序反了可能会出现最后几个字节发送不完整的情况。5.3 更实用的优化路线DMA并不是唯一手段甚至很多时候单纯上DMA提升有限因为瓶颈在数据的来源和窗口命令上。我更推荐的组合是局部刷新加DMA批量搬运。也就是只更新变化的区域每次设置一次窗口然后把一整块缓冲的数据通过DMA发出去。放在具体场景里就是显示数字变化时只有数字周围几十个像素需要刷新没必要整个屏幕跟着重绘。另一个优化思路是分块缓冲加双缓冲。比如把屏幕分成6个40行的区块每个区块的缓冲大小是24040219200字节约18.75KB内存完全能承受。先在一个区块内存里绘制好所有像素再一次性DMA发送给屏幕。这样既避免了全帧缓冲的巨额内存开销又比逐行发送少了很多次窗口命令设置实测刷新速度可以提升3倍以上。6. 调试中踩过的坑与最终排查清单6.1 白屏和乱码的波形检查遇到白屏时我第一个动作是打开逻辑分析仪抓SCLK、MOSI、DC三根线的波形。重点看两个地方一是初始化阶段有没有命令被遗漏比如SWRESET之后是不是立刻发了SLPOUT如果是需要加大延时二是DC线在命令和数据之间的切换是否和SCLK边沿对齐。有一个典型的假象是SPI波形看起来没有问题但MISO上没有任何返回数据。ST7789的MISO在某些模块上是悬空不接的这种情况只要确认发送端波形正常即可不用怀疑硬件。6.2 偏移量与屏幕版本问题1.69寸屏最常见的显示异常是“画面偏左偏上”或者“底部有一条黑边”。这是因为ST7789芯片原生支持240x320但屏幕只露出240x280。如果不重新设定RASET的结束坐标屏幕就会尝试访问不存在的像素区域。我遇到过一批屏幕显示区域起始坐标不是(0,0)而是需要从第0行偏移到第40行。处理方法是在初始化CASET/RASET时直接把起始坐标和结束坐标一起设置好比如将Y方向从40到319调整为0到279。如果显示内容上下颠倒了则修改MADCTL的BIT7和BIT6来翻转扫描方向。不同批次屏幕的差异是真实存在的同一型号的屏换了一家供应商初始化参数可能完全不同。调试时先用最简单的纯色填充测试比如整屏填红色然后依次填绿、蓝能快速确认RGB通道是否正常。6.3 排查清单表格现象先查什么再查什么白屏复位引脚是否拉高、延时是否足够逻辑分析仪看初始化命令是否完整颜色发紫/发灰COLMOD是否设为0x55是否误设成18位色模式颜色像底片反白命令0x20/0x21是否反了MADCTL中的RGB顺序画面偏移、有黑边CASET/RASET范围是否匹配实际像素屏幕批次起始坐标是否非0画面镜像/颠倒MADCTL扫描方向设置检查BIT7、BIT6、BIT3随机雪花点SCLK频率是否过高杜邦线长度和地线连接6.4 经验清单最后写几条自己总结的经验。初始化命令发完后要下意识地多等一段时间宁可多等也不能少等大部分白屏都是延时不够引起的。在调试阶段先用纯色填充验证硬件通路再用渐变或网格图案验证坐标和方向不要一上来就刷图片否则问题来源会非常难定位。SPI频率从低速开始稳定再慢慢提高不要一开始就追求42MHz。高频下的信号完整性问题比驱动代码更难查而且经常是“偶发故障”肉眼很难复现。另外如果用的是带触摸的1.69寸屏触摸IC和显示IC往往是分开的触摸部分一般走I2C或者单独的四线SPI别把触摸引脚搞混到显示驱动里面。我一开始就把触摸的INT引脚当成了RST引脚结果屏幕一直无法启动查了很久才发现接线错误。最后再分享一点实际体会。我一开始把SPI频率拉到42MHz以为越快越好结果屏幕偶尔会出现随机杂色点。后来把SCLK和MOSI的杜邦线剪短、重新焊线再把频率降到21MHz问题就消失了。这件事让我明白一个道理很多时候驱动代码本身没有错反而是物理链路的裕量不够降低了频率、缩短了连接距离看似“退步”实际稳定性反而更好。如果你也在调这块屏卡了很久不妨先把频率降下来试试。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →