从零开始:在STM32上驱动ST7735实现高效图形绘制
你有没有遇到过这样的情况?项目已经跑通了传感器数据采集,逻辑控制也写得差不多了,但用户却一脸茫然:“这东西到底在干什么?”——因为没有直观的反馈界面。
这时候,一块小巧便宜、能显示彩色图形的小屏幕就成了救星。而ST7735 + STM32的组合,正是解决这类问题的经典答案。
它不像段码屏那样只能显示数字,也不像搭载LVGL的大屏方案那样吃内存、烧Flash。它是轻量级可视化系统的“甜点”选择:成本低至几块钱,分辨率够用(128×160),颜色丰富(65K色),且完全可以用Cortex-M3/M4级别的MCU原生驱动。
本文将带你一步步打通从硬件连接到像素绘制的完整链路,不依赖GUI框架,手把手写出高效的底层驱动代码,让你真正掌握“如何让第一个像素亮起来”的全过程。
为什么是ST7735?一个小而强的TFT控制器
市面上的TFT驱动芯片不少,比如更常见的ILI9341(常用于2.4寸屏),那为什么我们选ST7735?
简单说:小尺寸、低功耗、接线少、启动快、价格便宜。
ST7735专为1.44~1.8英寸的小型TFT模块设计,典型分辨率为128×160。虽然物理驱动区域是132×162,但有效可视区通常裁剪为128×160。它的最大优势在于高度集成:
- 内置升压电路,可直接生成LCD所需的偏压;
- 支持SPI四线通信(SCK、MOSI、CS、DC)+ RST共5根控制线即可工作;
- 提供标准命令集,兼容主流MCU;
- 可配置RGB565色彩模式,每像素仅需2字节,非常适合资源受限系统。
更重要的是,国产厂商大量生产基于ST7735的廉价TFT模块,BOM成本极低,适合批量应用或DIY项目。
硬件怎么连?五线制SPI就够了
要让STM32和ST7735对话,首先要接好物理线路。以下是推荐的引脚映射:
| STM32引脚 | 连接到ST7735 | 功能说明 |
|---|---|---|
| PA5 | SCK | SPI时钟线 |
| PA7 | MOSI | 主机发送,从机接收 |
| PA4 | CS | 片选信号,低电平有效 |
| PA8 | DC | Data/Command选择 |
| PA9 | RST | 复位引脚,低电平复位 |
注意:VCC接3.3V,部分模块支持5V耐压,但务必查看规格书确认!GND必须共地。
其中最关键的是DC 引脚—— 它决定了当前传输的是“命令”还是“数据”。
- 当DC = 0:表示接下来发送的是命令(如0x2A设置列地址);
- 当DC = 1:表示接下来发送的是参数或像素数据。
这种机制使得主机可以通过简单的GPIO切换来区分两类操作,无需额外协议开销。
SPI通信怎么配?HAL库轻松搞定
STM32的SPI外设非常成熟,使用HAL库可以快速初始化。我们需要配置为Mode 0(CPOL=0, CPHA=0),即空闲时SCK为低电平,在上升沿采样数据。
建议SPI波特率预分频设为fpclk / 16或更低(例如72MHz主频下约为4.5MHz),以保证信号稳定性,尤其在面包板或长导线上更容易出错。
// SPI1 初始化示例(使用CubeMX生成后调整) hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_1LINE; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // ~4.5MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;由于ST7735只接收不回传,我们可以安全地禁用MISO,节省一个IO口。
最关键一步:初始化序列不能错
很多初学者遇到“黑屏”、“花屏”、“闪一下就灭”的问题,根源往往出在初始化流程不完整或延时不准确。
ST7735上电后处于睡眠状态,必须经过一系列精确的命令和等待才能唤醒。不同厂商的模块略有差异(比如Adafruit和WaveShare的偏移不同),但基本流程一致:
标准初始化步骤如下:
- 拉低RST至少10ms → 实现硬件复位;
- 拉高RST,延时120ms等待内部电源稳定;
- 发送软复位命令
0x01(可选); - 发送
0x11退出睡眠模式,必须延时≥120ms; - 配置像素格式为16位(
0x3A, 参数0x05); - 设置内存访问方向(
0x36,决定坐标系原点); - 设置列地址(
0x2A)和页地址(0x2B)范围; - 开启显示(
0x29)。
下面是优化后的初始化函数:
void ST7735_Init(void) { // 硬件复位 HAL_GPIO_WritePin(ST7735_RST_GPIO_Port, ST7735_RST_Pin, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(ST7735_RST_GPIO_Port, ST7735_RST_Pin, GPIO_PIN_SET); HAL_Delay(120); ST7735_WriteCmd(0x11); // Sleep out HAL_Delay(120); ST7735_WriteCmd(0x36); // MADCTL: 控制屏幕扫描方向 ST7735_WriteData(0xA0); // RGB顺序,从左到右,从上到下 ST7735_WriteCmd(0x3A); // COLMOD: 设置接口像素格式 ST7735_WriteData(0x05); // 16-bit/px, RGB565 ST7735_WriteCmd(0x21); // INVON: 开启显示反转(可选,增强对比) // 设置列地址范围 (0~127) ST7735_WriteCmd(0x2A); ST7735_WriteData(0x00); ST7735_WriteData(0x00); ST7735_WriteData(0x00); ST7735_WriteData(0x7F); // 设置页地址范围 (0~159) ST7735_WriteCmd(0x2B); ST7735_WriteData(0x00); ST7735_WriteData(0x00); ST7735_WriteData(0x00); ST7735_WriteData(0x9F); ST7735_WriteCmd(0x29); // Display ON }⚠️ 特别提醒:
0x11之后的120ms延时不可省略!否则内部稳压未建立,可能导致显示异常。
坐标与颜色:理解GRAM是如何被更新的
ST7735内部有一块叫GRAM(Graphic RAM)的显存区域,大小为132 × 162 × 18bit,但我们通常只使用其中的128×160区域。
每次绘图前,必须先通过CASET (0x2A)和PASET (0x2B)命令划定一个“窗口”,然后发送RAMWR (0x2C)命令,后续所有数据都会按行优先顺序自动填入该窗口内的GRAM中。
关于坐标偏移
有些模块的实际显示区域并不是从(0,0)开始的,存在物理偏移。常见的是x_offset = 2, y_offset = 1,因此我们在结构体中定义设备参数:
static st7735_dev_t dev = { .width=128, .height=160, .x_offset=2, .y_offset=1 };这个偏移必须在绘图时补偿,否则会出现边缘缺失或错位。
颜色格式:RGB565怎么算?
每个像素占2字节(16位),格式如下:
Bit: 15-------------------------0 RRRRR GGGGGG BBBBB例如红色(255,0,0)转换为:
color = ((255 >> 3) << 11) | ((0 >> 2) << 5) | (0 >> 3); // 即: 0b11111 << 11 = 0xF800你可以封装一个宏来简化转换:
#define RGB565(r,g,b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3))画点是基础,批量写才是性能关键
一切图形绘制都始于“画点”。下面是最基础的DrawPixel函数:
void ST7735_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x >= dev.width || y >= dev.height) return; x += dev.x_offset; y += dev.y_offset; ST7735_SetAddressWindow(x, y, 1, 1); // 设置单像素窗口 ST7735_WriteCmd(0x2C); // 写GRAM ST7735_WriteData(color >> 8); ST7735_WriteData(color & 0xFF); }但如果你用这个函数去画一个实心矩形,效率会非常低——每画一个点都要重新设置地址窗口,导致SPI事务频繁,刷新慢得肉眼可见。
正确做法:批量写入
我们应该一次性设置一个矩形区域,然后把所有像素数据打包发送:
void ST7735_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint32_t total = w * h; uint8_t *buf = malloc(total * 2); for (uint32_t i = 0; i < total; i++) { buf[2*i] = color >> 8; buf[2*i + 1] = color & 0xFF; } ST7735_SetAddressWindow(x, y, w, h); ST7735_WriteDataBuffer(buf, total * 2); free(buf); }💡 提示:若RAM紧张,也可采用分块发送方式,每次发256字节,避免动态分配大缓冲区。
更进一步:线条、矩形、文本都能自己写
有了FillRect,就可以轻松实现各种图形:
绘制空心矩形
void ST7735_DrawRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { ST7735_FillRect(x, y, w, 1, color); // 上边 ST7735_FillRect(x, y + h - 1, w, 1, color); // 下边 ST7735_FillRect(x, y, 1, h, color); // 左边 ST7735_FillRect(x + w - 1, y, 1, h, color); // 右边 }绘制水平/垂直线(比逐点快)
void ST7735_DrawHLine(uint16_t x, uint16_t y, uint16_t w, uint16_t color) { ST7735_FillRect(x, y, w, 1, color); } void ST7735_DrawVLine(uint16_t x, uint16_t y, uint16_t h, uint16_t color) { ST7735_FillRect(x, y, 1, h, color); }显示字符(基于点阵字体)
假设你有一个8×16的ASCII字体数组:
extern const uint8_t font8x16[95][16]; // 存储' ' to '~' void ST7735_DrawChar(uint16_t x, uint16_t y, char c, uint16_t color) { uint8_t idx = c - ' '; for (int row = 0; row < 16; row++) { uint8_t bits = font8x16[idx][row]; for (int col = 0; col < 8; col++) { if (bits & (0x80 >> col)) { ST7735_DrawPixel(x + col, y + row, color); } } } }虽然逐点绘制较慢,但对于少量文字仍可用。如需高性能文本渲染,建议启用DMA传输整行数据。
性能瓶颈在哪?SPI带宽说了算
ST7735的最大理论刷新率受限于SPI速率。以4.5MHz为例:
- 每秒传输位数:4.5M bit/s
- 每像素2字节 = 16 bit
- 全屏像素数:128 × 160 = 20,480 px
- 全屏数据量:20,480 × 2 = 40,960 字节 ≈ 327,680 bit
- 理论刷新率:4.5M / 327.68k ≈13.7 fps
也就是说,即使全速运行,也只能做到约14帧/秒的全屏刷新。
如何提升体验?
- 局部刷新:只更新变化区域,减少数据量;
- 使用DMA:释放CPU,实现后台传输;
- 压缩静态内容:图标、背景图可存储在Flash中按需加载;
- 背光PWM调光:通过PA6输出PWM控制BLK引脚,调节亮度节能。
实战建议:这些坑你一定要避开
电源不够稳?屏幕一闪就灭
ST7735启动瞬间电流可达50mA以上,LDO带载能力不足会导致电压跌落。建议使用独立LDO或增加10μF陶瓷电容。SPI走线太长?出现乱码
尽量缩短飞线长度,必要时在SCK线上串联100Ω电阻抑制反射。不同模块偏移不同?画面偏移或裁剪错误
记住:有的模块x_offset=0,有的是2,务必根据实际型号调整。用了DMA却卡住?记得开启DMA中断并正确处理完成回调
否则可能在传输未完成时就开始下一次操作。想省Flash?别把整个帧缓冲放RAM里
128×160×2 = 40KB,多数STM32没这么多SRAM。应采用“直接写屏”模式,边计算边发送。
结语:这是通往嵌入式GUI的第一步
看到这里,你应该已经掌握了如何在STM32上点亮一块ST7735屏幕,并亲手实现了基础图形绘制。
这套方案不需要任何第三方GUI库,代码体积小,执行效率高,特别适合以下场景:
- 智能仪表盘(温度、湿度、PM2.5)
- 手持测试工具(LCR表、频率计)
- 教学实验平台
- DIY游戏机、电子相册
下一步,你可以尝试:
- 添加触摸功能(配合XPT2046实现点击交互);
- 移植小型GUI引擎(如GUIslice或LittlevGL Lite);
- 使用外部SPI Flash存放图片资源;
- 在FreeRTOS中创建独立显示任务,实现非阻塞刷新。
当你能在指尖掌控每一个像素的时候,真正的嵌入式视觉世界才刚刚打开大门。
如果你正在做一个需要本地显示的小项目,不妨试试这块几块钱的TFT模块,也许它就是你产品体验升级的关键一环。
欢迎在评论区分享你的ST7735实战经验,或者提出你在驱动过程中遇到的问题,我们一起探讨解决方案。