☰
STM32 HAL库驱动ST7567点亮LCD12864:初始化、避坑与DMA刷新
2026/10/28 22:48:21 网站建设 项目流程

简介:资源是一套基于 STM32 HAL 库的 ST7567 液晶驱动代码,面向需要在 128×64 点阵屏上实现图形界面的嵌入式开发者。它的主要作用是帮助使用者跳过复杂的底层寄存器配置,直接获得可运行的 LCD 初始化、画点、画线与缓冲区刷新功能。压缩包共两个文件,分别是头文件和源文件,整体只有六 KB,代码量不大,适合直接加入现有工程。当前已有 1680 人学习下载,属于微型但实用的驱动类资源。阅读这套代码,可以学会 ST7567 控制器的指令发送顺序,理解 HAL 库下 SPI 通信的配置方式,并看到驱动层如何把显示需求转换为具体的时序操作。整个驱动被组织在单个 C 文件中,接口简单、注释清晰,便于学习者逐段分析,也方便裁剪出自己需要的部分。对于刚开始接触 LCD 驱动的读者,这是一份能较快上手的参考源码,同时也为后续移植其他型号显示器提供了可借鉴的结构。

1. ST7567 驱动的 LCD12864:为什么点亮它比想象中麻烦

ST7567 是 LCD 驱动 IC 里非常常见的一颗,专门驱动 12864 这类点阵液晶屏,市面上大量带转接板的 LCD12864 模块用的就是它。和 OLED 不一样,OLED 上电发几个初始化命令就能亮,ST7567 却经常遇到白屏、半屏花、对比度淡到像没加电这类问题,而且问题往往不在时序,而是在电压、偏置、段方向和列偏移这几组寄存器上。这篇文章从硬件接线、CubeMX 配置讲到初始化序列和绘图函数,最后给出我实际调过的踩坑记录,适合用 STM32 HAL 库做仪表盘、温控器、桌面小摆件的开发者直接照做。

2. 硬件连接与 CubeMX 配置:用 HAL 的硬件 SPI 把 ST7567 接起来

2.1 引脚规划和连线清单:SPI 四线还是六线

ST7567 模块对外引出的引脚一般是 8 个:VCC、GND、CS(片选)、A0(也叫 RS 或 D/C,数据/命令选择)、SCL(串行时钟)、SDA(串行数据)、RESET(复位)以及背光 LED 的正负极。市面上说的“四线 SPI”指的是 CS、A0、SCL、SDA 这四根控制线,RESET 通常单独接 GPIO,背光直接接 3.3V 或通过三极管控制。别把 A0 漏掉——它决定了当前字节是命令还是数据,没有这根线,屏幕收到的所有字节都会被当成显示数据。

我常用的接线方式如下表,用的是 STM32F103 的 SPI1,如果你用其他系列,引脚映射不同但逻辑完全一致。

模块引脚STM32 引脚说明
VCC3.3V部分模块 5V 兼容,以模块 PDF 为准
GNDGND共地
CSPB9片选,低有效
A0 / RSPB8数据/命令选择,高电平为数据
SCLPA5SPI1_SCK
SDAPA7SPI1_MOSI
RESETPB7低电平复位

注意 ST7567 是只写设备,没有数据回传,所以 SPI 的 MISO 引脚不接。MISO 要是在 CubeMX 里被配置成了别的外设功能也没关系,只要 SPI 选择 Transmit Only Master 模式就不会用到它。

2.2 CubeMX 里 SPI 的时钟极性和相位怎么设

ST7567 的串行接口时序是:数据在 SCL 上升沿被锁存,空闲时 SCL 为低。对应到 SPI 参数就是 CPOL = Low、CPHA = 1 Edge,也就是常说的 SPI Mode 0。如果你用的是默认配置没改,CubeMX 生成的初始值通常就是 Mode 0,所以很多情况下能直接跑通。但也不是绝对的,部分模块的转接板上有反向缓冲电路,会把时钟极性反转,这时候画面会变成花屏或者数据错位。

我之前用 CubeMX + HAL 库硬件 SPI 调 W25Q64 的时候,Mode 0 和 Mode 3 都试过,因为 Flash 对四种模式都兼容;但 ST7567 不行,极性错了就是满屏随机点。排查方法也很简单:初始化完成后写一个全屏 0x55 的测试图案,如果看到的不是均匀的竖条纹而是乱点,先把 CPOL 改成 High 再试,同时把 CPHA 也翻一下。四种模式最多试三轮就能定位。

2.3 为什么选 HAL 硬件 SPI 而不是 GPIO 模拟

很多人用 OLED 的习惯是 GPIO 模拟 SPI,觉得反正屏幕刷新不快,模拟够用了。但 ST7567 的刷新和 OLED 不太一样:它没有内置显存帧率的概念,你写入 RAM 什么它就显示什么,全屏更新要写 128×8 字节,页面切换还要穿插命令。GPIO 模拟在这种场景下不是不能用,而是 CPU 占用太高,刷一帧 8 页数据要几千次 GPIO 翻转,主循环稍微忙一点屏幕就闪。

所以我一般直接用硬件 SPI,CubeMX 里配置成 4 MHz 到 8 MHz 波特率,一页 128 字节的耗时在微秒级别,刷新全屏也就一毫秒左右。硬件 SPI 还有一个额外好处:后续要接 DMA 做双缓冲时,只需要改一行发送函数,GPIO 模拟就要整个重构。这就是 HAL 库相对标准库的优势——外设抽象层把 SPI 的底层寄存器差异挡住了,换芯片时驱动代码基本不用动。

3. ST7567 初始化序列:寄存器参数和时序的真正含义

3.1 初始化命令的完整序列与每个命令的作用

ST7567 没有厂家给的标准初始化序列,各家模块的推荐命令大同小异,但顺序有一点讲究。下面这段是我在多个模块上验证过的,可以直接抄:

void ST7567_Init(void) { /* 1. 硬件复位:低脉冲必须够宽,释放后等内部电源稳定 */ ST7567_RST_GPIO_Port->BSRR = ST7567_RST_Pin; // 复位脚拉高 HAL_Delay(5); ST7567_RST_GPIO_Port->BRR = ST7567_RST_Pin; // 拉低开始复位 HAL_Delay(20); ST7567_RST_GPIO_Port->BSRR = ST7567_RST_Pin; // 释放复位 HAL_Delay(50); // 等电源稳定 /* 2. 显示方向和扫描顺序 */ ST7567_WriteCmd(0x40); // 显示起始行寄存器 = 0 ST7567_WriteCmd(0xA1); // 段反向:S0 -> S127,适配常见走线 ST7567_WriteCmd(0xC0); // COM 扫描方向从 COM0 开始 /* 3. 偏置比与内部电源 */ ST7567_WriteCmd(0x22); // 偏置比 1/7(具体见 3.2 节) ST7567_WriteCmd(0x2C); // 打开升压电路 VOUT ST7567_WriteCmd(0x2E); // 打开稳压器 VREG ST7567_WriteCmd(0x2F); // 打开跟随器 VF /* 4. 对比度:0x81 后跟一个 0x00~0x3F 的值 */ ST7567_WriteCmd(0x81); ST7567_WriteCmd(0x20); // 初值 0x20,后面要按屏体实调 /* 5. 显示控制 */ ST7567_WriteCmd(0xA6); // 正常显示,不反白 ST7567_WriteCmd(0xA4); // 关闭全屏点亮 ST7567_WriteCmd(0xAF); // 开显示 ST7567_ClearScreen(); }

逐段解释一下。第一步的复位时序非常关键,RESET 拉低保持时间至少 10 微秒,很多模块要求 20 毫秒以上,我给的 20 毫秒是保守值;复位释放后必须等内部电荷泵电压稳定才能发命令,否则后面写对比度寄存器也是白写。第二步的三条命令决定画面方向,0xA1 是把段方向反过来,如果模块的走线是 S0 在左边而你用了 0xA0,画面会左右镜像,这个在 5.5 节再展开。

第三条的电源控制命令在 ST7567 里是分步开的:先开升压再把稳压器和跟随器打开,顺序不能反。如果跳过这几条直接开显示,屏幕最多只能看到极淡的残影。第四条对比度初值 0x20 是一个通用起点,不同模块的液晶材料不同,V0 电压区间差异很大,这一步是后面调试的重点。

3.2 对比度、偏置和升压比三个参数的调法

这三个参数决定了屏幕显示效果的“黑度”,它们互相牵扯,只调一个不一定有效果。

对比度寄存器由 0x81 加一个数据字节组成,数值范围 0x00 到 0x3F,实际对应的是 V0 电压的档位。值越小电压越低、显示越淡;值越大电压越高、显示越黑,但超过模块承受范围就会出残影。我调试时习惯从 0x15 开始往上加,每次加 0x02,直到背景微微发灰就退两档,这个位置一般是显示最舒服的区间。

偏置比是决定液晶驱动电压分压方式的参数,ST7567 支持 1/5、1/6、1/7 等几种偏置,对应命令在 0x20 到 0x23 这个区间,具体值以你的模块 PDF 为准。常见 12864 模块用的是 1/7 偏置,也就是我代码里的 0x22。这里有一个血泪经验:偏置比设太高,屏幕对比度会很“硬”,刷新时旧画面拖尾严重;设太低则整体发灰,怎么加对比度都救不回来。所以第一步先把偏置定死,再去调对比度。

升压比决定内部电荷泵的倍率,这个一般不用动。模块设计时已经按液晶面板的 VDD 规格选好了,除非你用的是 3.3V 供电但模块上丝印标 5V 这种不匹配情况,才需要去查升压比命令。我的建议是:三个参数里只调对比度,它是最安全、效果最直观的旋钮。

3.3 复位与上电时序:先开电源还是先发命令

ST7567 的上电顺序在数据手册里有推荐时序,实践里最重要的两条是:先上 VCC 再释放 RESET;RESET 释放后等足够长的时间再发第一条命令。我见过不少人把 RESET 直接接到 3.3V 上,图省事,结果偶尔上电后屏幕显示错乱,按一下复位键又正常了——这就是复位时序没处理好。

正确做法是让 RESET 由 GPIO 控制,代码里先拉低再拉高,保证每次初始化都有一次完整的硬件复位。CubeMX 生成代码后别把 RESET 引脚标成不上电默认高电平,否则单片机上电瞬间 RESET 已经被拉起来了,后面软件里再拉低拉高效果会打折扣。还有一点容易被忽略:初始化函数里第一步发命令之前,最好先发一个空操作或重复发送第一条命令,因为有些模块的 SPI 从机状态机在上电后并不在一个确定的帧边界上。

4. LCD12864 的显示驱动函数:画点、页缓冲和字符显示

4.1 ST7567 的 RAM 映射与页寻址结构

ST7567 内部的显示 RAM 是 132 列乘 64 行,LCD12864 模块只引出中间的 128 列。列地址由两条命令组成:高 4 位在 0x10~0x1F 范围内,低 4 位在 0x00~0x0F 范围内。页地址则是 0xB0 到 0xB7 共 8 页,每一页对应 8 行像素,也就是一列一字节。这种“页码 + 列”的寻址方式从 8080 并口时代继承下来,串口模式下依然是先发页命令再发列命令,然后连续传数据,数据按列从左到右填充当前页。

理解这个结构对写驱动很重要:画点不能用 XY 坐标直接算地址,必须先算出 y 所在页,再算出该页内的位偏移。而且列地址在写完一个字节后会自动加一,但页地址不会自动切换,写完一页 128 字节后如果不干预,数据会写到屏幕外的 RAM 区去。这就是半屏问题的根源,后面避坑章节详细说。

static uint8_t frameBuffer[8][128]; // 页 × 列 的帧缓冲 void ST7567_SetPageCol(uint8_t page, uint8_t col) { ST7567_WriteCmd(0xB0 | (page & 0x07)); // 页地址 0~7 ST7567_WriteCmd(0x10 | ((col >> 4) & 0x0F)); // 列地址高 4 位 ST7567_WriteCmd(0x00 | (col & 0x0F)); // 列地址低 4 位 }

这段代码里的 ST7567_WriteCmd 一般实现为:先拉低 A0,再拉低 CS,发送一个字节,然后拉高 CS。数据发送函数则把 A0 拉高。命令和数据的区别只在这一根引脚上,SPI 本身不感知。

4.2 画点和区域填充:页缓冲是关键

如果直接调用写数据函数去画点,每画一个点就要传一次命令加一次数据,刷一个 128×64 的全屏图案要近万次 SPI 传输,效率不可接受。正确做法是在内存里维护一个 8×128 字节的帧缓冲,所有绘图操作先改缓冲,最后统一刷新到 LCD。

void ST7567_DrawPixel(uint8_t x, uint8_t y, uint8_t color) { if (x >= 128 || y >= 64) return; // 越界直接丢弃 uint8_t page = y >> 3; // 行号除以 8,得到页号 uint8_t bit = y & 0x07; // 行号对 8 取余,得到页内位偏移 if (color) frameBuffer[page][x] |= (1 << bit); // 点亮 else frameBuffer[page][x] &= ~(1 << bit); // 熄灭 } void ST7567_Refresh(void) { for (uint8_t page = 0; page < 8; page++) { ST7567_SetPageCol(page, 0); // 每页从第 0 列开始 HAL_SPI_Transmit(&hspi1, frameBuffer[page], 128, 10); } }

逻辑说明:画点函数的核心是位运算,把 (x, y) 坐标映射到缓冲区的某个字节的某个 bit。页面刷新函数先设置页地址和列地址,然后一次性把该页 128 字节发出去。这里有个细节:每次切换页面后必须重新设置列地址为 0,因为上一页发送结束后列地址停留在 127,不归零的话下一页数据会从头覆盖当前页,导致画面错位。

超时参数 10 是 HAL_SPI_Transmit 的等待超时毫秒数。硬件 SPI 在 4 MHz 下发送 128 字节只需要约 0.25 毫秒,10 毫秒的超时足够宽裕,但如果系统时钟跑得慢或者 SPI 波特率被降得很低,这个值要相应加大,否则会触发 HAL 库的超时返回错误。

4.3 字符显示:取模方式和字库选择

有了画点函数和帧缓冲,字符显示就是查表填点。常用点阵是 6×8 和 8×16 两种,前者适合西文和数字,后者适合带中文的场景。取模工具我用的是 PCtoLCD2002,取模方式要选“纵向取模、字节正序”,这样每一列数据正好对应 ST7567 的一列 8 个像素,和帧缓冲的排列完全一致。

void ST7567_DrawChar(uint8_t x, uint8_t y, char c, const uint8_t* font) { uint8_t idx = (uint8_t)c - 0x20; // 字库从空格开始 uint8_t w = font[0]; // 字库首字节通常存字宽 for (uint8_t i = 0; i < w; i++) { uint8_t colData = font[2 + idx * w + i]; // 跳过字宽和高度两个头字节 for (uint8_t j = 0; j < 8; j++) { if (colData & (1 << j)) ST7567_DrawPixel(x + i, y + j, 1); } } }

这段代码假设字库格式是:第一字节字宽,第二字节字高,后面每字节代表一列 8 个像素。用 DrawPixel 逐位画点虽然慢一点,但好处是不用关心字库的位序差异,任何取模工具的产物只要核对过列方向就能用。等调试稳定后,想提速可以把这段改成直接写帧缓冲字节,省掉 DrawPixel 里的分支判断。字符显示最适合的场景是固定位置刷新数字,比如温控器上的温度值,只更新需要变化的区域,别整屏刷新。

5. ST7567 驱动 LCD12864 的常见问题与避坑记录

5.1 白屏:先查并行/串行模式引脚,再查对比度

现象:初始化跑完,屏幕全白或者全黑,背光亮,但没有任何内容。

原因有两个方向。第一,模块上的 C86 或 PS 引脚选择了并行模式,SPI 命令根本进不了芯片。第二,对比度寄存器实际写进去的值偏低,V0 电压没起来,显示内容淡到看不见。

解决:先把模块翻过来看 C86 跳线或电阻位。很多 12864 模块出厂默认是并行模式,需要把 C86 拉低才能切到串行。跳线确认没问题后,把初始化里对比度值从 0x20 临时改成 0x30,如果屏幕突然能看到了就说明是对比度问题,再按 3.2 节的方法回退到合适值。

5.2 只有上半屏或下半屏显示:页地址与列地址归零

现象:画面只出现在上半屏,下半屏全白;或者上下内容错位,出现两段重复图案。

原因:ST7567 的列地址在写满一页后不会自动跳转到下一页。刷新函数里如果只初始化一次页地址,后续的写数据操作全部落在同一页上,其他页自然没内容。另外,页地址低位由 0xB0 到 0xB7 控制,如果你在切换页面时只发了页命令没重新设置列命令,新页的数据会从上次列地址继续写,造成错位。

解决:刷新循环里每页发送前都调用 ST7567_SetPageCol(page, 0),确保页和列都回到已知位置。这也是 4.2 节代码里为什么强制先设列再发数据的原因。排查时可以加一个测试函数,连续 8 页写不同的固定数,比如第 0 页写 0x01,第 1 页写 0x02,看屏幕是否是 8 个亮度均匀的横条。

5.3 残影与重影:偏置比和对比度的匹配

现象:画面更新后,原来的内容还有淡淡的轮廓,或者文字边缘有拖尾,像 LCD 响应不过来。

原因:偏置比设置得太高,或者对比度电压过高,液晶分子被过度驱动,切回中间态需要更长时间。这个问题在廉价 12864 模块上特别明显,因为它们用的液晶材料响应速度本身就慢。

解决:先把对比度降到当前值的一半,看残影是否减弱;如果仍然明显,把偏置比命令从 0x22 换成 0x23(范围随模块型号略有不同),那代表更低的偏置比。这两个参数组合调试时,每次只改一个,改完刷新同一张图片观察 5 秒,别两块参数一起动,否则不知道是哪一项起的效果。

5.4 DMA 刷新画面抖动:CS 拉高时机不对

现象:用 HAL_SPI_Transmit_DMA 连续刷新整屏时,画面闪烁或出现随机横条纹,有时还会出现上一帧的内容残留。

原因:DMA 发送是异步的,发送完成回调触发时 SPI 外设可能还有最后的字节在移位寄存器里。如果这时候立刻拉高 CS,最后一两个字节的数据被截断,屏幕就花。另一个常见错误是在上一次传输还没完成时就发起下一次传输,HAL 库会直接返回 HAL_BUSY,代码里如果没检查返回值,数据就没发出去。

解决:CS 的拉高动作放在 HAL_SPI_TxTransmitCpltCallback 回调里做,不要放在发送函数之后立即执行。发起下一次传输前检查 hspi1.State 是否等于 HAL_SPI_STATE_READY,不满足就轮询等待。DMA 模式在刷新全屏时通常是一页一页地发,每页之间重新设置页地址,这个命令发送必须等上一次 DMA 彻底结束后再进行,否则命令会被 DMA 的数据流打断。

5.5 画面整体左右偏移或上下颠倒:段方向和 COM 扫描方向

现象:屏幕能显示,但内容像照镜子一样左右颠倒,或者上下颠倒,有时还伴随画面整体偏移几列的情况。

原因:0xA0 与 0xA1 控制段方向,也就是列地址和屏幕物理位置的映射关系;0xC0 与 0xC8 控制 COM 扫描方向,也就是行方向。不同模块的 PCB 走线方式不同,出厂时 SEG 引脚可能从左边引出也可能从右边引出,软件里写反就成了镜像。

解决:把初始化里的 0xA1 改成 0xA0、0xC0 改成 0xC8,四个组合顺序试一遍。判断标准是显示一个带左右不对称特征的图案,比如字母 F 或数字 7,镜像问题一眼就能看出来。列偏移则是另一个问题,ST7567 的 RAM 有 132 列,模块用 128 列,剩下几列在物理上不可见,如果发现画面整体向左或向右偏了几列,需要设置列偏移寄存器,偏移量通常为 0 到 4,具体看模块数据手册里的“RAM 地址偏移”说明。

6. 进阶:DMA 双缓冲刷新,把 CPU 占用降到最低

如果前面已经用帧缓冲加硬件 SPI 跑通了,下一步值得做的是把 HAL_SPI_Transmit 换成 HAL_SPI_Transmit_DMA,再加一块双缓冲。这样做最直接的收益是刷新一帧 1024 字节几乎不占 CPU 时间,主循环可以把剩余时间全花在 UI 逻辑、传感器读取和通信协议上。

static uint8_t fbDraw[8][128]; // 前台缓冲:绘图操作写这里 static uint8_t fbSend[8][128]; // 后台缓冲:正在被 DMA 发送 void ST7567_RefreshWithDMA(void) { /* 等待上一次发送完全结束 */ while (hspi1.State != HAL_SPI_STATE_READY); for (uint8_t page = 0; page < 8; page++) { ST7567_SetPageCol(page, 0); HAL_SPI_Transmit_DMA(&hspi1, fbSend[page], 128); while (hspi1.State != HAL_SPI_STATE_READY); // 页面之间必须有间隙 } /* 完成后交换缓冲指针 */ uint8_t (*tmp)[128] = fbDraw; fbDraw = fbSend; fbSend = tmp; }

实际工程里我不会真的用指针交换数组,因为编译期静态数组的地址交换需要指针变量来接,上面的写法只是为了表达“绘图形缓冲和发送缓冲互换”的思路。更稳妥的做法是绘图函数维护一个帧序号,每次刷新时按序号选缓冲,或者直接定义两个数组,刷新完成后用 memcpy 把绘制内容拷贝到发送缓冲。

DMA 刷新要验证的事情有两件:一是屏幕显示是否连续无闪烁,这需要把刷新频率提到至少 20 帧每秒,肉眼看起来要平滑;二是用逻辑分析仪看 SCL 和 CS 的波形,确认每一页数据的 CS 低电平区间完整覆盖 128 字节的发送时间。这个验证方法在做产品时很有用,因为有的主控在 DMA 优先级设置不当会和系统其他外设抢带宽,画面会周期性抖动。

ST7567 这个驱动芯片到我手里已经调了三四个项目,从最早的标准库 GPIO 模拟,到后来 HAL 库硬件 SPI,再到现在的 DMA 双缓冲,每次升级都是因为前一种方案在某种边界场景下顶不住了。回头看的教训是:驱动初始化和显示函数一定要留参数接口,对比度、偏置、段方向这些寄存器别硬编码在初始化函数里,后续换模块型号、改液晶批次的时候能省大量时间。希望帮到你。

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

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

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

立即咨询