
简介STM32F103RCT6驱动0.99寸TFT圆屏的硬件SPIDMA显示工程面向具备一定STM32基础、希望解决屏幕刷新速度瓶颈的中级开发者。压缩包共218个文件大小仅5.66MB其中核心为41个C和40个H源码并包含keil工程配置uvprojx/uvoptx、编译生成的hex/axf固件、map/lst/lnp等链接映射文件以及bat清理脚本和备份文件目录结构接近原始工程便于按需查阅。已有547人学习。资料完整演示了硬件SPI配合DMA的初始化流程与显示驱动写法实测动态图片刷新速度较快可满足日常显示需求作者同时指出主控内存有限、仅适合图片数量较少场景的局限。基于该源码读者可快速迁移到同系列芯片也可用于学习DMA传输、圆屏驱动或SPI时序优化。1. 一块 0.99 寸圆屏为什么值得折腾硬件 SPI DMA做嵌入式的小玩具做到一定阶段总绕不开一个诉求给设备加一块小屏既能显示状态又要把刷新做得流畅。STM32F103RCT6 配 0.99 寸 TFT 圆屏是 stm32 驱动 spi 显示屏里性价比很高的一套组合——屏幕便宜、主控余量足真正考验人的是显示链路能不能保持稳定。很多人点亮过屏幕就停下来了一刷全屏就花屏、DMA 传输一次就罢工。这篇文章想解决的问题很直接如何让这块圆屏在硬件 SPI DMA 下稳定跑起来并且把刷新率、局部更新、圆形裁剪这些中级玩法讲透。适合已经点亮过 OLED 或 TFT、想进一步吃透 SPI 传输的人不适合第一次接触单片机显示的人。2. 硬件连接与驱动 IC 选型先看圆屏的引脚再定接线2.1 圆屏模组的驱动 IC 与引脚定义市面上 0.99 寸圆屏分辨率通常是 240x240驱动 IC 常见的是 ST7735S。这块 IC 的引脚不多一个典型的 8 针模组长这样引脚方向功能说明GND电源地线VCC电源3.3V 供电不能接 5VSCL / SCK输入SPI 时钟SDA / MOSI输入SPI 数据RES输入复位低电平有效DC输入数据/命令选择高电平为数据CS输入片选低电平有效BLK输入背光控制高电平点亮这里有一个常被忽略的点BLK 引脚不是简单的“接 VCC 就完事”。如果你把 BLK 直接拉高背光是全亮的做低功耗或调亮度时完全没法控制。我一般把 BLK 接到定时器 PWM 输出脚用 1kHz 以上的频率调占空比这样亮度可调也避免低频 PWM 带来的明显闪烁。2.2 为什么选 F103RCT6 而不是 C8T6从资源角度看F103RCT6 是 256KB Flash、48KB RAMC8T6 是 64KB Flash、20KB RAM。一块 240x240 的 RGB565 全屏显存是 240x240x2 115200 字节约 112.5KB。这个数字意味着C8T6 的 RAM 放不下全屏双缓冲RCT6 的 RAM 也放不下但 RCT6 的 Flash 空间更大可以塞下 GB2312 字体库或多套图片素材不会在编译链接时被容量卡脖子。另外 RCT6 引脚多LQFP64 封装引出 51 个通用 IO接一个屏幕外还要接按键、传感器余量充足。硬件 SPI 的优势在于SPI1 挂载在 APB2 总线上时钟最高 72MHz分频后能轻松跑到 18MHz 甚至 24MHz而软件 SPI 用 GPIO 翻转即使主频 72MHz一次翻转加延时也远达不到这个速度。更关键的是硬件 SPI 能直接触发 DMADMA 搬运数据时 CPU 可以干别的刷新全屏时不再被 SPI 中断拖住。2.3 我的引脚映射与接线参考下面是我在这个项目里用的引脚组合避开 USB 串口和调试器占用的引脚屏幕引脚接 STM32 引脚说明SCLPA5SPI1_SCKSDAPA7SPI1_MOSICSPA4软件控制片选DCPA3数据/命令选择RESPA2复位控制BLKPA1TIM2_CH2 PWM 输出这段接线最关键的部分是片选。STM32 的 SPI 外设有硬件 NSS 功能但硬件 NSS 容易在空闲状态被拉低导致误触发我习惯完全用 GPIO 软件拉 CS。这不算玄学是 SPI 从设备时序的常见处理方式硬件 NSS 在多主机或引脚复用场景下确实容易翻车。3. 点亮屏幕前的关键配置SPI 时钟、DMA 通道与复位时序3.1 上电复位时序决定屏幕是否白屏ST7735S 这类驱动 IC 上电后必须给一个低电平复位脉冲。常见做法是上电后先把 RES 拉低延时 20ms 以上再拉高然后延时 120ms 等内部稳压器稳定。很多人写代码时把延时写成几个空循环了事结果屏幕偶尔白屏偶尔正常这就是时序裕量不够的表现。我用下面的流程void TFT_Reset(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_2); // RES 0 Delay_ms(50); // 保持低电平 50ms GPIO_SetBits(GPIOA, GPIO_Pin_2); // RES 1 Delay_ms(120); // 等待内部电路稳定 }RES 引脚拉低的时间不要小于 10ms这是驱动 IC 数据手册里给出的最小复位脉冲宽度拉高后的等待时间决定屏幕内部 DC-DC 转换器是否稳定宁可多等也不要赶时间。如果发现屏幕显示内容时好时坏先把这里的延时加倍测试通常能排除一大半问题。3.2 SPI1 的寄存器级配置和参数选择硬件 SPI 配置的核心参数有四个主模式、8 位数据、时钟极性和分频系数。ST7735S 要求 SPI 模式下 CPOL0、CPHA0也就是 SCK 空闲为低、第一个边沿采样数据。分频系数我建议先保守一点从 4 分频开始也就是 SPI 时钟 18MHz屏幕正常后再尝试 2 分频提到 36MHz——虽然很多屏幕宣称支持 15MHz 或更高但实际布线、杜邦线长度都会影响信号质量。void SPI1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能 GPIOA 和 SPI1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5 SCK、PA7 MOSI 配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA4 CS、PA3 DC、PA2 RES 配置为通用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_1Line_Tx; // 只发送 SPI_InitStructure.SPI_Mode SPI_Mode_Master; // 主模式 SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; // 8 位数据 SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // SCK 空闲低电平 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 第一个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件片选 SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 36MHz / 4 9MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; // 高位在前 SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }这里有一个参数容易被忽略SPI_Direction_1Line_Tx是只发送模式。因为屏幕只需要接收数据不需要读回显存用单线发送模式可以省掉 MISO 引脚的配置。如果你后续想读屏幕的寄存器来验证初始化是否成功就得改成双向模式并把 PA6 也配置好。我在调试初始化序列时通常会临时打开读模式确认驱动 IC 的 ID 寄存器返回的值符合预期然后再改回单线发送。另外注意分频器的计算结果取决于 APB2 时钟。F103 默认 SYSCLK72MHzAPB272MHz所以SPI_BaudRatePrescaler_4实际得到 18MHz。如果系统时钟被改过这里的实际频率也会跟着变。调试时如果屏幕花屏先确认系统时钟是不是真的 72MHz而不是靠猜。3.3 DMA 通道怎么选怎么配置F103 的 DMA1 通道与 SPI1 发送对应的映射关系是SPI1_TX 固定对应 DMA1_Channel3SPI1_RX 对应 DMA1_Channel2。这个映射是硬件写死的改不了。配置 DMA 时把内存地址设为发送缓冲区的地址外设地址设为 SPI1_DR 的地址传输方向是从内存到外设。void DMA1_SPI1_TX_Config(uint8_t *buffer, uint16_t size) { 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)buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 内存 - 外设 DMA_InitStructure.DMA_BufferSize size; 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 时最容易出问题的是DMA_DIR和DMA_M2M两个参数。DMA_DIR_PeripheralDST表示外设是目标数据从内存流向 SPI1_DRDMA_M2M_Disable则告诉 DMA 这是外设参与的数据传输不是内存拷贝。如果这两个参数设错DMA 要么不传输要么把 SPI 外设当成内存去读写表现就是 SPI 发送不可预期屏幕完全没反应。DMA 传输完成后通道 3 的传输完成标志位 TCIF3 会被置位。每次传输前要检查上一个传输是否真的结束了。这里有一个常见的坑DMA 配置好后必须先启动 DMA 再写 SPI1_DR 吗不正确顺序是先启动 SPI再启动 DMA 传输。SPI 内部有发送缓冲只要 SPI 使能且 TXE 为空DMA 就会自动把数据塞进 SPI1_DR。启动 DMA 后数据搬运完全由硬件完成CPU 可以在while(1)里做其他事。4. 显示窗口、画点和全屏刷新DMA 最大价值在这里体现4.1 显示窗口CASET/RASET与坐标体系ST7735S 的显示操作逻辑是先设置窗口再往窗口里灌像素数据。窗口由 CASET列地址、RASET行地址两条命令确定然后 RAMWR 命令开始写入像素。这个机制最大的好处是画一个局部矩形时不用重发全屏数据可以配合 DMA 只更新需要变化的区域。0.99 寸圆屏在 ST7735S 内部存在一个坐标偏移原因很简单圆形玻璃的边缘不是完整的 240x240 像素阵列驱动 IC 实际驱动的有效显示区域在行列上都有偏差。以常见的 0.99 寸圆屏模组为例我实测的偏移通常需要把显示列地址加上 26、行地址加上 1否则画面会整体偏移甚至出现边缘像素错位。void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { // 偏移量根据模组实测调整 x0 26; x1 26; y0 1; y1 1; TFT_Cmd(0x2A); // CASET 列地址 TFT_Data(x0 8); TFT_Data(x0 0xFF); TFT_Data(x1 8); TFT_Data(x1 0xFF); TFT_Cmd(0x2B); // RASET 行地址 TFT_Data(y0 8); TFT_Data(y0 0xFF); TFT_Data(y1 8); TFT_Data(y1 0xFF); TFT_Cmd(0x2C); // RAMWR 准备写入像素 }这个偏移值每家模组厂可能不同不同批次的屏幕也可能微调过。换一批屏幕或者换一家采购渠道后如果发现画面偏移第一时间修这里的偏移量。我见过有人在驱动代码里花几个小时查初始化命令最后发现只是偏移参数没对上。4.2 画点、矩形填充与 DMA 发送有了 SetWindow画一个矩形填充的流程变成了设置窗口、准备颜色缓冲区、用 DMA 把缓冲区发给 SPI。一个 240x240 的矩形缓冲区大小是 115200 字节在 F103 上只能放到外部数组里不能放到栈里。#define TFT_WIDTH 240 #define TFT_HEIGHT 240 static uint16_t fillBuffer[TFT_WIDTH * 2]; // 一次最多填充一行 void TFT_FillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { uint16_t i, j; TFT_SetWindow(x0, y0, x0 w - 1, y0 h - 1); for (i 0; i w; i) { fillBuffer[i] color; // 每行填充同一种颜色 } for (j 0; j h; j) { DMA_ClearFlag(DMA1_FLAG_TC3); // 清空上次传输完成标志 DMA1_Channel3-CNDTR w; // 设置本次传输数量 DMA_Cmd(DMA1_Channel3, ENABLE); // 启动 DMA while (!DMA_GetFlagStatus(DMA1_FLAG_TC3)); // 等这一行传完 } }这段代码的思路是因为填充的是纯色矩形每一行像素内容相同我先构造一行的颜色数据然后逐行 DMA 发送。这样缓冲区只需要一行大小RAM 占用从 112KB 降到不到 1KB。如果填充的是渐变或者图片就必须准备一块覆盖整个矩形区域的缓冲区。CNDTR是 DMA 的剩余传输计数字段注意这里写入的是传输像素字节数还是行宽。在这个例子里w是像素个数而 DMA 的传输单位是字节且颜色是 16 位 RGB565所以按理说CNDTR w * 2才对。这是很多初学者最容易迷惑的地方。DMA1 通道 3 绑定 SPI1 的 8 位外设数据寄存器传输单位若配置为DMA_PeripheralDataSize_Byte那么一个像素的两个字节会被分成两次传输缓冲区里每个像素其实占两个uint8_t。上面代码为了简化直接传了uint16_t实际工程里应把w * 2作为传输数量或者把数据长度设置成按字节计算。这个细节直接决定了屏幕显示出来的是正常颜色还是颜色错位的乱码。4.3 全屏刷新频率怎么估算局部刷新多少帧刷一块全屏数据量是 240x240x2 115200 字节。SPI 时钟按 18MHz 算理论每秒可传 2.25MB刷一屏需要 0.051 秒约 19.5 帧/秒。这个帧率做静态界面绰绰有余做表盘秒针动画也够用但如果做全屏平滑过渡就会明显感觉卡。加上命令开销、循环等待实际稳定帧率大约 15 帧/秒这是硬件 SPI 在这个频率下的正常表现。想提高帧率有两个方向一是把 SPI 时钟提到 36MHz全屏刷新数据量减半帧率翻倍二是只做局部刷新。表盘动画场景里秒针的移动范围通常只有一个小矩形比如 40x40 像素刷新这个区域只需要 3200 字节在 18MHz 下耗时不到 2ms100 帧都跑得出来。所以实际项目里基本不会无脑全屏刷新而是把变化区域算出来只刷那一块。局部刷新的代价是需要维护一份“屏幕当前内容”的映射或者在绘制函数里精确记录脏矩形坐标。项目中我维护了一个dirtyRect结构体每次画点、画线都会更新它然后在下一次主循环里只刷新这个区域。不要小看这个设计它把 CPU 占用从“刷新全屏期间 SPI 持续占用 DMA”降到了“刷新几十字节瞬间完成”对同时跑传感器采样的场景意义很大。5. 实战排查与避坑记录白屏、花屏、DMA 卡死的五个高发问题5.1 现象屏幕全白背光亮但没有任何内容屏幕全白是最常见的故障现象。原因通常集中在三个地方RES 复位脉冲宽度不够、初始化命令序列没有正确发送、SPI 时钟极性错误。我先查复位时序因为它最容易用肉眼确认看 RES 引脚是否在初始化时被拉低过 50ms 以上。解决方法是按上面TFT_Reset()的时序重跑一遍并把延时长一些。如果复位正常仍然是白屏就在初始化命令的前后通过调试串口打印关键标志或者在 DC 引脚上接逻辑分析仪验证发送出去的是命令还是数据。我遇到过一种情况初始化命令里第一个字节是 0x01SWRESFT 软复位但这条命令被 DC 引脚的高电平当成数据处理了屏幕自然白屏。DC 切换时序不对是白屏的高频原因排查时务必优先看 DC 引脚。5.2 现象画面显示出来但整体偏移右侧有缺失显示内容偏移通常出现在刚换屏幕模组之后。之前提到 0.99 寸圆屏存在 CASET/RASET 偏移不同模组厂商对偏移的定义不一样有的是 X 方向偏移 26 像素、Y 方向偏移 1 像素有的正好相反。解决方法是先用纯色全屏刷一遍观察露出的白边在哪一侧然后反向调整TFT_SetWindow()里的偏移量。这个调试过程很折磨刷 0xFFFF 看右边露、刷 0x0000 看左边露反复调几次才能定位。我给自己的习惯是准备一个调偏移的调试函数编译一次就可以通过串口指令修改偏移值不用每次烧录。5.3 现象花屏、颜色错乱像是每隔一个像素被吞掉花屏的常见原因是 SPI 速率过高。杜邦线连接时18MHz 信号在长线上会产生明显的过冲和反射导致 SDA 上的数据在采样点不稳定。另一种情况是 CPOL/CPHA 配置错误ST7735S 要求模式 0如果配成了模式 3数据在错误的边沿被采样就会出现规则的花屏纹理。解决办法是先把分频系数改到 8 或 16确认问题消失再逐步提升频率找到稳定上限。同时检查 SCK 和 SDA 是否靠得太近数据线不要和电源线绞在一起。换成杜邦线时高频 SPI 下最好剪短到 10cm 以内。5.4 现象DMA 只工作一次第二次刷新时屏幕完全不动DMA 第一次传输成功、第二次失效是典型的状态标志问题。DMA 控制器传输完成后 TCIF 标志是硬件置位的如果不手动清除下一次启动 DMA 会直接失败或者传输计数没有重新装载。解决方法是每次启动 DMA 前先调用DMA_ClearFlag(DMA1_FLAG_TC3)清除标志然后再设置CNDTR。还有一个容易被忽视的细节SPI1 的 BSY 标志也必须确认清除。当 DMA 搬完最后一个字节SPI 移位寄存器可能还没把数据完全发出去如果立刻再次启动 DMA 发送数据会互相覆盖。我一般会在启动前和发送后都检查SPI_GetFlagStatus(SPI1, SPI_FLAG_BSY)。这两步是从实际项目里磨出来的血泪经验按顺序执行能避免绝大多数 DMA 卡死问题。5.5 现象刷新时出现横条纹像是画面在“撕裂”横条纹通常是背光 PWM 频率太低和屏幕刷新不同步造成的。BLK 引脚如果直接用低频 PWM 调光比如 100Hz 以下人眼能感知到亮度波动而面板的刷新频率又与 SPI 传输节奏不一致就会叠加出滚动的横条纹。另一种情况是DMA 传输期间 SPI 时钟被其他中断频繁打断导致屏幕上出现多条亮度不均匀的“扫描线”。解决方法是把背光 PWM 频率提高到 1kHz 以上TIM2 的 ARR 设得小一些同时把 DMA 中断优先级调高避免 SPI 传输中途被抢占。如果板上同时跑着其他外设中断尽量让 DMA 优先级高于普通外设中断。这个坑比较容易忽略我最初排查了半天 SPI 参数最后才发现是 PWM 频率只有 200Hz 的问题。6. 进阶玩法圆形裁剪、局部动画与满负荷验证圆屏和方屏最本质的区别在于方屏显示内容可以直接铺满圆屏则需要在渲染阶段把“超出圆形的像素”裁掉。0.99 寸圆屏虽然在驱动 IC 层面是 240x240 的方阵但玻璃只露出中央一个圆形区域。如果你直接往四角画内容角的像素实际上显示不出来但会因为窗口正好在四角而浪费刷新字节数。圆屏的正确渲染思路是在绘制函数里加一个圆形范围判断。#define CENTER_X 120 #define CENTER_Y 120 #define RADIUS 118 int IsInsideCircle(uint16_t x, uint16_t y) { int dx x - CENTER_X; int dy y - CENTER_Y; return (dx * dx dy * dy) (RADIUS * RADIUS); }渲染表盘时每次画像素前先判断这个点是否落在圆内落在圆外直接跳过。这个判断会消耗一定的 CPU 时间因此在实际项目里我一般只对“首次全量绘制”做圆内裁剪动画阶段则是先计算脏矩形再对脏矩形内的每个像素逐个判断。圆内裁剪的积分运算量不小但脏矩形通常只有几十像素宽开销完全可接受。如果你对计算速度不满意还可以用查表法把每个扫描行在圆内的起止 x 坐标提前算好运行时直接用这个区间表来做裁剪效率比逐像素平方判断高很多。做完基础显示功能后我建议做一个满负荷验证在主循环里不停刷新全屏渐变图案同时用逻辑分析仪抓取 SCK 和 SDA。观察几个参数SCK 空闲电平是否稳定为低、数据采样时刻是否在稳定区域、CS 片选是否只在发送期间拉低。逻辑分析仪能看到硬件时序的真相。如果你手头只有示波器就测 SCK 的最高频率是否和代码里配置的分频一致。满负荷验证通过后才能说这个屏幕驱动方案真正“稳了”。这个项目的工程包里带了完整的初始化序列、驱动代码和一个表盘 Demo。下载后第一步直接编译烧录看效果第二步就是去改TFT_SetWindow()里的偏移量改成适合你手上模组的数值第三步再琢磨局部刷新和圆形裁剪。我从这个项目里学到最深的一课屏幕类外设的“玄学”问题十有八九是时序、电平、偏移这三类低级问题而不是芯片坏了。从那以后我每次换新屏幕都会先花 10 分钟确认 MADCTL 方向和窗口偏移再谈后续优化。这 10 分钟能省掉后面好几小时的排查时间。希望这篇文章能帮你在点圆屏这件事上少走几步弯路。本文还有配套的精品资源点击获取