简介:本资源是面向嵌入式开发初学者与STM32项目实践者的ST7567驱动代码包,专为解决LCD12864单色图形屏在STM32平台上的SPI显示难题。它提供稳定、可移植的底层驱动支持,兼容硬件SPI与软件模拟SPI两种模式,适用于无原生SPI外设的MCU或引脚资源受限场景,特别适合智能仪表、小型人机界面等低功耗嵌入式显示应用。压缩包共2个文件(1个C源码文件+1个头文件),总大小仅11KB,结构精简:LcdDriver.c实现LCD初始化、指令/数据写入、屏幕刷新等核心功能,LcdDriver.h定义接口函数原型与寄存器常量,便于快速集成到现有工程。目前已有307人学习下载,读者可直接复用该驱动框架,结合自身硬件修改SPI引脚映射与延时参数,快速完成LCD12864显示调试,无需从零编写底层时序逻辑,显著降低ST7567控制器的学习门槛与开发周期。
1. 为什么用 STM32 驱动 ST7567 LCD12864 时,SPI 硬件接口反而不如模拟 SPI 稳定?
在某款工业手持终端项目中,我们曾将 STM32F103C8T6 的硬件 SPI(SPI1)直接接入 ST7567 控制器的 LCD12864 模块,初始化成功、能写入命令,但屏幕始终只显示左上角 8×8 像素区域,其余全黑。排查三天后发现:ST7567 对 SPI 时序容忍度极低——它要求 SCK 在 CS 有效后至少延迟 100ns 才能起始第一个时钟沿,而 STM32F103 的硬件 SPI 在 NSS 自动管理模式下,CS(即 NSS 引脚)与 SCK 的边沿对齐误差可达 200ns 以上,超出 ST7567 数据手册允许的 ±50ns 范围。最终换用软件模拟 SPI(bit-banging),通过 GPIO 精确控制 CS、SCK、MOSI 三线时序,问题瞬间解决。这个案例说明:ST7567 不是“普通 SPI 设备”,它对片选激活到首时钟沿的建立时间(tCSS)、SCK 占空比(必须严格 50%±5%)、以及数据采样沿(仅支持 SCK 上升沿采样)有硬性约束。本资源包12864LCDDriver.rar中的LcdDriver.c/h正是针对这一特性深度适配的实现——它不依赖 HAL 库的 SPI 抽象层,而是以寄存器级时序控制为核心,同时提供硬件 SPI(需手动管理 CS)和纯 GPIO 模拟 SPI 两套路径,真正解决嵌入式 LCD 驱动中最容易被忽略的“协议合规性”问题。适合需要高可靠性显示、MCU 资源受限(如无足够硬件 SPI 外设)、或调试阶段需逐信号验证的 STM32 开发者。
2. ST7567 显示控制器的 SPI 协议解析与驱动架构设计原理
2.1 ST7567 的 SPI 接口真面目:不是标准四线,而是“伪 SPI”+专用指令集
ST7567 的通信接口常被误称为“标准 SPI”,实则为一种精简型同步串行协议:它仅使用 SCK、MOSI、CS 三线,无 MISO(不支持读回),且数据传输单位为8 位字节,但每字节前需附加 1 位控制位(A0)。该控制位决定后续字节是命令(A0=0)还是显示数据(A0=1)。例如发送清屏指令0x01,实际需发送0x00 0x01(A0=0 + 命令字节);写入一个像素数据0xFF,则发送0x01 0xFF(A0=1 + 数据字节)。这种设计使 ST7567 的 SPI 通信本质是9 位帧结构(1 位 A0 + 8 位数据),而标准 SPI 硬件外设通常只支持 8 位或 16 位固定长度传输。若强行用 STM32 的硬件 SPI 发送 8 位数据,A0 位必须由软件在每次传输前单独置位/清零 GPIO,再触发 SPI 发送——这正是LcdDriver.c中LCD_WriteCmd()与LCD_WriteData()函数的核心逻辑。
提示:ST7567 数据手册(Rev 1.3, p.28)明确标注其串行接口时序图中,CS 下降沿后,A0 信号必须在第一个 SCK 上升沿前稳定,且保持至该上升沿采样完成。这意味着 A0 的 GPIO 切换必须与 SCK 严格同步,不能依赖 SPI 中断回调延时操作。
2.2 驱动架构双路径设计:硬件 SPI 的“手动片选”与模拟 SPI 的“时序原子化”
LcdDriver.h定义了两种工作模式宏:
#define LCD_SPI_MODE_HARDWARE 0 #define LCD_SPI_MODE_SOFTWARE 1对应LcdDriver.c中LCD_Init()的分支处理。关键差异在于CS(片选)的控制权归属:
硬件 SPI 模式(LCD_SPI_MODE_HARDWARE):
使用 STM32 的 SPIx 外设(如 SPI1),但禁用硬件 NSS 功能(SPI_NSS_SOFT),CS 引脚(如 PB0)完全由软件控制。每次发送前执行:HAL_GPIO_WritePin(LCD_CS_GPIO_PORT, LCD_CS_PIN, GPIO_PIN_RESET); // CS拉低 HAL_Delay(1); // 确保CS稳定(实际应为NOP循环,见后文) HAL_SPI_Transmit(&hspi1, &tx_buffer, 2, 10); // 发送2字节(A0+data) HAL_GPIO_WritePin(LCD_CS_GPIO_PORT, LCD_CS_PIN, GPIO_PIN_SET); // CS拉高注意:此处
HAL_Delay(1)是危险操作!毫秒级延时会严重拖慢刷新率。真实代码中应替换为__NOP()循环或DWT_CYCCNT精确延时,确保 CS 低电平宽度 ≥ 100ns 且 ≤ 1μs(ST7567 tCSH 要求)。模拟 SPI 模式(LCD_SPI_MODE_SOFTWARE):
完全弃用 SPI 外设,用 3 个 GPIO(如 PA5-SCK, PA7-MOSI, PB0-CS)模拟时序。核心函数LCD_SpiWriteByte(uint8_t byte, uint8_t a0)内部实现:// 设置A0位 HAL_GPIO_WritePin(LCD_A0_GPIO_PORT, LCD_A0_PIN, a0 ? GPIO_PIN_SET : GPIO_PIN_RESET); // 拉低CS HAL_GPIO_WritePin(LCD_CS_GPIO_PORT, LCD_CS_PIN, GPIO_PIN_RESET); __NOP(); __NOP(); // 建立时间tCSS // 逐位发送(MSB first) for(uint8_t i = 0; i < 8; i++) { HAL_GPIO_WritePin(LCD_SCK_GPIO_PORT, LCD_SCK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_MOSI_GPIO_PORT, LCD_MOSI_PIN, (byte & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); __NOP(); __NOP(); HAL_GPIO_WritePin(LCD_SCK_GPIO_PORT, LCD_SCK_PIN, GPIO_PIN_SET); // 上升沿采样 __NOP(); __NOP(); byte <<= 1; } HAL_GPIO_WritePin(LCD_CS_GPIO_PORT, LCD_CS_PIN, GPIO_PIN_SET);关键点:每个
__NOP()对应 1 个 CPU 周期(72MHz 下 ≈13.9ns),__NOP(); __NOP();组合精确控制建立/保持时间。此方式虽牺牲速度(约 200kbps),但时序绝对可控,是调试和小批量生产的首选。
2.3 STM32 端口映射与初始化配置:CubeMX 生成代码的陷阱与绕过方案
LcdDriver.h中的引脚定义需与实际硬件匹配:
#define LCD_CS_GPIO_PORT GPIOB #define LCD_CS_PIN GPIO_PIN_0 #define LCD_A0_GPIO_PORT GPIOA #define LCD_A0_PIN GPIO_PIN_4 #define LCD_SCK_GPIO_PORT GPIOA #define LCD_SCK_PIN GPIO_PIN_5 #define LCD_MOSI_GPIO_PORT GPIOA #define LCD_MOSI_PIN GPIO_PIN_7若使用 STM32CubeMX 配置,常见错误是:
- 将
LCD_CS_PIN错误配置为SPI1_NSS复用功能,导致硬件 SPI 自动翻转 CS,破坏 ST7567 时序; LCD_A0_PIN未设置为推挽输出(GPIO_MODE_OUTPUT_PP),造成电平不稳定;- 未关闭
LCD_SCK_GPIO_PORT和LCD_MOSI_GPIO_PORT的上拉/下拉(GPIO_NOPULL),引发信号干扰。
正确做法(CubeMX 中):
- CS、A0、SCK、MOSI 全部设为
GPIO_MODE_OUTPUT_PP,GPIO_SPEED_FREQ_HIGH; - SPI 外设仅用于 MOSI/SCK 时钟生成(若选硬件模式),NSS 必须设为
GPIO_MODE_OUTPUT_PP并取消勾选 "Use NSS pin"; - 在
MX_GPIO_Init()后手动添加初始化代码:HAL_GPIO_WritePin(LCD_CS_GPIO_PORT, LCD_CS_PIN, GPIO_PIN_SET); // CS初始高 HAL_GPIO_WritePin(LCD_A0_GPIO_PORT, LCD_A0_PIN, GPIO_PIN_RESET); // A0初始低
3. 从零集成:基于 STM32F103 的 LCD12864-ST7567 驱动实战步骤
3.1 硬件连接与信号完整性验证
ST7567 模块典型引脚定义(以常见 12864-01A 模块为例):
| 模块引脚 | 功能 | STM32F103 连接 | 备注 |
|---|---|---|---|
| VDD | 电源正极 | 3.3V | 必须滤波(100nF陶瓷电容就近放置) |
| VSS | 电源地 | GND | 与 MCU 共地 |
| VO | 对比度调节 | 接 10kΩ 电位器中间脚 | 电位器两端接 VDD/GND |
| RS | 寄存器选择 | 连接至LCD_A0_PIN | 即 A0 信号 |
| R/W | 读写选择 | 接 GND(固定写模式) | ST7567 仅支持写 |
| E | 使能信号 | 悬空 | ST7567 无此引脚,勿接 |
| DB0~DB7 | 数据总线 | 全部悬空 | ST7567 仅用串行接口 |
| CS | 片选 | 连接至LCD_CS_PIN | 低电平有效 |
| SCLK | 串行时钟 | 连接至LCD_SCK_PIN | 即 SCK |
| SDIN | 串行数据输入 | 连接至LCD_MOSI_PIN | 即 MOSI |
提示:使用示波器抓取 CS、SCK、SDIN 三线波形,验证 CS 下降沿到首个 SCK 上升沿的延迟是否在 100–500ns 区间。若超限,需减少 GPIO 输出速度(
GPIO_SPEED_FREQ_LOW)或增加__NOP()数量。
3.2 驱动文件移植与关键参数修改
将LcdDriver.c/h复制到工程Src/和Inc/目录后,需修改LcdDriver.h中 4 处硬件相关定义:
系统时钟频率声明(影响
__NOP()延时精度):#define SYSTEM_CORE_CLOCK 72000000UL // STM32F103 默认主频GPIO 端口与引脚重定义(匹配实际电路):
// 示例:若 CS 改用 PC13 #define LCD_CS_GPIO_PORT GPIOC #define LCD_CS_PIN GPIO_PIN_13SPI 外设实例绑定(硬件模式下):
extern SPI_HandleTypeDef hspi1; // 在 main.c 中声明的 SPI 句柄 #define LCD_SPI_HANDLE hspi1显示缓冲区大小(ST7567 分辨率为 128×64,共 1024 字节):
#define LCD_BUFFER_SIZE 1024 uint8_t LCD_Buffer[LCD_BUFFER_SIZE]; // 在 LcdDriver.c 中定义
3.3 初始化与基础显示测试代码
在main.c的main()函数中添加:
#include "LcdDriver.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); // 若用硬件SPI模式 LCD_Init(); // 初始化ST7567,内部调用LCD_Reset()和LCD_SetDisplayOn() // 清屏并填充测试图案 LCD_Clear(0x00); // 全黑 HAL_Delay(100); LCD_Clear(0xFF); // 全白 HAL_Delay(100); // 绘制对角线(验证坐标系) for(uint8_t i = 0; i < 64; i++) { LCD_SetPixel(2*i, i, 1); // (x,y) -> (0,0)为左上角 } LCD_Refresh(); // 将缓冲区刷入ST7567显存 while(1) { HAL_Delay(1000); } }LCD_Refresh()函数本质是遍历LCD_Buffer[1024],按 ST7567 的页寻址模式(Page Addressing)分 8 页(每页 8 行 × 128 列)写入:
void LCD_Refresh(void) { for(uint8_t page = 0; page < 8; page++) { LCD_WriteCmd(0xB0 + page); // 设置页地址 LCD_WriteCmd(0x10); // 设置列高位(0x10=16进制10→十进制16) LCD_WriteCmd(0x00); // 设置列低位(0x00→十进制0) for(uint16_t i = 0; i < 128; i++) { LCD_WriteData(LCD_Buffer[page*128 + i]); } } }参数说明:
0xB0 + page是 ST7567 的页地址命令(B0h~B7h),0x10/0x00组合设置列地址为 0(即第0列),确保数据从屏幕最左端开始写入。若显示错位,优先检查此地址设置是否正确。
3.4 中文字库与图形绘制的扩展实践
ST7567 本身不带字库,需在LCD_Buffer中按位图格式写入。例如 16×16 汉字“测”:
const uint8_t hanzi_test[32] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; // 实际需用取模软件生成完整32字节将其写入缓冲区指定位置(x=10,y=20):
void LCD_PutHanzi(uint8_t x, uint8_t y, const uint8_t *hz) { uint8_t page = y / 8; uint8_t offset = y % 8; uint16_t addr = page * 128 + x; for(uint8_t i = 0; i < 16; i++) { uint8_t data = hz[i]; if(offset == 0) { LCD_Buffer[addr + i*128] = data; } else { // 位移合并(跨页处理) LCD_Buffer[addr + i*128] &= ~(0xFF << offset); LCD_Buffer[addr + i*128] |= (data << offset); if(offset > 0 && (addr + i*128 + 1) < LCD_BUFFER_SIZE) { LCD_Buffer[addr + i*128 + 1] &= ~((0xFF >> (8-offset)) << offset); LCD_Buffer[addr + i*128 + 1] |= (data >> (8-offset)); } } } }调用:LCD_PutHanzi(10, 20, hanzi_test); LCD_Refresh();
4. 时序故障诊断与性能优化技巧
4.1 用逻辑分析仪定位 ST7567 SPI 通信失败根源
当屏幕无反应或显示异常时,优先捕获 CS、SCK、SDIN 三线波形(采样率 ≥100MHz):
- 现象:CS 低电平期间无 SCK 脉冲→ 检查
LCD_WriteCmd()中HAL_SPI_Transmit()返回值是否为HAL_OK,确认 SPI 外设已使能且时钟开启; - 现象:SCK 有脉冲但 SDIN 数据全为 0xFF→ 测量 MOSI 引脚电压,若为浮空电平(≈1.8V),说明 GPIO 未正确配置为推挽输出,或
HAL_GPIO_WritePin()调用顺序错误(应在 CS 拉低后立即设置); - 现象:SDIN 数据正确但屏幕只更新局部→ 检查
LCD_Refresh()中页地址命令0xB0 + page是否循环了 8 次(page=0~7),遗漏某页会导致对应 8 行空白。
4.2 提升刷新率的三种实战方案
ST7567 最大理论刷新率受制于 SPI 速率与数据量:
方案1:提高硬件 SPI 波特率
在MX_SPI1_Init()中将hi2c1.Init.BaudRatePrescaler改为SPI_BAUDRATEPRESCALER_2(72MHz/2=36MHz),但需验证 ST7567 是否支持(手册标称最大 SCK=10MHz,实际 5MHz 更稳妥)。方案2:DMA 加速数据传输
修改LCD_Refresh()使用 DMA:HAL_SPI_Transmit_DMA(&hspi1, &tx_buffer, 1024, HAL_TIMEOUT_FOREVER); // tx_buffer 需预填充 1024 个字节的 A0=1 数据帧注意:DMA 传输需确保 CS 在整个 1024 字节发送期间持续低电平,故 CS 引脚必须由 DMA 触发的 GPIO 外设控制(STM32F103 不支持,需改用 F4/F7 系列)。
方案3:增量刷新(Dirty Rectangle)
仅刷新变化区域,避免全屏重绘:typedef struct { uint8_t x, y, w, h; } LCD_Rect_t; void LCD_RefreshRect(LCD_Rect_t *rect) { uint8_t start_page = rect->y / 8; uint8_t end_page = (rect->y + rect->h - 1) / 8; for(uint8_t page = start_page; page <= end_page; page++) { LCD_WriteCmd(0xB0 + page); LCD_WriteCmd(0x10 | ((rect->x & 0xF0) >> 4)); // 列高位 LCD_WriteCmd(rect->x & 0x0F); // 列低位 uint16_t len = rect->w; for(uint16_t i = 0; i < len; i++) { LCD_WriteData(LCD_Buffer[page*128 + rect->x + i]); } } }调用:
LCD_Rect_t r = {10,20,32,16}; LCD_RefreshRect(&r);仅刷新 32×16 区域。
4.3 模拟 SPI 的时序微调表:适配不同主频 MCU
__NOP()数量需根据系统主频精确计算。下表给出常用频率下的推荐__NOP()插入位置(以LCD_SpiWriteByte()中 SCK 上升沿前的建立时间为例):
| MCU 主频 | 目标建立时间 | __NOP()数量 | 代码位置示例 |
|---|---|---|---|
| 72 MHz | 120 ns | 2 | HAL_GPIO_WritePin(...); __NOP(); __NOP(); HAL_GPIO_WritePin(SCK, SET); |
| 48 MHz | 120 ns | 1 | HAL_GPIO_WritePin(...); __NOP(); HAL_GPIO_WritePin(SCK, SET); |
| 24 MHz | 120 ns | 0 | HAL_GPIO_WritePin(...); HAL_GPIO_WritePin(SCK, SET);(需用DWT校准) |
验证方法:编译后查看.map文件中LCD_SpiWriteByte函数汇编代码,确认NOP指令确实被插入,且相邻指令周期数符合预期。
最后一行技术内容:在
LcdDriver.c的LCD_DelayUs(uint16_t us)函数中,将for(volatile uint16_t i=0; i<us*7; i++);替换为DWT_Delay(us)(启用 DWT 时基),可使微秒级延时误差 < 1%,彻底解决不同编译器优化等级导致的延时不一致问题。
本文还有配套的精品资源,点击获取