ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103驱动0.99寸TFT圆屏:硬件SPI+DMA刷新优化实战

STM32F103驱动0.99寸TFT圆屏:硬件SPI+DMA刷新优化实战 简介面向STM32中级开发者该资源基于STM32F103RCT6主控驱动0.99寸TFT圆屏采用硬件SPIDMA方案替代软件模拟SPI显著提升图片刷新速度并能流畅显示动态图片解决了常规圆屏显示卡顿的问题。压缩包共218个文件、大小5.66MB包含完整的Keil工程源码.c/.h、编译过程文件.o/.crf/.d、工程配置文件.uvprojx/.uvoptx、链接脚本.sct及可烧录的.hex文件目录与文件层级完整便于直接打开和二次开发。已有545人学习该资源。代码清晰展示了硬件SPI初始化、DMA通道配置、圆屏驱动与图片数据组织的完整流程适合希望快速上手圆屏显示或研究SPIDMA加速的开发者参考。同时需注意主控内存有限仅能存储少量图片数据因此该方案更适用于图片数量少、以动态切换为主的显示场景。 0.99寸TFT圆屏第一眼看到实物的人都会觉得它是个漂亮的小东西但真正点起来才发现驱动它比驱动0.96寸OLED费劲得多。这块屏的显示区是240x240彩屏底层驱动IC按RGB565算一帧要115200字节这个量级如果还用51时代点OLED那种GPIO模拟SPI的方式硬刷一帧能卡到一秒上下做个简单表盘都会闪到没眼看。所以这次我把STM32F103RCT6的硬件SPI1和DMA1搬出来让外设自己搬数据把一帧的刷新时间压到几十毫秒。这篇文章直接按我的实操过程来讲为什么这样选型引脚怎么搭SPI和DMA的参数怎么定最后还有几个我踩过之后才明白的坑适合已经会点灯、准备认真玩彩屏的中级玩家。1. 一帧240x240的数据量决定了显示方案的上限1.1 圆屏的驱动IC是方的先说一个很多人会忽略的事实。0.99寸TFT圆屏的圆指的是最外层玻璃盖板玻璃下面贴的LCD面板本身还是一块完整的240x240方形屏驱动IC通常是ST7789V2或者GC9107它对世界的认知就是我有一个240x240的矩形点阵。四个角上的像素只是被圆形外壳挡住看不见了不是不存在。这意味着两件事第一无论你画不画四个角只要刷新全屏数据量就固定是240x240x2字节第二如果你想省流量就得自己在软件里做圆形区域裁剪。很多初学者上来就想我只要往圆形区域写数据就行了但实际上SPI总线上不会自动跳过被遮挡的像素驱动IC依然会扫描整个矩形区域。1.2 三种驱动方案的实测对比我在同一块板子、同一块屏幕上测试了三种驱动方式刷全屏纯色结果很能说明问题GPIO模拟SPI用IO翻转模拟SCK和MOSI时钟能撑到1MHz左右就比较极限了再高容易因为中断和函数调用开销导致时序乱。按1MHz算一帧115200字节就是115200x8/1000000921ms差不多一秒一帧。硬件SPI阻塞发送SPI1按18MHz跑但每发一个字节都要等TXE标志中间循环判断和函数调用有开销实测一帧大约120~140ms能看但拖窗口还是会闪。硬件SPIDMADMA把数据从内存按批搬到SPI数据寄存器CPU只负责在传输完成中断里收尾。SPI时钟同样18MHz一帧理论耗时51.2ms实测加上命令开销、片选切换大约60~70ms一帧。方案一帧耗时CPU占用适用场景GPIO模拟SPI约920ms全程占用静态显示、低分辨率黑白屏硬件SPI阻塞约130ms发送期间占用偶尔刷新小区域硬件SPIDMA约65ms几乎不占用动态表盘、动画、频繁刷新三组数据放在一起为什么硬件SPIDMA是彩屏显示的主流做法就不用再多解释了。1.3 DMA只在整块刷新里才划算不过要泼一盆冷水DMA不是万能的。如果每次只更新一个16x16的字模一次288字节的数据量DMA配置和中断的消耗可能比阻塞发送还大。而且DMA能加速的只是把已有数据搬到SPI的环节如果你还要CPU去逐像素算颜色、做粒子效果、算三角函数瓶颈又回到CPU这边。所以在这个项目里DMA主要用于两类场景一是整帧清屏或整张图片刷新二是把预先算好的表盘背景、指针缓冲区送入屏幕。至于小的文字、图标更新依然可以用阻塞发送反而简单直接。2. 引脚分配与SPI卡在72MHz总线上的位置2.1 选SPI1还是SPI2先看时钟树STM32F103RCT6的主频是72MHzAPB2是72MHz、APB1是36MHz而SPI1挂在APB2上SPI2和SPI3挂在APB1上。单看SPI外设上限都是18MHz但SPI1能直接从72MHz的PCLK2分频SPI2得从36MHz的PCLK1分频。以18MHz为例SPI1用4分频72/4SPI2用2分频36/2表面上都能到但SPI1的时钟余量和时序余量更好网上例程、库函数也几乎都按SPI1写出问题好排查所以这里直接锁定SPI1。SPI1默认引脚是PA5(SCK)、PA6(MISO)、PA7(MOSI)。TFT一般只需要SCK和MOSIMISO可以空着。如果PA5/PA7被占用可以切到SPI1的复用位置PB3/PB4/PB5但那几个脚是JTAG引脚必须先调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)关掉JTAG否则IO死活不出波形。我实际碰到过一次就是配了PB3/PB4没反应折腾半天才想起来是这个原因。2.2 硬件NSS和GPIO软控CS我为什么选后者STM32的SPI外设自带NSS脚和硬件从模式选择但做TFT项目我强烈建议CS用普通GPIOSPI内部设成软件NSS管理。原因有三个TFT的CS电平持续时间往往很长一个DMA全帧传输要几十毫秒如果CS由硬件NSS控制中途可能因为SPI状态机在字节间拉高拉低导致数据被误判成多条命令。TFT除了CS还有DC引脚用来区分命令/数据CS的低电平要包住整个命令加数据写入过程这个过程里DC还要来回切换硬件NSS管不了这么细。软件GPIO控制CS想什么时候拉低就拉低想什么时候抬高就抬高代码逻辑一目了然调试逻辑分析仪也方便。初始化时把SPI_NSS配成SPI_NSS_SoftNSS引脚就从SPI外设里释放了物理上可以不接。2.3 一张可以抄的接线表我手头这块0.99寸圆屏模块的接口顺序是VCC、GND、SCL、SDA、RES、DC、CS、BLK。不同卖家模块的丝印顺序差异很大接线前一定先对着屏幕背面的丝印和卖家引脚图确认。我这里给出的接法屏幕引脚接STM32说明VCC3.3V3.3V供电注意不要接5VGNDGND共地SCLPA5SPI1_SCKSDAPA7SPI1_MOSIRESPB0复位低电平有效DCPB1命令/数据选择CSPA4GPIO软控片选BLK3.3V背光常亮想调亮度接PWMRES脚不能直接接3.3V了事上电后最好由MCU拉低再拉高做一次复位。BLK如果不要求调背光接3.3V最省事。整个模块都按3.3V逻辑供电电流不大用板载LDO就能带起来。3. SPI与DMA的配合从寄存器到中断收尾3.1 SPI层的八位主模式配置这里直接用STM32标准外设库来写库的命名和寄存器一一对应方便理解改用HAL库时逻辑完全一致只是函数名换了。SPI1初始化主要做四件事打开GPIOA、GPIOB和SPI1的时钟把PA5、PA7配成复用推挽输出PA4配成普通推挽输出SPI按8位主模式、CPOL0、CPHA1采样沿、软件NSS、4分频、MSB先行的参数初始化最后使能SPI。void TFT_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS默认拉高 SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; 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_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }几个参数说明一下。SPI_DataSize_8b不是随便选的ST7789V2的命令、数据、RGB565像素在SPI线上都是以8位字节为单位传输的如果配成16位帧发命令时还要额外处理高低字节纯给自己找麻烦。CPOL0、CPHA1Edge对应SPI模式0是这类屏幕最常用的接口时序。4分频让SPI时钟到18MHz这是F103主模式的上限再往上已经超出硬件规格数据会出错。3.2 DMA通道绑定与65535字节的天花板F103的DMA1通道3固定对应SPI1_TXDMA1通道2固定对应SPI1_RX。做显示只发不收我们用DMA1_Channel3。配置时方向是内存到外设外设地址固定为SPI1-DR内存地址是显存缓冲区首地址外设和内存数据宽度都设成Byte内存地址递增、外设地址不递增传输模式Normal优先级给High。为了在发送完成后进中断打开DMA1_IT_TC。#define FRAME_W 240 #define FRAME_H 240 #define PIXEL_SIZE 2 uint8_t tftFrameBuffer[FRAME_W * FRAME_H * PIXEL_SIZE]; void TFT_DMA_Init(uint16_t transferLen) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tftFrameBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize transferLen; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, DMA_InitStructure); DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_TX, ENABLE); }这里有个大坑DMA1通道3的传输长度寄存器CNDTR只有16位单次传输最多65535字节。全屏240x240x2115200字节一次根本装不下。我第一次就直接把transferLen设成115200结果屏幕只有上半屏有画面。解决思路有两种分批发第一次传65535字节DMA完成中断里把内存地址改成tftFrameBuffer65535再设置DMA_BufferSize为49665重新启动一次DMA。按区域发一次只发一个窗口范围内的一行或几行数据每行480字节完全避开65535的上限。这也更符合实际界面的刷新策略——表盘上只有指针在动没必要每次全屏重发。推荐第二种动态界面本来就不需要全屏刷新但如果要整帧切换大图就在一个函数里把大批量拆成两段发。注意F103的DMA_CNDTR是16位寄存器单次传输量上限是65535字节不要在DMA_BufferSize里直接填2402402这个错误非常典型。3.3 传输完成中断里别急着拉CSDMA的传输完成中断TC代表数据已经全部从内存搬到SPI数据寄存器不代表SPI已经把数据全部移位输出了。SPI发送是边发送边置TXEDMA往SPI_DR里填数的速度和SPI移位输出的速度匹配最后一个字节写入SPI_DR后DMA就会进TC中断但SPI的移位寄存器可能还在把最后8个bit从MOSI上移出去。如果此时马上拉高CS最后一个字节的尾巴就被硬生生截断画面末尾会出现一行花点或颜色偏移。正确做法是TC中断里先等一下SPI的BSY标志复位再拉高CSvoid DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); DMA_Cmd(DMA1_Channel3, DISABLE); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) ! RESET); TFT_CS_HIGH(); dmaSendBusy 0; } }这个等待BSY复位的代码是我抓逻辑分析仪波形时才补上的。没等待之前CS波形显示最后一个字节的时钟还没收尾就已经跳高了补上之后波形干净偶发的最后一行花屏也跟着消失。4. 把方形驱动IC装进圆形玻璃窗口、坐标与字模4.1 窗口命令是显示优化的基础ST7789V2的显示流程是先通过CASET(0x2A)和RASET(0x2B)设定一个矩形窗口再发RAMWR(0x2C)之后所有SPI数据都会按窗口从左到右、从上到下填充。窗口设置对DMA方案特别重要因为DMA不理解我只想画一个圆形区域它只是一台无脑搬砖的机器给它的数据必须自己先按矩形窗口的格式组织好。填充整屏时设置窗口的代码大概是这样static void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { TFT_DC_LOW(); TFT_SendCmd8(0x2A); TFT_DC_HIGH(); TFT_SendData8(x0 8); TFT_SendData8(x0 0xFF); TFT_SendData8(x1 8); TFT_SendData8(x1 0xFF); TFT_DC_LOW(); TFT_SendCmd8(0x2B); TFT_DC_HIGH(); TFT_SendData8(y0 8); TFT_SendData8(y0 0xFF); TFT_SendData8(y1 8); TFT_SendData8(y1 0xFF); TFT_DC_LOW(); TFT_SendCmd8(0x2C); }TFT_SendCmd8和TFT_SendData8是阻塞式发送单个字节的函数只用于窗口设置和初始化序列不是DMA阶段。DMA只负责在窗口设置完成之后把准备好的像素块整个搬进SPI二者串行执行不会争抢总线。4.2 圆屏的原点方向由MADCTL决定0.99寸圆屏分辨率虽然统一是240x240但不同型号的原点方向不一定一致有的屏(0,0)在左上角有的在左下角。上电后写字会发现文字是倒的或者镜像的。ST7789V2的方向控制主要在MADCTL(0x36)寄存器里通过改变MY、MX、MV、ML、BGR这些位可以组合出不同的扫描方向。我没有死记每个值因为不同模组内部引线差异会改变实际效果。我的办法是写一个测试函数整屏填充红色然后分别试0x00、0xC0、0xA0、0x60这四组常见值看颜色从哪个角开始铺满、文字朝向对不对选中一个合适的固定写死在初始化里。如果显示颜色明显红蓝对调不需要换接线改一下0x36寄存器bit3BGR位就解决。有些屏初始化时还需要INVON(0x21)让颜色恢复正常如果整体看起来像底片往这个方向查。4.3 圆形区域裁剪能省掉约两成数据量前面说过四个角的像素被外壳挡住但驱动IC依然扫描。如果每次都全屏刷新等于把约21.5%的数据白白发给永远看不见的角落像素。圆面积和正方形面积的比是π/4约78.5%所以圆形外角落占21.5%。要用足这部分性能余量可以做个简单裁剪把圆心放在(120,120)半径120遍历每一行y计算半宽wsqrt(120^2-(y-120)^2)x的有效范围是[120-w, 120w]只需要把窗口设置到这段范围再发送对应像素。这样每次全屏刷新只传输约9万字节比直接全屏少了两成多同时也不会在看不见的角落上浪费DMA带宽。代码里用一个循环算出行范围即可逻辑很直白跑起来也就是几行整数运算的事CPU完全扛得住。4.4 中文字模与背景透明混合显示不出中文这个问题在彩屏上很常见本质是字模取模方式和写入方式没对上。0.99寸圆屏做表盘通常只需要显示少量汉字比如星期、温度、电量。我用的方案是取模软件生成16x16点阵字模按竖排或横排取模关键是要和写入顺序一致字模数据只有0/1代表这个像素是字色还是背景色写入时逐点判断如果是字色把字模对应的RGB565值写入显存如果是背景色保留显存原来的像素值而不是用白色或黑色覆盖。这样做出来的文字是透明的能压在表盘背景上。很多人图方便直接用一个16x16的矩形把字模贴上去结果字周围出现一个方块就是因为没有做逐点透明混合。尤其在表盘中央要叠指针转动的场景背景透明是做相关效果的前提。5. 实测中翻车最多的三个点色彩、DMA停发、CS时序5.1 颜色反了、泛白、像底片这大概是屏幕点亮后最先遇到的三个问题。颜色反了红蓝对调先改MADCTL(0x36)的bit3 BGR位一般就是0x00和0x08二选一。泛白看看SPI时钟是不是太高或者杜邦线太长18MHz算极限线超过10cm或者飞线很乱降到9MHz试试。像底片ST7789V2有INVON(0x21)和INVOFF(0x20)两条指令作用是全局颜色取反有些屏初始化序列要开反色才正常。排查顺序建议先在代码里固定填充纯红、纯绿、纯蓝三个色块根据实际显示颜色判断是格式问题还是取反问题一轮就能定位。不要一上来就怀疑接线圆屏模块引脚顺序再乱也架不住拿色块挨个试。5.2 DMA传一半就停的根因我遇到过屏幕上半屏正常、下半屏灰黑的情况最常见根因就是上面说的CNDTR长度超过65535。DMA_BufferSize在库里的类型是uint16_t直接给115200编译器会截断成错误长度DMA根本不知道要传这么多。验证方法很简单改成先传65535字节看画面是不是比之前多了一截。如果是按拆批或区域刷新的方案处理。还有一种传一半停住的情况是忘了开SPI的DMA请求。DMA初始化之后还要调SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_TX, ENABLE)否则SPI的TXE事件不会触发DMADMA只会自己空转不会开始搬运。这个函数漏了画面基本是黑的因为第一个字节都没发出去。5.3 CS毛刺和偶发错位前面的BSY等待问题主要影响就是CS跳变时机不对。实际现象是刷新多次之后屏幕偶尔整体错开几行或者冒出几个杂色像素。在这个问题上逻辑分析仪是最好的老师。抓SPI总线和CS线的波形看CS的有效低电平是否完整包住所有SCK脉冲如果最后一个SCK之后CS马上跳高说明少等了BSY。另一个容易忽略的是CS的初始电平屏幕刚上电时如果CS处于低电平外设会以为MCU在给它发命令可能锁在错误状态。所以初始化GPIO时要先把CS置高再去初始化SPI。最后一个小技巧如果CS毛刺问题怎么查都查不到试试把SPI时钟从18MHz降到9MHz很多奇怪错位可能只是信号质量不够降频之后就好了。做完这个0.99寸圆屏项目我最大的体感是DMA刷屏真正的价值不是把CPU解放出来打游戏而是逼你把显示流程重新拆成绘帧和送帧两步。先在一块内存缓冲区里把画面画好再让DMA负责送出去两个阶段解耦之后代码的可扩展性一下子好很多。后面想加LVGL、加FreeRTOS这套底子都能直接往上垒。如果你也准备拿F103RCT6玩彩屏建议不要急着抄完例程就跑先动手改三处改成窗口刷新、改成双缓冲、改成圆形裁剪改完你对DMA的理解会上一个台阶。本文还有配套的精品资源点击获取
返回列表