STM32F103RCT6驱动1.44寸TFT(ST7735)完整教程与避坑指南 简介一份基于STM32标准外设库的1.44英寸TFT屏幕显示完整工程面向嵌入式入门与进阶开发者解决STM32F103RCT6与SPI接口彩色液晶屏的驱动与显示实现问题。压缩包共86个文件以C/H源码为主38个h、32个c另有Keil工程文件、启动文件、编译脚本、hex固件与说明文档等整体仅289KB结构轻量清晰。已有4052人在线学习浏览。工程可直接用Keil MDK打开编译也可将生成的hex固件直接烧录验证效果代码分层清晰LCD驱动、字库、GUI绘制、LED控制与延时模块各自独立通过GUI.c、LCD_Config.h、TFT_demo.c等文件可完整跟踪屏幕初始化序列、SPI通信时序与像素数据组织方式。readme和JLink配置为环境搭建与调试提供了参考便于移植到其他STM32型号或扩展为多级菜单、仪表盘等图形界面应用。1. 把 1.44TFT 当窗口STM32F103RCT6 点亮屏幕前必须知道的三件事STM32F103RCT6 配 1.44TFT 屏幕几乎每个做小项目的人都想过这么干。屏体便宜、接口只占 SPI、分辨率 128x160足够显示菜单和实时数据但真正上手那一刻才知道这块小屏不是插电就能亮的“黑匣子”。电源噪声、初始化时序、SPI 速率、引脚复用任何一环出错结果就是白屏、花屏或者颜色不对。这篇文章把 STM32 1.44TFT 驱动的完整链路拆开讲从选型、接线到初始化序列和绘图代码再把调试中真实踩过的坑列成排查记录。适合正在移植屏库的人也适合打算把这块屏塞进自己设备的学生和从业者。资源包里的驱动代码可以直接放到工程里跑省去从零翻手册的时间。2. 选型与接线如何让 1.44TFT 在 STM32F103RCT6 上先通电再说2.1 先认识屏体ST7735 驱动被焊在模块上但不同版本有区别市面上的 1.44 寸 TFT 模块显示分辨率为 128x160像素格式以 RGB565 最常用。绝大多数模块的驱动 IC 是 ST7735R 或 ST7735S个别用 ILI9163 兼容方案。ST7735S 与 ST7735R 的寄存器基础命令一致但伽马参数和某些微调寄存器推荐值不一样。最让人头疼的是同一家淘宝铺子不同批次的模块可能会因为玻璃切割方向和驱动 IC 批次不同导致同一份初始化代码在另一块屏上出现镜像、偏色或者坐标偏移。所以拿到的第一件事不是复制代码而是确认屏体型号。识别驱动 IC 有两条路。一条是看模块背面丝印通常写 ST7735S、ST7735R 或兼容型号另一条是通过 SPI 读 0x04 寄存器ST7735 系列会返回类似 0x101 或 0x102 的 ID。不过大部分模块只引出 MOSI、SCK、CS、DC、RST 和 BLKMISO 可能被直接断开所以读 ID 不是总能成功。我一般先做物理确认模块背面印“ST7735S”就按 ST7735S 初始化印着兼容型号就按 ST7735R 流程试跑跑不通再逐条微调。模块引脚也不统一常见 8 脚排针定义是 VCC、GND、SCL、SDA、RES、DC、CS、BLK其中 SDA 是串行数据输入SCL 是时钟。个别人把 SDA 写成 MOSISCL 写成 SCK看丝印不要只看名字看实际网络走线。还有少数模块把 CS 和 DC 合并成一根数据/命令选择脚那种屏输出接口不同需要用带锁存引脚的特殊时序不在本文讨论范围。2.2 引脚分配SPI 总线、复位和背光怎么接这步是返工率最高的地方。有人把所有控制引脚一股脑塞到 PB 口结果发现和 SWD 调试口冲突代码下载不了有人把 CS 接在硬件 NSS 上SPI 主模式下 NSS 可以软件管理但初始化和手动拉低时机不对屏幕收到的第一段命令就丢了一半。我习惯的分法是SPI1 固定用 PA5 和 PA7SCK 和 MOSICS、DC、RST 用 PB12、PB11、PB10背光用 PB0这个引脚同时是 TIM3_CH3后续可以切 PWM 调亮度。#define TFT_SCK_PIN GPIO_Pin_5 // PA5 对应 SPI1_SCK #define TFT_MOSI_PIN GPIO_Pin_7 // PA7 对应 SPI1_MOSI #define TFT_CS_PIN GPIO_Pin_12 // PB12 片选低电平有效 #define TFT_DC_PIN GPIO_Pin_11 // PB110 为命令1 为数据 #define TFT_RST_PIN GPIO_Pin_10 // PB10 硬件复位 #define TFT_BLK_PIN GPIO_Pin_0 // PB0 背光控制逻辑说明PA5/PA7 是 SPI1 外设专用引脚复用成 AF 输出后由 SPI 模块控制不额外占用 GPIO 翻转开销PB12/11/10 全是普通 IO可以在初始化时精确控制片选和复位时序比交给硬件 NSS 灵活。PB0 接背光最重要的原因是后续要 PWM 调光普通 GPIO 只能做全亮或全灭定时器通道则可以做到 0~100% 亮度调节。参数说明F103RCT6 的 GPIO 默认最高翻转频率可以达到 50MHzSPI 波特率最高可以到 18MHz但 ST7735 手册建议的 SPI 写入时钟上限一般也是 15MHz 左右所以后续硬件 SPI 分频时最好先设在 9MHz 或 18MHz跑不稳再降一档不要上来就压满。GPIO 初始化部分把 SCK 和 MOSI 配置成复用推挽其余控制脚配置为普通推挽输出。注意上电顺序先把 CS、RST 拉高再把 DC 设置为命令模式然后才去跑初始化命令。有的代码一开始就把 CS 拉低这时 MCU 还没开始输出时钟屏幕内部的逻辑可能收到上电瞬间的噪声跳变进入未知状态。void TFT_GPIO_Config(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Pin TFT_SCK_PIN | TFT_MOSI_PIN; GPIO_Init(GPIOA, gpio); gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Pin TFT_CS_PIN | TFT_DC_PIN | TFT_RST_PIN; GPIO_Init(GPIOB, gpio); gpio.GPIO_Pin TFT_BLK_PIN; GPIO_Init(GPIOB, gpio); GPIO_SetBits(GPIOB, TFT_RST_PIN); // 复位拉高 GPIO_SetBits(GPIOB, TFT_CS_PIN); // 片选释放 GPIO_SetBits(GPIOB, TFT_BLK_PIN); // 背光打开 }逻辑说明PA5/PA7 开复用推挽PB 控制脚开普通推挽目的是让 SPI 外设能直接驱动时钟和数据线同时保证 RST/CS 在启动时处于稳定高电平。BLK 也在这里拉高避免屏幕背光处于低电平关闭状态。注意这个函数里没有配置 SPI 外设本身只配置 GPIO所以之后还要单独调 SPI1 初始化。参数说明GPIO_Speed_50MHz 是为了让 SCK 和 MOSI 在高速 SPI 传输时保持足够短的反应时间如果只跑软件模拟 SPI这个速度参数可以降到 10MHz。控制引脚用推挽输出是因为屏幕端需要明确的高/低电平开漏输出容易受到模块内部上拉影响。2.3 供电与电平转换3.3V 还是 5V要多加电容这块屏的供电很玄学。有的模块板载了 LDO可以直插 5V有的模块逻辑电路和屏幕玻璃共用 3.3V接 5V 就会读寄存器异常。识别方法是看模块背面的丝印和稳压芯片型号。最常见的模块使用 RT9013 或类似 LDO输入范围是 2.2V~5.5V接 5V 没问题但有些老模块没有 LDOVCC 直接进驱动芯片VCC 标 3.3V你接了 5V 屏幕还能亮只是内部发热严重时间长了容易偏色。最保险的做法是一律接 3.3V。F103 的 VDDA 和 VDD 也是 3.3V这样屏幕和 MCU 共用供电轨。屏幕背光全开的电流大约 60~90mA加上 ST7735 内部驱动电流瞬态可能到 120mA 左右。如果开发板上的 LDO 余量不大直接把屏幕插到 3.3V 引脚一刷新页面就可能把 MCU 电压拉低导致复位。所以我会在模块电源引脚旁边并联一个 10uF 钽电容或电解电容再加一个 100nF 陶瓷电容电容尽量靠近模块排针。信号电平方面STM32F103 的 GPIO 输出是 3.3V对 ST7735 的 3.3V 逻辑来说余量很足。问题出在 5V 系统上如果模块供电是 5V内部逻辑 VCC 也是 5V那你 SPI 信号高电平只有 3.3V达不到 0.7*VCC 的阈值屏幕会频繁乱码。常见解决方案是把模块改成 3.3V 供电或者用两片 74LVC245 做电平转换不要指望屏模块自带的限流电阻能把 5V 电平变成 3.3V 逻辑。之前遇到过一块模块MOSI 波形高电平 3.3V屏内识别阈值为 3.4V就差 0.1V怎么调初始化都没用最后把模块供电改 3.3V 就正常了。硬件复位的时序也属于供电方案的一部分。RST 引脚不要直接接地最好由 MCU 控制上电后等电源稳定至少 20ms再让 RST 从低变高。如果模块有独立的 RST 复位按钮把按钮接到 RST 脚和地之间不要悬空。悬空状态下模块内部的上电复位电路在慢速上电时会失效表现就是你烧录程序后第一次上电白屏按一下复位才好这类问题往往排查到最后都跟代码没关系。3. 驱动初始化流程把寄存器时序写成能复用的代码3.1 初始化序列的含义软复位、退出睡眠、像素格式和伽马设置从网上下载的 ST7735 初始化代码往往是一串看不懂的数组复制到工程里后白屏就会开始怀疑硬件。其实初始化流程的核心只有几条命令软件复位0x01让屏幕回到默认状态退出睡眠0x11唤醒显示电路设置像素格式0x3A写 0x05告诉它按 RGB565 处理设置内存访问控制0x36定义扫描方向和 RGB/BGR 色序打开反色修正0x21或关闭反色取决于屏体默认的极性最后打开显示0x29。每条命令之间要留出稳定时间。软件复位后至少等 120ms让内部逻辑完成复位退出睡眠后至少再等 120ms显示模块需要从休眠状态唤醒并稳定Display On 之后留 50ms。如果缺少这些延时屏幕很容易出现“上电后白屏按一下复位才正常”的奇怪现象。MADCTL 即 0x36 是导致坐标错乱最常见的地方。寄存器里的 bit6、bit7 控制行/列扫描方向bit3 控制色序是 RGB 还是 BGR。很多模块默认出厂值是 0xC0表示 RGB 顺序且坐标原点在左上角。但批号不同有的屏需要 0xA0 才是左上角开始。更麻烦的是如果你用横屏显示MADCTL 还需要组合 MX、MY 和 MV 位通常写成 0xA6、0xE6 等值。这些值不测试根本看不出来必须配合测试图案逐一验证。伽马设置同样影响显示效果。ST7735R 和 ST7735S 的伽马寄存器有差异有些屏库的初始化数组里包含 30 多条命令就是因为这部分在逐个调亮度曲线和偏色。如果只是想把屏幕点亮可以跳过大部分伽马命令只保留 0xB1、0xB2、0xB4、0xC0~0xC5 这组典型值。它们的作用是设置驱动电压、VCOM 电压和伽马曲线写错的主要表现是白平衡偏色、显示对比度不正常。3.2 ST7735 初始化函数实现逐字节解释关键参数初始化函数里WriteCmd 与 WriteData 的差别就是 DC 引脚的电平。DC 为低时屏把传入数据解释为命令DC 为高时解释为数据。下面这份代码是实际在用的精简版本static void TFT_WriteCmd(uint8_t cmd) { TFT_CS_LOW(); TFT_DC_LOW(); TFT_SPI_WriteByte(cmd); TFT_CS_HIGH(); } static void TFT_WriteData(uint8_t data) { TFT_CS_LOW(); TFT_DC_HIGH(); TFT_SPI_WriteByte(data); TFT_CS_HIGH(); }逻辑说明这两个函数把 CS 拉低后发送一个字节再拉高 CS。DC 引脚必须在时钟有效之前稳定因为屏幕在 CS 下降沿后就开始采样数据。如果先发数据再切换 DC恰好遇上 SPI 时钟会把数据当成命令处理初始化现场直接乱套。参数说明这里的 CS 每发送一个字节往返一次会拉低 SPI 吞吐量。在画大量像素时可以在写一长串数据前一次性拉低 CS把 DC 拉高连续传完再释放 CS。我建议保留这两份函数一份用于命令/数据交替阶段一份用于连续传输像素阶段。解析一下初始化函数的关键部分static void TFT_Init(void) { // 1. 硬件复位RST 低电平保持 100ms TFT_CS_HIGH(); TFT_RST_LOW(); delay_ms(100); TFT_RST_HIGH(); delay_ms(120); TFT_CS_LOW(); // 2. 软件复位 TFT_WriteCmd(0x01); delay_ms(120); // 3. 退出睡眠模式 TFT_WriteCmd(0x11); delay_ms(120); // 4. 像素格式RGB565 TFT_WriteCmd(0x3A); TFT_WriteData(0x05); // 5. 内存访问控制RGB 顺序 左上角坐标 TFT_WriteCmd(0x36); TFT_WriteData(0xC0); // 6. 驱动调参典型值直接决定亮度和白平衡 TFT_WriteCmd(0xB1); TFT_WriteData(0x01); TFT_WriteData(0x2C); TFT_WriteData(0x2D); TFT_WriteCmd(0xB2); TFT_WriteData(0x01); TFT_WriteData(0x2C); TFT_WriteData(0x2D); TFT_WriteCmd(0xB4); TFT_WriteData(0x07); TFT_WriteCmd(0xC0); TFT_WriteData(0xA2); TFT_WriteData(0x02); TFT_WriteData(0x84); TFT_WriteCmd(0xC1); TFT_WriteData(0xC5); TFT_WriteCmd(0xC2); TFT_WriteData(0x0A); TFT_WriteData(0x00); TFT_WriteCmd(0xC3); TFT_WriteData(0x8A); TFT_WriteData(0x2A); TFT_WriteCmd(0xC4); TFT_WriteData(0x8A); TFT_WriteData(0xEE); TFT_WriteCmd(0xC5); TFT_WriteData(0x0E); // 7. 反色修正与显示开启 TFT_WriteCmd(0x21); // INVON TFT_WriteCmd(0x29); // DISPON delay_ms(50); TFT_CS_HIGH(); }逻辑说明第一步硬件复位保证屏幕电源稳定后状态可控软件复位再把内部寄存器统一到默认值。Sleep Out 之后才开始设置显示参数否则命令可能被屏幕忽略。0xB1~0xC5 这组调参命令在大多数 ST7735 模块上通用但不同批次可能会有轻微色差这里不改也可以亮。参数说明0x36 里的 0xC0二进制第 7 位为 1表示列扫描从右到左第 6 位为 0行扫描从上到下第 3 位为 0RGB 顺序。如果你发现画面是镜像或红蓝互换就把这个字节换成 0x80、0x90 或 0xA0 试不要动其他寄存器。0x21 是反色修正命令如果屏幕颜色像底片执行一次 0x20 或 0x21 通常能解决它和显示坐标、像素格式没有关系。3.3 软件 SPI 还是硬件 SPI两种实现和切换方法初始化函数底部需要 TFT_SPI_WriteByte 这个底层函数。新手最稳的做法是先写软件 SPI因为它不依赖硬件 SPI 的复杂寄存器。软件 SPI 的代码就是手动拉 GRAM 翻转时钟逻辑是先把 MOSI 置为数据拉低 SCK再拉高 SCK在 SCK 上升沿屏幕采样。它的缺点很明显每发送一位都要翻转 GPIO发送 40KB 像素数据会占用大量 CPU 时间。不过我用它做过一个刷屏速度测试发现软件 SPI 在 72MHz 主频下大约每秒只能刷 5~10 屏如果只显示静态文本完全够用但做动画就很吃力。void TFT_SPI_WriteByte(uint8_t data) { uint8_t i; TFT_SCK_LOW(); for (i 0; i 8; i) { if (data 0x80) TFT_MOSI_HIGH(); else TFT_MOSI_LOW(); data 1; TFT_SCK_HIGH(); TFT_SCK_LOW(); } }逻辑说明从最高位开始发送SCK 先低后高形成标准的 SPI 模式 0 时序。每次循环先放数据到 MOSI再拉高 SCK让屏在上升沿采样。由于没有硬件 SPI 的分频限制这个函数可以在任意 GPIO 上运行方便快速验证屏幕好坏。参数说明这里 DATA 用 8bit 长度如果你切换到硬件 SPI 后代码不变直接替换函数内部实现即可。软件 SPI 的延时完全由 GPIO 翻转速度决定如果 GPIO 配置为 2MHz 低速发送位时间会拉长显示会更慢但稳定度不会有明显变化。硬件 SPI 的优势是主频快。SPI1 挂在 APB2 总线上时钟最高 72MHz设置分频系数 4 就是 18MHz。ST7735S 手册标称 SPI 写时钟一般在 15MHz~20MHz 之间所以 18MHz 是常用值。如果屏幕走线短并且电源干净18MHz 完全可以工作如果画面出现随机花点可以考虑降到 9MHz花点通常就会消失。void TFT_SPI1_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Pin TFT_SCK_PIN | TFT_MOSI_PIN; GPIO_Init(GPIOA, gpio); gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Pin TFT_CS_PIN | TFT_DC_PIN; GPIO_Init(GPIOB, gpio); spi.SPI_Direction SPI_Direction_1Line_Tx; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_1Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }逻辑说明SPI_Direction_1Line_Tx 表示只发送数据不读屏体数据。为什么不用双向模式因为大多屏模块没有引出 MISO驱动库根本不需要接收数据。这样配置可以省去一个 GPIO也避免在 MISO 悬空时读到随机电平干扰状态机。参数说明SPI_CPOL_Low 表示空闲时 SCK 为低电平SPI_CPHA_1Edge 表示第一个跳变沿采样这对应软件 SPI 实现里“SCK 先拉低再拉高数据在上升沿采样”的时序。如果换成 CPOL_High、CPHA_2Edge数据会在下降沿采样显示效果会出现细小的抖动。ST7735 数据手册推荐模式就是 CPOL0、CPHA0不要改动。4. 从画一个像素到刷一屏显存、坐标和滚动的细节4.1 设置地址窗口与像素写入显存与屏幕对应关系ST7735 内部有一块 GRAM所有像素数据都是先写入 GRAM再映射到屏幕。写入动作由 CASET0x2A、RASET0x2B和 RAMWR0x2C三条命令构成。CASET 设置列地址范围RASET 设置行地址范围RAMWR 之后连续写入的字节会被填充到这片矩形区域。这个顺序不能反先设地址再写数据地址窗口内的数据会按照从左到右、从上到下的顺序排列自动换行。void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { // 列地址范围 TFT_WriteCmd(0x2A); TFT_WriteData(x0 8); TFT_WriteData(x0 0xFF); TFT_WriteData(x1 8); TFT_WriteData(x1 0xFF); // 行地址范围 TFT_WriteCmd(0x2B); TFT_WriteData(y0 8); TFT_WriteData(y0 0xFF); TFT_WriteData(y1 8); TFT_WriteData(y1 0xFF); // 准备写显存 TFT_WriteCmd(0x2C); }逻辑说明坐标数据按高 8 位、低 8 位顺序发送因为 ST7735 的行列地址内部用 16 位寄存器保存。虽然屏幕只有 128x160但寄存器还是按 16 位来解析所以每个坐标都分两次发送。TFT_WriteCmd(0x2C) 只负责“开始写数据”接下来 MCU 发送的每一个字节都会被当成像素数据。参数说明如果窗口坐标 x1 小于 x0或者 y1 小于 y0某些 ST7735 批次的驱动会把这次写入当作非法操作直接忽略刷屏时表现为部分区域不更新。所以在调用填充函数前最好对传入坐标做大小判断出现反向窗口时交换坐标或者干脆返回不执行。横竖屏坐标转换是很多人头疼的地方。竖屏时 CASET 范围是 0~127RASET 范围是 0~159坐标点 (x,y) 直接映射到屏幕列、行。横屏时MADCTL 的 MV 位会交换行列扫描你在屏幕上看到的物理宽是 160、高是 128但底层 GRAM 仍是 128 列、160 行。如果屏库只改外层分辨率没有在 SetWindow 里做换算就会出现绘制区域偏移。常见做法是写一个坐标转换函数void TFT_Convert(uint16_t *x, uint16_t *y) { uint16_t tmp; tmp *x; *x *y; *y TFT_SCREEN_W - 1 - tmp; }逻辑说明横屏时把传入的 x、y 互换再对 y 取补实际上是把用户坐标系的点映射到底层 GRAM 的原始行列。这个函数的实现依赖 0x36 的 MX/MY 位不同屏库写的补数不一样需要实测。参数说明如果你的初始化 0x36 用的是 0xA0 而不是 0xC0那么补的对象可能是 x 而不是 y。与其硬背公式不如用一个 TFT_DrawPixel 在横屏模式下画一个十字线找到坐标映射规律再套进 Convert 函数这样比盲改快得多。4.2 填充和字符显示常见工具函数实现画点函数是其他绘图的基础核心就是设置窗口和写入两字节颜色void TFT_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { uint8_t hi color 8; uint8_t lo color 0xFF; TFT_SetWindow(x, y, x, y); TFT_WriteData(hi); TFT_WriteData(lo); }逻辑说明窗口被设为一个单点矩形随后写入两个字节的颜色数据。由于 SetWindow 内部会下发 CASET/RASET/RAMWR 三条命令每次画点都要额外传输约 10 个字节所以画点效率很低只适合绘制小图形或调试用。参数说明RGB565 颜色格式高字节在前低字节在后对应 MOSI 先发高字节。如果你的屏幕颜色偏蓝偏红大多不是字节次序问题而是 0x36 里的 BGR 位这个误区和第 3 章一致。填充函数就是画窗口加上大量数据但为了效率我会把颜色拆成两个字节后连续发送避免循环内部反复计算void TFT_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t i; uint32_t total (uint32_t)(x1 - x0 1) * (y1 - y0 1); uint8_t hi color 8; uint8_t lo color 0xFF; TFT_SetWindow(x0, y0, x1, y1); for (i 0; i total; i) { TFT_WriteData(hi); TFT_WriteData(lo); } }逻辑说明total 按矩形宽高算出像素总数每次写两字节。循环里只发送两个已拆好的字节没有位拼接操作。这里把 TFT_SetWindow 放在循环前底层写数据时不再重复发送地址命令能大幅提升填充速度。参数说明如果蓝色矩形变成紫色矩形很可能是颜色值拆成了 lo 在前、hi 在后屏幕把第二个字节当成了高字节。RGB565 红色 0xF800正确发送顺序是 0xF8 然后 0x00反了就是 0x00 0xF8屏幕解析成接近黑色的蓝绿色。字符显示依赖点阵字模。常见 6x8 字体每个字符有 6 字节每个字节的每一位对应 8 行像素。显示时需要把每个像素画成前景色或背景色代码开销比较大所以只适合静态文本。const uint8_t ascii_6x8[95][6] { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 {0x00, 0x00, 0x00, 0x2F, 0x00, 0x00}, // ! // 略 }; void TFT_DrawChar(uint16_t x, uint16_t y, char c, uint16_t color, uint16_t bg) { uint8_t i, j; uint8_t col_data; if (c 32 || c 126) return; for (i 0; i 6; i) { col_data ascii_6x8[c - 32][i]; for (j 0; j 8; j) { if (col_data (1 (7 - j))) { TFT_DrawPixel(x i, y j, color); } else if (bg ! TFT_COLOR_TRANSPARENT) { TFT_DrawPixel(x i, y j, bg); } } } }逻辑说明外层循环处理一列中的 6 个字节位内层处理字节的 8 个 bitbit7 代表最上方像素。每次调用最多画 48 个点刷新一屏文本时 CPU 占用会很高但绘制局部文本完全可以接受。参数说明c 减去 32 是 ASCII 数组索引因此空格字符对应第一行全 0。背景色如果是透明值则不会覆盖原图层如果是白色或黑色则会把字符底下的原有内容覆盖。注意透明模式下如果字符需要移动必须先清掉旧位置否则会出现残影。4.3 刷新策略局部刷新和全屏刷新的性能取舍这块屏没有片内帧缓冲它更像一个“只写显存”的外设。每次刷新都必须由 MCU 把数据重新刷过去因此刷新面积直接决定性能。全屏 128x160 共 20480 像素RGB565 下约 40KB 数据。即便用 18MHz 硬件 SPI把 40KB 推出去也要 2 毫秒以上算上写命令和等待停顿实际全屏刷新通常在 4~6 毫秒。如果使用软件 SPI这数字会拉长到几十毫秒CPU 几乎被占死。所以绘图策略应该是静态部分只画一次动态部分只更新最小矩形区域。比如做仪表盘时标题和刻度只在上电时绘制数值区域用一个局部矩形每次更新先画背景色再画新的数字文本。这样每次刷新的数据量从 40KB 降到几千字节。缓冲区管理也值得提一下。ST7735 内部没有像 SSD1306 那类 OLED 屏的页缓存你不能只改一个像素然后让整个屏幕自动刷新必须自己决定“在哪里写、写多少”。有些人为此在 MCU 端开辟一个 40KB 的显存镜像把绘图函数都改成写镜像再整块 DMA 给屏幕。40KB 对 F103RCT6 来说可以放下因为它的 SRAM 比 C8T6 大不少但如果你同时开大量数组就要注意溢出。折中方案是只做局部行缓冲把需要重绘的每一行按行缓存再写屏既不占太多 RAM又能保持一定刷新效率。画布旋转也是一个常见需求。如果实在不想在应用代码里做坐标换算可以在驱动层维护一个旋转标志竖屏时直接调用 TFT_SetWindow横屏时先经过坐标转换函数再调用底层的窗口设置。这样上层应用永远以屏幕物理方向为基准换屏时只需要改初始化里的 0x36不用改业务代码。5. 避坑与排查从白屏、花屏、乱码到闪烁的调试记录这章写的是真实调试记录每条都按照“现象 → 原因 → 解决”的格式给出。建议遇到问题时对号入座不要从头到尾读因为很多坑只在一类版本上出现。5.1 白屏与无背光现象屏幕有背光但是一直显示白色或者背光完全不亮把脸凑近屏幕才能隐约看到内容。原因白屏说明屏的显示电路没有收到初始化命令或者初始化时序不完整背光不亮大多不是 MCU 的问题而是模块背光供电、BLK 引脚配置问题。最常见的错误是只发了 Display On 命令没有先发 Sleep Out。ST7735 上电后默认处于睡眠状态直接 Display On 不会让屏幕正常工作。解决先量三个点BLK 引脚对地电压是否接近逻辑高电平模块 VCC 对地电压是否 3.3V 且稳定RST 引脚是否在初始化前经历了完整的低→高过程。如果 BLK 有电但背光不亮检查模块背光 LED 串联电阻是否烧断。如果 BLK 没电检查 MCU 引脚是不是被复用配置成 ADC 或哪个外设占用。代码层面把软复位 0x01 后面的延时加到 120msSleep Out 0x11 后面再加 120ms之后再 Display On 0x29。按照第 3 章的初始化流程重跑一遍白屏通常会在第一屏正常绘图后消失。5.2 颜色错乱与画面撕裂现象画一个红色方块显示成蓝色绿色偏紫或者画面边缘出现像素错位、斜向撕裂。原因颜色错乱大多是 0x36 寄存器里的 BGR 位不对。屏幕上电后ST7735 默认色序可能是 RGB也可能是 BGR这和模组玻璃排线有关。像素错位则常见于 SPI 时钟采样沿设反数据在错误的边沿被锁存导致低位和高位顺序混乱。解决把 0x36 参数从 0xC0 改成 0x80bit3 从 0 变 1屏幕在 RGB/BGR 之间切换看颜色是否恢复正常。撕裂或斜纹问题则要回到 SPI 配置检查 SPI_CPOL 和 SPI_CPHA。ST7735 默认建议 CPOL0、CPHA0对应硬件 SPI 的 SPI_CPOL_Low 和 SPI_CPHA_1Edge。如果边沿设反数据会在 SCK 下降沿采样而屏体期待上升沿结果就是像素“串位”。另外 SPI 波特率过高也可能把可行的信号打成斜纹尤其是杜邦线超过 10cm 时把预分频从 4 改成 8即从 18MHz 降到 9MHz看看撕裂是否消失。5.3 偶尔显示正常偶尔乱码现象上电后有时显示正常有时花屏或出现彩色横条纹按一下复位键可能就正常但断电再上电又复现。原因MCU 复位瞬间GPIO 处于浮空输入状态MOSI、SCK 引脚的电位不受控制。屏幕在这个时候收到噪声形成的伪波形内部状态机可能进入非法状态。初始化代码跑得再标准只要屏幕在上电早期收到垃圾数据后面就会花屏。解决在 main 函数最开头把所有屏幕控制引脚配置成推挽输出并拉高尤其 CS 拉高让数据通路处于“未选中”状态。然后延时 50ms 等待电源稳定再执行完整初始化。如果需要硬件级解决可以在 RST 引脚上并联一个 100nF 电容到地让屏幕自己在复位期间保持低电平但这片电容会影响复位沿速度所以不能加太大。另外初始化完成后可以执行两次软件复位第二次是保险能把第一次可能吞掉的命令补齐。5.4 坐标偏移或屏幕镜像现象绘制内容不在预期位置整体偏向某个角或者文字是反的画一个 16x16 方块位置相同但方向相反。原因ST7735 内部 GRAM 的扫描起始坐标由 0x36 的 MX、MY 位决定。不同的玻璃面板切割方向不同即使同一型号的模组不同批次也可能不同。初始化代码里写死 0xC0就假设扫描从左上角开始如果屏体从左下角开始显示时坐标就会整体 Y 轴翻转。解决不要用“猜”来解决写一个专用的 TestPattern 函数屏幕四个角各画一个不同颜色的实心方块中央画十字线然后循环切换 0x36 的 8 个常用值0x00、0x20、0x40、0x60、0x80、0xA0、0xC0、0xE0。观察哪个值能让左上角方块颜色与调用参数一致。实际测试中 0xC0、0xA0、0xE0 都能成为“正确值”取决于玻璃厂家的标记习惯。拿到正确值后把它写进初始化函数再把业务代码里的坐标转换函数调整为对应的 MX/MY 位组合。5.5 亮度忽亮忽暗或闪烁现象画面显示稳定但背光亮度在肉眼可见地闪烁尤其白色背景时最明显画面本身没有像素抖动。原因如果背光用普通 GPIO 或者低频 PWM 驱动频率低于 100Hz 时代码实现稍有抖动就会被人眼感知。另一个常见原因是背光供电从同一个 3.3V LDO 取电当 SPI 刷新屏幕时瞬时电流拉高LDO 输出出现周期性的几十毫伏波动LED 亮度跟着波动。解决把 BLK 引脚接到定时器 PWM 输出频率至少 1kHz推荐 5kHz 以上。F103 的 TIM3 输出频率可以由预分频器和周期寄存器算好。比如 72MHz 时预分频 71计数周期 99PWM 频率就是 10kHz占空比 70%。使用 PWM 模式后背光不再占用 CPU 时间也不会因为 GPIO 翻转抖动而闪烁。同时在模块电源脚附近补 10uF 和 100nF 电容如果用面包板供电还需要在总电源处加一个 100uF 以上电容。验证办法是用示波器看背光电压波形闪烁时会有明显的低频纹波处理后波纹会趋于平直。6. 进阶用 DMA 把刷屏从 CPU 换到总线顺便搭一个简易状态页点亮屏幕之后下一件值得做的事就是让刷屏不再占用 CPU。F103RCT6 的 SPI1 发送 DMA 请求固定走 DMA1_Channel3配置好之后一次刷屏只需启动一次 DMA剩下的发送由总线完成。代码层面与普通刷屏的区别只是把 WriteData 换成了 DMA 搬运内存到 SPI 数据寄存器。void TFT_FillByDMA(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t *buf) { uint32_t size (uint32_t)(x1 - x0 1) * (y1 - y0 1) * 2; TFT_SetWindow(x0, y0, x1, y1); DMA_DeInit(DMA1_Channel3); 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 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); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); DMA_ClearFlag(DMA1_FLAG_TC3); DMA_Cmd(DMA1_Channel3, ENABLE); while (DMA_GetFlagStatus(DMA1_FLAG_TC3) RESET); }逻辑说明DMA 把 buf 里的字节依次搬进 SPI1 数据寄存器SPI1 外设自动把这些字节通过 MOSI 发送。设置 PeripheralInc 为 Disable是因为 SPI 数据寄存器只有一个地址MemoryInc 为 Enable是因为要连续读缓冲区。DMA_BufferSize 用字节数而不是像素数因为每个像素是两字节。参数说明如果 SPI_DataSize 配置成 8 位DMA 的 PeripheralDataSize 和 MemoryDataSize 也要是 Byte保证长度一致。如果你以后切到 16 位 SPI 模式DMA 宽度要改成 HalfWordBufferSize 改成像素数量否则字节数和半字数不匹配DMA 会溢出计数。用 DMA 之后刷一屏的等待时间主要花在硬件发送上CPU 可以在启动 DMA 后继续做别的但要注意不能修改 buf 直到 DMA 传输完成。我的习惯是准备两块缓冲区一块给 DMA 发另一块给应用层绘图交替使用。这样既能持续刷新 20ms 一次的状态页也不需要每次等完整屏发完。顺便搭一个简易状态页上半部分画一条滚动曲线下半部分显示日期、时间或 ADC 采样值。曲线区只有新的采样点需要画数值区只有数字更新时重绘因此每次刷新只需要传几十到几百字节。此时 CPU 占用几乎可以忽略主循环还能稳定运行按键扫描和串口输出。验证 DMA 是否真正生效的方法很简单在定时器中断里翻转一个空闲 GPIO用示波器观察 GPIO 翻转周期对比刷新状态页前后。如果翻转周期稳定不变说明刷屏没有阻塞 CPU如果周期出现明显毛刺说明代码里还有等待 SPI 发送的空循环例如 TFT_WriteData 的 while 等待可以把等待放回 DMA 中断里去处理。从那以后我每次接到屏幕相关项目都会第一时间把 GPIO 复用、DMA 通道和 SPI 波特率三项先确认一遍再写绘图代码避免后期反复改电路和初始化代码。希望这套 STM32F103RCT6 搭配 1.44TFT 的驱动流程能帮到你。配套工程里的初始化序列、字库和 DMA 发送代码已整理好下载后把引脚宏改成你的板子就能跑起来。本文还有配套的精品资源点击获取