
简介ST7565_12864液晶驱动是嵌入式开发中常见的图形显示方案面向需要在小尺寸屏幕上绘制自定义图形或汉字的STM32项目开发者。驱动实现基于硬件SPI接口围绕屏幕初始化、命令发送、显示区域设定与像素数据写入等关键步骤展开由于该屏没有内置字符库资源还展示了如何通过编程控制每个像素点并给出了点阵字符与简单图形的生成思路。压缩包共111个文件体积约1.55MB核心代码为33个.h头文件与29个.c源文件涵盖STM32标准外设库的SPI、GPIO、定时器等驱动模块同时附带Keil工程文件、编译生成的.hex烧录文件以及.map、.sct等链接调试辅助文件方便直接烧录验证或按需裁剪移植。资源还包含中间编译文件.o、.crf与调试配置.scvd、.dep便于深入理解工程构建过程。已有676人学习该资源适合刚接触液晶驱动的初学者对照代码掌握MCU与SPI屏的交互方法也可作为实际项目中的显示驱动模板减少重复开发工作。1. ST7565_12864液晶为什么还在用它以及STM32驱动这件事到底难在哪ST7565_12864液晶是一种非常经典的 128×64 点阵 LCD 屏控组合工业仪表、医疗小屏、嵌入式人机界面里到现在仍大量出货。它的驱动难点不在于协议而在于三件小事初始化序列写不对就是白屏对比度调不好就是“全是黑块”刷新策略不对就是肉眼可见的闪烁。你拿 STM32 接这种屏本质上是在跟一组寄存器打交道跟 I2C 的 OLED 屏相比多了一层对时序和电源控制的理解。这篇文章就专门解决三个问题ST7565 到底是什么、怎么用 STM32 把它点亮、踩坑之后怎么定位。适合正在调屏的新手也适合想换掉 SSD1306 却卡在初始化的熟手。2. 从数据手册到引脚ST7565控制器特性和STM32硬件连接2.1 ST7565和SSD1306的本质差异先把控制器吃透ST7565 是 LCD 控制器SSD1306 是 OLED 控制器两者虽然都是 128×64 点阵的常用搭档但驱动思路完全不同。ST7565 驱动的是段式 LCD需要背光才能看见内容所以你的电路板上一定有一路 LED 背光控制脚通常用三极管或者 MOS 管开关。SSD1306 是自发光不需要背光这是第一个容易忽略的差异——很多人按 OLED 的接法接好 ST7565发现屏亮不起来其实是背光根本没开。ST7565 内部带升压电路可以把逻辑电源升到 LCD 驱动需要的 VLCD 电压典型值在 5V 到 8V 之间具体取决于屏的玻璃和偏压设置。这不是玄学而是 ST7565 必须搭配几个电容常见是 1uF 或者 0.1uF放在 VC1、VC2、C1、C1-、C2、C2- 这些引脚上。电容接错位置或者容量不对升压电路工作不正常表现就是屏幕对比度极低或者怎么调对比度寄存器都没反应。我一般会先看屏厂提供的“应用电路”那一页电容照着抄而不是自己猜。ST7565 的占空比通常配置成 1/65偏压比有 1/9、1/7 等选项这些值必须跟屏幕的物理特性匹配。同一个控制器不同厂家的玻璃参数不同初始化序列里的偏压设置也不一样。新手最容易犯的错误是把别人点亮 SSD1306 的流程拿过来改个引脚结果屏幕永远是一团黑。记住一个原则ST7565 的常识是“读屏厂手册”而不是“抄通用库”。2.2 硬件SPI还是GPIO模拟STM32接线和时序选择ST7565 的常用接口是 SPI引脚一共 5 根控制线加 1 根背光SCK、SI串行数据、CS片选、A0数据/命令选择也叫 RS、RESET复位、BL背光。A0 是 ST7565 最关键的控制脚高电平表示写入数据低电平表示写入命令。注意有些屏的 A0 会和 RESET 混在一起或者被拉到固定电平这时候你只能操作显存而无法执行初始化所以接线后要先用万用表确认 A0 是独立可控的。STM32 驱动 ST7565 有两种方式硬件 SPI 和 GPIO 模拟 SPI。硬件 SPI 的好处是速度快、CPU 占用低坏处是不同 STM32 型号复用脚不一样而且 SPI 极性问题偶尔会让人头疼。GPIO 模拟的好处是随便选脚、时序完全可控坏处是刷整屏的时候速度明显偏慢。我个人的习惯是如果屏幕只显示简单状态用 GPIO 模拟如果要做动画或者频繁刷新就用硬件 SPI。下面是我常用的 GPIO 模拟接线定义#define LCD_SCK GPIO_Pin_5 // 接屏 SCK #define LCD_SI GPIO_Pin_7 // 接屏 SI #define LCD_CS GPIO_Pin_3 // 接屏 CS #define LCD_A0 GPIO_Pin_2 // 接屏 A0/RS #define LCD_RESET GPIO_Pin_1 // 接屏 RESET #define LCD_BL GPIO_Pin_0 // 接屏 BL控制背光这段定义只说明引脚分配关系你在自己的板子上要换成实际连接的 GPIO。A0 必须跟片选分开不能偷懒。ST7565 的 SPI 时序通常支持模式 0 和模式 3也就是空闲时钟电平为低还是高。实战中我遇到过部分兼容屏只认模式 3所以建议先用模拟 SPI把时序写成“SCK 空闲为高、上升沿锁存”这种明确的状态点亮后再换成硬件 SPI 提速。void LCD_WriteByte(uint8_t byte) { for (uint8_t i 0; i 8; i) { LCD_SCK_Clr(); if (byte 0x80) LCD_SI_Set(); else LCD_SI_Clr(); LCD_SCK_Set(); byte 1; } }这段代码的逻辑是先把时钟拉低准备好数据位再把时钟拉高数据在上升沿被屏幕锁存。先传高位还是低位取决于屏幕时序ST7565 标准是高位在前如果你发现字体左右颠倒可以尝试先传低位。模拟 SPI 不需要配置复用功能但要注意 GPIO 速度别设成低速否则时钟上升沿太缓屏幕可能采样出错。2.3 最小目标点亮背光并让屏幕不白屏的初始化先导拿到 ST7565 屏之后第一件事不是急着调初始化而是确认两件事背光能亮、复位能拉。背光不亮后面所有寄存器操作都看不见结果排查起来等于摸黑。我一般会写一个最简单的测试程序只做三件事拉高 BL、拉低 RESET 再拉高、往屏幕写几个命令。如果背光正常屏幕应该显示均匀的底色灰块但不是花屏也不是全黑屏。这里有个常见判断点ST7565 上电后如果没有初始化屏幕默认是关闭显示状态的所以背光亮但屏幕全白是正常现象。如果你看到满屏黑色噪点说明 SPI 通信可能已经在乱传数据但不一定代表接线错误也可能是复位时序不对。复位脚必须遵循“低电平保持至少 1 微秒再拉高然后等待至少 5 毫秒”的时序这个时序写在屏的数据手册里比后面任何寄存器都重要。我建议用一个 GPIO 模拟的字节发送函数配合逻辑分析仪先确认写入的命令字节确实以正确时序出现在线上。没有逻辑分析仪时可以降低 SPI 时钟到 100kHz再用示波器看 SCK 和 SI 的毛刺。实际项目里我见过三次白屏两次是 RESET 没有真正复位一次是 A0 被上下拉电阻错误固定在低电平。所以先把基础打通再聊后面的初始化。3. 用STM32跑通ST7565最小初始化序列和画点/刷新函数3.1 初始化序列寄存器值背后的逻辑ST7565 的初始化没有标准答案因为玻璃参数不同偏压和对比度初值都要微调。但有一套命令序列是大多数 COG 封装屏通用的我提供一份自己常用的并把每个命令的意图写清楚。这份序列不是抄的某份神秘文档而是从 ST7565 数据手册的命令表里挑出来的配合屏厂推荐参数做了调整。void LCD_Init(void) { LCD_RESET_Set(); delay_ms(10); LCD_RESET_Clr(); delay_ms(10); LCD_RESET_Set(); delay_ms(50); LCD_WriteCmd(0xA0); // ADC 选择SEG 方向正常若显示镜像改为 0xA1 LCD_WriteCmd(0xC0); // COM 扫描方向正常若上下颠倒改为 0xC8 LCD_WriteCmd(0xA2); // LCD 偏压比 1/9部分屏用 0xA3 LCD_WriteCmd(0x2C); // 电源控制升压、稳压、跟随器全开 LCD_WriteCmd(0x2F); // 升压电路模式选择调节升压比 LCD_WriteCmd(0x81); // 对比度设置命令 LCD_WriteCmd(0x24); // 对比度值 0x24点屏后根据效果微调 LCD_WriteCmd(0xA6); // 正常显示0xA7 为反显 LCD_WriteCmd(0xAF); // 打开显示 }这段代码的逻辑先复位确保控制器从干净状态开始。0xA0 和 0xC0 决定了行和列的扫描方向如果屏幕上的内容左右或上下镜像改这两个命令而不是改显存坐标。0xA2 是偏压比它直接影响 LCD 的驱动电压等级设错会导致对比度完全调不出来。0x2C 和 0x2F 是电源控制ST7565 的升压电路是分模块的必须把升压器、稳压器、跟随器全部开启才能保证 VLCD 稳定输出。0x81 后跟的 0x24 是初始对比度实际值通常在 0x18 到 0x30 之间取决于室温和屏幕批次。如果你点亮后发现屏幕发黑先把对比度降到 0x10 再看如果屏幕浅得看不清再往上升。注意对比度寄存器是 6 位有效还是 8 位有效不同批次可能不同所以手册上写 0x00~0x3F 就按 6 位用写 0x00~0xFF 就按 8 位用。这只能实测没有捷径。3.2 从画点到整屏刷新显存组织与发送策略ST7565 的显示 RAM 是按“行分页”组织的64 行分成 8 页每页 8 行。发送数据时一页一页地发每页 128 字节一字节对应 8 行像素。所以最好的驱动方式是在 STM32 内部维护一个 8 行 × 128 列的缓冲区画图的时候只操作缓冲区刷新的时候一次性发到屏幕。static uint8_t LCD_Buffer[8][128]; void LCD_DrawPixel(uint8_t x, uint8_t y, uint8_t color) { if (x 128 || y 64) return; uint8_t page y 3; uint8_t bit y 0x07; if (color) { LCD_Buffer[page][x] | (0x01 bit); } else { LCD_Buffer[page][x] ~(0x01 bit); } }这个画点函数把 64 行拆成 8 页y 的高 3 位是页号低 3 位是页内行号。每字节的低 3 位分别代表该页的第 1 到第 8 行最低位对应 PAGE 顶部还是底部取决于 COM 扫描方向但这不影响画点逻辑因为刷新时屏幕会自己把位映射过去。如果你发现点像“掉在地上”一样上下错位可以试一下0x01 (7 - bit)也就是反转字节内的位序。刷新函数如下它把缓冲区按“页地址→列地址→数据”的顺序发送void LCD_Refresh(void) { for (uint8_t page 0; page 8; page) { LCD_WriteCmd(0xB0 | page); // 设置页地址 0~7 LCD_WriteCmd(0x10); // 列地址高 4 位从第 0 列开始 LCD_WriteCmd(0x00); // 列地址低 4 位 for (uint8_t col 0; col 128; col) { LCD_WriteData(LCD_Buffer[page][col]); } } }这段代码的核心是 ST7565 的列地址指针写入列地址后每发一个数据列指针自动加 1所以一整页可以连续发送。0x10 是高 4 位列地址0x00 是低 4 位合起来就是列 0。如果你在刷新前修改了页地址但列地址没重新设置屏幕可能从上次停下的列继续写造成错位。所以每次刷新前页地址和列地址都必须要重新设置完整。3.3 验证显示写一个能跑起来的自检程序初始化完成后的第一轮验证不要急着画自己的图形先跑一个自检程序用全屏填充和递减线来确认扫描方向、显存映射都正确。我常用的是画一个“四边框”然后做 8 页的全 0xFF 填充这样能看到每页是否连续。void LCD_Test(void) { for (uint8_t page 0; page 8; page) { for (uint8_t col 0; col 128; col) { LCD_Buffer[page][col] 0xFF; } } LCD_Refresh(); delay_ms(500); for (uint8_t i 0; i 64; i 8) { LCD_DrawPixel(0, i, 0); LCD_DrawPixel(127, i, 0); LCD_DrawPixel(0, i 7, 0); LCD_DrawPixel(127, i 7, 0); } LCD_Refresh(); }第一次跑自检时我会故意在屏幕上画一条 45 度斜线用坐标关系验证 X/Y 方向。如果斜线变成折线说明页映射错误如果斜线方向反了说明 ADC 或 COM 设置反了。别急着改驱动先把 0xA0/0xC0 命令做镜像反转测试大多数方向问题都能通过这两个寄存器修复。自检通过后再进入真正的应用界面开发。4. ST7565驱动避坑白屏花屏、对比度发黑和刷新闪烁的排查记录4.1 现象1上电白屏或满屏黑点复位时序没做好现象程序烧进去以后背光亮了但屏幕全白或者随机出现一些黑点怎么发命令都没反应。原因多半是 RESET 引脚没有干净地复位。有些屏的 RESET 内部有上拉但你如果用 STM32 推挽输出拉低可能拉得不够低或者拉低时间太短。解决方法是初始化函数一开始单独操作 RESET用延时拉开不确定窗口保证复位信号低电平至少 20 微秒。LCD_RESET_Clr(); delay_us(50); LCD_RESET_Set(); delay_ms(20);我遇到过一次很奇怪的情况引脚复位后再显式初始化屏幕正常但如果复位后立刻发命令前三个命令会丢失。原因是上电后内部电源还没稳定所以我在复位后加了 20 毫秒延时而不是手册上的 5 毫秒。如果你在初始化前就发现屏幕有随机黑点优先怀疑 SCL 和 SDA 在复位期间被干扰可以在复位的同时把 CS 拉高隔离信号输入。4.2 现象2对比度调不下去屏幕发黑或几乎看不见现象用 0x81 设置对比度从 0x00 调到 0x3F屏幕始终是一片黑或者完全透明看不清。原因通常不是对比度命令本身而是偏压比或电源控制错误。偏压比设得太高VLCD 电压过强像素全部打开升压电容焊接问题也会导致电压异常。解决方法是回到初始化命令把 0xA2 改成 0xA3 试试同时检查升压电容的值。如果是屏幕发黑可以先用一个低对比度值 0x10 观察如果屏幕浅到像没通电再检查 0x2C 是否真的开了稳压器。还有一次我把 A0 当作数据线接到 SI 上结果命令和数据混在一起对比度寄存器收到的值完全随机屏幕自然乱套。所以先确认 A0 的电平在发命令和数据时确实不同。4.3 现象3刷新时整屏闪烁像老式CRT现象图形显示正确但每次调用刷新函数屏幕就会闪一下特别是在显示数字变化时非常明显。原因有两个一是你每次刷新前都清空了一整屏再重新画所有内容导致屏幕在复旦过程中经历“空白”阶段二是 SPI 速度太慢刷新一帧需要几十毫秒肉眼能捕捉到过程。解决方法是引入局部刷新只刷新变化的区域。我下面的代码把刷新范围限制在一个矩形内只发送相关页的列区间void LCD_RefreshRect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { if (x0 x1) { uint8_t t x0; x0 x1; x1 t; } if (y0 y1) { uint8_t t y0; y0 y1; y1 t; } uint8_t page0 y0 3; uint8_t page1 y1 3; for (uint8_t page page0; page page1; page) { LCD_WriteCmd(0xB0 | page); LCD_WriteCmd(0x10 | (x0 4)); LCD_WriteCmd(x0 0x0F); for (uint8_t col x0; col x1; col) { LCD_WriteData(LCD_Buffer[page][col]); } } }这个函数的关键是列地址设置写高 4 位列地址时用0x10 | (x0 4)写低 4 位列地址时用x0 0x0F。如果你只刷新一个 10×10 的图标但发送了从 0 到 127 的整页数据闪烁问题依然存在。实际项目中我先把刷新函数改成LCD_RefreshRect再配合定时器控制刷新频率闪烁问题基本消失。4.4 现象4显示镜像或打印点阵字体上下颠倒现象显示字符时左右镜像或者英文文字颠倒换成列扫描方向设置也没有完全解决。原因可能是 ST7565 的 ADC 和 COM 方向设置组合与显存位序不匹配。0xA0/A1 控制的是 SEG 输出方向0xC0/C8 控制的是 COM 输出方向四个组合对应四种显示方向。解决方法是不要改显存代码只在这四个组合中切换测试哪一组显示正常。// 正常显示方向一般用 A0 C0 或 A1 C8 LCD_WriteCmd(0xA0); LCD_WriteCmd(0xC0); // 如果左右镜像改成 LCD_WriteCmd(0xA1); LCD_WriteCmd(0xC0);还有一种情况是字体上下颠倒但左右正常这时候需要检查字节内位序。有些屏把一字节的最低位放在了显示屏最上方有些则放在最下方。我建议在画点函数里多写一个宏让位序可以快速切换#define LCD_PIXEL_BIT(bit) (0x01 (bit))如果发现只有字体颠倒而矩形边框正常那就是取模软件的字节序和屏幕相反调整取模参数即可。5. 显示内容增强中文字库、图片取模和脏矩形局部刷新5.1 自绘点阵字体16×16中文取模与存储布局ST7565 没有内置字库所以显示汉字需要自己准备点阵数据。最常见的方案是 16×16 点阵一个汉字占 32 字节。取模软件通常会把每行 16 个点拆成两个字节第一字节是左边 8 点第二字节是右边 8 点。存储顺序是逐行存储共 16 行。下面的代码把 16×16 点阵写入画布void LCD_DrawChar16(uint8_t x, uint8_t y, const uint8_t *font) { for (uint8_t row 0; row 16; row) { uint8_t byteL font[row * 2]; uint8_t byteR font[row * 2 1]; for (uint8_t bit 0; bit 8; bit) { if (byteL (0x80 bit)) LCD_DrawPixel(x bit, y row, 1); if (byteR (0x80 bit)) LCD_DrawPixel(x bit 8, y row, 1); } } }这段代码会把汉字横向铺在屏幕的任意坐标上但每个像素都走画点函数速度很慢只适合低频绘制。如果要频繁更新直接操作LCD_Buffer获得更好性能。另一个注意点是字库的取模方向如果取模软件设置为“列行式”那么存储结构是逐列而非逐行上面的代码就需要改成先读列数据。我一般建议把取模软件设置为“横向取模行内高位在前”这样跟上面的代码完全兼容。对于多语言界面可以把常用汉字做成一个数组表用索引查表。比如typedef struct { const uint8_t *data; uint8_t width; uint8_t height; } FontItem;这样中英文混排时每个字符都有独立的宽高属性。注意 16×16 汉字的宽高都是 16但 ASCII 字符一般只有 8×16所以绘制函数要根据字符类型决定步长否则竖排会重影。5.2 脏矩形局部刷新让刷新率翻倍的常见做法前面提到的LCD_RefreshRect就是脏矩形刷新的基础它要求你在绘制之前先记录变化区域。一个简单的实现是每次调用绘制函数时自动更新全局脏矩形变量。static uint8_t dirty_x0 127, dirty_y0 63; static uint8_t dirty_x1 0, dirty_y1 0; void LCD_MarkDirty(uint8_t x, uint8_t y) { if (x dirty_x0) dirty_x0 x; if (x dirty_x1) dirty_x1 x; if (y dirty_y0) dirty_y0 y; if (y dirty_y1) dirty_y1 y; }画点函数里调用LCD_MarkDirty(x, y)等一帧绘制完成后用LCD_RefreshRect(dirty_x0, dirty_y0, dirty_x1, dirty_y1)只刷新变化区域。这样做的收益在显示数字时钟时非常明显一分钟只有两位数字变化整屏刷新是 128×64脏矩形刷新可能是 20×16数据量差了二十倍以上。注意脏矩形坐标要包含字的边界否则会出现残影。用左移位和或运算来替代取模运算也是一个办法因为 y 坐标除以 8 在编译器里可能被优化但写成y 3更保险。局部刷新唯一的副作用是如果像素不断变暗或变亮多次局部刷新会导致边界区域的对比度不一致解决办法是定期做一次整屏刷新让所有像素重新统一驱动。5.3 图片取模方向先弄清行偏移还是列偏移做开机 Logo 或图标显示时图片取模方向直接决定显示效果。ST7565 的页面是按 8 行一组划分的所以取模软件应该选择“垂直取模”或“列行式”。具体来说把图片按 8 行一个页的高度切成块每个块内按列顺序存储一字节。如果你取模成水平方向刷新后会出现雪崩状的花屏。void LCD_DrawImage(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *img) { for (uint8_t page 0; page (h / 8); page) { for (uint8_t col 0; col w; col) { uint8_t data img[page * w col]; for (uint8_t bit 0; bit 8; bit) { if (data (0x80 bit)) { LCD_DrawPixel(x col, y page * 8 bit, 1); } } } } }这个函数要求图片的高度是 8 的整数倍如果不是需要自己在外部补齐空白行。取模时还要注意图片是单色 B/W 格式不要用灰阶取模。我用过某取模软件默认输出是“列式”的但列方向会多一位偏移导致图标出现斜向重复。解决办法是在取模设置里选择“从左上角开始列方向取模”然后做一次小尺寸测试图验证。6. 进阶ST7565对比度校准与低功耗休眠的实用技巧做到这一步屏幕已经能稳定显示内容了但还有两个细节决定产品体验对比度在不同温度下的表现以及低功耗设备如何让屏幕在休眠时不漏电。对比度校准的本质是设置 VLCD 电压ST7565 通过命令 0x81 后跟一个值来调节。这个值受温度影响很大冬天和夏天同一块屏最佳值可能相差 0x08。我习惯把对比度值放到一个 EEPROM 或内部 Flash 的配置区产品首次开机时进入校准模式按键加或减对比度保存后后续启动直接读取。这样比在代码里写死一个万年 0x24 更可靠。校准增量可以用 2 或 4 的步进太细用户感觉不到变化太粗又容易越过最佳点。uint8_t contrast 0x24; LCD_WriteCmd(0x81); LCD_WriteCmd(contrast); // 校准模式下按键加减 if (key_increase) { if (contrast 0x3F) contrast 4; LCD_WriteCmd(0x81); LCD_WriteCmd(contrast); }低功耗休眠不是只关背光那么简单。ST7565 本身有电源控制命令0xAE 是关显示0x28 是关闭升压电路。如果你只关背光而让控制器继续运行屏幕会在背光亮起时出现一次瞬间的对比度跳动因为 VLCD 电压还在。正确的休眠顺序是先关显示再关升压最后关背光。唤醒顺序相反先开背光再开升压等升压稳定后打开显示。以下是我用过的顺序void LCD_Sleep(void) { LCD_WriteCmd(0xAE); // 先关显示 LCD_WriteCmd(0x28); // 关闭全部电源模块 LCD_BL_Clr(); // 最后关背光 } void LCD_Wake(void) { LCD_BL_Set(); // 先开背光 LCD_WriteCmd(0x2C); // 打开电源模块 delay_ms(20); // 等升压稳定 LCD_WriteCmd(0xAF); // 再开显示 }有一次我把这个顺序搞反了先开显示再开背光结果屏幕点亮瞬间显示错乱了几帧。后来看数据手册发现显示开启命令 0xAF 应该放在最后因为它等待内部电压建立。遇到休眠唤醒异常时也可以先只发送 0xAF再观察屏幕是否正常用这种二分法快速定位是哪一步引入的问题。ST7565 驱动这个方向没必要追求复杂只要把控制器特性摸清初始化序列写稳定再配上脏矩形刷新和合理的休眠策略它仍然是 12864 屏里性价比很高的选择。希望这些踩坑经验能帮到你少走几段弯路。本文还有配套的精品资源点击获取