
简介面向嵌入式开发者和电子制作爱好者这份ST7789V2驱动示例代码展示了在SPI/I2C接口液晶屏上显示字符与图片的完整实现。压缩包一共包含2个文件分别为C源文件与头文件整体只有6KB代码量非常精简。其中C源文件承担了控制器初始化、像素坐标设置、颜色数据转换等实际函数头文件则声明了相关接口与常用常量这样的分工便于快速整合到现有工程中。在功能方面代码覆盖初始化时序、ASCII字符点阵映射、图片数据到RGB565格式的转换、命令与数据的区分发送以及显示刷新等关键环节并加入了基础错误处理逻辑。作者亲测可用目前已有3880人学习下载对于正在调试小尺寸LCD屏或希望了解ST7789V2底层驱动的开发者这份资源提供了清晰的代码框架可直接借鉴并适配到智能手表、微型显示屏等项目中无论是学习LCD驱动原理还是进行快速原型验证都能从中获得实用参考。 ST7789V2 这颗驱动芯片玩嵌入式屏幕的人应该都不陌生。手里这块 1.3 寸 240x240 的 IPS 屏就是它网上资料不少但很多代码贴出来要么缺两行、要么初始化时序不对真正能拿到手直接编译烧录就能跑的不多。我这次把字符显示和图片显示的代码都调通了从零开始到全屏刷图前后折腾了一个晚上整理出来给大家一个可以直接抄作业的版本同时把每一步背后的原理也讲清楚免得你只是代码跑通了下次换个屏幕换个 MCU 又抓瞎。如果你手里刚好有 ST7789V2 的模块或者正准备买一块来玩这篇文章能帮你跳过大部分我踩过的坑。无论你用 STM32、ESP32 还是 GD32驱动思路完全一样只要我会把寄存器级别的代码给出来你移植的时候照着改底层 SPI 接口就行。这里多说一句我全程用的是标准 SPI 四线模式驱动不是八线并口这也是市面上大多数这类小板子常用的接法。1. 屏能亮起来事情就成了一半硬件准备与接线1.1 先确认你手里的屏确实是 ST7789V2这听起来像废话但真不是。淘宝上很多 1.3 寸 IPS 屏和 0.96 寸的屏外观长得几乎一模一样背面丝印乱七八糟有的写的是 ST7789V2有的写 V3甚至有写 ST7735 的。我第一次拿到板子看丝印写的是 TFT_V2想当然以为就是 ST7789结果读 ID 才发现是 ST7735S初始化序列完全不是一回事白折腾了一个多小时。判断方式有两个。第一直接看模块背面的 IC 丝印ST7789V2 会在芯片上印一行很细的字眼神好的拿放大镜能认出来。第二更可靠的办法是写代码读 IDST7789 的读 ID 指令是 0x04正常读回来三位数据我这块屏读出来是 0x85对应 ST7789V2。如果你代码烧进去屏幕有反应但花屏严重建议先做这一步确认别在错误的驱动上浪费时间。1.2 接线清单与电平匹配标准 SPI 屏幕一共八个引脚分别是 VCC、GND、DIN也叫 SDA/MOSI、CLKSCK、CS片选、DC数据/命令选择、RST复位、BLK背光。屏幕引脚STM32 示例ESP32 示例说明VCC3.3V3.3V部分模块标 5V但 IO 电平必须 3.3VGNDGNDGND共地必须连DINPA7 (SPI1_MOSI)GPIO23主机输出CLKPA5 (SPI1_SCK)GPIO18SPI 时钟CSPA4GPIO5片选低有效DCPA3GPIO17高电平为数据低电平为命令RSTPA2GPIO16复位低有效BLKPA1GPIO4背光控制高电平点亮接线里面最容易被忽略的是 BLK 脚。有些模块的 BLK 引脚悬空时默认就是亮的但更多模块必须拉高否则整个屏幕是黑的但你量电压又发现屏的电源、命令都正常非常迷惑。我建议不管什么模块BLK 脚直接接一个 GPIO 并初始化为高电平顺手还能做背光调节。另外一个坑是 VCC 供电1.3 寸这块屏满背光电流大概 30-50mA单片机 3.3V 直接供电没问题但如果同时给 ESP32 的 WiFi 和外设供电建议单独用 LDO避免背光导致的电压跌落。再一个是 SPI 速率。ST7789V2 的极限 SPI 时钟在 30MHz 左右理论上可以跑很高但我实测在 STM32F103 上把 SPI 预分频设成 236MHz时偶尔会出现花屏降到 418MHz就非常稳定。ESP32 上跑到 40MHz 也没事这跟具体的 MCU 的 SPI 输出质量和杜邦线长度都有关系。如果你用杜邦线连接建议不要超过 20MHz用 PCB 短连线的话可以适当提高。2. 初始化序列里没有一行是多余的2.1 复位时序与基础初始化ST7789V2 上电之后第一件事是复位而且时序有讲究拉高 RST等至少 10ms拉低至少 10ms再拉高然后等 120ms。这个 120ms 是让内部 DCDC 和振荡器稳定下来的时间一定不能省。我一开始图省事延时写短到 50ms屏幕偶发不亮或者显示错乱后来老老实实恢复 120ms 就再没出过问题。复位完成后依次发送初始化命令序列。下面这个是我亲测可用的完整序列每一步的寄存器作用我会在下一小节解释。void ST7789_Init(void) { // 复位时序 ST7789_RST_HIGH(); delay_ms(10); ST7789_RST_LOW(); delay_ms(10); ST7789_RST_HIGH(); delay_ms(120); // 软件复位 ST7789_WriteCmd(0x01); delay_ms(150); // 退出睡眠模式 ST7789_WriteCmd(0x11); delay_ms(200); // 像素格式RGB565 ST7789_WriteCmd(0x3A); ST7789_WriteData(0x05); // 扫描方向左上角开始默认 RGB 顺序 ST7789_WriteCmd(0x36); ST7789_WriteData(0x00); // 关闭颜色反转部分模块需要 0x21 开启反转否则偏色 ST7789_WriteCmd(0x20); // 电压相关设置不同模块略有差异 ST7789_WriteCmd(0xB0); ST7789_WriteData(0x00); ST7789_WriteCmd(0xB1); ST7789_WriteData(0x00); ST7789_WriteCmd(0xB2); ST7789_WriteData(0x00); // 开启显示 ST7789_WriteCmd(0x29); delay_ms(50); }这套序列发完之后屏幕应该处于点亮状态如果你这时候往显存里写数据就能看到对应的色块。我用的是标准四线 SPI命令和数据通过 DC 脚区分DC 拉低发命令拉高发数据。这个逻辑贯穿后面所有的显示操作。2.2 三个关键寄存器像素格式、扫描方向和颜色反转很多人的屏幕能亮但显示不对问题基本集中在三个寄存器上。 第一个是 0x3A像素格式。我们传 0x05 表示 RGB565 格式也就是每个像素 16 位红 5 位、绿 6 位、蓝 5 位。有些屏默认是 18 位模式0x06你以 16 位数据去刷屏颜色会很怪最常见的是偏绿或偏紫。做字符和图片显示之前先把这一步焊死。第二个是 0x36MADCTL 控制寄存器。这个寄存器的 bit7、bit6、bit5 分别控制行方向、列方向和行列交换bit3 控制 RGB 还是 BGR 序。我传 0x00表示从左上角开始正常扫描、RGB 颜色序。如果你的屏幕内容整体旋转了 90 度或者左右镜像改的就是这个寄存器的值。比如你想旋转 180 度可以试试 0xC0如果显示出来的图片颜色中红色和蓝色互换了就把 bit3 置 1改成 0x08。这个值没有绝对标准跟屏幕模组的排线接法有关同一颗芯片封装成不同模块可能也要调。第三个是颜色反转控制指令是 0x20 和 0x21。大部分 IPS 屏用 0x20关闭反转是正常的但确实有一部分模组内部电路设计得比较奇怪必须用 0x21 打开反转否则你往显存写白色屏幕显示黑色整屏颜色跟负片一样。这个没法从芯片规格书里直接判断只能实测我当时第一次用这块屏就是这种情况后来加了一行 0x21 立刻正常。你调试的时候如果其他都正常、颜色反了优先试这个。3. 字符显示十六字节一个字的魔法3.1 点阵字模是怎么来的字符在屏幕上的本质就是一堆点的亮和灭。对于 ASCII 字符最常见的是 8x16 的点阵也就是每个字符用 8 列、16 行来表示。每一行用一个字节来存这一字节的 8 个 bit 对应 8 列像素bit 为 1 就点亮为 0 就灭。这样一个字符正好 16 个字节看起来是十六字节一个字。平时我们用取模软件比如 PCtoLCD2002 或者 Image2Lcd把想要的字体生成数组。这里要特别注意一个地方取模方向。不同的取模软件、不同设置生成的数据排列顺序完全不同常见的有横向取模和纵向取模两种。我在本文的代码里用的是每一行一个字节、从左到右排列的方式也就是横向取模高位在前。如果你从网上找的字模拷贝过来发现字符左右颠倒或者整个倒过来不要怀疑代码先去确认取模方向是否和绘制函数匹配。比如大写的字母 A在 8x16 点阵里前面若干行可能是 0x00中间开始出现 0x18、0x24、0x42 这些值把这些字节按顺序填进去屏幕就会拼出一个 A。字模文件本质上是把人的字形翻译成机器能读的数字理解这一点你就算离开取模软件自己手工构造一个字符也能办到。3.2 绘制函数实现与取模方向匹配下面这套代码是我在实际项目里用的包含画点、画字符、画字符串三个层级。画点是最底层的操作字符和图片最终都会拆成一个一个像素点来写入。void ST7789_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ST7789_WriteCmd(0x2A); ST7789_WriteData16(x0); ST7789_WriteData16(x1); ST7789_WriteCmd(0x2B); ST7789_WriteData16(y0); ST7789_WriteData16(y1); } void ST7789_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { ST7789_SetWindow(x, y, x, y); ST7789_WriteCmd(0x2C); ST7789_WriteData16(color); }画字符的代码假设字模数组是 font8x16[95][16]从空格符号0x20开始存void ST7789_DrawChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bgColor) { const uint8_t *glyph font8x16[(uint8_t)ch - 0x20]; for (uint16_t row 0; row 16; row) { uint8_t data glyph[row]; for (uint16_t col 0; col 8; col) { if (data (0x80 col)) { ST7789_DrawPixel(x col, y row, color); } else if (bgColor ! color) { ST7789_DrawPixel(x col, y row, bgColor); } } } }注意这里 data 是当前行的 8 个像素状态从最高位开始判断。判断为 1 就画前景色为 0 就画背景色。如果你不想画背景色可以把背景色参数改成透明模式跳过但注意屏幕刷新是有痕迹的不填充背景色的话字符移动时会留下残影。字符串就是依次调用单个字符绘制函数同时让 x 坐标偏移 8 像素void ST7789_DrawString(uint16_t x, uint16_t y, const char *str, uint16_t color, uint16_t bgColor) { while (*str) { ST7789_DrawChar(x, y, *str, color, bgColor); x 8; if (x 240 - 8) { x 0; y 16; } } }你要显示中文字符原理完全一样只是字模不再只有 16 字节而是 16x16 点阵占 32 字节甚至更大。中文字的取模参数需要对得更仔细因为不同字库、不同编码取模出来的字节序差异更大。我建议把常用汉字做成字库数组直接放进代码里而不是临时从 SD 卡读这样实时性高得多。当然MCU 的 Flash 如果紧张像 STM32F103C8T6 只有 64KB塞几个中文汉字没问题塞整套字库就够呛了。4. 图片显示RGB565 才是主战场4.1 从图片到数组单片机上显示图片和电脑上完全不是一回事。电脑上你给个 PNG 路径就能读单片机上你需要把图片转成一串裸的像素数组。这串数组在 FLASH 里占一个坑显示的时候把位图数据按顺序写进屏幕显存即可。拿 Image2Lcd 这个工具举例它是做这个用的经典工具。打开一张图片后有几项设置必须先确认。输出类型选C 语言数组。最大宽度和高度设为 240 和 240和屏幕分辨率一致也可以小于屏幕分辨率显示的时候通过开窗让它居中。颜色格式也就是像素格式必须选 16 位真彩色RGB565。扫描方式是水平扫描。最后生成的是一个 uint16_t 数组数组里每个元素就是一个像素的颜色值比如 0xF800 是红色0x07E0 是绿色0x001F 是蓝色。为什么强调 RGB565因为 ST7789V2 初始化时如果设了 0x3A0x05屏幕就是按 16 位颜色读取数据数据线的 D0-D15 对应颜色值的各位。你在取模时就转成 RGB565后面 MCU 就不需要再做任何格式转换直接写进显存就行。如果在 PC 上处理好格式MCU 端只负责搬运这能省下大量计算时间。如果交给 MCU 现算比如从 SPI Flash 读一张 RGB888 的图再转 RGB565性能会差非常多不推荐。4.2 开窗写数据的高效刷新显示图片的核心在于开窗操作。前面 SetWindow 函数里的 0x2A 和 0x2B 就是设置 X 方向和 Y 方向的显示窗口。ST7789 的显存是按行列组织的设置好窗口之后连续写 0x2C数据就会一行一行自动填充到这个窗口内。窗口开得越小写的数据量越少刷新越快。void ST7789_ShowImage(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *img) { ST7789_SetWindow(x, y, x w - 1, y h - 1); ST7789_WriteCmd(0x2C); for (uint32_t i 0; i (uint32_t)w * h; i) { ST7789_WriteData16(img[i]); } }这段代码看起来很简单但对小白有个隐藏的优化点在 for 循环里逐次调用 WriteData16每调用一次就要操作一次 GPIO 或 SPI 发送寄存器开销非常大。更好的做法是打开 DMA让 SPI 外设自动从数组搬数据。在 STM32 上可以这样改先开启 SPI 的 DMA 发送通道然后把数组首地址和长度告诉 DMA之后就什么都不用管了。240x240 全屏一共 57600 个像素每个像素 2 字节也就是 115200 字节的数据量对 STM32F103 的 Flash 来说直接放 const 数组已经很大了如果放不下后续再说解决办法。要注意一个细节如果你的图片数组是从 Image2Lcd 生成的它默认包含了图片尺寸信息甚至还可以设置包括头文件生成一个带 IMAGE_HEADER 的格式。我建议生成的时候去掉这些额外的结构只要纯色数据数组这样 MCU 代码里使用更自由。4.3 大图跑不动的出路很多玩 STM32 的朋友会卡在一个很现实的问题上Flash 不够存图片。一块 240x240 的 RGB565 图片就要 115200 字节STM32F103C8T6 的 Flash 才 64KB一张图都放不下。有两条路可以走一是缩小图片显示区域。把图片预裁剪成 128x128 或 160x128 之类的尺寸配合开窗函数在有需要的位置显示。我实际项目里做菜单界面常用图标都是 48x48 或者 64x64一张图几 KB放上几十个都没问题。二是把图片放到外部 Flash比如 W25Q64 这类 SPI NOR Flash。图片数据可以直接用 ST-Link 配合烧录软件写进去MCU 上电后从 Flash 里周期性读出数据再送入屏幕。这个做法会多一倍的搬运开销但可以让一张板子存几百张图片。如果你的屏幕分辨率更大比如 320x240 的屏一张图 150KB 起外部 Flash 几乎是唯一出路。全屏刷新耗时上可以做一个预估SPI 时钟按 20MHz 算每次时钟传 1 bit传输 115200 字节 921600 bit理论最快 46ms实际加命令开销和 GPIO 翻转整屏刷新大概 70-80ms。这个速度做静态界面完全够用但如果要刷视频流畅度要求 30fps 即每帧小于 33ms就需要把 SPI 提到 40MHz 以上并且配合 DMA 双缓冲才可能达到。5. 实测记录与常见故障快速定位5.1 本次实测环境我是在 STM32F103C8T6 上做的验证SPI1 驱动时钟 18MHz屏幕是淘宝几块钱一块的 1.3 寸 240x240 IPS 模组背面丝印 TFT_V2。复位、初始化、画字符、显示全屏图片整个过程一次跑通后连续运行了一周没出过问题包括反复断电上电、复位以及温度变化的场景。字符显示用的是上面给的自绘字体方案因为只用了数字和 26 个字母16 字节一个字符总共 95 个字符才 1520 字节Flash 压力非常小。图片我放了 5 张 64x64 的图标加一张 240x240 全屏图Flash 也够用。另外我还顺手在 ESP32 上测试了同一套初始化序列只把底层 SPI 发送函数替换成 ESP32 的 SPI 库其他部分完全没动同样正常。这说明这套驱动代码跨平台移植性没问题问题只可能在 SPI 底层接口上。这个结论对想从 STM32 换到 ESP32 的朋友应该有用。5.2 几个让我查了半天的坑第一个坑是屏幕一直黑屏。现象很诡异代码和接线都检查过逻辑分析仪看 SPI 波形也有数据但屏幕就是黑的。后来发现是 RST 引脚没初始化好复位信号给的太随意。ST7789 上电时如果 RST 引脚悬空内部上电复位有时候不可靠必须由 MCU 主动拉一次低电平复位时序。第二个坑是全屏刷新时出现水平条纹就像刷新到一半卡住一样。我最初以为是代码问题反复检查发现是 SPI 发送频率太高36MHz 时钟下这种杜邦线连接很容易发生信号串扰降到 18MHz 立刻好了。如果你用的是面包板加长杜邦线尤其容易出现这个问题建议先从低速开始排查。第三个坑是字符左右镜像。取模软件生成的字模有的默认是纵向取模有的默认是横向取模我在一个网上找的字库里没有确认方向结果字母全是反的。这个问题最坑因为它看起来像硬件扫描方向错乱了实际只是数据位序不对。你把生成字模的软件设置改成横向取模高位在前或者把绘制函数里的判断顺序反过来问题就解决了。第四个坑是图片颜色偏蓝偏紫。原因就是我前面讲的那个 0x36 寄存器的 bit3。不同模块这个值不一样需要晚点试。你把 0x00 改成 0x08颜色就正常了。另外如果整体颜色像负片记得检查 0x21 和 0x20。第五个坑也是很多新手容易忽略的就是数组溢出。你在 C 语言里定义一个 const uint16_t image_240x240[57600]编译会报 Flash 不够或者数据过大。这时候优先检查你有没有把 MCU 的 Flash 大小选对我见过有人在 Keil 里芯片型号选成 STM32F103C632KB Flash结果一张图放进去直接编译不过。换个思路也可以把全屏图用 RLE 压缩算法处理一下简单的图形界面图能压到原来的三分之一。5.3 关于后续扩展的一点想法代码跑通之后你还可以做很多事把最小系统移植到自己的 PCB 上或者加上触摸芯片做一个完整的人机交互界面。我最近就在做一个小型环境监测站屏幕上显示温湿度曲线和几个状态图标字符和图片混排这套驱动当底层渲染引擎非常合适。如果后续需要显示动态波形或者进度条建议直接把画线、画矩形、画圆的函数也实现一遍基础都是 DrawPixel稍微封装一下就行。祝你也早日点亮自己的屏幕有问题欢迎在评论区交流。本文还有配套的精品资源点击获取