☰
STM32驱动ST7565 12864液晶:从初始化到排错全流程解析
2026/10/15 18:23:41 网站建设 项目流程

简介:针对ST7565 12864点阵液晶在STM32平台上的驱动需求,这份资源提供了基于硬件SPI接口的完整Keil工程方案。压缩包共111个文件,主要包含C源文件与头文件、STM32标准外设库驱动、工程配置文件(如uvprojx/uvoptx)以及编译生成的hex/axf文件,整体大小仅1.55MB,可直接用MDK打开、查看或烧录验证。源文件涵盖初始化、命令写入、显示数据传输等核心模块,并附有工程辅助脚本和编译输出文件,便于对比学习与二次移植;同时涉及SPI通信时序、无字库液晶逐像素控制思路,适用于需要自绘字符或图形的嵌入式人机界面。该资源已有676人浏览学习,适合刚接触ST7565驱动、或想快速在STM32上复用该液晶屏的开发者作为代码级参考资料。

1. ST7565_12864液晶STM32驱动:不是SSD1306的换皮,是另一套时序逻辑

接手过一个攒了很久的项目,主控是STM32F103,原来跑的是SSD1306的OLED屏,后来因为户外强光下看不清,换了一块ST7565_12864液晶。我以为驱动逻辑差不多,都是单色点阵,结果照搬过去之后——黑屏。查了半天不是供电问题,不是接线问题,是ST7565和SSD1306的显存映射、命令格式、电源开关时序完全不同,甚至SPI模式都分两种。这块屏在工控仪表、手持设备里很常见,带内部升压,3.3V供电能驱动,比ST7920那种带字库的屏快,比SSD1306的OLED抗造,但它属于“老实但不好惹”的类型:命令不多,可组合出来的坑不少。这篇文章不写手册,按我实际调通一块ST7565_12864液晶STM32驱动的全过程来写,从硬件接口到初始化命令,再到显存寻址和滚动技巧,最后把常见的黑屏、镜像、花屏问题一次说透。适不适合你的项目,看完前两章基本就有数了。

2. 硬件连接与接口选型:SPI还是8080并口,先看这几根引脚

2.1 ST7565、ST7920、SSD1306的区别,决定了你接线不能照搬

ST7565是一个LCD驱动控制器,它本身不带字库,也不带GRAM以外的任何“智能”功能。市面上常见的12864液晶模块用的是三种控制器:ST7920、SSD1306、ST7565。ST7920带中文字库,串口模式下时钟很慢,刷全屏能明显看到拖影,适合只显示固定文本的场合。SSD1306是OLED驱动,它有一个很出名的电荷泵命令0x8D,但它是为OLED面板设计的,电压和扫描时序跟LCD不一样。ST7565是纯LCD段驱动,内部集成了升压电路和温度补偿,适合用3.3V单电源推动偏压较高的LCD面板,而且它的GRAM是按“页×列”组织的,写一列就是竖着的8个点,这一点和SSD1306写显存的方式形似而神不似。

接口方面,ST7565模块一般引出两种接口:一种是SPI串行(SCL、SDA、A0、CS、RST),另一种是8080并口(DB0~DB7、A0、RD、WR、CS、RST)。很多模块做了PS引脚来切换接口模式,有的模块直接把并口和SPI引脚都引出来,接错了就是白屏。选型时我的建议是:如果STM32引脚够用但总线不紧张,SPI更省IO;如果要刷视频级别的动画,8080并口吞吐更高,但占用8个数据脚,F103C8这种48脚的片子会捉襟见肘。多数项目用SPI就够,本文以SPI四线模式为主线,最后提一下并口的改动点。

2.2 实际接线与电平匹配:别让5V MCU直接怼3.3V模块

我常用的接法如下表,针对STM32F103系列,PA5~PA7做SPI1,再补两个GPIO做A0和CS:

ST7565模块引脚接STM32引脚说明
VDD3.3V模块逻辑电源,多数模块标3.3V
GNDGND共地
SCLPA5 (SPI1_SCK)时钟
SDAPA7 (SPI1_MOSI)数据,ST7565不用MISO
A0PA4 (GPIO)低电平写命令,高电平写数据
CSPA3 (GPIO)片选,低有效
RSTPA2 (GPIO)复位,低有效
VOUT悬空或接电容内部升压输出,模块一般已处理
LEDA背光正极经限流电阻接3.3V或5V

电平匹配是最容易被忽略的坑。如果STM32是5V供电,GPIO输出高电平5V,直接进3.3V模块的SDA/SCL,长期用会损伤模块的输入缓冲。我在一个量产板上吃过亏,屏幕用了半年开始随机花屏。后来统一改成:MCU供3.3V或者加电平转换芯片。如果只是做实验,可以在所有信号线上串330Ω电阻,能压住大部分过冲,但不建议量产这么干。RST的复位时序也有讲究:上电后拉低至少10ms,再拉高,然后等10ms再初始化,太短会偶发初始化失败。

2.3 硬件SPI配置:模式0和模式3都有可能,先跑起来再说

我用的是STM32的硬件SPI1,配置关键参数如下:

SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工,但只发不收 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0,即SPI Mode 0 hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 72MHz/4=18MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }

注意BaudRatePrescaler我最后选了4,但实际跑的时候如果发现花屏,改成8或16再试。ST7565模块的SPI时钟上限一般标8MHz或10MHz,18MHz理论超了,部分模块能跑但余量小。稳妥做法:量产选分频8,即9MHz;调试阶段为了排除时序问题可以先选16,也就是4.5MHz,调通后再提速度。

CLKPolarity和CLKPhase两个参数是另一个坑。我调过两种模块,一种必须Mode 0(CPOL=0, CPHA=0),另一种必须Mode 3(CPOL=1, CPHA=1),都是ST7565控制器,只是模块厂商在设计输入缓冲时选择了不同的采样沿。所以手头有屏但不知道SPI模式时,先用Mode 0试,不行就切Mode 3,再不行检查时钟相位是否和数据建立时间冲突——这块我会在第5章展开。

3. 初始化序列与寄存器配置:点亮屏幕的13条命令和对比度调整

3.1 关键寄存器先立起来:偏压比、扫描方向、电源控制

ST7565的初始化命令不多,但每一条都有讲究。我先列一张表,按写入顺序排列,后面代码里会原样复现:

命令值作用为什么这么设
偏压比0xA21/9偏压多数3.3V模块配1/9,屏幕均匀性好
ADC选择0xA0列方向正常0xA1会左右镜像
SHL选择0xC8行扫描反向0xC0会上下镜像
电源控制0x2F开升压、稳压、跟随不开升压就是黑屏
电阻比0x24粗调对比度值越小对比度越高,范围0x20~0x27
对比度寄存器0x81后跟0x1F微调对比度0x00最淡,0x3F最深
显示起始行0x40从GRAM第0行显示改这个值可以做滚动
显示开0xAF打开显示最后一步

电源控制0x2F是最关键的一条。ST7565的电源系统分成三个部分:升压电路(VC)、稳压电路(VR)、跟随电路(VF),0x2F是全部打开,让内部产生LCD驱动需要的VOUT电压。有些代码只写0x28或者0x2C,结果屏幕有显示但极淡,或者完全没显示,就是升压没起来。

偏压比0xA2和0xA3的选择影响液晶的偏压电压,选错了最典型的现象是屏幕对比度不均匀——上半部分深、下半部分浅。3.3V供电的12864模块绝大多数用1/9偏压,也就是0xA2。如果你用的模块是5V供电的,可能就需要0xA3,也就是1/7偏压。实在不确定就切换试,这个现象视觉上非常明显。

ADC和SHL这两条决定显示方向。0xA0是正常列方向,0xC8是反向行扫描,这两个组合起来,在大部分模块上正立显示。但模块PCB布局不同,有的需要0xA1配合0xC0才是正的。我的习惯是先把画面点亮,再根据实际显示的“上下/左右颠倒”来调整这两个值,不要一开始就纠结“标准值”,第5章会给出四组试错组合。

3.2 完整初始化函数:直接可抄,但看注释再改

下面是我在STM32上用的初始化代码,基于HAL库,A0和CS用两个GPIO控制,SPI发送一个字节:

#define ST7565_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET) #define ST7565_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET) #define ST7565_A0_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define ST7565_A0_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define ST7565_RST_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET) #define ST7565_RST_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET) static void ST7565_WriteCmd(uint8_t cmd) { ST7565_A0_LOW(); // A0=0,命令 ST7565_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); ST7565_CS_HIGH(); } static void ST7565_WriteData(uint8_t data) { ST7565_A0_HIGH(); // A0=1,数据 ST7565_CS_LOW(); HAL_SPI_Transmit(&hspi1, &data, 1, 100); ST7565_CS_HIGH(); } void ST7565_Init(void) { // 复位时序:低电平至少10ms,再拉高 ST7565_RST_LOW(); HAL_Delay(20); ST7565_RST_HIGH(); HAL_Delay(20); ST7565_WriteCmd(0xA2); // 1/9偏压 ST7565_WriteCmd(0xA0); // 列方向正常,如左右镜像改0xA1 ST7565_WriteCmd(0xC8); // 行扫描反向,如上下颠倒改0xC0 ST7565_WriteCmd(0x2F); // 打开所有电源电路,升压起来 HAL_Delay(10); // 等升压稳定,这里不能省 ST7565_WriteCmd(0x24); // 内部电阻比,粗调对比度 ST7565_WriteCmd(0x81); // 对比度寄存器设置 ST7565_WriteCmd(0x1F); // EV=31,中间值,后面再细调 ST7565_WriteCmd(0x40); // 显示起始行=0 ST7565_WriteCmd(0xAF); // 显示开 ST7565_Clear(0x00); // 清屏 }

逻辑说明:复位后立刻写偏压比,因为电源电路是按偏压比来生成参考电压的;0x2F之后必须给一个延时,内部升压电路需要时间建立电压,我见过有人把延时去掉,结果10次里有3次黑屏。对比度先写粗调0x24再写微调0x81+0x1F,顺序颠倒会出现对比度不随EV值变化的假象。

清屏函数单独说一句:

void ST7565_Clear(uint8_t fill) { for (uint8_t page = 0; page < 8; page++) { ST7565_WriteCmd(0xB0 + page); // 选择第page页 ST7565_WriteCmd(0x10); // 列地址高4位=0 ST7565_WriteCmd(0x00); // 列地址低4位=0 for (uint8_t col = 0; col < 128; col++) { ST7565_WriteData(fill); } } }

这段代码看起来像SSD1306的清屏,但注意一个区别:SSD1306在同一页里列地址会自动递增,ST7565也递增,但页地址不会自动切换,必须手动写0xB0+page。

3.3 对比度调整:EV、电阻比、偏压比三者的配合是玄学

对比度是ST7565项目里最玄学的部分。很多开发者只调0x81后面的EV值,发现调满还是淡,就以为屏坏了。实际上ST7565的对比度由三个寄存器共同决定:偏压比(0xA2/0xA3)、电阻比(0x20~0x27)、EV值(0x81)。偏压比负责总体电压档位,电阻比负责LCD驱动电压的粗调,EV做细调。常见组合:

现象偏压比电阻比EV值
整体太淡保持0xA20x24调到0x220x1F调到0x2E
整体太深,甚至出现残影保持0xA20x24调到0x260x1F调到0x10
上半深下半浅0xA2换0xA3试保持0x24保持0x1F

我的实测经验:EV加到0x30以上容易出现“糊”,笔画粗、拖影;电阻比调到0x22以下,低温环境下会明显发黑。如果项目要在-20℃环境用,对比度要往淡了调,留出温度补偿的余量。

4. 显存结构与画点画字:列扫描顺序反直觉,坐标变换是第一个坑

4.1 GRAM映射:一列对齐一字节,页和列的配合方式

ST7565的GRAM有132列64行,12864模块实际用128列。它把显示区分成8页,每页8行,页地址由0xB0~0xB7选择。写入一个数据字节时,这个字节的8个bit对应同一列里上下相邻的8行像素。也就是说,你往列地址0写一个0x01,屏幕左上角那一列的顶部第1行出现一个点;写0x80,则是同一列的第8行出现一个点。

这与我们平时理解的行扫描完全不同,反而和SSD1306类似。但坐标变换时有一个关键坑:SSD1306的列地址从0开始,连续写128字节后一页完成;ST7565也是连续写,但如果你要从屏幕左上角(行0列0)开始画一个点,必须把坐标拆成“页=行/8”和“列=行%8对应的bit位”,这个拆法我在第一次写画点函数时搞反了,导致画出来的点竖直方向上每隔8行多一个。

ST7565页排序的方向也受SHL影响。0xC8的行扫描反转会让GRAM的页0显示在屏幕底部而不是顶部,所以你在framebuffer里越界写页0,屏幕上显示的是底下那部分画面。这就是为什么很多人清屏后写字符,字符上下颠倒或者割裂——坐标在逻辑上是反的。

4.2 画点函数:framebuffer是必须的,ST7565不能读RAM

ST7565不支持读GRAM(SPI模式下更是没有数据回传通道),所以想改一个点必须先保存完整的帧数据。我做得最顺手的方式是开一个1KB的数组做显存镜像:

static uint8_t framebuffer[8][128]; void ST7565_SetPixel(uint8_t x, uint8_t y) { if (x >= 128 || y >= 64) return; // 坐标变换:页=行/8,字节内bit位=行%8 uint8_t page = y / 8; uint8_t bit = y % 8; framebuffer[page][x] |= (0x01 << bit); } void ST7565_ClearPixel(uint8_t x, uint8_t y) { if (x >= 128 || y >= 64) return; uint8_t page = y / 8; uint8_t bit = y % 8; framebuffer[page][x] &= ~(0x01 << bit); } void ST7565_RefreshAll(void) { for (uint8_t page = 0; page < 8; page++) { ST7565_WriteCmd(0xB0 + page); ST7565_WriteCmd(0x10); ST7565_WriteCmd(0x00); for (uint8_t col = 0; col < 128; col++) { ST7565_WriteData(framebuffer[page][col]); } } }

逻辑说明:framebuffer定义为[8][128],第一维是页,第二维是列,初始化时全清零。画点先算页再算列,最后用位或的方式写进对应字节的对应bit。这里要注意x和y的范围判断,我在调试时有一次坐标越界直接把数组写穿了,画面出现随机噪点——不是驱动问题,是内存越界。

把整个framebuffer刷到屏幕,按页循环,每次先设置页地址,再设置列地址为0,然后连续写128字节。列地址在写满128字节后会自动停在127,所以下一页必须重新设置页地址和列地址。如果你只写了部分列就换页,剩下没写的列会保留上一次的内容,这样画面上会出现“拉丝”或者“残影”。

4.3 字模和图片:纵向取模优先,横向取模要去转换

显示中文或者图片时,取模方向决定你写显存的顺序。ST7565硬件是一字节对应8行1列,所以取模必须使用纵向取模,也就是“每字节8个bit沿垂直方向排列”。在取模软件里,通常选择“纵向取模,逐列扫描,字节倒序”或者“字节不倒序”,取决于你画点时bit位的方向。

以显示一个8x16字符为例,字符分上下两半,上半部8行占页0,下半部8行占页1。每一半都是8列数据,每列一字节。我把代码写成这样:

void ST7565_ShowChar(uint8_t x, uint8_t y, uint8_t ch, const uint8_t* font8x16) { uint8_t i; const uint8_t* addr = font8x16 + ch * 16; // 上半部分,写到y所在页 for (i = 0; i < 8; i++) { framebuffer[y / 8][x + i] = addr[i]; } // 下半部分,写到下一页,需要检查页边界 if (y / 8 + 1 < 8) { for (i = 0; i < 8; i++) { framebuffer[y / 8 + 1][x + i] = addr[i + 8]; } } }

参数说明:font8x16是12×16的等宽ASCII字模表,每个字符占16字节,前8字节是上半行,后8字节是下半行。x表示字符左上角的列坐标,范围0~119(8像素宽),y是行坐标。下半部分写入页的时候要判断页不能超过7,字符显示在屏幕最底部时下半部分会失去内容——这是很多字符库共有的边界问题,我通常会在UI设计上保证字符不贴底。

图片显示的道理一样,取模选择“纵向8bit,逐列扫描”,生成的数组可以直接按下标顺序写入framebuffer。横向取模的图也不是不能用,但必须在代码里做bit旋转,我一般不这么干,因为既浪费MCU时间又容易出bug。

5. 实战排错:黑屏、镜像、花屏翻车的5个排查点

5.1 黑屏:电荷泵没开和对比度为0最容易混淆

现象:初始化代码全部执行完,屏幕什么都看不见,用手电筒贴着屏幕能隐约看到有笔画,说明液晶已经响应但驱动电压不够。

原因:两种可能。第一种是0x2F没写,升压电路没工作,LCD偏压不足;第二种是写了0x2F但电阻比写成了0x27或者EV写成了0x00,对比度降到了最低。

解决:先写0x2F,等10ms,然后把电阻比固定到0x24,EV值设0x2E。如果能看到画面,再往回调EV。还有一种特殊情况:模块的VOUT引脚如果悬空且没有外接电容,升压会不稳定,表现为闪烁或暗。检查模块原理图,确认VOUT有10uF电容。

5.2 屏幕左右或上下镜像:ADC与SHL的四组组合

现象:文字能显示,但像是照镜子;或者屏幕上下颠倒;又或者旋转180度才对。

原因:ST7565的列扫描由0xA0/0xA1控制,行扫描由0xC0/0xC8控制。模块厂的PCB走线不同,默认方向会不一样。

解决:直接试四组组合,看哪组方向全对,我一般按下面的表五分钟定位:

组合列方向命令行方向命令效果
组合10xA00xC0常规方向
组合20xA00xC8上下翻转
组合30xA10xC0左右翻转
组合40xA10xC8180度翻转

注意0xA0/0xA1和0xC0/0xC8修改后,清屏也要重新执行一次,否则残留数据会让判断产生错觉。

5.3 A0接反或者时序不对:显示乱码但屏幕是亮的

现象:屏幕点亮,背光正常,但显示的是无规律的点阵,或者写一个字符出来六个错位的点。

原因:A0信号在命令和数据之间切换的时间点不对。HAL_SPI_Transmit是阻塞函数,A0切换发生在CS拉低之前,理论上没问题。但如果你用了软件模拟SPI,在翻转SCL过程中切换A0,数据就会被采样到错误的总线上。另一种情况是A0和CS接反了,CS变成高低切换,屏幕每次写入都像是重新片选。

解决:用逻辑分析仪抓SCL、SDA、A0、CS四根线,写命令时A0应保持低电平至少一个SCL周期;写数据时A0保持高。如果用的是模拟SPI,在A0_set之后加2个空指令的延时再拉低CS,给引脚电平建立留时间。

5.4 SPI速率太快导致花屏:余量要留,不是越快越好

现象:低速SPI一切正常,提高到18MHz之后随机花屏,低概率出现,偶尔一整行错位。

原因:模块上的长走线和连接排线引入的容性负载,导致SCL上升沿变缓,数据建立时间不够。ST7565手册标称最大SPI时钟约10MHz,但这是理想负载下的值。我遇到过8MHz也偶发错位的模块,换线材之后又正常。

解决:把BaudRatePrescaler从4改到8,也就是9MHz;再不行改16,4.5MHz。量产品我直接固定9MHz,为了省电加长了SCL低电平时间。另外检查SDA和SCL两路有没有串电阻或电容,有些模块板上自带RC滤波,会明显限制可用速率。如果一定要跑满速,做DMA发送,同时把SCK极性从Mode 0换Mode 3试一次——有一些模块在Mode 3下反而更稳。

5.5 初始化代码从SSD1306移植过来不能用:命令体系完全不同

现象:之前调好的SSD1306驱动,把初始化命令里0x8D那一段替换掉,还是不行,屏幕完全无反应。

原因:SSD1306和ST7565的命令集有几个关键区别。SSD1306用0x8D开电荷泵,ST7565没有这个命令,它是用0x2F直接控制电源组合;SSD1306的显存页地址和列地址在写满后自动回卷,ST7565必须手动设页设列;SSD1306通常用I2C或者SPI带控制字节(0x00命令/0x40数据),ST7565是A0引脚区分,没有控制字节的概念。

解决:不要试图在旧代码上打补丁,直接按ST7565写一套底层。命令长度和格式完全不同,硬套必然出问题。我那次踩坑的血泪经验是:把SSD1306的显存缓冲数组复用过来没问题,但发送显存的函数必须重写,ST7565每一页要先写0xB0+page再写列地址,顺序错了满屏乱点。

6. 进阶:用显示起始行寄存器做平滑滚动和局部刷新

ST7565有个SSD1306也有的隐藏技能:显示起始行寄存器(命令0x40~0x7F)。它控制的是从GRAM哪一行开始显示,改这个值等于把整个画面上下平移,而不需要搬动一字节显存。实现垂直滚动的代码量极少:

void ST7565_Scroll(uint8_t offset) { // offset范围0~63,超过则回卷 ST7565_WriteCmd(0x40 | (offset & 0x3F)); }

调用时用一个定时器中断,比如每10ms将offset加1,就能看到整屏平滑向上滚动。这个方法的开销几乎为零,不像刷新framebuffer那样要搬运1KB数据。注意滚动是整个屏幕一起滚,没办法只滚某个窗口——如果只滚动部分内容,还是得走framebuffer。

局部刷新是我在显示波形和菜单时养成的习惯。ST7565的页地址是单独设置的,所以能给“脏页”打标记,只刷新有变化的页:

uint8_t dirty_pages[8]; void ST7565_RefreshDirty(void) { for (uint8_t page = 0; page < 8; page++) { if (!dirty_pages[page]) continue; ST7565_WriteCmd(0xB0 + page); ST7565_WriteCmd(0x10); ST7565_WriteCmd(0x00); for (uint8_t col = 0; col < 128; col++) { ST7565_WriteData(framebuffer[page][col]); } dirty_pages[page] = 0; } }

画点或者画字符时,同时标记涉及的页为dirty,刷新时只重写这些页。对于实时性要求高的仪表界面,比如CPU占用率曲线,只刷新下半部分的3页,比全屏刷新省掉一多半的SPI传输时间,而且能减少LCD的拖影感。

我现在做ST7565的项目,习惯把初始化里四个常调的值——SHL方向、ADC方向、电阻比、EV值——做成四个宏定义放在文件头部,换屏时只改头文件不碰驱动函数。这和一开始逐条硬调相比省了大量时间,也希望帮到你,让你在一块ST7565_12864液晶STM32驱动上少走两轮“黑屏—查手册—换命令”的弯路。

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

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

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

立即咨询