简介:面向嵌入式开发与液晶显示初学者的ST7567a驱动源码包,围绕132x65点阵LCD常见驱动IC,提供基于SPI接口的底层控制与显示逻辑实现。源码包含驱动初始化、SPI收发、窗口设置、清屏以及点、线、图形绘制等核心函数,并给出段码位码组合表的映射思路,便于直接移植到不同型号LCD模块,或作为入门学习模板。压缩包共3个文件,以2个h头文件和1个c源文件构成,整体仅5KB,代码精简清晰,适合快速阅读和二次修改。目前已有2130人学习下载,主要面向需要快速上手ST7567a驱动开发的硬件工程师、嵌入式爱好者及课程设计场景。通过阅读这份驱动程序,可掌握SPI总线的配置方法,理解驱动IC命令集与显示数据组织方式,并学会将图像数据映射至物理像素的显示流程,为自研显示驱动或项目集成提供可复用的参考实现。
1. ST7567a 不是 ST7565P:12864 屏点不亮的真相
很多人拿到 ST7567a 驱动资源时,第一反应是“这不就是 ST7565P 的升级版吗,驱动直接改个名字就能用”。这个判断在 60% 的板子上会翻车。ST7567a 虽然指令集大体继承自 ST7565 系列,但寄存器映射、偏压设置、以及掉电检测行为都有重定义,直接套旧驱动最常见的现象是:初始化看起来成功、背光亮了、屏幕却一片白或者出现规律性花屏。另一个反直觉的点是:这颗 IC 内部没有页地址概念,显存是连续的 8 页 × 128 字节结构,写地址可以跨页连续递增,但很多人用 ST7565P 的“设置页地址+设置列地址”套路去刷新,结果只在第一页里打转。这份资源解决的正是这类问题——它把 ST7567a 从硬件管脚、SPI 时序、初始化序列到上层绘图接口全部打通,适合正在做 128×64 点阵 COG 液晶模块、需要移植驱动或从零驱动这块屏的嵌入式开发者。下面我从时序和寄存器层面把它拆开讲。
2. 硬件接口与 SPI 时序:管脚电平、模式与上电顺序
2.1 ST7567a 引脚清单与模块对应关系
ST7567a 最常见的封装是 COG(Chip On Glass),也就是驱动 IC 直接绑定在液晶玻璃上。市面上绝大多数 12864 COG 模块引出的是 8 个引脚:VDD、GND、SCL、SDA、A0(即 RS,数据/命令选择)、RES(复位)、LEDA(背光正极)、LEDK(背光负极)。有的模块会多引一个 VOUT 或者 NC,一般不接。
引脚对应关系要注意三点。第一,A0 的电平方向不要搞反:A0 = 0 时写入的是命令,A0 = 1 时写入的是显存数据。部分模块丝印上写的是 RS,含义相同,但有些国产模块把 A0 和 RES 顺序印反,焊接前务必用万用表跟随 PCB 走线确认。第二,RES 是低电平有效复位,模块内部通常有 RC 复位电路,但为了初始化时序可控,我建议用 MCU 的 GPIO 单独控制,不要依赖模块自带的 RC。第三,LEDA/LEDK 只是背光回路,和逻辑电压完全独立,如果背光不亮而屏幕正常显示,先查这两根线,别急着怀疑驱动。
上电顺序是另一个容易忽略的点。正确做法是:先给 VDD 供电,等电源稳定后再把 RES 拉低复位,复位释放后至少等待 10ms 再开始发初始化命令。如果 MCU 和 LCD 共用一个电源轨,需要在代码里加一个延时,不能开机后立刻初始化,否则 ST7567a 内部上电复位未完成,第一条命令会被吞掉,后续所有配置全部偏移,表现就是屏幕显示乱码或者完全不亮。
2.2 SPI 写时序与模式选择:坑藏在时钟空闲极性里
ST7567a 的 SPI 是写-only 接口,没有 MISO,所以只需要三根线:SCL、SDA、A0。注意它不支持读操作,这一点和 ST7565P 相同,意味着你想回读显存或者寄存器状态是做不到的,调试时只能通过显示效果反推。
SPI 模式的选择需要看数据手册里的时序图。ST7567a 的典型要求是:SCL 空闲为低电平,数据在 SCL 下降沿被 IC 采样,即 SPI Mode 0(CPOL = 0,CPHA = 0)。但 IC 本身不校验模式,只要满足建立时间和保持时间就能工作,所以在部分 MCU 上用 Mode 1(CPOL = 0,CPHA = 1)也能点亮,这是因为 ST7567a 对 SCL 边沿采样的容差较大。我在两个不同平台上实测过:GPIO 模拟 SPI 时无所谓模式,但硬件 SPI 外设建议固定配置为 Mode 0,避免某些 MCU 的 SPI 外设在 Mode 1 下先输出半个周期的无效数据导致首字节错误。
写命令和写数据的时序差异只在于 A0 电平。一个字节的传输流程是:A0 先建立目标电平,然后 SCL 产生 8 个上升/下降沿,期间 SDA 按 bit7 到 bit0 顺序输出。需要特别注意的是,ST7567a 的 SDA 数据在 SCL 下降沿采样,所以你如果用 MCU 的硬件 SPI,要确认外设的采样边沿和输出边沿配置和这个匹配。我一般不用硬件 SPI 而用 GPIO 模拟,原因很简单:12864 全屏刷新也就 1KB 显存,SPI 时钟跑 2MHz 就够,而 GPIO 模拟可以完全避开“外设时钟极性配置错误导致整屏数据错位”的问题。
// GPIO 模拟 SPI 写字节,兼容 ST7567a / ST7565P #define LCD_SCL_H() GPIO_SetPin(GPIO_LCD_SCL, 1) #define LCD_SCL_L() GPIO_SetPin(GPIO_LCD_SCL, 0) #define LCD_SDA_H() GPIO_SetPin(GPIO_LCD_SDA, 1) #define LCD_SDA_L() GPIO_SetPin(GPIO_LCD_SDA, 0) #define LCD_A0_H() GPIO_SetPin(GPIO_LCD_A0, 1) #define LCD_A0_L() GPIO_SetPin(GPIO_LCD_A0, 0) #define LCD_RST_L() GPIO_SetPin(GPIO_LCD_RES, 0) #define LCD_RST_H() GPIO_SetPin(GPIO_LCD_RES, 1) static void lcd_write_byte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { LCD_SCL_L(); if (data & 0x80) LCD_SDA_H(); else LCD_SDA_L(); LCD_SCL_H(); // 上升沿锁存数据 data <<= 1; } } void lcd_cmd(uint8_t cmd) { LCD_A0_L(); // A0 = 0 表示命令 lcd_write_byte(cmd); } void lcd_data(uint8_t dat) { LCD_A0_H(); // A0 = 1 表示显存数据 lcd_write_byte(dat); }这段代码核心是lcd_write_byte:每次先把 SCL 拉低,然后设置 SDA 电平,再把 SCL 拉高产生上升沿,让 IC 锁存当前 bit。lcd_cmd和lcd_data的区别仅在 A0 的建立顺序。参数上需要注意,data & 0x80判断的是最高位,因为 ST7567a 的 SPI 传输是 MSB first,如果改成 LSB first,所有命令和显存数据都会镜像错乱。延时方面,GPIO 模拟不需要额外延时函数,GPIO 翻转本身就有几百纳秒的上升沿,ST7567a 的 SCL 最低高电平宽度要求约 50ns,完全满足。
3. 驱动分层设计:把 IC 指令和上层绘图解耦
3.1 三层结构:硬件抽象、IC 底层与绘图 API
直接在主程序里满天飞lcd_cmd和lcd_data的驱动,一两个月后自己都看不懂。我拿到这份资源后发现它的分层很干净,值得直接复用。整个驱动分三层:
最底层是硬件抽象,封装 GPIO 初始化、SPI 字节发送、复位控制。这一层只认管脚,不认 ST7567a。第二层是 IC 底层驱动,包含初始化序列、设置列地址、设置页地址、写显示数据、设置偏压、设置对比度等,这层只和 ST7567a 寄存器打交道。第三层是上层绘图 API,提供清屏、画点、画线、画矩形、显示字符串,这层不直接操作寄存器,只往显存 buffer 里写数据,最后统一刷新。
这套分层的核心价值是:换 MCU 平台时只需改第一层,换 LCD 驱动 IC 时只需改第二层,第三层完全不动。我在迁移到另一颗 IC 时,花了大约半小时替换第二层,上层画图代码零改动。如果你接手别人的 ST7567a 工程,看到初始化、画点、清屏全在一个文件里,建议先按这个结构拆开再改功能,否则后面每加一个按钮图标都是灾难。
// 上层绘图接口只面对显存 buffer,不感知 ST7567a 的存在 // 显存定义: 8 页 x 128 列,每字节表示一列 8 个垂直像素 static uint8_t lcd_gram[8][128]; void lcd_set_pixel(uint8_t x, uint8_t y, uint8_t color) { if (x > 127 || y > 63) return; // 边界裁剪,防止越界写坏显存 uint8_t page = y >> 3; // y / 8 得到页号 uint8_t bit = 1 << (y & 0x07); // y % 8 得到页内 bit 位 if (color) lcd_gram[page][x] |= bit; else lcd_gram[page][x] &= ~bit; } void lcd_refresh(void) { lcd_cmd(0x40); // 设置起始行 = 0 for (uint8_t page = 0; page < 8; page++) { lcd_cmd(0xB0 | page); // 设置页地址 lcd_cmd(0x10); // 列地址高 4 位 = 0 lcd_cmd(0x00); // 列地址低 4 位 = 0 for (uint8_t col = 0; col < 128; col++) { lcd_data(lcd_gram[page][col]); // 逐列写一字节 } } }lcd_set_pixel里y >> 3和y & 0x07是把像素坐标映射到页内 bit 的标准做法。ST7567a 显存是一列一字节的结构,即每一列有 8 个像素点垂直排列,一个字节的 bit0 是上方第一个像素。特别注意1 << (y & 0x07)的方向:如果你的屏上下显示颠倒,不是改这个宏,而是要在初始化里设置 SHL 位(即 MX 镜像),或者在lcd_refresh里把起始行从0x40改成0x48之类的值。lcd_refresh里每次刷新先发0x40设置起始行为第 0 行,这是为了保证滚动或残影后显示不偏移,代价是每次刷新多出 1 条命令,12864 场景下完全可忽略。
3.2 显存管理与局部刷新:无页寻址也能精确改点
ST7567a 显存是 8 页固定结构,但和 ST7565P 的页地址模式略有不同:ST7567a 的列地址在写完一页后可以自动跨页递增到下一页。也就是说,你可以连续写 8×128 = 1024 字节而不需要中途切页指令。这一点在做整屏刷新时能省掉大量命令字节。
局部刷新有两种策略。第一种是把整个 1KB 显存在 MCU 里保持一份镜像,改哪个点就同步改镜像,然后整屏刷。第二种是只刷新改变区域对应的页和列范围。第二种方案的命令开销更小,但需要追踪脏区域。我的习惯是:菜单界面这种局部变化场景用脏矩形跟踪,图形曲线动态绘制场景用全屏刷新。ST7567a 的 SPI 时钟 2MHz 时全屏刷新约 4ms,这个速度在 60fps 需求下已经不够用,但对大多数菜单类界面绰绰有余。
还有个容易踩的细节:写显存前必须设置列地址。列地址由两条指令组合完成,0x10是高 4 位,0x00是低 4 位。如果你只设置了页地址却没设置列地址,写完一列后列地址会自动加 1,但等到跨页时列地址不会重置,第二页会从上次结束的列开始,显示画面会整体错位 16 列。很多人的驱动“看起来发虚”就是这个原因。
4. 初始化序列与关键寄存器:一份能点亮屏的配置
4.1 初始化序列逐条拆解
ST7567a 的初始化序列没有官方强制顺序,但稳定性好的配置通常按照“复位 → 开关电源 → 设置偏压 → 设置 ADC 方向 → 设置显示起始行 → 清屏 → 开显示”这样的顺序。下面是这个资源里实际可用的初始化序列,逐条说明含义。
void lcd_init(void) { LCD_RST_L(); delay_ms(50); LCD_RST_H(); delay_ms(50); lcd_cmd(0xE2); // 软件复位,内部初始化状态机 delay_ms(10); lcd_cmd(0x2C); // 升压电路开启: 内部电荷泵 DC/DC 启动 delay_ms(50); // 等待电荷泵稳定,否则对比度不足或闪烁 lcd_cmd(0x2E); // 调整电压输出级别 lcd_cmd(0x81); // 对比度设置命令第一段 lcd_cmd(0x28); // 对比度值,范围 0x00~0x3F,0x28 是常见可用值 lcd_cmd(0xA2); // 偏压比 1/9,适配多数 COG 模块功耗需求 lcd_cmd(0xA0); // ADC 正常方向,列地址从左到右递增 lcd_cmd(0xC8); // SHL 反向,多数模块上下颠倒时需要此配置 lcd_cmd(0x40); // 显示起始行 = 0 lcd_cmd(0xA6); // 正常显示,非反色 lcd_cmd(0xA4); // 关闭全屏点亮测试模式 lcd_cmd(0xAF); // 开启显示 delay_ms(20); lcd_clear(); // 清空显存,避免上电随机内容残留 }逐条说明三个容易改错的参数。0x2C是升压电路开启命令,有些参考代码写成0x2E(内部电压调节器开启),两者区别在于电荷泵输出电压的稳定路径。ST7567a 必须用0x2C开启内部 DC/DC,否则 VOUT 无输出,屏幕连影都没有。0x81和后面跟的0x28是成对出现的对比度设置,0x28对应约 1/10 的电压基准,如果你觉得屏幕颜色太浅,逐步增大到0x35即可,超过0x3F会溢出寄存器上限,有可能直接回到最小值。0xA2是偏压比 1/9,1/9 偏压用在 3.3V 供电时功耗较低但对比度略淡,如果你的模块在 5V 逻辑下工作,可以改用0xA3(1/7 偏压)提高显示浓度,代价是功耗上升。
初始化后的验证步骤:调用lcd_clear()后再调用lcd_refresh(),最后写几个测试点。一个简单的自检方式是填充固定的行列图案,比如把第一行所有像素点亮,如果显示为顶部一条黑线,说明页寻址和列地址方向正确,如果黑线在屏幕中部或分成两段,优先检查页地址计算和显存数组的页序。
4.2 对比度、偏压与镜像位:直接影响显示效果
对比度和偏压是显示“浓淡”的核心变量。对比度寄存器只影响 LCD 驱动电压的振幅,偏压比影响的是 LCD 电压的分压级数。ST7567a 的对比度寄存器是 6 位宽(0x00~0x3F),但某些模块因为液晶材料不同,最佳值差异极大。同一份初始化代码,在 A 厂商的屏上显示清晰,在 B 厂商的屏上可能发白或出现全屏轻微闪烁。
遇到这种情况不要慌,先做线性扫描:写一个循环,每隔 500ms 把对比度从 0x10 递增到 0x35,观察哪个区间显示效果最好,然后把该值固定写入初始化。这个扫描过程建议做成调试宏,量产前删除。镜像位同理,0xA0与0xA1控制列方向(ADC 选择),0xC0与0xC8控制行方向(SHL 选择)。如果屏的左右镜像了,改0xA0/0xA1;如果上下镜像了,改0xC0/0xC8。这两个配置同时决定你画坐标系时 x 轴和 y 轴的递增方向,改之后会导致现有的画图 API 的坐标方向反转,需要一并检查。
另一个经常被忽略的是0xA4与0xA5全屏点亮测试位。如果初始化时把0xA5误写成0xA4反了,现象是屏幕全黑但是背光亮,很多人误判为“驱动失败”,其实就是测试模式没关。还有个类似情况是0xA6/0xA7反色位,如果设置反色,屏幕会像照镜子一样亮底黑字,不是故障而是配置。
5. 避坑与常见问题排查:白屏、花屏、残影与功耗异常
很多人在 ST7567a 上栽过的坑,我按现象、原因、解决的顺序整理出来,这些几乎覆盖了所有基础问题,建议收藏对照排查。
现象 1:初始化后屏幕全白,背光正常。
原因是初始化序列中漏了0xAF开显示命令,或者0x2C升压电路未开启导致 LCD 驱动电压为 0。我的排查经验是:用示波器量 VOUT 引脚,如果电压接近 VDD 则说明电荷泵没工作,问题出在0x2C之前时序。如果 VOUT 电压正常但屏幕白屏,打一条0xA5暂时开启全屏测试,如果屏幕变成全黑,说明驱动方向和显存访问路径正常,问题在显存数据或对比度设置上。
解决:检查代码里0xAF是否存在并且位于初始化序列尾部;确认0x2C到0x2E这两条命令没有缺失;在0xA5测试模式下观察屏幕是否全黑来判断 LCD 电压是否真正加到玻璃上。
现象 2:屏幕出现上下颠倒或左右镜像。
原因是 ADC 选择位和 SHL 位方向配置反了。常见做法是直接改0xA0为0xA1,或者改0xC0为0xC8,逐一尝试直到方向和物理安装一致。注意,如果你先在 PC 上位机软件里做好取模,取模方向和镜像是互相影响的,改驱动方向不如改取模方向来得直观。
解决:优先在初始化里改0xA0/0xA1和0xC0/0xC8来适配物理方向,改完之后再验证一次画点 API 的坐标是否符合预期。
现象 3:刷新屏幕出现残影或重影,尤其快速更新时明显。
原因是 ST7567a 内部 LCD 驱动电压切换需要稳定时间,连续快速写显存后玻璃上的电荷没来得及释放,同时偏压比设置太高也会加剧残影。另一个原因是刷新函数里没有在写显存前重新设置起始行0x40,导致显示内容和 RAM 的映射错位。
解决:在lcd_refresh开头强制写0x40重置起始行;如果残影依旧,把偏压从0xA2改到0xA3提高刷新时的电压摆幅,或者降低刷新频率,两步之间加 1~2ms 延时。
现象 4:屏幕局部区域出现规律性错乱,比如竖条纹。
原因是列地址高 4 位设置丢失或初始化被中断,导致后续所有列写入从错误的位置开始。ST7567a 的列地址是 7 位宽度,0x10+0x00组合可能被误写成0x10带低 4 位值,高位重复设置会导致地址累加错误。
解决:每次刷新前完整执行两条列地址命令:lcd_cmd(0x10 | (col >> 4))和lcd_cmd(0x00 | (col & 0x0F)),不要只在初始化里设置一次。实测这种竖条纹 90% 是这里出错,高级外设代码里很难一眼看出来。
现象 5:进入低功耗模式后屏幕自动消失或功耗异常。
原因是 ST7567a 没有专用的睡眠命令,低功耗前没有发送0xAE(关显示),电荷泵还在工作,电流消耗可能高达几百微安。如果 MCU 进入停止模式而 LCD 电荷泵继续运行,整机功耗会超标。
解决:进入低功耗前依次执行0xAE关显示,等待 50ms 让电荷泵放电,再把 RES 拉低确保 IC 完全失能。唤醒时重新跑完整初始化序列,不能只发0xAF,因为内部寄存器状态已丢失。
这条经验对我影响最大:曾经一次量产测试时整机待机电流超标,排查半天最后发现是 LCD 没关显示。从那以后我的所有低功耗工程里,LCD 关闭代码必须放在 MCU 睡眠之前的最后一个函数调用。
6. 进阶技巧:中文显示、低功耗刷新与对比度校准
6.1 中文显示方案:字模提取与两区显存组合
ST7567a 单屏 128×64 像素,显示中文最直接的方式是用 16×16 点阵字模。常见做法是:取模软件里设置纵向取模、字节正序、逐列式,生成的字模数组是每字 32 字节。显示时按两个 8×16 半字拼接,先画左半部分,再画右半部分,两部分正好覆盖一个字宽。
// 显示 16x16 中文点阵字模,font_index 为字模数组下标 void lcd_show_chinese(uint8_t x, uint8_t y, const uint8_t *font_index) { for (uint8_t col = 0; col < 16; col++) { // 左半区: 上半字节 8 像素, 下半字节 8 像素 for (uint8_t row = 0; row < 8; row++) { if (font_index[col * 2] & (1 << row)) lcd_set_pixel(x + col, y + row, 1); if (font_index[col * 2 + 1] & (1 << row)) lcd_set_pixel(x + col, y + row + 8, 1); } } }font_index[col * 2]对应左半字节的上 8 行,col * 2 + 1对应下 8 行,逐列扫描填充像素。这里最重要的一点是取模方向和绘图方向必须一致:取模时如果设置的是行优先,代码里就要按行扫;如果设置的是列优先,就按列扫。我见过太多翻车案例是因为取模软件默认“横向取模”,而代码却按纵向解析,结果汉字变成乱码条纹。验证方法是先显示一个“口”字,如果左侧竖线完整、右侧竖线完整、上下横线完整,则方向正确,否则调整取模方式。
6.2 对比度校准与批量产线的可视化验证
同一型号液晶屏来自不同批次时,最佳对比度可能偏移 20% 左右。量产代码里固定一个对比度值有风险,我习惯把对比度寄存器开放出来,在产线上通过按键或串口指令调整后保存到 Flash,这样每台设备出厂的显示浓度一致。
具体做法是:初始化阶段先读 Flash 里的对比度值,合法就写入0x81后的字节,非法就用默认值0x28。产线校准界面画一个灰阶测试图,操作人员通过按键调整到灰度层次分明即可确认保存。这个方案能有效避免液晶材料批次差异带来的售后投诉。
关于刷新率的最后一点:ST7567a 的 SPI 极限时钟大概在 10MHz 左右,但很多 MCU 的 GPIO 模拟只能跑到 1~2MHz,整屏刷新约 4ms 到 8ms。如果界面有动画滚动需求,建议用硬件 SPI,同时打开 DMA 批量发送显存,能显著释放 CPU 占用。如果只是菜单切换,GPIO 模拟完全够用,还能少踩 SPI 外设极性配置的坑。
从那次“整机待机电流超标”之后,我每次做 ST7567a 驱动的第一步都是先跑一遍自检序列:白屏、黑屏、横线、竖线、外框、灰阶,全部通过才开始写应用代码。这套习惯帮我筛掉了很多隐性问题,这次拆这份 ST7567a 驱动资源时同样先跑了一遍自检才继续移植,希望帮到你。
本文还有配套的精品资源,点击获取