
简介一套STM32驱动LCD12864实现串行显示的完整工程面向嵌入式入门者与有一定基础的STM32开发人员可直接用于毕业设计、课程实验或产品原型中的液晶显示模块开发解决串行模式下接线混乱、时序不对、显示花屏等常见问题。包内共642个文件以343个C源码、122个头文件为主还包含链接脚本、启动汇编文件、HAL库相关配置、IOC工程文件以及编译产物hex/axf压缩包约3.47MB目录结构清晰方便对照学习与二次开发。目前已有4069人学习下载。该工程经实际项目验证串行驱动稳定可靠包含LCD初始化、清屏、字符/汉字显示、绘图等核心函数并已配置好时钟与GPIO可直接编译烧录通过研读代码既能快速复现LCD12864串行显示效果也可掌握GPIO模拟时序、底层寄存器操作和驱动模块化的设计思路为后续移植到其他MCU或扩展菜单界面提供可靠参考。1. 串行接法一根数据线换掉 11 个引脚的选择第一次拿到 LCD12864 的人多半是按并行方式布的线8 根数据线加上 RS、RW、E再算上 PSB 和背光一块屏吃掉 11 个 GPIO。等到还要挂按键、传感器、电机驱动的时候引脚就明显不够用了。标题里的「串行实现」就是把数据通道从 8 根并线收成 SID 单线加上 SCLK 和 CS一共三根信号线屏幕照常显示中文和图形给 STM32 省出的引脚足够再挂两个串口外设。这里有个常见误解12864 本身不是只有一种控制器带中文字库的屏幕绝大多数用 ST7920本文所有时序、帧格式和代码都基于这一颗控制器。串行模式正是 ST7920 提供的功能把 PSB 引脚拉低后原本并行的数据口全部失效屏幕只认三线协议。后面要讲的三字节帧怎么组、初始化顺序为什么不能乱、中文字库地址为什么第三行会错位都是围绕这颗控制器展开的。适合的场景是引脚紧张的板子、需要频繁改显示内容的项目以及想少飞几根杜邦线的桌面调试。2. ST7920 串行协议与关键引脚先看懂三字节帧再说驱动2.1 串行模式省下的引脚和必须改的接法ST7920 的串行模式不是简单地把 8 根数据线并到一起而是换了一套物理接口。模块上的 RS、RW、E 在串行模式下基本失效DB0~DB7 全部悬空。真正在工作的是 CS、SCLK、SID 三根线加上电源和背光总共五根线就能点亮。模块接口并行接法串行接法说明RS数据/命令选择悬空或接 GND串行下 RS 信息在控制字里RW读写选择悬空或接 GND串行不支持读忙检测不可用E使能时钟接 STM32 的 SCLK串行时钟输入CS片选接任意 GPIO低电平有效一帧内保持低SID / MOSI不使用接任意 GPIO单线串行数据输入DB0~DB78 位数据线全部悬空串行模式下无效PSB接 VCC接 GND关键PSB 拉低才进串行模式模块丝印不同有的把 SID 标成 MOSI有的没引出 CS需要以模块背面丝印为准。PSB 是这里最容易出错的引脚它在并行接法里是接高电平的有人换了串行只改了数据线PSB 忘了动屏幕完全黑屏。上电后用万用表量一下 PSB 对地电压接近 0V 才是串行模式。2.2 三字节帧格式每个有效字节占 24 个时钟串行模式下ST7920 不认单个字节它认的是一帧三字节。每次想写入一个命令或一个数据SID 线上要连续送出三个字节SCLK 给出 24 个上升沿。第一字节是同步头第二、三字节承载真正的数据。字节序内容格式举例命令 0x80举例数据 0x41第 1 字节11111 RW RS 00xF80xFA第 2 字节D7 D6 D5 D4 0 0 0 00x800x40第 3 字节D3 D2 D1 D0 0 0 0 00x000x10第一字节的高五位固定是 11111这是同步头ST7920 靠它识别一帧的开始。紧接着的 RW 位在写操作里固定为 0RS 位决定这一帧是命令还是数据最后一位固定是 0。于是命令帧的同步字就是 11111000 即 0xF8数据帧同步字是 11111010 即 0xFA。第二字节和第三字节很好理解数据高四位左对齐放第二字节低四位左对齐放第三字节。注意低四位位置必须补零不是把数据直接拆成两个 4 位拼上去而是各自左移到高四位的位置。位序是 MSB first也就是每个字节内部从最高位开始送SCLK 上升沿采样。理解了这个帧结构后面写底层函数就是机械地拼字节。2.3 时序参数、电平匹配和硬件 SPI 的关系ST7920 的串行时序并不苛刻。手册给出的 SCLK 高电平宽度和 SID 建立时间都在百纳秒级别STM32 的 GPIO 翻转速度远高于这个要求。真正需要控制的是命令与命令之间的间隔而不是单个时钟的速度。时序参数典型值实际建议SCLK 高低电平宽度约 200nsGPIO 翻转天然满足SID 建立时间约 60ns先放数据再拉高时钟即可CS 低电平保持覆盖整帧 24 个时钟一帧期间不能拉高命令间隔忙时间约 72us初始化阶段按 1~5ms 用电平匹配是很多人忽视的坑。屏幕模块有 3.3V 和 5V 两个版本ST7920 的 VIH 约为 0.7 倍 VDD。如果模块是 5V 供电0.7 倍就是 3.5VSTM32 的 3.3V 高电平处于不确定区间偶尔能亮但属于边缘工作。稳妥做法是选 3.3V 供电的模块或者 5V 模块加一路电平转换。桌面调试阶段3.3V 供电模块加杜邦线直连最省事。硬件 SPI 也能驱动这块屏SID 对应 MOSICS 用软件控制SPI 模式选 CPOL0、CPHA0。但硬件 SPI 的引脚被外设锁定波特率还得压到 1MHz 以内灵活性不如 GPIO 模拟。所以下面的实现全部用 GPIO 模拟这也是网上标准库例程最常见的做法。3. STM32 上用 GPIO 模拟三线时序不挑引脚的驱动实现3.1 引脚定义与初始化配置GPIO 模拟的好处是引脚任意选CubeMX 里把目标引脚配成推挽输出、无上下拉、初始电平拉高即可。时钟只需要 GPIO 时钟不需要开 SPI 外设。HAL 库和标准库在这几个函数上的差别只在引脚写入那一层换库成本很低。// lcd12864_serial.h 摘录 #include stm32f1xx_hal.h // 按实际芯片型号包含头文件 // 以下是引脚定义根据自己板子改端口和引脚号 #define LCD_CS_PORT GPIOB #define LCD_CS_PIN GPIO_PIN_12 #define LCD_SCLK_PORT GPIOB #define LCD_SCLK_PIN GPIO_PIN_13 #define LCD_SID_PORT GPIOB #define LCD_SID_PIN GPIO_PIN_14 #define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET) #define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET) #define LCD_SCLK_LOW() HAL_GPIO_WritePin(LCD_SCLK_PORT, LCD_SCLK_PIN, GPIO_PIN_RESET) #define LCD_SCLK_HIGH() HAL_GPIO_WritePin(LCD_SCLK_PORT, LCD_SCLK_PIN, GPIO_PIN_SET) #define LCD_SID_LOW() HAL_GPIO_WritePin(LCD_SID_PORT, LCD_SID_PIN, GPIO_PIN_RESET) #define LCD_SID_HIGH() HAL_GPIO_WritePin(LCD_SID_PORT, LCD_SID_PIN, GPIO_PIN_SET)这些宏把引脚操作收口换板子只改上面六行。标准库工程把 HAL_GPIO_WritePin 换成 GPIO_SetBits 和 GPIO_ResetBits 就行像 APM32 这类兼容 STM32 的国产芯片也能直接编译通过。GPIO 初始化本身简单CubeMX 生成后不需要额外改配置三个引脚保持默认推挽输出即可。延时函数这里给一个最简单的忙等待实现。串行时序对延时精度不敏感关键是初始化阶段需要几十毫秒的大延时给个粗延时足够static void lcd_delay_us(uint32_t us) { // 72MHz 主频下约等于 us 微秒不同主频需要重新调参 for (uint32_t i 0; i us * 8; i) { __NOP(); } }这个延时不追求精确。ST7920 的串行时钟没有严格下限慢个十倍八倍都能正常工作只有太快才会出问题。如果工程里已经有 DWT 或 SysTick 延时直接替换成自己的函数更好。3.2 底层位发送函数上升沿采样要写对底层发送是整个驱动的地基。每个字节要从最高位开始一位一位放到 SID 上每放一位给一个 SCLK 上升沿。正确顺序是先把 SCLK 拉低、数据放到 SID、再把 SCLK 拉高这样 ST7920 在上升沿采样时 SID 已经稳定。static void lcd_send_byte(uint8_t byte) { for (uint8_t i 0; i 8; i) { LCD_SCLK_LOW(); // 时钟拉低准备传下一bit if (byte 0x80) { LCD_SID_HIGH(); // 数据位为1SID置高 } else { LCD_SID_LOW(); // 数据位为0SID置低 } LCD_SCLK_HIGH(); // 时钟拉高上升沿采样 byte 1; // 把下一位移到最高位 } }每次循环先把 SCLK 拉低这时候改 SID 电平不会触发采样。byte 0x80 取出当前最高位决定 SID 电平然后 SCLK 拉高完成一次采样。byte 1 让下一位顶到 bit7 位置方便下一轮判断。一个容易写错的反模式是先放数据、再拉低 SCLK、再拉高 SCLK。这个流程看起来差不多但 SCLK 下降沿到上升沿之间的数据变化可能导致建立时间不足尤其在线长、寄生电容大的时候。写驱动时固定成「拉低时钟 → 放数据 → 拉高时钟」的顺序能少踩一个隐性问题。3.3 把有效字节拆成三字节帧写命令和写数据有了 lcd_send_byte 之后命令和数据的区别只在第一字节的 RS 位上。命令帧第一字节固定 0xF8数据帧固定 0xFA后面两字节按高半字节、低半字节顺序送。static void lcd_write_cmd(uint8_t cmd) { LCD_CS_LOW(); // 片选拉低开始一帧 lcd_send_byte(0xF8); // 11111 0 0 0写命令同步头 lcd_send_byte(cmd 0xF0); // 高四位左对齐 lcd_send_byte((cmd 0x0F) 4); // 低四位左对齐 LCD_CS_HIGH(); // 片选拉高结束一帧 } static void lcd_write_data(uint8_t dat) { LCD_CS_LOW(); lcd_send_byte(0xFA); // 11111 0 1 0写数据同步头 lcd_send_byte(dat 0xF0); // 高四位 lcd_send_byte((dat 0x0F) 4); // 低四位 LCD_CS_HIGH(); }0xF8 的二进制是 11111000拆开看就是同步头 11111、RW0、RS0、固定 0。0xFA 是 11111010只有 RS 位从 0 变 1。第二字节 cmd 0xF0 把低四位清零高四位保留在自己的高位位置正好满足帧格式。第三字节先把低四位取出来再左移四位让它也站到高位位置。CS 的控制是这里最容易错的地方必须覆盖完整的三字节也就是 CS 低电平要持续 24 个 SCLK。如果每发一个字节就拉高 CS 再拉低ST7920 会把每个字节都当成新帧的第一字节来解析连续三个同步头互相干扰显示结果完全乱掉。上面代码把 CS_LOW 放在第一字节之前、CS_HIGH 放在第三字节之后就是保证一帧完整。3.4 初始化序列三遍 0x30 不是多余初始化是对着 ST7920 手册的时序要求一步步来的。上电后必须先等屏幕内部电源稳定然后发送功能设定命令之后才能操作显示开关和清屏。串行模式下没有忙标志可读所有等待都靠延时实现这里宁可给大不要给小。void lcd_init(void) { LCD_CS_HIGH(); LCD_SCLK_HIGH(); LCD_SID_HIGH(); lcd_delay_us(50000); // 上电稳定ST7920要求大于40ms lcd_write_cmd(0x30); // 功能设定8位接口、基本指令表 lcd_delay_us(5000); // 手册要求大于4.1ms lcd_write_cmd(0x30); // 第二次设定 lcd_delay_us(1000); // 大于100us lcd_write_cmd(0x30); // 第三次保险起见 lcd_write_cmd(0x0C); // 显示开、光标关、反白关 lcd_delay_us(1000); lcd_write_cmd(0x01); // 清屏 lcd_delay_us(5000); lcd_write_cmd(0x06); // 写入后地址自动加1光标右移 lcd_delay_us(1000); }0x30 发三遍是因为 ST7920 上电后内部状态不确定不同电源上升速度下进入稳定状态的时间不一样。第一遍 0x30 可能在内部还没起来时被丢弃第二遍才真正生效第三遍确保无误。0x0C 控制显示开关和光标形态0x01 清屏同时把地址归零0x06 决定写完 DDRAM 后地址自动加一。顺序不能换功能设定必须最先做清屏必须在显示开之后否则屏幕可能亮但内容错乱。提示初始化里的延时时长可以统一放大到几十毫秒不影响任何功能。真正重要的是「顺序」和「至少满足手册最小值」不是精确到微秒。有些并行例程里会出现 0x34那是切换到扩展指令表用的命令基础文本显示不需要。如果后续要做绘图模式再在初始化末尾追加 0x34 相关序列这里先用 0x30 系列把文本显示跑通。4. 字符串显示与中文字库DDRAM 地址和编码两个坑4.1 DDRAM 四行地址表第三行才是 0x88ST7920 的显示数据 RAM 用 16 位地址空间映射四行汉字每行 8 个汉字、16 个半角字符。启动一个位置显示的指令格式是「0x80 行首地址 列偏移」。行首地址是这里最容易踩坑的地方它并不是按 0x80、0x88、0x90、0x98 顺序排下来的。逻辑行行首地址常用 line 参数第 1 行0x800第 2 行0x901第 3 行0x882第 4 行0x983第二行是 0x90第三行是 0x88地址差只有 0x08。很多显示错位的问题就出在这里想在第三行显示结果把地址当成 0x9080x98直接跑到了第四行。写驱动时最好把行地址做成查表数组调用侧只传 line 编号不要在业务代码里手动算地址。4.2 ASCII 与汉字混排时的字节宽度DDRAM 的一个地址对应 16 位点阵空间。半角 ASCII 字符占 8 位宽、一个地址汉字占 16 位宽、连续两个地址。这意味着发送字符串时不能简单地把字符串字节流按顺序全部送到写数据函数必须判断每个字符是单字节还是双字节。void lcd_show_string(uint8_t x, uint8_t y, const char *str) { static const uint8_t row_addr[4] {0x80, 0x90, 0x88, 0x98}; // 先定位到目标位置x是半角列坐标0~15 lcd_write_cmd(0x80 row_addr[y] x); while (*str) { if ((uint8_t)*str 0x80) { // 单字节ASCII直接写列坐标加1 lcd_write_data((uint8_t)*str); str; x; } else { // 双字节汉字先写高字节再写低字节列坐标加2 lcd_write_data((uint8_t)*str); str; lcd_write_data((uint8_t)*str); str; x 2; } } }这个函数有两个前提条件。第一源文件的字符编码必须是 GB2312 或 GBKKeil MDK 默认就是这种编码直接写lcd_show_string(0, 0, 电压 12.5V)不用额外处理。第二判断依据是每个字节是否小于 0x80GB2312 汉字内码的两个字节都大于 0x7F所以这个条件能正确区分 ASCII 和汉字。如果工程用 GCC 且源文件存成了 UTF-8这个函数会显示乱码。UTF-8 的汉字是三字节编码用双字节切分会错位后半个汉字和一串空白全乱。处理办法是把源文件转成 GB2312或者工程里统一强制源文件编码为 UTF-8 再单独写内码转换函数。论坛上大量「12864 显示中文乱码」的问题追到根上十有八九是编译器编码和屏幕字库编码不匹配。4.3 光标定位与局部刷新避免整屏重绘闪烁显示场景里经常需要改了数值只更新某一行这时候要单独提供定位函数让业务代码能精确跳到目标位置覆盖旧内容。void lcd_set_cursor(uint8_t line, uint8_t col) { static const uint8_t addr[4] {0x80, 0x90, 0x88, 0x98}; if (line 3) line 3; // 行越界保护 if (col 15) col 15; // 半角列坐标上限 lcd_write_cmd(addr[line] col); }col 按半角列计算范围 0~15。显示数字、冒号、小数点这些半角字符时 col 加 1显示汉字时 col 加 2。举例要在一行显示「温度:25.5C」冒号、数字、字母共占 8 个半角列「温度」占 4 个半角列总宽 12 列从 col0 开始刚好一行放下。刷新策略上有个常见误用每次更新数据都调用清屏再重新显示。12864 的响应速度本来就不快整屏清屏加重绘在视觉上会闪而且清屏后到重新写完之间有几百微秒的空窗。正确做法是调用 lcd_set_cursor 定位到变化区域只写变化的字符。比如电压从 12.5V 变成 13.2V只需要定位到数字起始列重新写一遍数值字符串后面没变化的字符不用动。5. 上电点不亮的 3 个典型坑从接线到时序逐项排查5.1 PSB 悬空或接高串行帧根本送不进去PSB 是 ST7920 的接口模式选择引脚接 VCC 是并行、接 GND 是串行。很多人换串行接法时只改了 RS、RW、E 这三根线PSB 还留在原来的高电平位置屏幕一直在等并行时序你发的串行帧它完全识别不了现象是背光亮但屏幕全黑。排查方法上电后用万用表量 PSB 对地电压必须接近 0V。有些开发板配套的 12864 模块上 PSB 由跳线或 0 欧电阻控制需要先改硬件再谈软件。5.2 初始化延时不够命令丢失后显示全乱串行模式下 ST7920 不提供忙标志读取因为读操作在串行接口上不可用所有命令间的等待只能靠延时。数据手册标的忙时间是 72 微秒但那是内部操作时间不包含升压电路和显示驱动的启动过程。初始化阶段的延时我会放大到手册最小值的十倍以上命令间等 1~5 毫秒清屏后等 5 毫秒。如果反复调试发现某条命令偶尔不生效把延时再拉大一倍多数情况能解决。这类问题典型表现是屏幕亮了但显示内容缺行、缺字或者光标位置乱跳。5.3 用逻辑分析仪核对 SID 波形一帧应该是 24 位调试串行协议最有效的手段是看波形而不是反复改代码碰运气。把 lcd_delay_us 里的空循环倍数调大让每一位的时钟周期放到 100 微秒左右然后只调用一次lcd_write_cmd(0x80)用逻辑分析仪抓 CS、SCLK、SID 三根线。预期结果是CS 低电平期间 SCLK 出现 24 个上升沿SID 依次输出 11111000、10000000、00000000 三段。如果 SID 波形里看不到 0xF8 开头的帧结构问题在发送函数或 GPIO 配置如果波形完全正确但屏幕不亮问题在 PSB 电平或模块供电电压。这一步能直接把问题从「软件故障」和「硬件故障」分成两半省掉大量盲调时间。只有示波器也行不需要分析具体位只要验证 CS 低电平期间 SCLK 有 24 个上升沿就说明底层发送在正常工作。顺带提一个调试顺序问题ST-LINK 报 no target found 的时候别急着查 LCD先用一个 GPIO 翻转工程确认 SWD 口还能连上芯片、电源供电正常再回头查显示。下载器都连不上时屏幕大概率也是供电问题逐级排查比在显示函数里找原因高效得多。ST7920 的串行协议本身不复杂一帧 24 位、顺序固定、时序宽容把 PSB 电平、初始化延时、CS 低电平覆盖这三点确认到位屏幕基本就能一次点亮。本文还有配套的精品资源点击获取