news 2026/10/21 23:12:24

STM32F103硬件SPI驱动1.44寸TFT屏:ST7735S初始化与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103硬件SPI驱动1.44寸TFT屏:ST7735S初始化与工程实践

简介:面向嵌入式开发初学者与电子爱好者,资源演示STM32F103RCT6通过SPI接口驱动1.44英寸TFT彩屏(128×128像素)的完整工程方案。压缩包共86个文件、约289KB,以源码为主:38个.h头文件与32个.c源文件细致划分了LCD底层驱动(Lcd_Driver.c)、GUI图形接口、图片/字模数据、LED控制、延时与中断服务等模块,另有Keil工程配置、STM32启动汇编、hex固件和readme说明等,可直接打开工程编译或烧录到开发板验证。资料从引脚定义、SPI外设初始化到命令/数据发送、显示刷新均有体现,还包含图像缓冲区管理等内容,可帮助理解SPI通信与屏幕控制流程。已有4052人学习下载,适合用于入门STM32驱动TFT屏幕、快速搭建彩色显示应用。

1. 项目认知:这块 128x128 的小屏,光会点灯可玩不转

先说一个反直觉的结论:驱动 1.44 寸 TFT 屏幕,难点根本不在「显示」——SPI 发数据谁都会,难的是引脚分配、初始化时序和显存管理这三件事。很多开发者在 F103 上点亮 0.96 寸 OLED 后觉得不过瘾,换上 1.44 寸彩屏直接白屏、花屏、颜色错位,然后开始怀疑硬件,实际上九成是代码层面的坑。

这个项目做的就是一件很具体的事:用 STM32F103RCT6(256KB Flash、64KB SRAM)通过硬件 SPI2 驱动 1.44 寸 ST7735S 屏幕,完成字符显示、图形绘制和简单 UI 刷新。适合刚学完 GPIO 和串口、想碰彩色屏幕的入门者,也适合要做桌面小仪表、传感器数据可视化但又不想上 RTOS 的开发者——这份工程把驱动层、中间层、应用层拆得清清楚楚,拿过来改一改就能用在你的板子上。

2. 硬件接线与环境搭建:引脚分配表、SPI2 选择和点屏前的自检

2.1 引脚分配:为什么我把 SCK 放在 PB13 而不是 PA5

1.44 寸 TFT 屏幕模块通常是 8 个引脚:GND、VCC、SCL、SDA、RES、DC、CS、BL。其中 SCL 是时钟、SDA 是数据,RES 是硬件复位,DC 是数据/命令选择,CS 是片选。典型的连接方式如下表:

TFT 引脚功能接 STM32F103RCT6说明
GND电源地GND与板子共地
VCC电源 3.3V3.3V不要接 5V,ST7735S 逻辑电平是 3.3V
SCLSPI 时钟PB13SPI2_SCK
SDASPI 数据PB15SPI2_MOSI,只写不读
RES复位PB12推挽输出,低电平复位
DC数据/命令PB11高电平数据,低电平命令
CS片选PB10拉低选中屏幕
BL背光PB14接 3.3V 常亮,或 PWM 调亮度

这里有个容易被忽略的点:为什么不用 SPI1?SPI1 的 SCK 在 PA5、MOSI 在 PA7,很多最小系统板的 PA9/PA10 已经被串口占用,PA5/PA6 还可能接了板载 LED 或者调试接口,来回跳线特别容易出错。而 SPI2 的 PB13/PB14/PB15 在 RCT6 上是一组干净的引脚,避开调试器、避开串口,接线更舒服。我一般会优先选 SPI2,除非你的板子把 PB 引脚挪作他用。

2.2 开发环境与工程导入:标准外设库还是 HAL,选一个别纠结

这个项目用的是 STM32 标准外设库(常见版本是 V3.5 那一代)。如果你习惯 HAL 库也没问题,SPI 时序逻辑完全一样,只是 API 名字从SPI_I2S_SendData变成HAL_SPI_Transmit而已。工程的大致形态:

  • 芯片选型:STM32F103RC
  • 启动文件:startup_stm32f10x_hd.s(RCT6 属于高密度器件,选 hd 启动文件)
  • 时钟:外部 8MHz 晶振,PLL 倍频到 72MHz 系统主频,APB1 总线 36MHz,SPI2 挂在 APB1 上

导入工程后第一件事不是编译,而是检查两个宏:STM32F10X_HD和USE_STDPERIPH_DRIVER。前者决定芯片型号对应的外设寄存器定义,后者是标准外设库的开关。这两个宏没加,编译大概率报 undefined identifier,而且报错位置极其隐蔽——都是在库文件深处,新手容易误判成库没装好。血泪经验:先看宏定义,再怀疑环境。

2.3 点亮前的自检:先让 SPI 输出一段时钟,再连屏幕

很多初学者屏幕不亮就直接改代码,越改越玄学。我习惯的做法是:在焊接/接线之后、跑 TFT 初始化之前,先写一段 10 行的 SPI 自检代码,让 SPI2 以 100kHz 左右的低速时钟循环输出0x00和0xFF,然后用逻辑分析仪或者示波器看 PB13 上有没有正常的时钟波形。

这一步能过滤掉三类问题:引脚虚焊、SPI 外设时钟没开、GPIO 复用模式配错。如果没有示波器也简单——把 PB15 接地,量 PB15 电平变化;或者直接用 Keil 的硬件仿真,在SPI_I2S_SendData之后打一个断点,读SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE)是否为 SET。自检通过之后再接屏幕,后面出问题就能基本锁定在 TFT 初始化序列上,排查半径小一大半。

注意:STM32F103RCT6 的 Flash 是 256KB、SRAM 是 64KB。属于高密度产品,选启动文件和宏定义时不要按 512KB 的型号去选。

3. 工程结构拆解:驱动层、中间层、应用层各自管什么

3.1 三层划分:为什么驱动要单独拆出来

拿到工程先看目录结构。这个项目不是把代码都堆在main.c里的那种 Demo,而是拆了三层,每一层各管各的,二次开发时能精准定位要改哪个文件:

Project ├── Core │ ├── startup_stm32f10x_hd.s │ ├── stm32f10x_it.c │ └── system_stm32f10x.c ├── Drivers │ ├── bsp_spi.c / bsp_spi.h // SPI 初始化与底层收发 │ ├── bsp_tft.c / bsp_tft.h // ST7735S 驱动:初始化、画点、区域填充 │ └── font.c / font.h // ASCII 字模数据与字符显示 ├── App │ ├── main.c // 主函数与测试流程 │ └── demo_ui.c / demo_ui.h // 简单的 UI 页面与数据刷新逻辑 └── MDK-ARM ├── Project.uvprojx └── JLinkSettings.ini

这个结构的核心思想是:bsp_spi不认识 TFT 是什么,它只管把一字节数据从 PB15 发出去;bsp_tft不认识具体业务,它只管把坐标和颜色变成 SPI 指令序列;demo_ui才是真正知道「温度要显示在第几行」的地方。这样改屏幕型号时,只需要动bsp_tft内部实现,demo_ui的调用代码一行都不用改。

3.2 头文件的职责边界:哪些函数对外暴露,哪些留在文件内部

看头文件能快速判断这个模块的封装水平。举个例子,bsp_tft.h里暴露出来的接口应该是这样的:

#ifndef __BSP_TFT_H #define __BSP_TFT_H #include "stm32f10x.h" /* 颜色定义,RGB565 格式 */ #define TFT_BLACK 0x0000 #define TFT_WHITE 0xFFFF #define TFT_RED 0xF800 #define TFT_GREEN 0x07E0 #define TFT_BLUE 0x001F #define TFT_YELLOW 0xFFE0 /* 屏幕分辨率 */ #define TFT_WIDTH 128 #define TFT_HEIGHT 128 void TFT_Init(void); void TFT_FillScreen(uint16_t color); void TFT_DrawPixel(uint16_t x, uint16_t y, uint16_t color); void TFT_DrawChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bg); void TFT_DrawString(uint16_t x, uint16_t y, char *str, uint16_t color, uint16_t bg); #endif

注意这里没有暴露SPI_WriteByte和TFT_SetWindow,因为它们是底层实现细节,外部调用者不需要知道。如果哪天你想把 SPI 换成模拟 GPIO 方式,只需要把bsp_spi.c里的实现改掉,头文件接口保持SPI_WriteByte(uint8_t)即可,上层完全无感。这就是「接口稳定、实现可变」的价值——别小看这个设计,很多小项目翻车就是因为在main.c里到处直接调用 SPI 写函数,换屏幕驱动时改到怀疑人生。

3.3 编译配置与存储布局:256KB Flash 里字模放哪里

F103RCT6 有 256KB Flash 和 64KB SRAM,听起来很大,但 1.44 寸屏幕的问题在于:一张 128x128 的图片是 128×128×2 = 32KB,如果定义成普通变量放 RAM,直接吃掉 SRAM 一半;如果定义成数组放 Flash,又会占 32KB Flash。这个项目在编译配置上做了两件值得借鉴的事:

  • 字模和图片数据一律用const修饰,放在 Flash,不占 SRAM;
  • 帧缓冲(framebuffer)默认不开启,改用「直接写屏」模式——每画一个点或一块区域就通过 SPI 发出去,不缓存整帧。

在 MDK 的 Options for Target -> Target 页签里,ROM 和 RAM 地址保持默认即可,不需要手动改分散加载文件。但如果后面你要加图片资源,记得把 IROM1 的大小从 0x40000(256KB)往下调一点、给 Bootloader 留位置,或者直接把图片放到外部 SPI Flash 里,这是后话,第 6 章会展开。

4. 核心代码实战:从 SPI 初始化到字模上屏的完整链路

4.1 SPI2 引脚初始化和时钟设置:复用推挽与波特率预分频

SPI 底层是整个项目的基石。它要做的事情有两件:把 PB13/PB15 配置成复用推挽输出,然后把 SPI2 外设配置成主机模式。代码如下:

#include "bsp_spi.h" static void SPI2_GPIO_Config(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); gpio.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; // SCK + MOSI gpio.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); /* 复位、DC、CS、背光引脚配置为通用推挽输出 */ gpio.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_14; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &gpio); } void SPI2_Init(void) { SPI_InitTypeDef spi; SPI2_GPIO_Config(); SPI_Cmd(SPI2, DISABLE); // 先关闭外设,配置完成后再使能 spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工 spi.SPI_Mode = SPI_Mode_Master; // 主机模式 spi.SPI_DataSize = SPI_DataSize_8b; // 8 位数据帧 spi.SPI_CPOL = SPI_CPOL_Low; // 时钟空闲为低 spi.SPI_CPHA = SPI_CPHA_1Edge; // 第一边沿采样数据 spi.SPI_NSS = SPI_NSS_Soft; // 软件管理片选 spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 36MHz / 4 = 9MHz spi.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先出 SPI_Init(SPI2, &spi); SPI_Cmd(SPI2, ENABLE); }

参数说明:SPI2 挂在 APB1 总线上,系统时钟 72MHz 时 APB1 最高 36MHz,所以预分频 4 后实际 SCK 频率是 9MHz。对 128x128 的屏幕来说这个速度完全够用,整屏填色大约 30ms 左右;如果后续刷新频率不够,可以改成预分频 2 提到 18MHz,但要注意飞线质量和屏幕模块的 PCB 走线,速度上去后花屏概率会上升。

SPI_CPOL_Low和SPI_CPHA_1Edge是 ST7735S 最常用的 SPI 模式 0。这里出问题最典型的症状是「屏幕能亮但全是雪花噪点」,因为数据在错误的时钟沿被采样。如果你在别的例程里看到CPOL_High / CPHA_2Edge,先别改,那是另一种屏幕的时序要求。

SPI 发送函数的实现很简洁,注意要等 TXE 标志位:

void SPI2_WriteByte(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI2, data); }

4.2 ST7735S 初始化序列:把屏幕从休眠状态叫醒

ST7735S 上电后默认是睡眠模式,必须按数据手册给出的序列初始化才能显示。这个序列每家屏幕模组厂商可能略有差异,但核心步骤是一样的:软件复位 -> 退出睡眠 -> 设置扫描方向 -> 设置像素格式 -> 打开显示。下面是这个项目用的初始化代码:

#include "bsp_tft.h" static void TFT_WriteCmd(uint8_t cmd) { TFT_DC_LOW(); // DC 拉低表示命令 TFT_CS_LOW(); SPI2_WriteByte(cmd); TFT_CS_HIGH(); } static void TFT_WriteData(uint8_t data) { TFT_DC_HIGH(); // DC 拉高表示数据 TFT_CS_LOW(); SPI2_WriteByte(data); TFT_CS_HIGH(); } void TFT_Init(void) { /* 硬件复位:RES 拉低至少 10ms 再拉高 */ TFT_RES_LOW(); delay_ms(20); TFT_RES_HIGH(); delay_ms(50); /* 软件复位 */ TFT_WriteCmd(0x01); delay_ms(20); /* 退出睡眠模式 */ TFT_WriteCmd(0x11); delay_ms(120); /* 设置像素格式:16 位 RGB565 */ TFT_WriteCmd(0x3A); TFT_WriteData(0x05); /* 设置屏幕扫描方向,0xC8 是常见默认值 */ TFT_WriteCmd(0x36); TFT_WriteData(0xC8); /* 关闭到打开的显示控制 */ TFT_WriteCmd(0x29); TFT_FillScreen(TFT_BLACK); }

这段代码里有两个关键参数值得抠一下:0x3A后面的0x05表示每像素 16 位,这个必须和后面画点时发送的两字节颜色数据对应;0x36(MADCTL)后面的0xC8控制行/列扫描方向和 RGB/BGR 顺序。如果你的屏幕显示出来是镜像的、上下颠倒的、或者颜色偏红偏蓝,改的就是这个字节——bit7 控制行扫描方向,bit6 控制列扫描方向,bit3 控制 RGB/BGR 顺序。改错了颜色看起来像「负片」,改对了才是真彩色。

4.3 SetWindow 与像素写入:RAMWR 之前先圈定区域

ST7735S 的写像素逻辑是「先圈一块矩形区域,再往里面灌像素数据」。核心函数是设置列地址(CASET)和行地址(PASET):

static void TFT_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { TFT_WriteCmd(0x2A); // CASET:列地址 TFT_WriteData(x0 >> 8); TFT_WriteData(x0 & 0xFF); TFT_WriteData(x1 >> 8); TFT_WriteData(x1 & 0xFF); TFT_WriteCmd(0x2B); // PASET:行地址 TFT_WriteData(y0 >> 8); TFT_WriteData(y0 & 0xFF); TFT_WriteData(y1 >> 8); TFT_WriteData(y1 & 0xFF); TFT_WriteCmd(0x2C); // RAMWR:开始写显存 }

然后画一个像素就是先圈 1x1 的区域,再发两个字节的颜色数据。RGB565 格式下,16 位颜色拆成高字节和低字节,先发高字节:

void TFT_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x >= TFT_WIDTH || y >= TFT_HEIGHT) return; // 越界保护 TFT_SetWindow(x, y, x, y); TFT_DC_HIGH(); TFT_CS_LOW(); SPI2_WriteByte(color >> 8); // 高字节 SPI2_WriteByte(color & 0xFF); // 低字节 TFT_CS_HIGH(); }

这里有个性能优化点:如果每个像素都调用TFT_SetWindow,会产生 6 条额外命令,刷一屏 128x128 = 16384 个像素,命令开销感人。所以填色函数会改为「先 SetWindow 一次,再连续发送整块数据」。这解释了一个常见现象——为什么同样画一个全屏红,用DrawPixel循环和用FillRect的速度差好几倍。

void TFT_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t i, total; if (x0 >= TFT_WIDTH || y0 >= TFT_HEIGHT) return; if (x1 >= TFT_WIDTH) x1 = TFT_WIDTH - 1; if (y1 >= TFT_HEIGHT) y1 = TFT_HEIGHT - 1; TFT_SetWindow(x0, y0, x1, y1); total = (x1 - x0 + 1) * (y1 - y0 + 1); TFT_DC_HIGH(); TFT_CS_LOW(); for (i = 0; i < total; i++) { SPI2_WriteByte(color >> 8); SPI2_WriteByte(color & 0xFF); } TFT_CS_HIGH(); }

4.4 字模显示:8x16 字体从取模到上屏

字符显示是这类小屏最常见的需求。这个工程用的是 8x16 ASCII 字模,每个字符在font.h里对应 16 个字节,每字节表示一列 8 个像素,高位在上面。显示一个字符的核心逻辑是:逐列取字节,逐位判断该点是否要画前景色。

void TFT_DrawChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bg) { const uint8_t *p; uint8_t pixel; uint8_t i, j; if (ch < 0x20 || ch > 0x7F) ch = '?'; // 非法字符兜底 p = &ASCII_8x16[(ch - 0x20) * 16]; for (i = 0; i < 16; i++) { // 16 行 pixel = p[i]; for (j = 0; j < 8; j++) { // 每行 8 个像素 if (pixel & (0x80 >> j)) { // 高位在前,j=0 是最左侧 TFT_DrawPixel(x + j, y + i, color); } else if (bg != color) { TFT_DrawPixel(x + j, y + i, bg); } } } }

参数说明:color是字体颜色,bg是背景色。背景色设置成和color相同可以跳过画背景,提高刷新速度;如果你要在图片背景上叠加文字,就把bg设成透明色的一种粗糙替代方案——不过这里没有真正的 alpha 混合,「透明」实际是「不画背景像素」,前提是背景刚好是纯色。

字模数据在font.h里用const数组存放:

const uint8_t ASCII_8x16[][16] = { /* 空格 ' ',ASCII 0x20 */ {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /* 感叹号 '!',ASCII 0x21 */ {0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00}, /* ... 更多字符 ... */ };

取模时注意一个高频踩坑点:取模软件里「顺向」和「逆向」分别对应高位在前和低位在前。如果你用pixel & (0x80 >> j)去解析,取模时必须设置成「高位在前(MSB first)」,不然显示出来的字符是水平镜像的。我见过太多人屏幕画线画圆都正常,一显示字符就反着,最后检查发现是取模方向问题,白调半天。

4.5 主程序 Demo:让屏幕滚动起来

最后是main.c里的演示逻辑。用定时器产生 tick,每隔一秒钟在屏幕上刷新一组模拟数据(比如把电压值显示成字符串),顺便画一条动态的进度条:

int main(void) { uint16_t voltage; char buf[20]; SystemInit(); delay_init(); SPI2_Init(); TFT_Init(); while (1) { voltage = Read_ADC(); // 假设从某个通道采电压值 sprintf(buf, "VOL: %d.%02dV", voltage / 100, voltage % 100); TFT_DrawString(8, 10, buf, TFT_GREEN, TFT_BLACK); TFT_FillRect(8, 40, 8 + (voltage % 120), 48, TFT_RED); delay_ms(500); } }

这段代码展示了这个工程的扩展思路:业务代码只关心「读数据 -> 格式化字符串 -> 调用 TFT 接口」,完全不涉及 SPI 细节。你想换成 DHT11 温湿度、MPU6050 姿态角或者 GPS 坐标,改的就是Read_ADC()那部分,界面代码继续复用。

5. 避坑手册:白屏、镜像、跑飞和 DMA 颜色错位的四个翻车现场

5.1 现象:屏幕全白,复位引脚明明已经拉高过

原因:白屏说明屏幕没有收到有效初始化序列,最常见的是 RES 引脚复位时序不对。ST7735S 要求 RES 拉低后保持至少 10ms 再释放,而很多人用 GPIO 模拟复位时只延时了 1ms,甚至初始化代码里根本忘了拉低这一步。另一个高发原因是 DC 引脚配置成了复用推挽(因为它旁边就是 SPI 引脚,容易复制粘贴错配置)。

解决:硬件复位时把TFT_RES_LOW()之后延时加大到 20ms,释放后延时 50ms 再送软件复位命令;检查 DC、CS、RES 的GPIO_Mode是否都是GPIO_Mode_Out_PP,不是复用推挽。我遇到这种白屏问题,八成都是 DC 引脚错了——DC 一旦配置错误,命令和数据全部写进一个通道,屏幕不知道你到底要干嘛。

5.2 现象:字符上下颠倒或左右镜像,但图形显示正常

原因:图形正常说明像素写入链路没问题,问题出在扫描方向。ST7735S 的 MADCTL(0x36)寄存器默认值往往不是你要的方向,显示器的 X/Y 坐标和扫描方向不匹配时,字符就会呈现镜像或翻转。取模方向错误也会造成镜像,但那是逐字符镜像,和整屏镜像有明显区别。

解决:整屏镜像就调TFT_WriteCmd(0x36)后面的参数。常见的调试路径是:左右镜像改 bit6(MX),上下镜像改 bit7(MY),想旋转 90 度改 bit5(MV)。从0xC8开始试,每次改一个 bit,观察变化方向——这个过程不用重新编译下载,用串口下发命令调试最快。

5.3 现象:程序一进入显示图片的函数就 HardFault

原因:HardFault 基本可以锁定是内存问题。最常见的是在函数内部定义了一个大数组,比如uint8_t img[1024],局部变量是放在栈里的,而默认的启动文件把栈大小设成 0x400(1KB),一个 1KB 数组直接把你栈顶撑爆。另一种情况是图片数组用const放在了 Flash,但取了地址后按 RAM 方式写,直接触发总线错误。

解决:图片/字模数据一律放到文件作用域并加const,让它进 Flash 的只读区;如果确实需要 RAM 缓存,定义成全局变量而不是局部变量。想省 RAM 的话,可以调启动文件里的Stack_Size和Heap_Size——但治本的方法是把大缓存挪出栈。从那以后我只要往工程里加大数组,一定先问自己一句「这个数组在 Flash 还是 RAM、在全局还是局部」。

5.4 现象:用 DMA 刷屏时颜色错位,红色变成蓝色

原因:DMA 刷屏高效,但有一个隐蔽的坑:SPI2 外设地址按字节对齐要求,而你在内存里维护的是一个uint16_t数组,DMA 传输宽度设成了DMA_PeripheralDataSize_Byte,结果每个像素的高字节和低字节被打乱。

解决:DMA 刷屏时把外设数据宽度和内存数据宽度都设为半字(DMA_PeripheralDataSize_HalfWord和DMA_MemoryDataSize_HalfWord),缓存数组用uint16_t *类型,传输前把颜色数组按 16 位打包。不要图省事把uint16_t强制转换成uint8_t *然后按字节传——那是能跑,但颜色字节顺序就乱了,而且这种错位几乎无法通过看波形察觉,只能靠代码审查发现。

6. 二次开发:把「点亮」升级成「能用」的三个方向

第一个改造方向是刷新提速。当前直接写屏模式在绘制复杂界面时会有明显的从上到下的扫描感,想提速有两个手段:把 SPI2 预分频从 4 改成 2,SCK 从 9MHz 提到 18MHz,但这条对飞线质量敏感;更稳妥的是开 DMA 传输,SPI 发送不占 CPU,刷一屏的时间可以缩短近一半,代价是代码复杂度上一个台阶——要管理 DMA 传输完成中断和缓冲区的生命周期。

第二个改造方向是加图片和汉字字库。512KB 的 Flash 放不下一整张全屏图片池和 GBK 字库,常见做法是外挂一块 SPI Flash:开机时把字库从 SPI Flash 读到 RAM 或直接按页读取,图片素材用「先瘦身再压缩」的思路存储。小屏上 128x128 的图片转成 RGB565 后是 32KB,如果是 24 位色 BMP 再缩放到 16 位色,体积还能再降。这样改下去,工程就从「能点亮一块屏」进化成「能做一个离线图片浏览器」了。

第三个方向是把屏幕变成某个系统的显示终端。比如接一个 DHT11 做温湿度记录仪,或者接 MPU6050 做姿态显示。你会发现这套三层架构的价值在这里体现得最充分:传感器数据解析写在 App 层,业务逻辑改来改去,bsp_tft.c里的画点函数从来不需要动。我一般会把demo_ui.c里那些模拟数据全部删掉,换成真实的数据源,再定义一个 100ms 的刷新 tick,屏幕内容自然就活起来了。

这次的教训是:调屏幕花了一个晚上,最后发现是 MADCTL 一个字节的问题。从那以后我每次初始化屏幕都强制走一遍「硬件复位时序 -> SPI 字节自检 -> 纯色填充 -> 方向调试」的流程,一步都不跳。1.44 寸屏确实不大,但调试方法论和别的屏幕完全一样。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 23:11:13

把零散技能变成结构化资产:个人技能管理体系搭建指南

很多人的工作区里都有一个名为“skills”的项目&#xff0c;或者是一个专门的仓库。我见过不少开发者&#xff0c;攒了三四年技术清单&#xff0c;从框架到中间件&#xff0c;记了好几百条&#xff0c;真到写简历、做汇报、规划学习路径的时候&#xff0c;却一条都用不上。这个…

作者头像 李华
网站建设 2026/10/21 23:10:36

图书系统软件测试课程设计:从测试需求分析到用例设计全流程

简介&#xff1a;这份资源是一份面向软件工程、计算机相关专业学生的软件测试课程设计报告&#xff0c;主题为图书管理系统软件测试&#xff0c;适合正在完成测试类课程设计或需要撰写测试文档的读者参考。报告围绕图书管理系统展开&#xff0c;涵盖测试需求分析、测试概要、测…

作者头像 李华
网站建设 2026/10/21 23:10:30

AI技术更新太快怕过时?分层学习法帮你建立稳固能力护城河

这几天在几个开发者社群里&#xff0c;看到不少人有同样的困惑&#xff1a;花了好几个月学某个框架&#xff0c;结果新版本一出来&#xff0c;接口全变了&#xff1b;刚把一个主流模型调通&#xff0c;转头又听说某开源社区出了个更强的方案&#xff1b;甚至有人开玩笑说&#…

作者头像 李华
网站建设 2026/10/21 23:09:02

恒定驱动原子阵列+神经网络:突破标准量子极限的新型传感范式

1. 为什么“恒定驱动下的原子阵列”突然成了量子传感的新突破口&#xff1f;最近在几个实验室的组会上&#xff0c;我连续听到三组不同方向的同事不约而同地提到一个词&#xff1a;“恒定驱动”。不是脉冲、不是调制、不是反馈闭环——就一根稳稳的直流偏置电压&#xff0c;或者…

作者头像 李华
网站建设 2026/10/21 23:08:19

数据库原理核心解析:从文件存储痛点谈关系模型与事务索引

1. 数据库到底解决什么问题——从一个文件存储的困局讲起1.1 没有数据库的年代&#xff0c;程序是怎么存数据的很多初学者一上来就背数据库原理的概念&#xff0c;结果越背越糊涂。我想换个方式&#xff0c;先回到一个老问题&#xff1a;如果我们不用数据库&#xff0c;程序要怎…

作者头像 李华
网站建设 2026/10/21 23:08:19

PHP文件包含漏洞实战:用php://filter读取源码与过滤器链解析

搞PHP安全测试的朋友&#xff0c;几乎没有不跟文件包含打交道的。而php://filter伪协议&#xff0c;可以说是这类漏洞里最实用的一板斧&#xff1a;它能在你只有一个不算起眼的包含点时&#xff0c;把服务器上的PHP源码直接“隔空”读出来&#xff0c;甚至在特定条件下&#xf…

作者头像 李华