STC单片机驱动ST7567液晶屏:SPI通信与帧缓冲实现详解 简介STC单片机搭配ST7567显示屏的SPI通信示例面向使用STC15W系列等51内核单片机的学习者与开发者解决点阵LCD驱动、SPI时序配置和显示控制等常见问题。资源共22个文件压缩包约35KB含3个C源文件、5个头文件以及Hex固件、Keil工程配置和调试列表文件目录紧凑、结构清晰便于直接导入编译与烧录验证。已有1514人学习下载适合入门参考和项目复用。读者可从中获取完整SPI通信初始化流程、ST7567指令写入与显示数据发送示例掌握清屏、定位光标、显示开关等基础控制方法同时可参考工程中的引脚连接与程序组织方式快速迁移到实际项目中。对于需要快速点亮ST7567屏或排查显示异常的开发者既能获得直接可用的代码框架也能得到调试提示。1. STC单片机ST7567显示屏的SPI通信先点亮再提速STM32 生态里 12864 屏的例子一抓一大把8051 系用 ST7567 的完整工程却经常找到一半就断在初始化序列上。STC15 和 STC8 都内置硬件 SPI但很多人宁可一边翻 datasheet 一边用软件模拟原因不外乎三个ST7567 没有字库画图要自建帧缓冲它的显存按页寻址和平时按行存点阵的习惯不一样SPI 模式到底是 Mode 0 还是 Mode 3不同模块还不太一致。这里给出的做法是先把页寻址和显存映射讲透再给一套硬件 SPI 的寄存器配置和驱动代码最后把你很可能遇上的白屏、花屏、镜像问题按现象定位。2. ST7567驱动原理与STC硬件SPI寄存器配置2.1 ST7567是什么与ST7920、SSD1306的关键区别ST7567 是一款 LCD 驱动控制器典型的玻璃分辨率是 132×65多数模块厂商把它裁成 128×64 使用。它内部没有字符发生器也没有完整帧缓冲只有一页一页的显示 RAM 和配套的地址计数器MCU 通过 SPI 或并口把像素写进去再靠内部的偏压电路去刷新玻璃。ST7920 的普及度高是因为内置了常用汉字库但它的绘图模式写一个字节要处理命令/数据交替速度上不去。ST7567 没有字库反而让“往显存里写什么就显示什么”这件事变得直接。SSD1306 用在 OLED 上功耗和对比度逻辑不一样初始化里没有电荷泵和偏压比的概念。ST7567 的驱动方式更像老款的 KS0108但没有左右半屏区分也不需要读忙标志只要按页地址写入即可。选 SPI 而不选并口除了省引脚还有两个实际因素STC15/STC8 的硬件 SPI 可以做到主频的 1/424MHz 主频下约 6MHz一个字节传输不到 2μs而软件模拟在 1T 的 STC8 上要做到稳定至少需要 8 次 IO 翻转加上边界判断往往要 10μs 以上。并口要多占 8 根数据线在 STC8 的小封装上确实不现实。2.2 STC硬件SPI的引脚映射、SPCTL配置与发送函数以 STC15W4K32S4 为例SPI 默认映射在 P1.2SCLK、P1.3MOSI、P1.4MISO、P1.5SS通过 P_SW1 的 SPI_S 位可以整组切到 P2.2~P2.5。STC8 系列的大部分型号同样默认放在 P1部分型号可切到 P5.4~P5.7。ST7567 只需要 SCLK 和 MOSIMISO 可以不接但硬件 SPI 模块仍要求这两个引脚配置为推挽输出否则边沿变缓会导致采样错误。信号STC15W4K32S4STC8 常见引脚ST7567 端说明SCLKP1.2 或 P2.2P1.2 或 P5.4SCLSPI 时钟输出MOSIP1.3 或 P2.3P1.3 或 P5.5SDA主机发送MISOP1.4 或 P2.4P1.4 或 P5.6不接只写不读CS任意 IO任意 IOCS低电平有效A0任意 IO任意 IOA01数据0命令SPCTL 的配置要抓住四个位SSIG 置 1 后不使用硬件 SS 引脚CS 完全由普通 IO 控制这对驱动 ST7567 是必须的因为如果 SSIG0一旦 SS 引脚被拉低SPI 会自动从主机切到从机SPEN 是 SPI 总开关MSTR 决定主机模式CPOL 和 CPHA 决定采样沿。ST7567 数据手册里 SPI 写操作采用上升沿采样对应 Mode 0CPOL0CPHA0也就是 SCL 空闲为低、数据在上升沿被锁存。// STC15/STC8 硬件 SPI 初始化用于 ST7567 void ST7567_SPI_Init(void) { SPCTL 0xD0; // SSIG1, SPEN1, DORD0(MSB先), MSTR1, // CPOL0, CPHA0, SPR00 - SCLKfosc/4 SPSTAT 0xC0; // 写1清 SPIF、WCOL 标志 } // 查询方式发送一字节 void ST7567_SPI_Write(unsigned char dat) { SPSTAT 0x80; // 清 SPIF SPDAT dat; // 写入 SPDAT 即启动移位发送 while ((SPSTAT 0x80) 0) { } }SPDAT 是发送/接收共用的数据寄存器写入后硬件自动把 8 个 bit 按 SCLK 移出。对这个应用读回的内容没有意义只需要等 SPIF 置位。STC 的 SPIF 标志比较特殊清零方式是“写 1”所以每次发送前先SPSTAT 0x80不会影响 WCOL。查询等待不会死锁因为 SPI 时钟是主机自己产生的真正要小心的是开启中断后 SPIF 的竞争第 5 章会单独说。2.3 软件SPI通信代码引脚被占用时的兜底方案有些应用里 P1.2/P1.3 已被 ADC 或 PWM 占用STC8 虽然能切换 SPI 引脚组但切换后可能影响其他外设。这时候用软件 SPI 驱动 ST7567 完全可行尤其刷新不频繁的静态界面。软件实现要包含intrins.h以使用_nop_()。// 软件 SPIMode 0SCL 空闲低上升沿采样 // SCL_PIN、SDA_PIN 在头文件中定义为 sbit void SW_SPI_Write(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { SCL_PIN 0; if (dat 0x80) { SDA_PIN 1; } else { SDA_PIN 0; } _nop_(); // 数据建立时间短杜邦线下 1~2 个 NOP 足够 SCL_PIN 1; _nop_(); // 保持时间让 LCM 在上升沿采样 dat 1; } SCL_PIN 0; }每个 bit 的操作顺序是先拉低 SCL再改 SDA最后拉高 SCL。这个顺序不能反过来因为如果 SDA 在 SCL 为高时变化ST7567 会把它当作非法边沿造成显示错乱。NOP 数量与主频强相关STC8 在 24MHz 1T 模式下1 个 NOP 只有约 42ns对多数模块不够稳建议至少 2 个如果是 12MHz 的 STC89 系列普通 IO 翻转本身够慢不加 NOP 也可以。选硬件还是软件我的习惯是工程刚起步先软件点亮确认屏和玻璃方向没问题后再切到硬件 SPI 提速度这样硬件配置出错时不会和屏本身的问题混在一起。3. ST7567初始化序列与页寻址显存映射从指令到首帧画面3.1 初始化指令表与参数说明ST7567 初始化没有标准答案不同厂商的模块对偏压比和对比度预设值差异很大但命令集合是一致的。下面这版序列兼容 UC1701很多 ST7567 模块的 datasheet 直接复用。步骤命令参数/值作用软复位0xE2无复位内部状态和地址计数器电源控制0x2F无打开内部升压、稳压、跟随器偏压比0xA2无1/9 bias多数 12864 玻璃适用ADC 选择0xA1无列扫描方向玻璃走线决定SHL 选择0xC8无COM 扫描方向玻璃走线决定稳压比0x26无内部电阻率粗调对比度电子音量0x810x18~0x25细调对比度显示开0xAF无打开显示void ST7567_Init(void) { ST7567_SPI_Init(); CS_PIN 0; // 片选有效整段初始化保持低 A0_PIN 0; // 进入命令模式 ST7567_WriteCmd(0xE2); // 软复位 delay_10ms(1); ST7567_WriteCmd(0x2F); // 电源boosterregulatorfollower 全开 delay_10ms(2); // 等升压稳定 ST7567_WriteCmd(0xA2); // 1/9 bias ST7567_WriteCmd(0xA1); // ADC 反向多数模块列扫描从左到右 ST7567_WriteCmd(0xC8); // COM 反向 ST7567_WriteCmd(0x26); // 内部电阻率按模块推荐值取中档 ST7567_WriteCmd(0x81); // 电子音量细调对比度 ST7567_WriteCmd(0x20); // 初始给中间值后续按实屏校正 ST7567_WriteCmd(0xAF); // 显示开 }需要说明的是0xA0/0xA1、0xC0/0xC8 这两组的方向没有绝对的对错完全看模块玻璃的 FPC 走线。很多模块厂商会在同一块 PCBA 上用不同批次的玻璃同一型号两个批次可能一个要 0xA1/0xC8另一个要 0xA0/0xC0。所以初始化里一定要把这两个命令单独拉出来做成宏方便排线时切换。命令和数据函数也要写成独立接口void ST7567_WriteCmd(unsigned char cmd) { A0_PIN 0; // 命令模式 ST7567_SPI_Write(cmd); } void ST7567_WriteData(unsigned char dat) { A0_PIN 1; // 数据模式 ST7567_SPI_Write(dat); }A0 与数据时序的关系是ST7567 在 SPI 字节的第一个上升沿采样 A0所以写入 SPDAT 之前必须完成 A0 切换。初始化里的命令函数每次都把 A0 拉低进入连续数据区后再切为高直到这轮写结束。3.2 页地址与列地址ST7567显存坐标怎么映射ST7567 的显示 RAM 按页组织一页对应 8 行像素。以 128×64 屏幕来说就是 8 页 × 132 列其中 128 列可见。垂直方向页 0 覆盖行 0~7页 1 覆盖行 8~15依此类推。要往指定位置写像素先发 0xB0页号再发列地址高 4 位和低 4 位然后连续写数据。列地址会在写完一个字节后自动加 1。void ST7567_SetCursor(unsigned char page, unsigned char col) { ST7567_WriteCmd(0xB0 | (page 0x0F)); // 页号 0~8 ST7567_WriteCmd(0x10 | ((col 4) 0x0F)); // 列地址高 4 位 ST7567_WriteCmd(0x00 | (col 0x0F)); // 列地址低 4 位 }参数含义0xB0 是页指令基地址0x10 是列高位基地址0x00 是列低位基地址。如果 col0x53则实际发送 0x15、0x03。虽然 132 列用 8 位地址足够但指令格式仍分两段拼接这个写法在 UC1701、ST7565、ST7567 上通用。3.3 8页还是9页65行玻璃的边界处理ST7567 的物理分辨率是 132×6565 行意味着完整显存有 9 页而 12864 模块只使用前 8 页。不要把第 9 页写在 0xB8 上让它参与刷新否则某些玻璃会出现一行残影或滚动错位。如果要做平滑滚动用显示起始行指令 0x40~0x7F它改的是 LCD 驱动从哪一行开始取数不改显存内容滚动代价很低。注意列地址计数器在到达 131 之后回绕到隐藏列不是自动换页。所以在循环里连续写完 128 列后必须重新调用 ST7567_SetCursor否则下一行会从错误列开始。调试时还可以使用 0xA5 命令做全亮测试。0xA5 会忽略显存内容强制全亮不改变显存数据退出测试用 0xA4。先用这条命令确认排线和玻璃没问题再往下调显存写入能省很多排查时间。4. 帧缓冲与字模在STC上实现可复用的绘图库4.1 为什么要在单片机侧维护128×8的帧缓冲ST7567 支持读回显存但那要接 MISO而且 STC 硬件 SPI 在半双工读模式下需要额外控制 MOSI 方向时序很麻烦。更常见也更稳的做法是在 STC 内部 RAM 里维护一份 128×8 的帧缓冲所有绘图操作先改缓冲需要显示时整体刷到 LCD。// 帧缓冲8 页 × 128 列共 1024 字节 // Keil C51 用 xdata若用 SDCC 写作 __xdata xdata unsigned char frame_buffer[8][128]; void draw_pixel(unsigned char x, unsigned char y, bit color) { unsigned char page, mask; if (x 127 || y 63) return; // 边界裁剪 page y 3; // 行号除以 8 得到页 mask 1 (y 7); // 页内位偏移bit0 是页首行 if (color) { frame_buffer[page][x] | mask; } else { frame_buffer[page][x] ~mask; } }注意 Keil C51 里数组超过 256 字节必须放 xdataSTC15 的 edata 只有 256 字节去掉堆栈和寄存器组余量不到 200 字节所以帧缓冲只能进 xdata。STC8 的 XRAM 更大1KB 宽裕。全屏刷新的开销在 6MHz SPI 下约 1.4ms加上页地址切换指令和函数调用不到 2ms对仪表类应用足够了。void flush_all(void) { unsigned char page, col; for (page 0; page 8; page) { ST7567_SetCursor(page, 0); A0_PIN 1; // 数据模式 for (col 0; col 128; col) { ST7567_SPI_Write(frame_buffer[page][col]); } } }4.2 按页刷新与脏页标记增量调优如果只改了一页的内容全屏重发就浪费了 7/8 的 SPI 带宽。简单做法是维护一个脏页标志每次画点后标记所在页刷新时只重发脏页。void flush_page(unsigned char page) { if (page 8) return; ST7567_SetCursor(page, 0); A0_PIN 1; for (unsigned char col 0; col 128; col) { ST7567_SPI_Write(frame_buffer[page][col]); } } unsigned char dirty_pages; // bit0~bit7 对应 8 页 void flush_dirty(void) { unsigned char page 0; while (dirty_pages) { if (dirty_pages 1) { flush_page(page); dirty_pages ~1; } dirty_pages 1; page; } }按页刷新要理解一个底层限制页里一个字节包含垂直 8 个像素画任意一个点都会导致该字节所在的整列被重写所以脏页的粒度是整页 128 字节不是像素。这也是为什么闪烁优化要先从脏页做起而不是从“只更新变化点”做起。4.3 字模方向为什么8×8、16×16字库要先做转换ST7567 的每字节纵向排列而大多数汉字取模工具默认横向排列。直接横向字模按字节刷进纵向显存字符会侧躺。建议在取模工具里直接选择“列行式、低位在前”让字模存储顺序与显存一致否则需要转置。取模方式字节排列方向写入 ST7567 前列行式纵向每字节是一列的 8 个点可直接写入横向逐行每字节是一行的 8 个点需要旋转后拆分到页跨页画 8×8 ASCII 的通用版代码如下它允许 y 落在任意行而不是只能从页首开始void draw_char(unsigned char x, unsigned char y, unsigned char ch) { unsigned char i, page, offset; page y 3; offset y 7; // ascii_0808 是纵向 8x8 字模每字符 8 字节 for (i 0; i 8; i) { frame_buffer[page][x i] | ascii_0808[ch - 0x20][i] offset; if (page 1 8) { frame_buffer[page 1][x i] | ascii_0808[ch - 0x20][i] (8 - offset); } } }逻辑说明当 y 落在页中间时一列像素要拆到两页。低位部分 offset留在本页高位溢出部分 (8-offset)放到下一页。如果 offset0第二个表达式右移 8 位的结果是 0不影响下一页。16×16 中文字模的转换类似区别是行序字模 HZK16 要先从每一列收集 16 个点再拆成上下两个页字节。做 GUI 时最省事的组合就是纵向字模 帧缓冲文字和图形都先画到缓冲界面结束后一次 Flush。5. SPI通信疑难排错白屏、花屏、镜像的四个定位入口5.1 白屏优先查电源、复位和升压白屏是最容易被冤枉成“SPI 没写对”的现象实际按这个顺序查量 VCC 与 GND 之间的电压。模块逻辑电压范围一般是 2.7~5.5V低于 2.7V 时控制器不工作。RST 引脚上电后必须被拉高。如果 RC 复位电容太大会出现上电后几秒无显示。初始化里有没有发 0x2F。ST7567 的电荷泵必须由软件打开只发显示开 0xAF 不会亮。对比度 0x81 后的参数不能太低低于 0x10 时就算显示开了也几乎看不见。void st7567_hard_reset(void) { RST_PIN 0; delay_10ms(1); RST_PIN 1; delay_10ms(5); }这个硬复位放在初始化最前面比只发 0xE2 软复位更可靠。注意如果用的是模块自带的 RC 复位RST 引脚悬空即可但上电瞬间要保证 VDD 爬升曲线没有反复否则偶尔会出现需要重新上电才能点亮的情况。5.2 花屏大概率是SPI采样沿与时钟极性不匹配我见过不少花屏是 CPHA 设置反了。ST7567 官方手册支持 Mode 0 和 Mode 3绝大多数模块按 Mode 0 布线但也有模块因为 SCL 线上接了 RC 滤波实际要 Mode 3 才稳定。分辨方法是看空闲 SCL 电平空闲低是 Mode 0空闲高是 Mode 3。软件上最省事的做法是准备两个宏#define SPI_MODE0 0xD0 // SSIG1 SPEN1 MSTR1 CPOL0 CPHA0 #define SPI_MODE3 0xD8 // SSIG1 SPEN1 MSTR1 CPOL1 CPHA1先按 Mode 0 初始化显示花屏或错位时改成 Mode 3 重新下载。另一种“花”是垂直方向上一条条噪点多半是 SCL 太快、杜邦线太长。线长超过 20cm 时把 SPI 时钟降到 fosc/16也就是 SPR10。ST7567 本身能跑几 MHz瓶颈往往是飞线和面包板寄生电容不是驱动 IC。5.3 镜像与上下颠倒ADC/SHL的方向组合ST7567 的列方向由 ADC 指令控制行方向由 SHL 指令控制两两组合出四种扫描方向。现象ADC 命令SHL 命令文字左右镜像0xA0 与 0xA1 互换不变文字上下颠倒不变0xC0 与 0xC8 互换上下颠倒且左右镜像0xA0/0xA1 互换0xC0/0xC8 互换调试时用四个组合依次试每次初始化后显示一个带数字的图形比如左上角画一个 5×5 的 L 形边框看到 L 的开口朝向正确就停下。注意改方向后必须重新走完整初始化或至少重发页地址单纯补发命令可能被地址计数器掩盖。5.4 SPIF标志被中断干扰发送函数的临界区保护如果开了全局中断且 SPI 中断使能SPIF 置位会进入中断服务程序。中断里无论是否清了这位外层 while 也可能错过置位特别是中断来得早一步刚准备发送时被打断。STC 官方例程一般不建议 SPI 用中断除非做了缓冲队列。void SPI_WriteByte_Atomic(unsigned char dat) { EA 0; // 关总中断 SPSTAT 0x80; SPDAT dat; while (!(SPSTAT 0x80)) { } EA 1; // 发送完成立即恢复 }这个函数发送期间关中断最长时间约 2μs对定时器中断的累积误差影响可以接受。如果系统里有必须严格定时器的中断比如红外解码那就把 SPI 放到主循环里做不要在中断服务函数里直接调用发送函数否则容易出现 SPIF 等待死循环。6. 把ST7567驱动封装成库验证技巧与最小工程骨架6.1 把引脚与SPI传输层分离的接口设计做 C51 的 SPI 通信库文件时最常见的问题是一上来就把所有函数写成全局可调结果换一块屏要改 10 个文件。建议拆三层HAL 层只负责引脚和寄存器驱动层只负责 ST7567 指令应用层只面对帧缓冲。// st7567.h 建议的最小接口 void ST7567_Init(void); void ST7567_Cmd(unsigned char cmd); void ST7567_Data(unsigned char dat); void ST7567_SetCursor(unsigned char page, unsigned char col); void ST7567_FlushAll(void); void ST7567_DrawPixel(unsigned char x, unsigned char y, unsigned char color);分离之后换引脚只改 HAL 文件换驱动芯片只重写 ST7567 层应用层永远不直接调 SPI 发送函数。这样调试时也能快速分清是硬件接线问题还是驱动逻辑问题。STC15 和 STC8 的寄存器基本兼容这套库在两个系列间移植时通常只需要改头文件里的 sbit 定义和 SPI 初始化里的时钟分频。6.2 一张图验证链路是否通初始化后先清屏再画一个 127×63 的边框对角线各画一条线然后显示一行“ST7567 OK”的 8×8 字符。这个自检能同时暴露 SPI 时钟极性、页地址顺序、字模方向和显示范围四类问题。确认无异常后再测一次全亮 0xA5 和全灭 0xA4确认对比度电位器或电子音量参数在有效区间内。帧率验证可以直接在定时器中断里累计 Flush 次数1 秒后读出来。对 12864 静息显示来说10fps 已经够用如果做简易动画优先优化脏页刷新把不变页跳过。后续接 GUI 库时只要保证 ST7567_SetCursor 之后按页连续写数据画线和反色这类效果都能直接在 frame_buffer 上做不再需要关心 SPI 时序。本文还有配套的精品资源点击获取