简介:一份面向嵌入式开发者与电子爱好者的ST7789液晶驱动源码包,基于STM32F103微控制器,适用于小尺寸彩色TFT屏幕的快速接入与显示控制,可移植到各类物联网终端、手持设备或学习项目中。压缩包共4个文件,以ST7789.c与ST7789.h源文件为核心,涵盖SPI接口配置、初始化命令序列、显示区域设置、颜色写入等基础驱动功能;同时包括对应的备份文件,便于开发时对照与恢复,整体仅1KB,代码量精简、结构清晰,适合直接阅读与二次开发。已有365人学习浏览,尤其适合正在学习STM32外设驱动、或希望低成本为项目增加屏幕显示的开发者参考。通过这份源码,可以快速理解驱动芯片与MCU之间的通信流程,减少翻阅数据手册和反复调试底层寄存器的时间,并可直接将关键函数移植到自身工程中。
1. 一块 240x240 IPS 屏和 STM32F103 之间,隔着哪些东西
拿一块 240x240 的 IPS 彩屏配一块 STM32F103 最小系统板,很多人第一周的时间都耗在让屏幕亮起来这件事上。ST7789 是小尺寸 TFT 模组里出现频率最高的驱动 IC,1.3 寸、1.54 寸、2.0 寸屏幕大多用它;四根 SPI 线加 DC、RST 就能跑起来。st7789.zip 里放的就是一套 ST7789.c / ST7789.h 驱动源码,还带着两个 .bak 备份文件,说明是有人在真机上反复调过的版本,比直接啃英文参考手册省事得多。
源码已经把初始化序列、画点、区域填充、清屏这类底层操作封装好了,你要补的只是 STM32F103 这一侧的 SPI 外设配置,以及想清楚大块像素数据是走 DMA 还是靠 CPU 逐字节阻塞发送。按 9MHz SPI 时钟估算,刷新一帧 240x240 RGB565 约 115KB 数据,纯阻塞方式耗时接近 130ms,做静态菜单勉强够用,做动画会明显卡顿。所以这篇文章不满足于把屏幕点亮,而是把 SPI 时序参数、初始化命令含义、DMA 传输和局部刷新这些最终决定显示效果的环节逐个拆开。
2. STM32F103 SPI1 与 ST7789 的接线和时序控制
2.1 引脚映射:DC 和 RST 选 GPIO 而不是 SPI 复用
先看一张最小系统下最常用的接线表。ST7789 模组引脚命名各厂略有出入,但功能对得上:
| 信号 | 模组引脚 | STM32F103 建议引脚 | 方向 | 说明 |
|---|---|---|---|---|
| SCK | SCL / SCLK | PA5(SPI1_SCK) | 输出 | SPI1 外设控制 |
| MOSI | SDA | PA7(SPI1_MOSI) | 输出 | 三线制,无 MISO 回读 |
| CS | CS | PB12(任意 GPIO) | 输出 | 低电平有效 |
| DC / A0 | DC | PB13(任意 GPIO) | 输出 | 0 发命令,1 发数据 |
| RST | RES / RST | PB14(任意 GPIO) | 输出 | 低脉冲复位 |
| BLK | BLK / LED | 3.3V 或定时器 PWM 脚 | 输出 | 背光控制 |
三个细节先说在前面。第一,SPI1 在 APB2 总线上,时钟源是 72MHz,SPI2 挂在 APB1 上只有 36MHz,所以追求刷屏速度优先选 SPI1。第二,ST7789 只用了 SPI 的单向写通道,MISO 没人接,PA6(SPI1_MISO)可以省下来做触摸或其他功能。第三,CS 不要图省事直接接地,虽然单设备场景下能工作,但后续做 DMA 批量传输、中途插命令时,没有 CS 控制会很难看。
DC 必须接普通 GPIO 而不是 SPI 复用脚,因为它要在每个字节之间切换高低电平:发命令字节时拉低,发数据字节时拉高。常见错误是把 DC 接到某个串口或 I2C 的复用引脚上,初始化序列里第一个 0x36 就被误判成数据,屏幕整个花掉。用宏把引脚操作集中起来,后续换板子只需要改这一处:
#define ST7789_CS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define ST7789_CS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define ST7789_DC_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define ST7789_DC_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_13) #define ST7789_RST_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_14) #define ST7789_RST_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_14)宏定义的好处是编译器在调用点直接展开,没有函数调用开销。注意这些引脚在进入驱动初始化之前必须配置成推挽输出,我在 stm32f10x_gpio.c 里写了一组GPIO_Mode_Out_PP的初始化函数,时钟由 RCC_APB2PeriphClockCmd 使能 GPIOB。别漏了 RCC 时钟,GPIOB 没开时钟时所有写操作都静默失效,这是仿真器看不出来的问题。
2.2 SPI 模式 0 / 模式 3:CPOL 和 CPHA 怎么选
ST7789 的 SPI 接口从参考手册上的时序图看,兼容模式 0 和模式 3。大多数现成驱动(包括 Adafruit 系和很多国产模组资料)默认用模式 0,也就是SPI_CPOL_Low配SPI_CPHA_1Edge,数据在 SCK 上升沿被采样。如果参考代码跑不通,先检查是不是平台间的SPI_MODE宏定义差异——同一套 ST7789.c 从 Arduino 移植到 F103 标准外设库,最容易栽在这一处。
基于 stm32f10x 标准外设库 v3.5.0 的配置代码:
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // SCK 和 MOSI 复用推挽,速度拉到 50MHz 保证翻转率 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); 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_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }逐个参数说清楚。SPI_Direction_2Lines_FullDuplex是主模式最通用的配置,虽然 ST7789 不接 MISO,但 F103 的 SPI 硬件必须选双线全双工,否则发送路径会被 RX 逻辑拖慢。SPI_NSS_Soft表示 NSS 引脚不用硬件管理,由 GPIO 手动拉 CS,这样多屏共用一条 SPI 总线时更灵活。SPI_BaudRatePrescaler_8在 APB2 72MHz 下得到 9MHz 的 SCK 频率,模组走线 5cm 以内都能稳定工作;把分频改成SPI_BaudRatePrescaler_4能上到 18MHz,接近 ST7789 的极限,但长排线和高容性负载会把波形弄成圆顶,反而容易丢位。
这里有个常见误用:把 GPIO_Speed_50MHz 理解成 SPI 时钟的倍频关系。GPIO 速度指的是 IO 驱动强度,与 SPI 波特率分频是两个独立配置项。GPIO_Speed_2MHz 配 SPI 9MHz 也能跑,但边沿变缓,信号完整性差。稳妥的做法是一律配 50MHz。
2.3 逻辑分析仪验证时序再写应用层
代码写完先别急着刷动画,用逻辑分析仪挂 SCK、MOSI、DC、CS 四个通道,把初始化过程抓下来看三件事:复位低脉冲是否大于 1ms;SLPOUT 之后的延时是否足够;DC 翻转是否发生在 CS 低电平期间,而不是字节发送中间。如果 DC 在 SCK 边沿附近抖动,ST7789 会把数据误判为命令。
我一般会写一个只发0x00的测试循环,把逻辑分析仪触发条件设成 CS 下降沿,抓 16 个字节确认协议骨架。没有逻辑分析仪也可以用一个 GPIO 翻转 + 示波器两通道替代,但四个信号同时观察效率高得多。这一步做好,后面排错大概率不会再回到硬件层。
3. ST7789 初始化序列拆解:寄存器与显存窗口
3.1 初始化命令流程与 .bak 备份的对照意义
打开 st7789.zip 的目录:顶层ST7789.c是主驱动,ST7789.h是头文件,另外还有ST7789.c.bak和ST7789.h.bak。这两个 .bak 本质上是修改前的还原点,也就是作者在某一次调参失败之后留的备份。如果你拿到的 .c 编译不过或者屏幕点不亮,用 diff 工具对比主文件和 .bak,能直接看到寄存器参数改动过哪些地方,这些改动往往是针对某款具体模组的兼容性修正。
初始化序列是驱动里最敏感的部分。以常见 ST7789V 模组为例,完整的初始化流程包含退出睡眠、设置扫描方向、设置颜色格式、调节电压和打开显示。下面这段是 st7789.zip 里最常见结构的简化版:
void ST7789_Init(void) { ST7789_RST_LOW(); delay_ms(20); ST7789_RST_HIGH(); delay_ms(120); // 上电后到 SLPOUT 之间至少等 5ms ST7789_WriteCommand(0x11); // SLPOUT,退出睡眠 delay_ms(120); ST7789_WriteCommand(0x36); // MADCTL:扫描方向和颜色序 ST7789_WriteData(0x00); ST7789_WriteCommand(0x3A); // COLMOD:像素格式 ST7789_WriteData(0x05); // 16bit RGB565 ST7789_WriteCommand(0xB2); // PORCTRL:帧率相关 ST7789_WriteData(0x0C); ST7789_WriteData(0x0C); ST7789_WriteData(0x00); ST7789_WriteData(0x33); ST7789_WriteData(0x33); ST7789_WriteCommand(0xC5); // VCOM 电压微调 ST7789_WriteData(0x35); ST7789_WriteCommand(0x21); // INVON:打开反色 ST7789_WriteCommand(0x29); // DISPON:点亮显示 }这里的坑几乎全在延时上。RST 低电平保持时间至少要 10us,上电后到 SLPOUT 必须留出稳定时间,太多移植失败不是命令写错,而是延时被裁得太狠,芯片还没完成内部振荡器起振就被发了后续命令。delay_ms(120)在这套代码里是一个基于 SysTick 的阻塞延时,如果你改用了实际项目里的非阻塞延时框架,要防止初始化期间的 DMA 或定时器中断把流程打断。
命令与数据的发送落到底层就是 SPI 字节写操作。DC 的翻转时机必须是先置低,再拉低 CS,最后写字节:
void ST7789_WriteCommand(uint8_t cmd) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等发送缓冲空 ST7789_DC_LOW(); ST7789_CS_LOW(); SPI_I2S_SendData(SPI1, cmd); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); // 等发送完成 ST7789_CS_HIGH(); ST7789_DC_HIGH(); }SPI_I2S_FLAG_TXE表示发送数据寄存器已空,SPI_I2S_FLAG_BSY表示 SPI 外设忙。顺序上先查 TXE 再拉低 DC 和 CS,能保证进入写入时总线已空闲。如果 DC 信号放在 CS 之后翻转,第一个字节的起始位会被 ST7789 错当命令处理,初始化直接乱掉。
3.2 MADCTL 和 COLMOD:方向、镜像与颜色格式
MADCTL(0x36)是显示方向控制寄存器,一个字节里同时管了行扫描方向、列扫描方向和 RGB/BGR 颜色顺序。位定义在 ST7789 中文参考手册的表里写得很直白,但实际调的时候最容易翻车的是主控坐标系与 GRAM 坐标系的映射。常用值的意义:
| MADCTL 值 | 效果 | 适用场景 |
|---|---|---|
| 0x00 | 竖屏,原点左上,RGB 顺序 | 默认竖屏 |
| 0x60 | 旋转 90 度,横屏 | 横向显示 |
| 0xC0 | 旋转 180 度,倒屏 | 模组安装方向反过来 |
| 0xA0 | 旋转 270 度 | 另一侧横屏 |
| 0x08 | RGB 与 BGR 互换 | 红色和蓝色对调时 |
镜像问题的判断方法:如果初始化后文字左右反,说明 MY 位反了;如果颜色通道对调,则是 RGB/BGR 位反了。很多人喜欢把 MADCTL 写死在代码里,但如果你的产品有横竖屏切换需求,这个寄存器必须做成可配置项,切换时还要连带修改显存窗口的起止坐标。
COLMOD(0x3A)决定像素格式。三个常见值:
| COLMOD 值 | 格式 | 每像素字节 | 说明 |
|---|---|---|---|
| 0x05 | RGB565 | 2 | 默认选择,SPI 负载最小 |
| 0x06 | RGB666 | 3 | 色彩过渡细腻,传输量多 50% |
| 0x07 | RGB888 | 4 | 极少用,SPI 带宽完全不够 |
RGB565 里每个像素 16 位,高 5 位红、中间 6 位绿、低 5 位蓝。如果你是直接从 PC 端图片转数组,常见的转码工具默认输出大端格式,也就是说数组里先放高位字节再放低位字节。ST7789 的 RAMWR 写入顺序也是高字节在前,这一点要和后面ST7789_WriteData(color >> 8)的写法对应,否则颜色错位成噪点。
3.3 CASET/RASET:把写入窗口精确到像素
ST7789 把显存划分成一个 240x320 的 GRAM 矩阵,CASET 和 RASET 两个命令划出你真正要写入的矩形区域,之后 RAMWR 命令后面跟着的数据就按这个窗口自动换行填充。代码实现:
void ST7789_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ST7789_WriteCommand(0x2A); // CASET:列地址 ST7789_WriteData(x0 >> 8); ST7789_WriteData(x0 & 0xFF); ST7789_WriteData(x1 >> 8); ST7789_WriteData(x1 & 0xFF); ST7789_WriteCommand(0x2B); // RASET:行地址 ST7789_WriteData(y0 >> 8); ST7789_WriteData(y0 & 0xFF); ST7789_WriteData(y1 >> 8); ST7789_WriteData(y1 & 0xFF); ST7789_WriteCommand(0x2C); // RAMWR:开始写像素 }CASET 的参数是列的起止地址,RASET 是行的起止地址,都是 16 位参数,高字节在前。240x240 的模组内部 GRAM 虽然是 240x320,但出厂时可见区域已经把多余的部分遮掉,所以 CASET 从 0 到 239、RASET 从 0 到 239 就可以。如果你买的是 135x240 这类异形屏,就必须按模组资料在 CASET/RASET 上加偏移量,否则完整画面会被裁掉一块,错位症状通常是图像底部跑到顶部去。
基于窗口的填充函数可以复用上面的设置:
void ST7789_FillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { uint32_t npixels = (uint32_t)w * h; ST7789_SetWindow(x0, y0, x0 + w - 1, y0 + h - 1); while (npixels--) { ST7789_WriteData(color >> 8); ST7789_WriteData(color & 0xFF); } }npixels用 uint32_t 是因为 240x240 的整屏填充是 57600 次迭代,uint16_t 虽然够用,但乘法表达式里 w 和 h 都被提升为 int,转发给局部变量容易在优化级别开高后产生符号位告警,直接写 uint32_t 省心。填充颜色分两次发,是高字节先行的时序约束,不能合并成一个 16 位一次性发送,除非你写的是SPI_DataSize_16b的模式。
4. 显示数据通路:DMA 传输与局部刷新方案
4.1 F103 的内存约束下怎么设计缓冲
STM32F103C8T6 只有 20KB SRAM,一块 240x240 全屏 RGB565 帧缓冲就要 115200 字节,想放全帧根本不可能。所以在 F103 上做图形成像,核心问题不是画得快,而是缓冲怎么分。
| 缓冲方案 | 内存开销 | 传输方式 | 适用场景 |
|---|---|---|---|
| 全帧缓冲 | 115.2KB | 一次 DMA 传到底 | 需要 32KB+ RAM 的型号,F103 放不下 |
| 行缓冲 | 480B | 每行开一次窗口 | 内存极紧,但每行都要切换窗口,慢 |
| 块缓冲 | 8KB~16KB | 按矩形块 DMA | F103 上的折中方案 |
我一般在 F103C8 上做一个 64x64 的块缓冲,占 8KB SRAM,剩余内存留给协议栈和 UI 状态。这个尺寸的块做按钮、弹窗和滚动列表时足够覆盖高频率变化的区域,静态背景走一次全屏 DMA 之后就不用再动。块缓冲的定义放在栈外,用 static 声明,避免栈溢出。代码结构上这样组织:
static uint8_t lcd_block_buf[64 * 64 * 2]; void LCD_DrawBlock(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h) { // 从图形引擎生成像素到 lcd_block_buf RenderBlock(x0, y0, w, h, (uint16_t *)lcd_block_buf, 64); // 再把块写到屏幕 ST7789_SetWindow(x0, y0, x0 + w - 1, y0 + h - 1); ST7789_DMA_Send(lcd_block_buf, w * h * 2); }注意lcd_block_buf的宽度固定为 64,而不是用动态的 w。固定宽度能让编译器把地址计算简化成移位和加法,省掉乘法指令。块宽小于缓冲宽度时只有前面一部分被 DMA 发送,这没有关系,DMA 的len参数精确控制发送字节数。清屏函数不需要单独做全屏循环,直接设一次大窗口,不断发同样的颜色数据即可。
4.2 SPI DMA 发送实现与性能实测
STM32F103 的 SPI1_TX 请求映射在 DMA1 通道 3。用标准外设库初始化 DMA 的完整代码:
void ST7789_DMA_Send(uint8_t *buf, uint32_t len) { DMA_InitTypeDef DMA_InitStructure; // 等上一次 DMA 传输完成标志位 while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET) {} DMA_ClearFlag(DMA1_FLAG_TC1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(SPI1->DR)); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = len; 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_VeryHigh; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); DMA_Cmd(DMA1_Channel3, ENABLE); }先说标志位的处理。DMA1_FLAG_TC1 是通道 3 传输完成标志,发送前必须等它置位并清除,否则上一次传输还没结束就重新配置 DMA,会让这次的数据覆盖掉上一个 buffer 的残余状态。DMA_Mode_Normal是单次模式,待传输字节数等于DMA_BufferSize;别用DMA_Mode_Circular,那会无限循环发送同一块内存。
性能上做个实测估算。SPI 时钟 9MHz 时每秒传 1.125MB,115200 字节的一帧理论耗时 102ms,加上 CASET/RASET 窗口切换和 DMA 启动开销,实际约 110ms。不开 DMA 的阻塞发送会再慢 20ms 到 30ms,因为每发一个字节都要轮询TXE标志,CPU 白白等待。DMA 另一个价值是释放 CPU:传输期间主循环可以继续画下一帧的矢量图形或处理串口命令。你如果开了SPI_BaudRatePrescaler_4到 18MHz,理论帧时间降到 51ms,但要注意排线长度对波形的劣化,降不下来的话就维持 9MHz。
DMA 传输还有一个生命周期问题:buf在 DMA 启动后不能被立即改写,函数返回时 DMA 可能还在传。常见的做法是传输前等待 TC 标志,或者用 DMA 传输完成中断来通知外部:
void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); lcd_dma_busy = 0; // 允许外部改写缓冲 } }lcd_dma_busy做成全局标志或者用信号量,UI 层在往lcd_block_buf写数据前先检查它。这比盲目延时可靠,性能也好。
4.3 局部刷新与 TE 同步,避免画面撕裂
ST7789 在连续大窗口刷新时,如果恰好写到一半,GRAM 正在从顶部扫描显示,就会产生撕裂感——上一帧的下半屏和这一帧的上半屏叠在一起。支持 TE 引脚的模组把帧同步信号拉出来,这个信号在每个显示扫描周期开始前产生一个脉冲。配合它做局部更新,可以保证窗口数据只在帧间隙写入。
void ST7789_UpdateRegion(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t *pixels) { // PA3 接 TE 脚,等一个高电平脉冲再写窗口 while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) == RESET); ST7789_SetWindow(x0, y0, x0 + w - 1, y0 + h - 1); ST7789_DMA_Send((uint8_t *)pixels, w * h * 2); }TE 引脚的采样电路要注意:如果模组的 TE 输出是开漏,还要在主控侧加外部上拉电阻;如果是推挽输出就不需要。全屏刷新时把 x0/y0 设成 0、w/h 设成屏幕分辨率,代码同样成立。如果模组没引出 TE 引脚,退而求其次的做法是用一个定时器中断模拟固定帧周期,但效果不如硬件同步精确。
5. 硬件滚动、PWM 调光和 FreeRTOS 集成时的三个移植点
5.1 用 VSCRDEF 实现文本日志的无像素滚动
ST7789 内置垂直滚动功能,不需要重写 GRAM 就能让一块显示区域向上滚动。控制它的命令是 0x33(VSCRDEF)和 0x37(VSCRSAD)。VSCRDEF 一次传入 6 个字节,分别是顶部固定区 TFA、滚动区 VSA、底部固定区 BFA 三个 12 位参数的高字节和低字节。假设屏幕 240x240,想让从第 40 行开始的 160 行区域滚动,顶部固定 40 行、底部固定 40 行,参数就是 0x00 0x28、0x00 0xA0、0x00 0x28。
void ST7789_EnableScroll(uint16_t bfa, uint16_t vsa, uint16_t tfa) { ST7789_WriteCommand(0x33); ST7789_WriteData(bfa >> 8); ST7789_WriteData(bfa & 0xFF); ST7789_WriteData(vsa >> 8); ST7789_WriteData(vsa & 0xFF); ST7789_WriteData(tfa >> 8); ST7789_WriteData(tfa & 0xFF); ST7789_WriteCommand(0x37); // VSCRSAD:滚动起始地址 ST7789_WriteData(0x00); ST7789_WriteData(0x00); }这条路径省了 CPU 也省了 SPI 带宽,做传感器日志或者跑马灯提示信息时很实用。要恢复静态显示,再写一次 VSCRSAD 把起始地址设为 0,然后补一条 0x13(NORON)正常显示命令。注意滚动只改变显示读出的起点,不改 GRAM 内容。
5.2 PWM 背光调光:占比与刷新率
BLK 引脚接一个定时器通道而不是固定高电平,亮度就能软件可调。用 TIM2 输出 PWM 的初始化参数:
TIM2_PWM_Init(999, 71); // 72MHz / (72 * 1000) = 1kHz TIM_SetCompare1(TIM2, 800); // 占空比 80%PWM 频率至少要 1kHz,低于这个值人眼能察觉频闪,暗光环境更明显。占空比从 0 到 999 线性变化,800 就是 80% 亮度。不要从 0 直接跳到 100%,加一点渐变逻辑,视觉体验差别很大。
5.3 FreeRTOS 下给 SPI 外设加互斥锁
工程里同时有多个任务往屏幕写内容时,SPI 总线必须变成关键资源。最简单有效的做法是在 st7789 驱动里加一把互斥信号量,每个写屏幕的入口函数先拿锁再操作:
static SemaphoreHandle_t spi_mutex; void ST7789_Lock(void) { xSemaphoreTake(spi_mutex, portMAX_DELAY); } void ST7789_Unlock(void) { xSemaphoreGive(spi_mutex); }初始化驱动前用spi_mutex = xSemaphoreCreateMutex()建好信号量。调度策略上,锁的粒度要小,只在单条命令或一次 DMA 发送期间持锁,不要让刷新全屏这种长操作一直占着锁,否则其他任务会被长期阻塞。更细的做法是给 DMA 传输加一个 xSemaphoreGive 放在传输完成中断里,任务侧用带超时的 take,这样既保证互斥,又避免死锁。最后一招:如果两个任务同时写同一块区域,与其锁来锁去,不如在逻辑层就定好分区,各画各的窗口,省掉一半调度开销。
本文还有配套的精品资源,点击获取