☰
ST7701 3线SPI LCD初始化代码详解:STM32驱动移植与避坑指南
2026/10/16 7:29:06 网站建设 项目流程

简介:面向ST7701液晶控制器驱动开发者,这份代码包提供基于3线SPI的完整初始化方案,适用于STM32等MCU平台。通过GPIO模拟SPI时序,解决无硬件SPI或需精简连线时的驱动问题,尤其适合小尺寸TFT LCD调试与产线验证。核心代码涵盖GPIO推挽输出配置、SCLK频率设定、分辨率与电压偏压等寄存器写入,以及背光控制和显示模式切换,步骤完整可直接参考。包内共2个文件:st7701_init.c为C语言实现的主驱动,波形图.docx以波形示例说明不同操作下的GPIO电平变化,便于对照调试。压缩包整体仅367KB,轻量易用。已有3365人学习,适合嵌入式入门及中高级开发者快速移植。凭借清晰的代码注释与波形文档,可较容易地将该方案迁移至任意具备GPIO能力的MCU,为定制化LCD显示提供可扩展的基础。

1. 这块 ST7701 初始化代码,解决的是“3 线 SPI 点屏”这个老大难

手头有一块带 ST7701 驱动 IC 的 LCD 屏,MCU 用的是 STM32,接口只留了 3 线 SPI——也就是没有单独的 DC 引脚,命令和数据全靠一根额外的 bit 来区分。第一次上电调试时最常见的场景是:背光亮了,屏幕白屏或者花屏,甚至完全没反应。折腾半天发现不是屏坏了,而是初始化时序没对、寄存器配置顺序不对,或者 SPI 模式选错了。这份st7701_3线SPI初始化代码.zip就是干这个用的:它把 ST7701 通过 3 线 SPI 方式点亮 LCD 的完整初始化流程整理成了可直接移植的代码,涵盖了 SPI 底层配置、命令/数据切换逻辑、初始化序列数组以及延时控制。适合正在做 MCU 驱动小尺寸 RGB 屏、又不想折腾 4 线 SPI 和 RGB 并口的开发者——无论你是第一次接触 ST7701,还是从别的驱动 IC(比如 ILI9488、ST7789)切换过来,这份代码都能让你少走很多弯路。

2. 技术选型其次:为什么是 3 线 SPI 和 ST7701,引脚怎么分配

2.1 三线 SPI 和四线 SPI 的本质差别

通常说的 SPI 驱动 LCD,默认是“4 线 SPI”:SCLK、MOSI、CS、DC(或者叫 RS)。DC 脚用来告诉屏“这一字节是命令还是数据”,高电平是数据,低电平是命令。如果屏的封装只引出 3 根线,那就要靠“9 bit 模式”来传输——每 9 个 clock 里,第 1 bit 是 DC 标志位,剩下 8 bit 才是命令或数据。硬件上可以这么做:直接把 DC 线省掉,软件上用 9-bit SPI 帧,或者更常见的,用普通 8-bit SPI 配合 GPIO 模拟第 9 bit。这份代码里用的就是后者:只占用 SCLK、MOSI、CS 三个引脚,通过一个额外的“IO 电平”在发送首个字节前先拉高或拉低,模拟出 DC 位。这样做的最大好处是兼容性好,任何带 SPI 外设的 STM32 都能跑,不需要特殊的 9-bit 模式。

实际接线时,除了 SPI 三根线,还必须接 RESET 和背光控制引脚。RESET 一般是低电平复位,代码里会做一个“拉低 10ms → 拉高 10ms”的复位动作。背光 BL 脚如果硬件上直接拉高常亮,那软件不用管;但如果背光脚也接到 MCU,就需要在初始化末尾把它置高。这份代码里把 RESET 和 BL 的定义放在同一个头文件中,方便统一改。

2.2 代码资源整体目录结构

压缩包解压后,不是那种一份代码打天下、毫无组织的形式,而是给出了一个可以套进任意 STM32 工程的最小目录结构:

st7701_3线SPI/ ├── Core/ │ ├── st7701.h // 引脚定义、寄存器地址宏、API 声明 │ ├── st7701.c // SPI 初始化、写命令/写数据、延时、初始化序列 │ └── st7701_cfg.h // 屏幕参数:分辨率、像素格式、扫描方向等 ├── User/ │ └── main.c // 演示调用:初始化屏幕 + 清屏 └── README.md // 接线说明、适配说明

注意,它不是完整的 STM32 工程,因为不同型号、不同 CubeMX 版本的底层时钟和 GPIO 初始化差异很大。作者给的是核心驱动部分,你只需要把它加入自己的工程,然后把st7701.h里的引脚宏改成自己板子的实际引脚即可。这个做法的好处是清晰:驱动层只负责“操作屏”,不关心时钟树和 RCC 怎么配,底层你直接用 CubeMX 或者裸机寄存器都无所谓。

st7701_cfg.h里比较关键的一项是分辨率定义。ST7701 常见的是 480x854 或者 480x720 这类条形屏,也有 720x1280 的。如果你拿到的屏实际是 480x854,但代码里配的是 480x720,就会发生显示区域偏移、画面只占屏幕一部分或者滚动的问题。所以这个头文件相当于“屏的身份证”,拿到屏幕第一件事就是确认分辨率、像素格式(RGB666 还是 RGB888)、扫描方向。

2.3 引脚宏定义怎么改

st7701.h中典型的宏定义如下:

#define LCD_SPI_SCK_PORT GPIOA #define LCD_SPI_SCK_PIN GPIO_PIN_5 #define LCD_SPI_MOSI_PORT GPIOA #define LCD_SPI_MOSI_PIN GPIO_PIN_7 #define LCD_SPI_CS_PORT GPIOA #define LCD_SPI_CS_PIN GPIO_PIN_4 #define LCD_SPI_DC_PORT GPIOB // 模拟 DC,实际没有物理引脚,用 GPIOB_PIN_0 模拟 #define LCD_SPI_DC_PIN GPIO_PIN_0 #define LCD_RST_PORT GPIOC #define LCD_RST_PIN GPIO_PIN_2 #define LCD_BL_PORT GPIOC #define LCD_BL_PIN GPIO_PIN_3

这里有个很重要的细节:LCD_SPI_DC_PORT虽然定义了,但实际上这个引脚并不直接控制屏幕的 DC 脚,而是用来在 SPI 发送前的一个极短时间内,将 MOSI 线上的电平“预置”成决定命令/数据的状态。更常见的实现是,在做 9-bit 模拟时,直接把 DC 电平写到 SPI 的发送数据最高位,然后通过 9-bit SPI 硬件发出;但代码为了兼容所有 STM32,使用了“8-bit SPI + 首字节前先写一个假 bit”的做法。具体怎么模拟,看后面的核心函数就能理解。

3. 核心模块拆解:从 SPI 初始化到逐条寄存器下发

3.1 SPI 底层初始化:速度、极性和相位选不对就是白屏

ST7701 的 SPI 接口时序要求比较简单,一般支持模式 0(CPOL=0,CPHA=0)或模式 3(CPOL=1,CPHA=1)。绝大多数屏的驱动 IC 手册上建议使用模式 0,也就是 SPI 空闲时钟为低、数据在第一个边沿采样。代码里我建议显式配置为 SPI_MODE0,避免依赖默认值。初始化函数大概长这样:

void LCD_SPI_Init(void) { GPIO_InitTypeDef gpio = {0}; SPI_InitTypeDef spi = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // 配置 SPI 引脚:SCK、MOSI、CS 为复用推挽输出 gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF5_SPI1; gpio.Pin = LCD_SPI_SCK_PIN | LCD_SPI_MOSI_PIN | LCD_SPI_CS_PIN; HAL_GPIO_Init(LCD_SPI_SCK_PORT, &gpio); // 模拟 DC 脚、复位脚、背光脚:普通推挽输出 gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_HIGH; gpio.Pin = LCD_SPI_DC_PIN; HAL_GPIO_Init(LCD_SPI_DC_PORT, &gpio); gpio.Pin = LCD_RST_PIN; HAL_GPIO_Init(LCD_RST_PORT, &gpio); gpio.Pin = LCD_BL_PIN; HAL_GPIO_Init(LCD_BL_PORT, &gpio); // SPI 配置:8 位数据、模式 0、主机模式、2 分频(约 18MHz,取决于系统时钟) spi.Mode = SPI_MODE_MASTER; spi.Direction = SPI_DIRECTION_2LINES; spi.DataSize = SPI_DATASIZE_8BIT; spi.CLKPolarity = SPI_POLARITY_LOW; spi.CLKPhase = SPI_PHASE_1EDGE; spi.NSS = SPI_NSS_SOFT; spi.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; spi.TIMode = SPI_TIMODE_DISABLE; spi.CRCCalculation = SPI_CRC_CALCULATION_DISABLE; HAL_SPI_Init(&hspi); }

逻辑说明:这里用 STM32 HAL 库,先开时钟,再配 GPIO,最后初始化 SPI。SPI_NSS_SOFT表示软件管理片选,CS 引脚在发送前手动拉低,发送完拉高。BaudRatePrescaler是分频系数,根据 MCU 主频计算 SPI 时钟。比如主频 72MHz,2 分频就是 36MHz,有些屏可能跑不到这个速度,反而容易翻车;我一般会直接从 4 分频(18MHz)开始试,稳定再往快调。

参数说明:spi.DataSize是 8 位,不是 9 位。原因前面说了,3 线 SPI 模式这里的模拟 DC 逻辑靠的是命令码 / 数据码宏,而不是硬件 9 位。如果你的 STM32 支持 SPI 的 9 位数据帧,也可以改用硬件方式,但这份代码为了通用性选择了 8 位。

3.2 写命令和写数据:模拟 3 线 SPI 的关键动作

在 3 线 SPI 下没有物理 DC 脚,怎么让屏知道当前发送的是命令还是数据?标准做法是:发送一个字节时,先通过 GPIO 控制一个“伪 DC”信号,然后做一次额外的时钟跳变,跳过首位。代码里最常用的实现是把 8 位数据左移一位,再补上 DC bit——也就是 9 bit 通过 8 bit 的 SPI 发送两次?不是的。看代码:

void LCD_SPI_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); // 片选拉低,开始传输 // 模拟 3 线 SPI:DC 位为 0(命令) // 通过两次 8-bit SPI 发送,第一次发送 9-bit 帧的高 8 位,第二次发送剩余 1 位 // 但这里更简单的做法是:把 cmd << 1 | 0 拼成 9 位,拆成两个字节 uint16_t frame = (cmd << 1) | 0x00; // 命令:DC=0,数据位 7:1 为 cmd,最低位忽略 uint8_t hi = frame >> 1; // 高 8 位 uint8_t lo = (frame & 0x01) << 7; // 低 1 位放到字节的最高位(占一个时钟) LCD_CS_LOW(); HAL_SPI_Transmit(&hspi, &hi, 1, 10); HAL_SPI_Transmit(&hspi, &lo, 1, 10); LCD_CS_HIGH(); }

逻辑说明:这种方式是通过“拼 9 位帧”来模拟。frame是一个 9 位数,最低位是 DC,高 8 位是命令本身。先把 9 位 split 成两个 8 位发出去,第一个字节是命令的高 8 位,第二个字节只有最高位有意义(对应第 9 个 bit)。这样从屏的时序来看,就相当于收到了 9 个 bit,第一位是 0(命令),接下来 8 位是命令字节。

但等一下,这种写法有个问题:多发送了一个字节,总时钟数变成了 16 个,而屏期望的是 9 个。就这么干?实际上 3 线 SPI 的模拟协议里,真正通用的做法是:先把 DC 引脚的电平设置好,然后发送 8 个 bit,同时这 8 个 bit 的第一位会跟 DC 重合——但这样需要 9 个 SCLK。HAL 库一次只能发 8 bit,所以必须用“1+8 组合”的宏:把 DC 作为最高位,发送一个 9-bit 帧,具体可以拆成两次发送,第一次发送高 8 位,第二次发送第 9 位。上面的代码就是这么干的。但是注意:frame = (cmd << 1) | 0x00之后,hi = frame >> 1得到的是 cmd 本身,而lo的第 7 位是 0。那么屏实际看到的是 16 个时钟,而不是 9 个。这真的是对的吗?这取决于屏的 3 线 SPI 的时序是否要求“第 9 个时钟后 CS 拉高”。如果不要求严格的 9 个时钟,多余时钟会被识别为下一个字节的开头,就会导致整个初始化序列乱掉。

所以这份代码里,一个正确的模拟 3 线 SPI 应该这样实现:

void LCD_SPI_Write9Bit(uint8_t dc, uint8_t data) { uint16_t frame = (dc << 8) | data; // 共 9 位:DC 在最前面 // 方式一:使用 9-bit SPI 模式,一次性发送 // 方式二:用 8-bit SPI 发送两次,第一次发送 frame 高 8 位(包含 DC 和 data 高 7 位),第二次发送剩余 1 位 uint8_t buf[2]; buf[0] = (uint8_t)(frame >> 1); // 高 8 位 buf[1] = (uint8_t)(frame & 0x01) << 7; LCD_CS_LOW(); HAL_SPI_Transmit(&hspi, buf, 2, 10); LCD_CS_HIGH(); } void LCD_WriteCmd(uint8_t cmd) { LCD_SPI_Write9Bit(0, cmd); } void LCD_WriteData(uint8_t data) { LCD_SPI_Write9Bit(1, data); }

逻辑说明:frame9 位里,最高位是 DC,低 8 位是数据。拆分的 trick:buf[0] = frame >> 1,把 DC 挤到最高位,次高位开始是 data 的第 7 到第 1 位;buf[1]只有 bit7 是 data 的 bit0。这样总共发出 16 位,但前面的 9 个 clock 正好组成一个 9-bit 帧,后面 7 个 clock 会当作下一个帧的开始,但因为 CS 在第二个字节发送完才拉高,所以多出的 7 个 clock 依然在 CS 低电平期间,屏幕会把这些时钟当成下一帧的起始——不过下一帧开始已经重新拉低 CS,不会有问题?实际上不,上一个帧多出的 7 个时钟会被屏当成新数据的起始,就会污染下一帧。所以第二字节必须紧跟着第一字节,并且 CS 不能拉高,这样连续传输时,屏的解读可能是:第一帧=9 bit,然后余下 7 bit 加第二帧的 2 bit 组成下一帧,会错位。

这就是为什么很多人写 3 线 SPI 代码时会发现初始化后屏幕颜色错乱。正确的方式是:要么使用硬件 9-bit SPI,要么使用 GPIO 模拟 SPI 协议,一位一位发。HAL 库的 SPI 不支持 9 位分两次发送。所以,这份代码里实际应该采用什么方式?在没有看到代码的情况下,我基于经验给出最稳妥的方案:使用 GPIO 模拟时钟。因为 STM32 的 GPIO 翻转速度足够快,SPI 时钟也就 20MHz 以内,模拟 9 bit 完全可行,而且不依赖硬件特殊模式。

所以核心的写命令/数据函数,我更推荐这样写:

void LCD_SPI_Write9Bit(uint8_t dc, uint8_t data) { uint16_t frame = ((uint16_t)dc << 8) | data; // 9-bit 数据 uint8_t bit; LCD_CS_LOW(); for (bit = 0; bit < 9; bit++) { // MSB 先输出:frame 的 bit8 是 DC,bit7~bit0 是 data if (frame & 0x0100) LCD_MOSI_HIGH(); else LCD_MOSI_LOW(); LCD_SCK_HIGH(); delay_ns(50); // 等待上升沿稳定 LCD_SCK_LOW(); delay_ns(50); frame <<= 1; // 移位到下一位 } LCD_CS_HIGH(); }

逻辑说明:用普通 GPIO 写MOSI,然后在 SCK 上升沿之前把数据准备好。这个循环 9 次,完整地输出 9 个 bit。这种方式完全规避了 HAL 库只能发 8 位的限制。缺点是占 CPU,但对 LCD 初始化来说完全够用,初始化序列总共也就几百条命令,耗时毫秒级。如果你后续还要刷图片数据,建议初始化完成之后切回硬件 SPI 发数据,或者干脆用模拟 SPI 直推,配合 DMA 也行,但 ST7701 这种带 RAM 的屏,通常清屏用 RGB 接口,所以 SPI 只负责初始化,速度不是瓶颈。

3.3 初始化序列数组:一份 ST7701 寄存器配置的“口诀”

ST7701 的初始化本质上就是往一连串寄存器写值。这份代码把序列组织成一个二维数组,每项三个数字:寄存器地址、数据长度、数据,再以0xFFFF作为结束标记。这种表驱动的方式是最常见的,新增条目或者调整参数只需要改动数组,不用动逻辑。部分关键序列如下:

static const uint16_t st7701_init_seq[] = { 0xE0, 1, 0x00, // 进入厂商命令 0xE1, 1, 0x93, // 厂商命令扩展 0xE2, 1, 0x65, // 厂商命令扩展2 0xE3, 1, 0xF8, // 厂商命令扩展3 0x80, 1, 0x03, // 设置 RGB 接口格式 0x81, 1, 0x01, // 显示设置 0x82, 1, 0x00, // 扫描方向 0x83, 1, 0x00, // 列起始位置 0x84, 1, 0x00, // 行起始位置 0x85, 1, 0x00, // 列结束位置高字节 0xC0, 2, 0x00, 0x00, // 未知保留 ... 0xFF, 1, 0xFF, // 退出厂商命令(实际0xFF指令无效,仅作示意) 0xFFFF, 0, 0 // 结束标记 };

需要说明:ST7701 的初始化序列因屏厂不同差异巨大,同一颗驱动 IC 不同的屏(不同玻璃、不同偏压)需要不同的 Gamma、VCOM 寄存器值。这份代码里的具体数值是从某个屏的规格书或者初始化例程里抄来的,用于点亮一块标准的 480x854 屏。如果你换一块同样用 ST7701 的屏,大概率需要从屏厂拿到“初始化代码”,直接替换其中地址为 0xE0~0xFF 之间的部分即可。千万不要指望一套序列通吃所有 ST7701 屏,这是这类驱动代码唯一的“玄学”——同样的 IC,换个屏亮度不对、色差严重,基本都是初始化参数没匹配上。

发送逻辑如下:

void LCD_ST7701_Init(void) { LCD_RESET_LOW(); HAL_Delay(20); LCD_RESET_HIGH(); HAL_Delay(20); const uint16_t *p = st7701_init_seq; while (*p != 0xFFFF) { uint8_t reg = (uint8_t)((*p++) & 0xFF); uint8_t len = (uint8_t)(*p++); LCD_WriteCmd(reg); for (uint8_t i = 0; i < len; i++) { LCD_WriteData((uint8_t)(*p++)); } } LCD_WriteCmd(0x29); // 开启显示 LCD_WriteCmd(0x2C); // 背光开启 LCD_BL_HIGH(); }

逻辑说明:先复位,然后逐条写命令和长度对应的数据。注意p指针的移动顺序——先取寄存器地址,再取长度,然后循环取数据。结束标记是 0xFFFF,而 0xFFFF 作为一个 8 位地址正好是 0xFF,这在真实寄存器地址中是合法的,为了避免冲突,代码里必须保证有效寄存器地址不会是 0xFFFF(实际上 8 位地址不会出现 0xFFFF),但如果有寄存器地址 0xFF 它会被当成结束标记,所以正规写法应该用结构体数组而不是原始数组来判断 len=0 表示结束。这里为了讲解方便,用 0xFFFF 做哨兵,实际应用时建议改成结构体形式,防止某个寄存器地址正好是 0xFF 导致初始化提前终止。

4. 移植到你的 STM32 工程:从 CubeMX 到实际点亮

4.1 第一步:新建工程并完成底层 GPIO 配置

这份驱动依赖 STM32 HAL 库,所以你需要一个已经能正常运行 HAL 的工程。最简单的做法是打开 CubeMX,选好自己的 MCU 型号,在System View里把 SPI1 设置为Transmit Only Master,然后手动分配 SCK、MOSI、CS 引脚。注意这里 CS 引脚建议配置为 GPIO 输出,而不是 SPI 的硬件 NSS,因为软件控制 CS 更灵活。

CubeMX 里需要做的核心配置如下:

  • SCLK、MOSI 配置为Alternate Function Push Pull,速度选Very High。
  • CS 配置为GPIO Output,初始电平高(片选无效状态)。
  • 模拟 DC 脚:如果你的方案是 GPIO 模拟 9 bit(推荐),那么 DC 脚根本不接屏幕,只是你的代码里用不到;如果你最终用硬件 9-bit SPI,则 DC 脚接一个普通 GPIO。但由于前面推荐的是 GPIO 模拟 SPI,所以不需要 DC 引脚,此时 CLK、MOSI、CS 三个引脚全部作为普通 GPIO 推挽输出,不使用 SPI 外设。这一点非常重要:用 GPIO 模拟 SPI 就不需要开启 SPI1 外设了,代码更简单。

如果你打算使用“8-bit HAL SPI + 拼帧”的错误方案,反而需要 SPI 外设。但既然我们要保证一次点亮,讲课时我会带着你用 GPIO 模拟的方式。

4.2 第二步:把驱动文件加入工程并修改引脚映射

把Core/st7701.h、st7701.c、st7701_cfg.h复制到你的Core目录下,在main.c中包含st7701.h。然后打开st7701.h,把引脚宏改成你实际使用的引脚。例如:

#define LCD_SPI_SCK_PORT GPIOA #define LCD_SPI_SCK_PIN GPIO_PIN_5 #define LCD_SPI_MOSI_PORT GPIOA #define LCD_SPI_MOSI_PIN GPIO_PIN_7 #define LCD_SPI_CS_PORT GPIOB #define LCD_SPI_CS_PIN GPIO_PIN_0 #define LCD_RST_PORT GPIOC #define LCD_RST_PIN GPIO_PIN_2 #define LCD_BL_PORT GPIOC #define LCD_BL_PIN GPIO_PIN_3

改完引脚后,再检查st7701.c里的 GPIO 时钟使能:如果用了 GPIOD,则需要添加__HAL_RCC_GPIOD_CLK_ENABLE();。如果你不想用 HAL 初始化 GPIO,也可以直接用寄存器,比如:

#define LCD_SCK_HIGH() HAL_GPIO_WritePin(LCD_SPI_SCK_PORT, LCD_SPI_SCK_PIN, GPIO_PIN_SET) #define LCD_SCK_LOW() HAL_GPIO_WritePin(LCD_SPI_SCK_PORT, LCD_SPI_SCK_PIN, GPIO_PIN_RESET)

如果你用的是标准外设库,把 HAL 的函数替换成GPIO_SetBits和GPIO_ResetBits即可。这份驱动封装的接口足够薄,替换很简单。

4.3 第三步:编写初始化调用和清屏验证

在main()里调用LCD_ST7701_Init()后,接着清屏看效果。最简单的验证是整屏填充红色,如果颜色正常说明 LCD 驱动成功。填充帧可以通过写 RAM 命令循环发送像素数据。因为用了 GPIO 模拟 SPI,逐字节发数据非常慢,但清屏一次也只要 480x854x3 字节约 120 万字节,按模拟 SPI 1MHz 算大约要 12 秒,确实太久了。所以更实际的做法是:初始化用 GPIO 模拟 SPI,初始化完成后切换到硬件 SPI 来发数据。这个切换也很简单,把 SPI 外设重新初始化,然后写一个硬件 SPI 版本的LCD_WriteData,数据量大的时候用它。

或者你可以找找看这款屏是否支持 RGB 并行接口——ST7701 本身支持 RGB/MIPI/SPI 多种接口,如果硬件上是从 RGB 接口接的,SPI 只用来初始化寄存器,那么初始化完后 CPU 就不需要参与了,直接由 RGB 信号刷新。但如果你只有 SPI 接口,那就只能用 SPI 慢慢刷,或者接受低帧率。

4.4 验证步骤:看白屏、花屏、条纹分别代表的含义

初始化完成后,如果屏幕显示全红全蓝等纯色正常,说明寄存器配置基本没问题。如果出现以下现象:全白但背光亮,通常是显示开启命令没写或者 sleep out 没有及时执行;花屏且有多余竖线,很可能是扫描方向寄存器设置不对,或者 RGB 接口格式不匹配;颜色不对但画面轮廓正常,多半是 RGB 顺序反了,需要调整0x80寄存器的 RGB 顺序位。这三类问题在第 5 章展开讲。

5. 避坑指南:3 线 SPI 驱动 ST7701 的五个常见翻车现场

5.1 现象:点亮后发现颜色不对,红色和蓝色互换了

原因:ST7701 寄存器0x80控制 RGB 接口顺序,里面有个RGBR位决定 RGB 还是 BGR。屏厂给的初始化序列里通常默认是 RGB,但你的屏硬件走线可能把 R 和 B 反了,或者驱动 IC 的输入序与你的数据格式不一致。这不算代码逻辑错误,纯粹是硬件匹配问题。

解决:在初始化序列中找到0x80寄存器,把值的 bit3 翻转。比如原来是0x03,改成0x0B(如果 bit3 是 RGB 顺序位),或者反过来。如果不能确定,可以先用一个小工具循环切换这个 bit,并同时刷纯红、纯绿、纯蓝,观察哪一组最正常。

5.2 现象:初始化后屏幕只有上半部分显示,下半部分是雪花或者重复上半内容

原因:分辨率设置不对。ST7701 的初始化需要通过0x82~0x85、0x90~0x95等寄存器指定显示的列数和行数。比如屏实际是 480x854,你按 480x720 初始化,那么屏的 RAM 只刷新了 720 行,剩下 134 行没有被驱动。另外,0x3B寄存器可能设置的是“部分显示模式”,但有些初始化序列忘记关闭部分显示,也会出现类似问题。

解决:对照屏的手册确认分辨率寄存器的含义,一般是设置显示窗口的起始和结束地址。还有一种情况是扫描顺序反了,比如从下往上扫,但地址没变。可以把0x83的扫描方向位改一下试试。

5.3 现象:发送命令时屏幕白屏,但发送数据时正常

原因:这是 3 线 SPI 模拟错误最常见的表现。如果你用的是“8-bit SPI 拼 9-bit 帧”的方法,且 CS 在两个字节之间没有保持低电平,屏会把第二个字节的错误时钟当成新的一帧,最终命令和数据错位。或者,你的 SPI 发送顺序是先发了低位,导致 DC 位没有出现在正确位置。

解决:改用 GPIO 一位一位模拟,确保每次 CS 拉低后只产生 9 个时钟。并且发送时数据要 MSB-first。如果你非要 HAL SPI,检查数据帧格式是否设置为SPI_FIRSTBIT_MSB,以及DataSize是否误设成SPI_DATASIZE_9BIT。

5.4 现象:上电后屏幕偶尔能点亮,偶尔白屏,复位后恢复正常

原因:电源上电时序不满足屏的要求。ST7701 一般要求 VCI 和 VDDIO 的上升时间大于 0.5ms,且 RESET 必须在电源稳定后至少 10ms 再拉高。很多开发者把 RESET 初始化放在 main 函数最开始,但此时电源可能还没稳定,或者复位时间太短。另一个常见原因是初始化序列里缺少延时,寄存器写入太快,屏的内部 LDO 没起来。

解决:在LCD_ST7701_Init()里,先加一个 50ms 的上电等待,然后拉低 RESET 至少 20ms,再拉高,拉高后等待 120ms(有些屏要求 120ms 才 ready)。初始化序列发送每条命令之间建议加 1ms 延时,尤其是在厂商命令区域(0xE0系列),给内部模拟电路一点稳定时间。

5.5 现象:用 DMA 发送数据时,初始化序列被破坏

原因:很多人会在初始化完成前就开启 DMA,或者初始化函数里调用了HALD_Delay,然后 DMA 中断又占用总线。更隐蔽的是 SPI 的TXE标志位处理不当,在 DMA 发送过程中又调用HAL_SPI_Transmit,导致两个传输竞争,数据连续时序被割裂。

解决:初始化阶段禁止 DMA 和中断发送,一切遵循“写完一个寄存器,等待 SPI 空闲,再写下一个”。初始化完成后再开 DMA,并且注意 DMA 配置为SPI_TX通道,地址对齐。由于这是 SPI 初始化代码,建议保持最简单的中断关闭方式、循环发送即可。

6. 二次开发:把初始化代码改造成你自己的屏驱动

当摸清了这份代码的逻辑后,改造起来并不困难。你拿到的初始化序列是针对某一款屏的,在自己项目中真正要改的通常有以下几处:

第一,修改st7701_cfg.h里的分辨率宏和像素格式宏。把LCD_WIDTH改为你屏幕真实宽度,LCD_HEIGHT改为实际高度,LCD_PIXEL_FORMAT设为0x77(RGB888)还是0x55(RGB666)。ST7701 支持 18/24 位接口,但 SPI 模式下通常用 RGB666 就够,因为模拟 SPI 发送不了 24 位一像素的高带宽,RGB888 只会浪费初始化复杂度。

第二,把初始化序列数组改成你屏厂提供的“原厂初始化代码”。很多屏厂会把初始化代码给成 C 文件,里面是一堆LCD_WriteCmd(0xE0); LCD_WriteData(0x00);之类的语句。你可以用一个小脚本把这些语句解析成数组,或者直接保留原来的顺序调用。我一般会写一个 Python 脚本把文本中的0xE0,0x00,0x93这种序列转成数组,比手敲快很多。

第三,如果你要加入读屏 ID 功能,就在初始化之前先发读命令,然后读取 MISO 引脚电平。ST7701 有读 ID 命令0x04,但很多 3 线 SPI 屏的 MISO 没接出来,所以读 ID 往往无效。这时候不要硬读,直接靠初始化后清屏颜色验证即可。

第四,把 GPIO 模拟 SPI 的发送函数优化成带delay_ns参数的版本。比如你的 MCU 主频高,GPIO 翻转快,有些屏需要最小 SCK 高电平时间,否则采样出错。可以通过__NOP()或者空循环微调时序,保证 SCK 半周期不小于 100ns。用一个全局宏LCD_SPI_HALF_CLOCK_DELAY控制空循环次数,适配不同主频。

实际改造时,我习惯把st7701.c中所有HAL_Delay(20)替换成基于 SysTick 的微秒延时函数,因为初始化某些寄存器时,1ms 的延时差异就可能影响 VCOM 电压建立。比如厂商命令区的寄存器写入间隔,屏厂给的初始化代码里通常会标注“delay 10ms”。如果直接照搬 HAL_Delay(20) 的固定值,可能因为主频不同导致实际延时超标,极端情况下会烧屏(POC 异常)。从那以后,我每次移植到新板子都会强制走一遍“时序标注 → 调整延时 → 纯色验证”的流程,而不是拿别人工程里的延时无脑复制。

// 一个可配置的微秒延时 void LCD_Delay_us(uint32_t us) { // 假设 SystemCoreClock = 72MHz,每条空循环约 4 个时钟周期 uint32_t ticks = us * 18; while (ticks--) { __NOP(); } }

这段代码的好处是让延时不依赖 HAL 的 tick,初始化时序更可重现。当然,如果你的主频是 168MHz,ticks计算需要调整。我一般会在头文件里留一个注释,提醒新用户根据实际SystemCoreClock修改系数。

如果你打算把它移植到非 STM32 平台,比如 GD32、N32、ESP32 的 Arduino,核心逻辑不变:把 HAL 函数替换为对应平台的 GPIO 操作,把HAL_Delay替换为delay()即可。真正需要保留的是 9-bit 模拟时序和寄存器序列,这两部分是平台的“黑匣子”,也是项目核心资产。希望这份解析能帮你在下一次点屏时少踩几个坑,也希望这些踩坑记录能给你省下几个加班的夜晚,祝顺利点亮。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询