简介:STM32与LCD12864串行驱动工程资源,面向嵌入式开发者和电子爱好者,解决基于STM32微控制器以串行方式驱动LCD12864液晶显示模块的问题。压缩包含188个文件,约5.26MB,核心包括多个c/h源文件与头文件、o/axf/hex等编译输出文件、uvprojx工程配置以及PDF说明等,覆盖标准外设库与完整项目工程,可直接在Keil等环境编译研究。已有344人学习。资源不仅给出串行接口初始化、命令发送与数据显示代码,还包含字符与图形显示控制示例,以及通过库函数、中断和帧缓冲区提升显示效率的思路,适合希望系统掌握SPI/I2C串行驱动LCD12864的开发者参考学习。
1. 串行驱动的第一句话:省下的8根IO,花在协议上
接手一个小仪表项目,面板上要放一块12864点阵液晶,MCU用STM32F103C8T6。第一反应是“直接怼并行总线”,但算完引脚就心疼了:DB0~DB7、RS、RW、E、PSB,再算上背光,十几根IO直接被吃掉一块,留给按键、传感器和通讯的余量所剩无几。把PSB拉低切到串行模式之后,数据通道只剩SCLK、SID和CS三根线,连上RS一共四个GPIO,这就是标题里“stm32 lcd12864串行驱动”的核心诉求:用最少的引脚,把这块屏幕跑起来。
市面上绝大多数12864屏用的都是ST7920控制器,内置GB2312简体中文字库,串行协议本质上是SPI的变体。串行驱动对5年以上经验的人来说,难点不在“点亮”,而在协议帧的结构、硬件SPI与模拟SPI的坑、初始化时序和DDRAM地址映射。这篇顺着“协议→配置→代码→排错”把整套方案讲清楚,新手可以直接抄,熟手可以对照着避坑。
2. ST7920串行协议:PSB拉低之后,三个引脚怎么把数据发进去
理解串行驱动,先要明白ST7920在串行模式下到底接收什么。网上一搜lcd12864串行驱动,代码满天飞,但很多人直接把并行初始化命令搬过来,屏幕不亮就怪接线——其实是没搞懂一帧数据的组成。这一节把协议拆开讲。
2.1 串行模式引脚映射与模块丝印对照
不同厂商的12864模块丝印差别很大,有的直接标CS、SID、SCLK,有的沿用并行时代的RS、RW、E。串行模式下,ST7920的引脚功能会重新分配,对照关系如下:
| 模块丝印(并行习惯) | 串行模式角色 | STM32 GPIO方向 |
|---|---|---|
| RS | 命令/数据选择(串行标志位的一部分) | 输出 |
| RW | SID,串行数据输入 | 输出 |
| E | SCLK,串行时钟 | 输出 |
| CS(有的模块叫CS1/CS2,ST7920只有一个CS) | 片选,低电平有效 | 输出 |
| PSB | 接GND,切换到串行模式 | 接GND |
接线时先把PSB(有的丝印叫PSB/PS)接低,这一步漏了,后面所有代码都白写。若用的模块自带转接板,丝印已经简化成CS、SID、SCLK,就不用管RS/RW/E那套旧名字了。串行模式下的电源和对比度引脚照常接。注意串行模式只支持“写”,不支持读,所以不需要MISO,这也是为什么用硬件SPI传输时,可以只打开MOSI而不关心MISO的电平。
2.2 0xF8/0xFA控制字节与一帧的组成
ST7920串行传输的基本单位不是普通SPI里的一个字节,而是一个“控制字节 + 数据字节”的组合。控制字节的格式是:先发5个连续的1作为同步位,紧接着是RW(写方向,恒为0)、RS(命令/数据选择)、一个固定的0。拼起来就是:
- 写命令:11111000,即 0xF8
- 写数据:11111010,即 0xFA
所以每次写操作要发16个SCLK时钟:前8个时钟是控制字节,后8个时钟是真正的命令或数据。很多驱动代码里写成先write_byte(0xF8)再write_byte(cmd),就是干这个事。为什么前面要有5个1?ST7920没有独立的片选握手信号,接收端靠这串1来同步位边界,然后从第6个时钟开始解析方向位。CS在整个传输过程中必须保持低电平,两个字节之间不能把CS拉高,否则控制器会认为这是一次新传输,前面的同步就白做了。
2.3 用GPIO先写一个不依赖SPI的发送函数
在引入硬件SPI之前,先实现一个最朴素的模拟SPI发送函数。好处有两个:一是逻辑清晰,二是后面排错时可以直接用GPIO点灯的方式验证时序。直接贴一个最小实现:
// 引脚宏定义,按实际接线修改 #define LCD_CS_GPIO GPIOB #define LCD_CS_PIN GPIO_Pin_12 #define LCD_SCLK_GPIO GPIOB #define LCD_SCLK_PIN GPIO_Pin_13 #define LCD_SID_GPIO GPIOB #define LCD_SID_PIN GPIO_Pin_15 #define LCD_CS_L() GPIO_ResetBits(LCD_CS_GPIO, LCD_CS_PIN) #define LCD_CS_H() GPIO_SetBits(LCD_CS_GPIO, LCD_CS_PIN) #define LCD_SCLK_L() GPIO_ResetBits(LCD_SCLK_GPIO, LCD_SCLK_PIN) #define LCD_SCLK_H() GPIO_SetBits(LCD_SCLK_GPIO, LCD_SCLK_PIN) #define LCD_SID_L() GPIO_ResetBits(LCD_SID_GPIO, LCD_SID_PIN) #define LCD_SID_H() GPIO_SetBits(LCD_SID_GPIO, LCD_SID_PIN) static void lcd_delay_us(volatile uint32_t us) { // 72MHz主频下粗略延时:每个循环约6~8个周期 for (volatile uint32_t i = 0; i < us * 12; i++) { __NOP(); } } static void lcd_write_byte(uint8_t dat) { // 高位在前,一个数据位一个时钟周期 for (uint8_t i = 0; i < 8; i++) { if (dat & 0x80) { LCD_SID_H(); } else { LCD_SID_L(); } LCD_SCLK_H(); LCD_SCLK_L(); dat <<= 1; } } static void lcd_write_cmd(uint8_t cmd) { LCD_CS_L(); lcd_write_byte(0xF8); lcd_write_byte(cmd); LCD_CS_H(); } static void lcd_write_data(uint8_t dat) { LCD_CS_L(); lcd_write_byte(0xFA); lcd_write_byte(dat); LCD_CS_H(); }代码里lcd_write_byte的工作方式是:把字节最高位移到SID,然后SCLK拉高再拉低,形成一个上升沿用采集数据。ST7920在SCLK上升沿采样,所以先摆数据再给时钟。lcd_delay_us用一条粗略循环实现,72MHz下每个迭代大约几个周期,实际值不精确但够用。真正需要关注的是SCLK频率不能太高,手册上串行时钟建议不超过2MHz,所以模拟方式下每周期加一点延时是值得的。
这里有个常见误用:有人把lcd_write_cmd和lcd_write_data里的CS拉高放在函数最末尾,没问题;但如果在两个lcd_write_byte之间插了CS拉高的操作,屏幕就会收到一个不完整的帧,表现是花屏或者某条指令偶尔不生效。CS在整帧传输内必须保持低。
3. STM32硬件SPI驱动配置:模式0、分频比和CS时序
GPIO模拟方式灵活,但占CPU、耗时,而且在72MHz主频下写的延时循环换个编译器水平就变了。跑熟了协议之后,更干净的方案是直接用STM32的硬件SPI。ST7920的串行时序跟SPI模式0高度吻合,但有几个细节要提前处理,否则硬件SPI反而比GPIO模拟更让人头疼。
3.1 SPI模式0的CubeMX参数表
打开STM32CubeMX,选好芯片,把SPI1的SCK、MOSI引脚分配出来,再把片选CS配成普通GPIO输出。ST7920串行模式下只收不发,MISO引脚可以不接。核心参数如下:
| 参数项 | 值 | 说明 |
|---|---|---|
| Mode | Transmit Only Master | 只发送,不需要接收 |
| Hardware NSS Signal | Disable | CS用普通GPIO控制,拉低拉高时机由自己掌握 |
| Data Size | 8 Bits | 每次发一个字节,控制字节和数据字节分两次发 |
| First Bit | MSB First | ST7920要求高位先出 |
| Clock Polarity (CPOL) | Low | SPI模式0,空闲时SCLK为低 |
| Clock Phase (CPHA) | 1 Edge | 第一个边沿采样,即上升沿 |
| Baud Rate Prescaler | 16 或 32 | 72MHz下对应4.5MHz或2.25MHz,建议从32起步 |
| CRC | Disable | 不需要 |
分频这里多说一句:ST7920手册给出的串行时钟上限约2MHz。F103的SPI1挂APB2总线,72MHz主频下Prescaler=16得到4.5MHz,超了;Prescaler=32得到2.25MHz,仍然略超但实测大多数屏没问题;Prescaler=64是1.125MHz,绝对稳,只是刷屏速度慢一点。做产品建议把分频留足余量,跑demo图快可以用除以16。
3.2 标准库/寄存器下的SPI发送实现
CubeMX生成的是HAL库代码,直接往里填一个发送函数即可:
void lcd_spi_send_byte(uint8_t dat) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, dat); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); } void lcd_write_cmd(uint8_t cmd) { LCD_CS_L(); lcd_spi_send_byte(0xF8); lcd_spi_send_byte(cmd); LCD_CS_H(); } void lcd_write_data(uint8_t dat) { LCD_CS_L(); lcd_spi_send_byte(0xFA); lcd_spi_send_byte(dat); LCD_CS_H(); }第一个while等发送缓冲区空,第二个while等总线不忙。两个都要,只看TXE的话,最后一个字节可能还没移出去就把CS拉高了,导致帧尾被截断。如果用的是HAL库,对应的是HAL_SPI_Transmit,但要注意每次调用都有额外开销,连续发两个字节时中间可能产生几个微秒的间隔,这对ST7920完全没问题,它只是采样慢,不怕间隔。
3.3 硬件SPI的拆帧问题与模拟SPI的取舍
硬件SPI最大的坑不在参配,而在CS和SPI字节帧的对齐。前面说了,一个完整的ST7920写操作包含两个8位字节,CS在整帧内必须保持低。如果用硬件NSS,它会在每次SPI_I2S_SendData传输完成后自动拉高,正好把帧拆断。所以这里必须禁用硬NSS,用普通GPIO当CS,自己控制拉低和拉高的时机。
另一个坑是引脚复用冲突。F103的SPI1_SCK、SPI1_MOSI分别是PA5、PA7,这两个引脚同时也被用作普通GPIO。程序里如果用到JTAG相关的PB3/PB4,还涉及复用重映射。建议优先用SPI1的标准引脚,省去重映射的麻烦。
那么,到底用模拟还是硬件SPI?我的判断标准是:项目里SPI总线空闲、引脚充裕,就上硬件SPI,省CPU;如果SCLK引脚被别的外设占用,或者板子走线把SID接到了某个不支持硬件SPI的引脚,就老老实实用GPIO模拟。模拟方式的代码可移植性更好,换GD32、APM32这类国产芯片时几乎不用改,这也是为什么很多开源项目的lcd12864串行驱动默认就是模拟SPI。
4. 初始化序列与显示代码:从点屏到第一行汉字
协议通了,接下来就是让屏幕出字。ST7920初始化不复杂,但顺序敏感:上电后要先给控制器留出复位时间,然后按固定顺序写入功能设置、显示开关、清屏等指令。很多“按下复位能亮、断电再上电就白屏”的怪现象,都出在初始化时序上。
4.1 上电延时与命令表
ST7920数据手册要求上电后等待至少40ms再发第一条命令,实际工程里这个时间还要多留一点,尤其是电源用了DC-DC升压的板子,液晶供电起来得更慢。初始化命令序列如下:
| 步骤 | 命令 | 含义 |
|---|---|---|
| 1 | 0x30 | 8位接口,基本指令集 |
| 2 | 0x30 | 重复发送,确保控制器完成内部复位 |
| 3 | 0x30 | 再发一次,三次更稳 |
| 4 | 0x0C | 显示开,光标关,反白关 |
| 5 | 0x01 | 清屏 |
| 6 | 0x06 | 光标右移,DDRAM地址自动加1 |
第一到第三条0x30是“唤醒”过程。晶振起振、内部状态机复位都需要时间,连续发三次是行业里通行的做法,不能省。0x0C是开显示但不显示光标,0x06决定后续写字符串时地址往哪个方向走。清屏0x01要额外注意,它的执行时间比普通指令长得多。
4.2 写命令、写数据与内部的1.6ms长操作
清屏命令发出后,ST7920内部要逐字把显示RAM清空,这个过程约1.6ms。如果紧接着就发下一条命令,控制器还没忙完,命令会被丢掉。初始化代码里清屏后必须插入延时:
void lcd_init(void) { lcd_delay_ms(50); // 上电稳定 lcd_write_cmd(0x30); lcd_delay_ms(5); lcd_write_cmd(0x30); lcd_delay_ms(5); lcd_write_cmd(0x30); lcd_delay_ms(5); lcd_write_cmd(0x0C); // 显示开,光标关 lcd_delay_ms(2); lcd_write_cmd(0x01); // 清屏 lcd_delay_ms(5); // 清屏执行时间约1.6ms,延时留余量 lcd_write_cmd(0x06); // 地址自动加1 lcd_delay_ms(2); }注意0x30、0x0C、0x01这些命令后面的延时长度并不一样。功能设置和显示开关这类指令执行时间在微秒级,延时可短;清屏是唯一一个必须等毫秒级的。如果初始化完发现屏幕亮但字是乱的,先检查清屏后的延时够不够。
4.3 显示字符串:GB2312中文与ASCII混排
初始化完成就能写字了。写一个字符前要先设置DDRAM地址,用0x80加上列偏移量。ST7920内置GB2312字库,中文两个字节一个汉字,ASCII一个字节一个半角字符。写字符串的代码要按字节流判断:遇到小于0x80的按ASCII处理,遇到大于等于0x80的按汉字处理,取两个字节一起发:
void lcd_show_string(uint8_t row, uint8_t col, const char *str) { // row取值范围0~3,对应4行;col取值范围0~15 static const uint8_t row_addr[4] = {0x80, 0x90, 0xA0, 0xB0}; lcd_write_cmd(row_addr[row] + col); while (*str) { uint8_t c = (uint8_t)(*str); if (c < 0x80) { lcd_write_data(c); // ASCII字符 str++; } else { lcd_write_data(c); // 汉字高字节 lcd_write_data((uint8_t)(*(str + 1))); // 汉字低字节 str += 2; } } }这个函数配合前面的lcd_write_cmd、lcd_write_data就能直接显示字符串。调用示例:
lcd_init(); lcd_show_string(0, 0, "串行驱动OK 123");要点在row_addr这张表:第0行映射到0x80,第1行映射到0x90,第2行映射到0xA0,第3行映射到0xB0,列偏移量直接往上加。字符串里混排中文和数字时,字符宽度不一样,但对DDRAM地址来说,每个ASCII字符占1个地址,每个汉字也占1个地址,只是视觉上汉字比ASCII宽一倍。所以一行16个地址,能放16个汉字或32个ASCII字符,混排时以实际地址步进为准。
5. 坐标、字模与对比度:把“能显示”变成“显示得对”
屏幕点亮、字符串能出来,只是第一步。实际项目里要排版、要对齐、要调可视角度,这些涉及到DDRAM地址映射的细节和模拟电位器的调校。
5.1 4行DDRAM地址表与x坐标换算
ST7920的DDRAM地址并不是按物理行从上到下连续排列的。12864屏在ST7920内部被分成上下两个半屏,每个半屏有2行字符区,物理第3行在内存里的地址反而在第1行附近。完整映射如下:
| 物理行(上位机视觉) | DDRAM起始地址 | 列偏移范围 |
|---|---|---|
| 第1行(上半屏第一行) | 0x80 | 0x80 ~ 0x8F |
| 第2行(上半屏第二行) | 0xA0 | 0xA0 ~ 0xAF |
| 第3行(下半屏第一行) | 0x90 | 0x90 ~ 0x9F |
| 第4行(下半屏第二行) | 0xB0 | 0xB0 ~ 0xBF |
注意第2行和第3行的地址顺序和物理顺序不一致。写代码时最容易错的就是这个:想在第2行显示,结果写到0x90,出来发现去了下半屏。上面lcd_show_string里的row_addr表就是按“物理行”顺序排列的,直接用即可,不要自己去推0x80+偏移。
列偏移x的取值范围是0~15,对应屏幕最左到最右的16个汉字宽度。有的代码里出现“第2行使0x88、第4行使0x98”,那是另一种地址解释方式,本质是把一个物理行拆成了两段各8列来看,容易把人绕晕。这里统一采用4行16列的线性映射,简单可靠。
5.2 中英文字符宽度与布局对齐
做菜单时,中英文混排的对齐是个高频需求。ST7920的ASCII码表其实仍然以16x16点阵的显示空间为基准,半角字符只占左半边8像素,右半边保持空白。因此视觉上一个ASCII字符占8像素,一个汉字占16像素。设计界面时,同样10个字符的宽度,中英文差一倍:
- 一行16个地址,纯中文可放16字,纯ASCII可放32字符。
- 如果居中显示,中文字符串“系统正常”占4字宽,英文“OK”占2字宽。计算偏移时按字符数算,不能按字节数算。
- 数字和英文字母混排时,每个字符宽度相同,一个地址存一个字符,不用特殊处理。
真正麻烦的是要显示“半个字”的画面,比如“正在加载>”这类进度条。ASCII字符画进度条只能画左半边,右半边是空的,想填满就得用图形方式。ST7920的扩展指令集里有一条0x34切换命令,可以进入扩展模式直接操作GDRAM,写自定义图形或进度条。这部分驱动代码要另起一套读改写逻辑,这里不展开,但要知道内置字库模式下做不了任意像素级图形。
5.3 对比度、背光与3.3V供电的取舍
对比度调节在ST7920上不是软件指令能解决的。VO引脚需要一个可调电压,典型值在1~5V之间。常见接法是VO接10K电位器中间抽头,两端分别接VCC和GND,调到屏上出现清晰黑字且背景不残留重影为止。如果VO悬空,屏幕可能全黑或全白,不是代码问题。
3.3V供电的STM32驱动5V供电的12864屏,这是最常见的配置。逻辑电平上,STM32的GPIO输出高电平是3.3V,ST7920的输入高电平门槛约0.7×VDD,5V供电下VIH约3.5V,刚好卡在边缘。所以要么选3.3V兼容版本的模块,要么在SID和SCLK上加电平转换。很多标称5V的模块实测3.3V直接驱动也能稳定跑,因为ST7920的输入缓冲器实际门槛没有手册那么苛刻,但量产产品不建议赌这一点,加个74LVC245最稳妥。
背光电流一般20~60mA,限流电阻根据背光LED正向压降算。注意3.3V系统里如果模块带5V转3.3V的稳压芯片(很多模块上有AMS1117-3.3),供电要按模块丝印要求接,不要想当然只给3.3V。
6. 时序验证与花屏排错:用逻辑分析仪看一帧数据
驱动写完、屏幕亮了,不代表万事大吉。换一块屏、换一个芯片型号、把主频从72MHz改成108MHz(F103超频场景),都可能冒出奇奇怪怪的显示问题。这一章把验证方法和高频故障点列出来,照着排查能省半天时间。
6.1 抓取CS低电平内的SCLK脉冲
调试串行驱动最直接的工具是逻辑分析仪,几十块钱的8通道就够用。把探针接到CS、SCLK、SID三个引脚,抓一次写命令操作,看波形是否符合预期:CS先拉低,然后SCLK连续输出16个脉冲,SID在第一个字节里是按0xF8排列的高位在前,第二个字节是命令内容。
重点看三处:CS低电平时SCLK的脉冲数是不是16;SID在SCLK上升沿时是否稳定;CS拉高的时刻是不是在最后一个SCLK下降沿之后。如果脉冲数不足16,说明SPI配置里数据位设成了16位,或者CS在中间被硬件NSS拉高了。如果SID电平在上升沿附近跳变,说明GPIO模拟代码里先拉时钟后摆数据的顺序写反了。
提示:写命令和写数据在波形上的区别只有第一个字节的bit1,0xF8和0xFA差的就是这一位。抓波形时先写0x30命令再写0x30数据,对比两次波形就能确认RS位是否正确。
6.2 花屏的三个高频原因
花屏、乱码、显示残影,九成是下面三个原因之一:
第一,初始化时序太急。上电后立刻发命令,或者清屏后延时不足,最常见的表现是屏上出现随机亮块。把开头lcd_delay_ms(50)加到100ms,清屏后延时加到10ms,多数问题直接消失。
第二,CS没有整帧保持低电平。用硬件SPI时如果开了NSS硬件控制,每个字节传完CS自动拉高,ST7920会把一个命令拆成两半处理,表现出来就是某些字符位置错乱。解决方法是CS必须用软件GPIO控制,这一点在3.3节已经强调过。
第三,电平不匹配或电源纹波过大。5V供电的屏用3.3V驱动时,SID高电平可能刚好低于门槛。观察现象:屏幕能初始化、能显示背光,但所有字符都是乱的,或者同一帧里前半段正常后半段花掉。这种问题用示波器量SID的高电平电压就能确认,低于2.5V基本就是逻辑电平问题。纹波大的场合,SCLK边沿会产生毛刺,导致ST7920误采样,可以在SCLK引脚串联一个33Ω电阻。
6.3 一个自检技巧:整屏刷白测试
代码驱动基本正常但显示不满意时,我习惯先做一个“整屏刷白”自检:切到扩展指令集,把整个GDRAM写成固定数据,让屏幕全亮或全灭。这一步能快速区分是控制器问题、显示RAM问题还是字库映射问题。
// 进入扩展指令集 lcd_write_cmd(0x34); // 依次写8行GDRAM,每行16字节 for (uint8_t page = 0; page < 8; page++) { lcd_write_cmd(0x80 + page); // 设置GDRAM垂直地址 lcd_write_cmd(0x80); // 设置GDRAM水平地址(首列) for (uint8_t col = 0; col < 16; col++) { lcd_write_data(0xFF); // 全亮,0xFF逐位对应8个像素 lcd_write_data(0xFF); } } // 回到基本指令集 lcd_write_cmd(0x30);这段代码会分8页把整个屏幕刷成点亮状态。注意扩展指令集下写入GDRAM需要连续发两个数据字节(低字节在前、高字节在后),地址会自动累加。刷白正常而写中文异常,说明问题出在字库编码或DDRAM地址映射;刷白直接花屏,问题基本在控制器的时序或供电上。用这个方法把屏幕本身的读写逻辑验证干净,再回头查字符串驱动,排查范围就小很多。
最后留一个实际操作上的建议:CS拉高后,下一帧之前不要做多余的空操作,直接发下一帧就行。ST7920对CS高电平的保持时间没有严格要求,真正要盯住的只有CS低电平时那16个SCLK脉冲。把这条记进代码注释,以后再调这个屏,照着波形对,一行一行查,不会迷路。
本文还有配套的精品资源,点击获取