LCD12864串行驱动深度解析:GPIO时序到SPI移植与排错
2026/9/14 4:51:00 网站建设 项目流程

简介:一份面向嵌入式开发者的STM32 LCD12864串行驱动完整工程示例。资源基于STM32与LCD12864点阵屏,重点演示通过SPI或I2C串行接口实现屏幕初始化、字符与图形显示、滚动控制等操作,并提供硬件GPIO配置、波特率设定及调试优化思路,适合正在学习STM32外设驱动或需要快速集成LCD12864显示功能的工程师参考。压缩包内共188个文件,包含核心的C源文件与头文件、编译生成的o/crf等中间文件、Keil工程配置(uvprojx/uvoptx)以及说明文档与PDF资料,整体体积仅5.26MB,结构清晰便于查阅。目前已有344人学习下载,资源中包含示例代码、库函数调用演示和关键时序配置,可直接导入Keil工程,对照实物调试,能帮助使用者节省底层驱动开发时间,快速实现LCD12864的串口显示。

1. 为什么把 lcd12864 驱动从并行换成串行不会更慢

拿到一块 12864 屏,多数人第一反应是接 8 根数据线加 RS、RW、E,算下来十几个引脚,在 STM32 最小系统板上能剩下几个 GPIO 给按键和传感器?而 lcd12864 的控制器 ST7920 其实内置了串行模式,只需要 CS、SCLK、SID 三根线就能完成全部读写。速度上也不用担心:常见应用里往屏幕上写一帧字符数据本身就只有几百字节,ST7920 内部刷新率远高于人眼可感知的变化,串行传输那点时间差在菜单切换、仪表显示这种场景里完全感知不到。真正被并行方式消耗掉的,是宝贵的引脚资源和布线的耐心。这篇按 GPIO 模拟时序讲透串行驱动,从波形、初始化、中文编码到硬件 SPI 移植,给一套能直接抄进标准库或 CubeMX 工程的写法。

2. LCD12864 串行模式时序:ST7920 三线协议与 GPIO 模拟实现

2.1 串行模式下 CS、SCLK、SID 分别接什么

LCD12864 的接口排针上通常标着 RS、RW、E,但在 ST7920 的串行模式下,这三个引脚的语义完全变了。RS 变成 CS(片选),RW 变成 SID(串行数据),E 变成 SCLK(串行时钟)。也就是说,你不需要额外飞线,直接用原来的并行接口引脚,只是驱动方式不同。

下面是一张常见的连接表,以 STM32F103C8T6 为例:

LCD12864 引脚功能(并行)功能(串行)STM32 GPIO
RS寄存器选择CS 片选PB12
RW读写选择SID 数据PB13
E使能时钟SCLK 时钟PB14
PSB并/串选择接 GNDGND
V0对比度对比度接 10K 电位器
LED_A / LED_K背光背光3.3V / GND

这里最关键的引脚是 PSB。PSB 接高电平是并行模式,接低电平才启用串行模式。很多人代码写得没问题,屏幕却毫无反应,就是 PSB 悬空或接错。低电平要直接接地,不要通过电阻,防止接触不良导致模式漂移。

GPIO 配置上,三个信号脚全部设为推挽输出,速度选 50MHz。注意 STM32F1 的 GPIO 默认是开漏状态,不初始化直接拉电平是无效的。

2.2 一个字节的发送波形:同步位、高四位、低四位

ST7920 的串行协议每一帧数据是 24 bit,分成三段:同步位 + 数据高四位 + 数据低四位。同步位固定是11111加一个 RW 位和 RS 位。实际上代码里通常直接用两个魔数:0xF8表示写命令,0xFA表示写数据。

发送顺序是:先发0xF80xFA,再发命令或数据的高四位(放到字节的高 4 位,低 4 位为 0),最后发低四位(放到高 4 位,低 4 位为 0)。每个字节发送时按 MSB first,SCLK 拉高时 SID 上的电平被锁存。

下面是最小实现:

#define LCD_CS_H GPIO_SetBits(GPIOB, GPIO_Pin_12) #define LCD_CS_L GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define LCD_SID_H GPIO_SetBits(GPIOB, GPIO_Pin_13) #define LCD_SID_L GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define LCD_SCLK_H GPIO_SetBits(GPIOB, GPIO_Pin_14) #define LCD_SCLK_L GPIO_ResetBits(GPIOB, GPIO_Pin_14) void LCD_SendByte(u8 dat) { u8 i; for (i = 0; i < 8; i++) { LCD_SCLK_L; // 时钟拉低,准备数据 if (dat & 0x80) LCD_SID_H; else LCD_SID_L; LCD_SCLK_H; // 上升沿锁存数据 dat <<= 1; } }

这段代码是一次发送一个完整字节的底层函数。循环里先拉低 SCLK,再设置 SID,最后拉高 SCLK,顺序不能反,否则第一个 bit 会被漏掉。dat <<= 1是把次高位移到最高位,配合0x80判断实现 MSB first。

2.3 为什么软件延时 5us 是安全值,不是越小越好

ST7920 串行时钟的极限频率并不高,而且不同批次屏幕体质差异很大。再加上 GPIO 模拟时序本身没有硬件 SPI 那样稳定的占空比,把延时压得太小,长排线或者接触不良时波形边沿会变差。我一般的做法是每个 bit 周期留 5us,对应一个字节 40us,一帧命令 3 个字节约 120us,初始化几十条指令加起来也就几毫秒,完全无感。

如果后续想提速,可以在写命令循环里逐步减小延时测试稳定性,但注意屏幕刷新不是瓶颈,功耗和 EMI 才是。给一个经验参考表:

SCLK 周期效果适用场景
2us多数屏幕可跑,长线易出错飞线较短、无干扰环境
5us稳定,推荐默认大多数开发板和自制 PCB
10us非常保守排线超过 20cm 或与电机驱动共用电源

GPIO 速度等级设在 50MHz,但延时不加照样可能误码。核心原因是 ST7920 内部对 SID 的采样窗口有要求,数据建立时间不足时,采样到的就是上一次的电平。

3. 在标准库与 CubeMX 工程中搭建 lcd12864 串行驱动代码框架

3.1 工程接入:标准库新建工程后需要准备什么

这里默认你已经用标准库建好了 STM32F103 的最小工程。如果是从 Keil5 里新建,记得先装好对应的芯片包,否则 Device 列表里找不到芯片型号。标准库工程需要包含stm32f10x_gpio.cstm32f10x_rcc.c,这两个文件会在编译时提供 GPIO 初始化和时钟开启的函数。

GPIO 初始化代码如下:

void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); LCD_CS_H; LCD_SCLK_H; LCD_SID_H; }

注意GPIO_Mode_Out_PP是推挽输出,不是开漏。开漏模式下高电平要靠外部上拉,而 STM32 内部上拉太弱,会导致 SCLK 上升沿过缓,ST7920 采样出错。初始化末尾把三个引脚都拉高,是为了让屏幕在正式发送数据前处于空闲状态。

3.2 初始化序列:哪些指令可以省,哪些不能省

ST7920 上电后默认处于 8 位并行模式,即使 PSB 接地,控制器内部也需要一个“切换”过程。所以初始化序列里第一条 0x30 必须发送两次以上,确保控制器稳定进入基本指令集。下面是完整初始化函数:

void LCD12864_Init(void) { LCD_GPIO_Init(); delay_ms(50); LCD12864_WriteCmd(0x30); // 基本指令集,8 位数据长度 delay_ms(5); LCD12864_WriteCmd(0x30); delay_ms(5); LCD12864_WriteCmd(0x0C); // 显示开,光标关,反显关 delay_ms(5); LCD12864_WriteCmd(0x01); // 清屏 delay_ms(10); LCD12864_WriteCmd(0x06); // 地址自动加一 }

初始化指令中 0x30 不能省,0x0C 不能省,0x01 不能省。0x06 是设置写入后地址自动加一,省掉的话连续写字符时每次都要重新设置地址,显示字符串会全部堆在同一个位置。0x30 发送后要留至少 5ms 延时,这是 ST7920 内部模式切换的稳定时间,不少人的黑屏问题就是这里延时不够。

3.3 写命令、写数据与 DDRAM 地址换算

写命令和写数据是整个驱动的中枢:

void LCD12864_WriteCmd(u8 cmd) { LCD_CS_L; // 片选拉低,开始传输 LCD_SendByte(0xF8); // 同步位 + RS=0(RW=0),写命令 LCD_SendByte(cmd & 0xF0); // 高四位 LCD_SendByte((cmd & 0x0F) << 4); // 低四位 LCD_CS_H; // 片选拉高,结束传输 } void LCD12864_WriteData(u8 dat) { LCD_CS_L; LCD_SendByte(0xFA); // 同步位 + RS=1,写数据 LCD_SendByte(dat & 0xF0); LCD_SendByte((dat & 0x0F) << 4); LCD_CS_H; }

0xF8的二进制是11111000,前五位是同步位,最后三位中包含了 RS 和 RW 的置位组合。0xFA则是11111010,对应 RS=1。这里不要自己去拼每一位,直接用这两个魔数最不容易出错。

DDRAM 地址换算有个经典坑:ST7920 的第三行地址是 0x88 而不是线性递增的 0xA0。写一个定位函数:

void LCD12864_SetPos(u8 row, u8 col) { u8 addr[] = {0x80, 0x90, 0x88, 0x98}; if (row > 3) row = 3; if (col > 7) col = 7; LCD12864_WriteCmd(addr[row] + col); }

行地址与坐标对应关系如下:

行号DDRAM 起始地址实际显示行
00x80第一行
10x90第二行
20x88第三行
30x98第四行

列地址 0 到 7 对应屏幕左到右的 8 个汉字位置。这个函数在刷字符串前必须调用一次,否则字符串会接着上次的位置往下写。

3.4 一个最小显示例程:清屏、显示字符串、显示整数

驱动函数齐了,组合起来就是一个完整菜单:

void LCD12864_ShowString(u8 row, u8 col, char *str) { LCD12864_SetPos(row, col); while (*str) { LCD12864_WriteData(*str++); } } void LCD12864_ShowInt(u8 row, u8 col, int val) { char buf[12]; sprintf(buf, "%d", val); LCD12864_ShowString(row, col, buf); } int main(void) { delay_init(); LCD12864_Init(); LCD12864_ShowString(0, 0, "温度:"); LCD12864_ShowInt(0, 5, 26); while (1) { } }

sprintf需要包含stdio.h,标准库工程默认支持。频繁调用sprintf有栈开销,MCU 内存吃紧时可以改用自定义的整数转字符串函数,但大多数 F103 工程这点开销可以忽略。字符串函数里的while (*str)会一直发送直到遇到\0,注意确认传入的字符串以空字符结尾。

4. 显示中文:内建字库、GB2312 编码和字符串乱码

4.1 ST7920 内建字库到底有哪几个区

ST7920 内部固化了多组字库:基本指令集下可以访问 ASCII 半角字符,扩展指令集下可以访问中文字库。中文区编码遵循 GB2312,每字两字节,高字节范围0xB0-0xF7,低字节范围0xA1-0xFE。写入时先高字节后低字节,屏幕才会显示对应汉字。

不要用 UTF-8 编码直接写字符串。UTF-8 的汉字是三字节编码,ST7920 按两字节解析会全部错位,显示出来就是乱码。这个问题在 Keil MDK 里尤其常见,默认保存编码是 UTF-8,直接LCD12864_ShowString(0, 0, "温度")大概率乱码。

解决办法有两个:在 Keil 的 Edit > Configuration > Editor 里把 Encoding 改成 Chinese GB2312,或者在代码中内置一张转换表。改编辑器编码是治本方案,但会影响到整个工程文件的换行和注释,老项目慎动。

如果工程里既有 UTF-8 文件又不能整体改编码,可以在代码里做一个简单的转码查表,只针对常用汉字做映射:

const char *gb2312_table[] = { "温", "度", "湿", "压", "设", "置", };

这种方式不适合全字库,但做固定菜单文本足够。还有一种思路是在源码里直接写转义序列,比如"\xC4\xE3"是 UTF-8 还是 GB2312 取决于你如何生成这个序列,这种方法最可靠,但可读性差,只适合几个固定词。

4.2 显示自定义图形:16×16 点阵取模的方向问题

除了内建字库,ST7920 也支持用扩展指令集写绘图模式,或者直接在文本模式下通过 CGRAM 定义自定义字符。自定义字符需要自己取模。取模方向和 SSD1306 不一样,12864 常见的是逐行式、每行 2 字节、高位在前,这样才和 ST7920 的显存排列吻合。

取模软件设置如下:

设置项推荐值
点阵大小16×16
取模方式逐行式
每行字节2 字节
字节顺序高位在前
行扫描方向从上到下

发送自定义图形时,要把控制器切换到扩展指令集,写入 CGRAM 地址后再发数据。切回基本指令集要重新发 0x30,否则后面文本显示全部错乱。这个切来切去的过程非常容易漏掉恢复指令,写入前和写完后各打一条延时。

4.3 与 GPIO、定时器等外设共用时的刷新策略

LCD12864 串行驱动本身不占用中断,但如果显示函数在中断里调用,或者主循环里刷得太频繁,会出现两个典型问题:一是每个字符 3 个字节的发送过程被打断,导致花屏;二是 ST7920 内部命令处理需要时间,连续发送不等待,高字节和低字节会粘在一起。

常见的处理方式是主循环里设置一个display_dirty标志,需要刷新时置位,主循环检测到标志再调用显示函数。数据更新频率控制在 10Hz 左右就足够,毕竟 12864 的响应速度远低于这个值。与定时器中断、串口中断配合时,显示函数里发送指令的循环要避免被更高优先级中断长时间打断,可以在发送前关中断、发完再开。

__disable_irq(); LCD12864_ShowString(0, 0, "temp=26"); __enable_irq();

这段代码很短,关中断时间约几百微秒,对实时性要求不高的场景完全可以接受。如果中断里有电机控制这类不允许长时间屏蔽的任务,那就把显示刷新放在中断优先级最低的上下文里,或者用硬件 SPI 加 DMA,把发送任务交给外设。

5. 硬件 SPI 与 DMA 驱动 lcd12864 串行的可行方案

5.1 硬件 SPI 为什么能跑但没人敢直接上 DMA

ST7920 的串行数据格式从电气角度看就是 SPI,具体来说是 SPI Mode 3(CPOL=1, CPHA=1),数据在上升沿被锁存,8 位数据宽度,MSB first。用 STM32 的硬件 SPI 发送时,每个字节的发送由外设完成,CPU 只需要往数据寄存器写值。

但 DMA 方案有一点尴尬:一帧命令由 3 个字节组成,片选 CS 要求在整个传输期间保持拉低。如果 DMA 发送完成后立刻拉高 CS,那发送间隔由 DMA 中断响应时间决定,抖动大;如果延长 CS 低电平时间,又拖慢了整体刷新速度。所以常见做法还是 DMA 发送完一整帧,最后一次传输完成中断里拉高 CS。

5.2 用 CubeMX 配置 SPI 并发送一帧命令

CubeMX 里把对应引脚复用为 SPI,配置如下:

SPI 参数设置值
ModeTransmit Only Master
Data Size8 Bit
Clock PolarityHigh
Clock Phase2 Edge
Prescaler16(对应约 4.5MHz,保守值)
NSSSoftware

片选 CS 不要交给 SPI 硬件的 NSS 管理,用普通 GPIO。代码里发送一帧命令:

uint8_t frame[3]; void LCD_SPI_WriteCmd(uint8_t cmd) { frame[0] = 0xF8; frame[1] = cmd & 0xF0; frame[2] = (cmd & 0x0F) << 4; HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, frame, 3, 100); HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET); }

frame数组复用同一块缓冲区,写数据和写命令可以共用一个发送函数。预分频选 16 是因为 ST7920 的串行时钟上限并不高,保守一点反而稳定。代码里HAL_SPI_Transmit是阻塞式,超时时间给 100ms 足够。

5.3 GD32F103 等替代芯片上直接移植的注意点

GD32F103 的 SPI 外设和 STM32F103 高度兼容,寄存器布局几乎一致,标准库和 HAL 代码基本可以无缝平移。移植时真正要改的是时钟树:GD32 的 APB2 总线时钟来源和分频系数与 STM32 不同,SPI 的波特率实际值会不一样,所以不要照抄 STM32 上的预分频值,先降到 8 分频跑通再说。

GPIO 的复用功能配置在 GD32 上是GPIO_Mode_AF_PP,这一点和 STM32 的标准库一致。唯一容易出问题的是 GD32 的 GPIO 翻转速度选项,必须设为最高档,否则 SCLK 边沿变缓,高速传输下会误码。如果用的是 GD32 自己的固件库,函数名不同但参数含义一致,对照数据手册逐个替换即可。

6. 黑屏、乱码、第一行不显示:lcd12864 串行驱动的验证与排错

6.1 上电先看三个信号,不要先改代码

遇到屏幕没反应,先别急着改延时和初始化序列。用示波器或逻辑分析仪抓 CS、SCLK、SID 三根线的波形,对比下面几个特征:

观察现象可能原因处理方向
CS 一直高电平GPIO 配置错或代码没执行到写命令检查初始化函数是否被调用
SCLK 有脉冲但 SID 恒低SID 引脚驱动能力不足或 GPIO 开漏检查推挽输出配置
SCLK 无脉冲GPIO 时钟未开启检查 RCC 时钟
波形正常但黑屏PSB 未接地或对比度电位器没调检查硬件连接和 V0 电压
只有第一行花屏初始化时 0x30 发送过快加大初始化各步骤延时

逻辑分析仪采样率不用太高,10MHz 足够看清 5us 的脉冲时序。没有仪器也可以先把 SCLK 的延时调到最大,用 LED 观察 SID 发送数据时是否有电平跳变,能区分是软件问题还是硬件问题。

6.2 串行模式最常见的三个坑

第一个坑是 CS 在发送一个字节后就被拉高。ST7920 的串行数据是一帧一帧处理的,一帧由同步位和高低位组成,CS 必须保持低直到最后一个位发送完。很多人把三字节发送拆成三次片选,屏幕偶尔显示正常偶尔乱码,就是这个原因。

第二个坑是清屏指令 0x01 后不加延时。ST7920 清屏需要约 1.6ms 内部处理时间,立即写入下一条指令会丢失。我习惯给 10ms,反正不影响体验。

第三个坑是初始化时只发一次 0x30。控制器上电后可能停在 8 位并行模式,只发一次指令无法完成模式切换,必须连续两次以上并保证间隔。一个简单的最小验证代码如下:

LCD12864_WriteCmd(0x30); delay_ms(20); LCD12864_WriteCmd(0x30); delay_ms(20); LCD12864_WriteCmd(0x0C); delay_ms(20); LCD12864_WriteCmd(0x01); delay_ms(20); LCD12864_WriteCmd(0x80); LCD12864_WriteData('A');

如果这段代码能在屏幕第一行显示一个A,说明协议、接线、GPIO 全部正常,剩下的问题都出在编码和坐标地址上。

6.3 验证显示通路后,再改编码和字体

显示通路通了,中文乱码就和驱动无关了。建议先用 ASCII 字符验证每一行的起始地址,比如在四行分别显示1234567891011121,确认地址换算函数无误后再接中文。中文乱码时先检查源码文件编码是否为 GB2312,再看发送顺序是否先高字节后低字节。这两个位置排查完,剩下的就是取模方向问题了。

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

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

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

立即咨询