1. 项目概述与核心思路
最近在做一个基于STM32的小项目,需要用到一块12864液晶屏来显示一些参数和状态。手头这块屏是那种很常见的ST7920控制器、带中文字库的型号,接口是并行8位数据总线。网上找了一圈驱动,要么是51单片机的,要么是SPI接口的,直接能用在STM32上、并且是并行模式的完整工程不多。索性就自己从头写了一套,过程中踩了不少坑,也积累了一些经验。今天就把这个STM32的12864液晶并行控制程序的完整实现过程、代码细节和调试心得整理出来,给有同样需求的兄弟做个参考。
这套驱动代码的核心目标很明确:在STM32F103系列MCU上,通过并行8位数据总线的方式,稳定、高效地驱动12864液晶屏(以ST7920控制器为例)。代码结构上,我把它分成了几个清晰的模块:一个专门的头文件(mpu12864.h)来声明所有对外的接口函数;一个源文件(mpu12864.c)来实现底层的时序操作和初始化;还有一个自制的GPIO位操作头文件(gpiobitmap.h),用来实现类似51单片机那种直接操作单个IO口的便捷性。主程序(main.c)的职责就变得非常干净,只需要调用初始化函数和字符串显示函数即可。这种模块化设计,不仅让代码可读性更好,也极大地方便了移植到其他STM32型号或其他液晶屏控制器上。
为什么选择并行模式而不是更省IO的串行(SPI)模式?这得看具体应用场景。并行模式虽然占用IO口多(至少需要11个:8个数据线,RS,RW,EN),但它的优势在于速度快,尤其是在需要频繁刷新整屏内容或者显示动态图形时,优势非常明显。对于STM32F103这类主频在72MHz的芯片来说,操作GPIO的速度绰绰有余,完全能发挥并行接口的潜力。另一个考虑是,很多现有的12864屏模块,其默认跳线帽就是接的并行模式,直接用起来更省事。当然,如果你项目IO口非常紧张,或者对刷新速度要求不高,串行模式肯定是更优解。这里我们主要探讨并行的实现。
2. 硬件连接与底层GPIO操作封装
硬件连接是驱动的基础,绝对不能出错。我使用的MCU是STM32F103C8T6,液晶屏是通用的12864(ST7920)。连接关系如下:
- 数据总线 D0-D7: 连接到 GPIOD 的 PIN0 至 PIN7。选择GPIOD是因为它刚好有连续的8个引脚,方便一次性操作一个完整的字节。
- 控制线:
- RS (寄存器选择): 连接到 GPIOA 的 PIN0。RS=0表示写入的是命令(比如清屏、设置地址),RS=1表示写入的是要显示的数据(字符或图形数据)。
- RW (读写选择): 连接到 GPIOA 的 PIN1。RW=0表示写操作(MCU向液晶写),RW=1表示读操作(MCU从液晶读状态或数据)。在我们的驱动里,为了简化,只实现了写操作,所以RW可以始终接低电平,或者在代码里固定置0。我选择在代码中控制,保留了灵活性。
- EN (使能信号): 连接到 GPIOA 的 PIN2。这是一个下降沿触发的锁存信号,是并行总线操作时序的关键。
- 电源: VCC接3.3V或5V(根据屏的规格书),GND接地。背光LED通过一个限流电阻接电源,也可以用一个IO口控制以便调节亮度。
在软件层面,为了能像在51单片机里那样用sbit RS = P2^0;的方式来操作单个IO口,我并没有直接使用STM32标准库的GPIO_WriteBit函数,而是实现了一个“位带”操作的封装。这就是gpiobitmap.h文件的作用。STM32的Cortex-M3内核有一个位带(Bit-band)特性,可以将某个地址位(比如GPIO输出数据寄存器的某一位)映射到别名区的另一个字地址上,对这个别名地址进行读写,就等同于对原地址的某一位进行读写,而且这个操作是“原子”的。
gpiobitmap.h里的核心宏定义如下:
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))它先计算出某个IO端口输出数据寄存器(ODR)的位带别名地址,然后定义了像PAout(n)这样的宏,PAout(0)就对应着GPIOA第0个输出引脚。这样,我们在代码里写PAout(0)=1;,就相当于将PA0引脚置高,编译后就是一条单周期的存储指令,效率极高。后续在驱动代码里使用的PAo_0、PDo_0等,就是基于PAout(0)、PDout(0)的再次宏定义,让代码看起来更直观。
注意:使用位带操作的前提是,你清楚地知道所用MCU的GPIO外设基地址,并且位带区域映射是正确的。对于STM32F1系列,这个定义是通用的。如果你的工程已经使用了STM32 HAL库或者LL库,它们通常也提供了类似的位操作宏或函数,可以不必自己定义这套,直接使用库函数即可。我这里自己定义,是为了减少对特定库的依赖,让代码更“裸机”、更直观。
3. 驱动层代码实现详解
驱动层是连接底层硬件和上层应用的关键,主要包含在mpu12864.c和mpu12864.h中。我们逐部分拆解。
3.1 头文件(mpu12864.h)接口设计
头文件的作用是声明模块对外提供的功能,并防止重复包含。
#ifndef __MPU12864_H__ #define __MPU12864_H__ #include "stm32f10x.h" extern void write12864_com(u8 com); //写命令 extern void write12864_dat(u8 dat); //写数据 extern void lcd12864_init(void); //初始化 extern void write12864_string(u8 hang, u8 numadd, u8 *p); //在指定行、列写字符串 #endif接口非常简洁,只有四个函数:
write12864_com: 向液晶写入命令码,用于控制液晶的内部状态,如显示开关、光标设置、地址指针移动等。write12864_dat: 向液晶写入显示数据,就是最终要显示在屏幕上的字符代码(ASCII码或GB2312汉字内码)。lcd12864_init: 初始化函数,负责配置液晶屏的工作模式(8位并行、基本指令集等),并执行清屏、开显示等操作。write12864_string: 这是一个应用层辅助函数,封装了定位和连续写字符的操作,方便显示字符串。
3.2 核心时序函数:写命令与写数据
这是驱动最核心的部分,直接关系到液晶屏能否被正确驱动。ST7920的并行写时序有严格的时间要求,我们需要用GPIO操作来模拟这个时序。
先看write12864_com(u8 com)函数:
void write12864_com(u8 com) //写命令 { // 1. 准备数据:先清空数据端口的高8位(因为我们用PD0-PD7,ODR是16位的) GPIOD->ODR &= 0xff00; // 2. 设置控制线状态:EN=0, RS=0(命令), RW=0(写) en = 0; rs = 0; rw = 0; // 注意:原示例代码可能省略了rw操作,但按标准时序,写操作时应拉低RW // 3. 放置命令数据到数据端口 GPIOD->ODR |= com; // com是u8类型,与低8位或运算 // 4. 建立时间(tAS)和使能脉冲宽度(tPW):短暂延时后产生一个下降沿 delayms(1); // 这个延时很关键,不能太短。1ms对于STM32来说足够稳定。 en = 1; // 使能线拉高 delayms(1); // 保持高电平一段时间(tPW) en = 0; // 下降沿,液晶在此刻锁存数据线上的命令 }时序要点解析:
GPIOD->ODR &= 0xff00;: 这行代码确保了在放入新数据前,PORTD的低8位被清零。因为ODR寄存器是16位的,我们只操作低8位,所以要先清除旧数据,避免干扰。这是一种稳妥的做法。rs=0;: 明确告诉液晶,接下来数据线上的是命令。rw=0;: 明确为写模式。虽然很多简单应用将RW引脚硬件接地,但在代码中控制它是个好习惯。- 两个
delayms(1)和en信号的变化,共同构成了使能脉冲。第一个延时是数据建立时间(RS, RW, Data 到 EN 上升沿的间隔),第二个延时是使能高电平脉冲宽度。ST7920的数据手册要求这两个时间最小都是几十纳秒,我们的1ms延时是远远超出的,保证了在最恶劣的电源和温度环境下也能稳定工作。在实际产品中,为了追求速度,可以用微秒级延时(如delay_us(5)),但调试阶段用毫秒延时更保险。 en=1再en=0产生一个下降沿,液晶在下降沿时刻采样并锁存数据线上的数据。
write12864_dat(u8 dat)函数与写命令函数几乎一模一样,唯一的区别就是rs=1,告诉液晶这是要显示的数据。
void write12864_dat(u8 dat)//写数据 { GPIOD->ODR &= 0xff00; en = 0; rs = 1; // 区别在这里,RS置1表示写数据 rw = 0; GPIOD->ODR |= dat; delayms(1); en = 1; delayms(1); en = 0; }实操心得:这里的
delayms函数是一个简单的软件延时循环。在正式项目中,如果系统使用了SysTick定时器或者其他定时器,建议替换为更精确的延时函数(如HAL_Delay或自己基于SysTick封装的delay_us)。软件循环延时的缺点是时间不精确,且会占用CPU。但在初学或快速验证阶段,它最简单直接。另外,我曾遇到过因为延时太短导致液晶初始化不成功的案例,所以**“宁可延时长,不可延时短”**,是调试液晶驱动的一条安全准则。
3.3 液晶初始化流程
液晶屏上电后需要一个正确的初始化序列才能进入工作状态。lcd12864_init函数就是干这个的。
void lcd12864_init(void) { // 1. 上电后等待液晶内部复位稳定(非常重要!) delayms(50); // 等待时间建议大于40ms // 2. 多次发送功能设定命令,确保进入8位并行模式 write12864_com(0x30); // 功能设定:8位数据,基本指令集 delayms(5); // 指令执行需要时间 write12864_com(0x30); delayms(1); write12864_com(0x30); delayms(1); // 有的屏可能需要0x20(4位数据)先切换,再0x30。这里直接发三次0x30是常见做法。 // 3. 设定显示模式 write12864_com(0x0c); // 显示开,光标关,闪烁关 // 0x0c: 0000 1100 // 也可以尝试 0x0f: 显示开,光标开,闪烁开(用于调试光标位置) // 4. 清屏 write12864_com(0x01); // 清显示,并将地址指针归零 delayms(2); // 清屏指令执行时间较长,需延时 // 5. 设定进入模式 write12864_com(0x06); // 地址指针自动右移,显示不移动 // 这样写入一个字符后,光标会自动移到下一个位置,方便连续显示。 }初始化关键点:
- 上电延时: 液晶模块的控制器需要时间从复位状态稳定下来。这个延时绝对不能省,通常需要30ms以上。我习惯给50ms,求稳。
- 重复发送功能设定: 连续发送几次
0x30是为了确保液晶可靠地进入8位并行接口、基本指令集模式。这是一种“喂”指令的土办法,但非常有效。 - 指令执行延时: 在发送某些指令(如清屏
0x01、功能设定0x30)后,必须给予足够的延时,等待液晶内部操作完成。ST7920的清屏指令需要1.6ms左右,所以延时2ms是安全的。其他指令一般几微秒到几十微秒,但我们用毫秒级延时都覆盖了。 - 显示模式选择:
0x0c是最常用的显示模式,只开显示,关掉光标和闪烁。如果你在调试时想知道当前写入的位置,可以临时改成0x0f,屏幕上会出现一个闪烁的光标。
3.4 应用层辅助函数:字符串显示
直接使用write12864_dat一个字符一个字符地写很麻烦,而且还要自己控制换行。write12864_string函数封装了定位和连续写入的功能。
void write12864_string(u8 hang, u8 numadd, u8 *p) { // 1. 根据行号计算DDRAM地址 u8 addr; switch(hang) { case 1: addr = 0x80; break; // 第一行起始地址 case 2: addr = 0x90; break; // 第二行起始地址 case 3: addr = 0x88; break; // 第三行起始地址(对于2行显示的屏,这是第二页) case 4: addr = 0x98; break; // 第四行起始地址 default: addr = 0x80; break; // 默认第一行 } addr += numadd; // 加上列偏移 // 2. 发送地址设置命令 write12864_com(addr); // 3. 循环写入字符串,直到遇到字符串结束符'\0' while(*p != '\0') { write12864_dat(*p++); } }地址计算解析: ST7920的显示数据存储器(DDRAM)地址映射对于常见的122x32点阵(可显示4行x8汉字)的屏是这样的:
- 第一行:0x80 ~ 0x87 (对应8个汉字位置)
- 第二行:0x90 ~ 0x97
- 第三行:0x88 ~ 0x8F
- 第四行:0x98 ~ 0x9F
所以,hang参数传入1~4,分别对应这四行。numadd参数是列偏移,范围是0~7(因为一行最多8个汉字)。addr = 0x80 + numadd就是第一行第numadd+1列的地址。其他行以此类推。
注意:这里假设你的屏是带中文字库的,并且是标准的4行显示布局。如果你的屏是128x64点阵的图形点阵屏(不带字库),或者控制器是KS0108等其他型号,DDRAM的地址映射会完全不同,这个函数需要重写。在调用前,务必确认你液晶屏控制器的数据手册。
4. 主程序与系统初始化
主程序main.c的任务是初始化整个系统,然后调用我们的液晶驱动来显示内容。代码结构如下:
#include "stm32f10x.h" #include "mpu12864.h" #include "gpiobitmap.h" #include "delay.h" // 假设有一个延时函数库 // 全局变量定义 unsigned char table[] = "2023-10-27"; unsigned char table0[] = "Hello World!"; int main(void) { // 1. 系统时钟初始化(至关重要!) RCC_Configuration(); // 2. 中断向量表配置 NVIC_Configuration(); // 3. GPIO初始化(在mpu12864_init内部已包含,此处可简化或保留) // GPIO_Configuration(); // 如果驱动初始化里做了,这里可以不要 // 4. 液晶初始化 lcd12864_init(); // 5. 主循环显示 while (1) { write12864_string(1, 2, table); // 在第一行第3列开始显示日期 write12864_string(2, 0, table0); // 在第二行第1列开始显示字符串 // 可以在这里添加其他逻辑,比如按键扫描更新显示内容 // while(1); // 原示例代码在这里死循环,实际项目应去掉,让程序能执行其他任务 } }系统初始化关键(RCC_Configuration): 这部分代码配置了STM32的时钟树,将系统时钟设置为72MHz(外部8MHz晶振经过PLL 9倍频)。最重要的是,它开启了GPIOA和GPIOD的时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOD, ENABLE);任何STM32的GPIO在使用前,必须开启其对应的外设时钟!这是新手最容易忽略的问题,会导致IO口怎么操作都没反应。
GPIO初始化(GPIO_Configuration): 在原示例代码中,这个函数是空的。这是因为我们在gpiobitmap.h中使用了直接操作寄存器的方式,并没有调用标准库的GPIO_Init函数来配置引脚模式。这是一个需要特别注意的地方。
重要补充:为了使用位带操作,我们必须先正确配置GPIO引脚的工作模式。对于控制线RS、RW、EN,它们都是MCU输出到液晶,应配置为推挽输出模式。对于数据线D0-D7,在只写不读的情况下,也配置为推挽输出即可。如果后续需要读液晶的忙碌状态,则需要将数据线配置为上拉输入/浮空输入,并在读操作前切换模式,这比较复杂。我们目前的驱动是“盲写”,即不检查忙碌标志,依靠足够的延时来保证操作间隔,所以数据线可以固定为输出模式。
因此,一个完整的GPIO_Configuration函数应该如下(使用标准库):
void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; // 配置PA0, PA1, PA2 为推挽输出(控制线) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置PD0-PD7 为推挽输出(数据线) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStructure); // 初始化所有控制线为低电平 GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2); // 数据线可以不用初始化,因为每次写操作都会设置其值 }务必在主函数中调用这个GPIO_Configuration(),或者在lcd12864_init函数的最开始调用它。否则,IO口没有初始化模式,位带操作也不会生效。
5. 常见问题排查与调试技巧
驱动液晶屏,尤其是第一次,大概率会遇到屏幕不亮、显示乱码、只有一行显示等问题。下面是我总结的排查清单和技巧。
5.1 屏幕完全无显示(背光亮但无内容)
- 检查电源和背光: 首先确认VCC和GND连接正确,电压是否匹配(3.3V屏接5V可能烧,5V屏接3.3V可能驱动不足)。用万用表测一下电压。背光是否点亮?如果背光不亮,检查背光引脚接线和限流电阻。
- 检查对比度(VO/V0): 12864屏通常有一个对比度调节引脚,接一个可调电阻到GND。对比度电压不合适,屏幕可能有背光但看不到内容。尝试调节该电阻。
- 检查初始化序列和延时: 这是软件问题的高发区。确保
lcd12864_init函数被正确调用,并且内部的延时(尤其是上电后的长延时和清屏后的延时)足够长。可以尝试将所有的delayms(1)改成delayms(10)或更长来测试。 - 检查GPIO初始化和时钟: 确认
RCC_Configuration和GPIO_Configuration函数被调用,并且正确开启了GPIOA和GPIOD的时钟。可以用一个简单的测试程序,让某个控制线(如EN)周期性翻转,用示波器或LED观察是否有输出,来验证GPIO配置是否正确。 - 检查硬件连接: 用万用表通断档,一根线一根线地检查MCU和液晶屏之间的连接,确保没有虚焊、错位。特别注意数据线D0-D7是否顺序对应。
5.2 显示乱码或错位
- 检查数据线顺序: 并行数据线D0-D7必须顺序连接。如果D7和D0接反了,发送的字符代码就会完全错乱。这是最可能的原因。
- 检查字符编码: 确保你发送的数据是液晶屏字库支持的编码。对于带中文字库的ST7920,它内部是GB2312编码。如果你发送的是ASCII字符(如
Hello),它是可以正常显示的,因为ASCII是GB2312的子集。但如果你通过某些方式发送了非ASCII的字节,就可能显示为乱码汉字。 - 检查DDRAM地址: 使用
write12864_string函数时,确认传入的行号(hang)和列号(numadd)在有效范围内(1-4行,0-7列)。如果地址超出范围,写入的数据可能会到不可见的显示区域,或者覆盖其他内容导致乱码。 - 尝试清屏后显示: 在
main函数的while(1)循环前,先只写一条清屏指令write12864_com(0x01)和一个简单的字符串,看是否正常。排除多次写入导致地址指针错乱的问题。
5.3 只有一行显示或显示重叠
- 确认液晶屏型号: 你用的屏真的是4行8汉字的吗?有些12864屏是2行16字符的(西文字符),它们的地址映射完全不同。请查阅你的液晶屏型号对应的数据手册。
- 检查行地址计算: 对照ST7920的数据手册,核对
write12864_string函数中的行地址映射(0x80, 0x90, 0x88, 0x98)是否正确。有些屏的第三行地址可能是0x88,第四行是0x98,但也存在变种。 - 地址指针自动移动模式: 初始化时我们设置了
write12864_com(0x06),即地址指针自动右移。如果你在写完一行后没有正确设置下一行的起始地址,而继续写数据,它就会接着上一行的末尾写,造成“显示重叠”的错觉。确保每次换行显示都重新调用write12864_string指定新的行号。
5.4 使用逻辑分析仪或示波器抓取时序
这是最强大的调试手段。将逻辑分析仪的探头连接到RS、RW、EN和一条数据线(如D0)上,运行你的程序。
- 看EN信号: 是否有一个清晰的、宽度约1ms的脉冲?
- 看RS信号: 在发送命令(
write12864_com)时是否为低电平,发送数据(write12864_dat)时是否为高电平? - 看数据线: 在EN脉冲高电平期间,数据线上的电平是否稳定?是否与你想要发送的命令/数据字节相符?
通过对比抓取到的波形和ST7920数据手册中的时序图,可以精确地定位是建立时间、保持时间还是脉冲宽度不满足要求。
5.5 软件调试技巧
- 简化测试: 注释掉所有复杂的显示,只在主循环里反复发送清屏命令
write12864_com(0x01)和开显示命令write12864_com(0x0c)。如果屏幕能全亮然后全灭(清屏),说明最基本的通信已经建立。 - 分步初始化: 在
lcd12864_init函数里每一步后加一个长延时(如delayms(1000)),然后观察屏幕反应。比如,发送完0x0c后屏幕应该打开,发送完0x01后屏幕应该清空。这可以帮助你定位初始化序列中哪一步失败了。 - 检查忙标志(进阶): 我们当前的驱动是“延时等待”,不够高效。更专业的做法是,在每次写命令/数据前,先读取液晶的“忙标志”(通过读操作,并检查数据最高位)。当液晶忙时,需要等待。这需要将数据线配置为输入模式来读,实现起来稍复杂,但可以消除不必要的延时,提高程序效率。对于初学者,先用延时法是稳妥的。
6. 驱动优化与功能扩展
基础驱动跑通后,可以考虑以下优化和扩展,让代码更健壮、功能更强大。
6.1 增加忙检测功能
如前所述,忙检测可以避免盲目延时。思路是:
- 将数据线(GPIOD0-7)在读写前动态配置为上拉输入模式(用于读状态)。
- 实现一个
ReadStatus函数,流程为:RS=0, RW=1, EN产生一个脉冲,在EN高电平期间读取数据线,数据的最高位(DB7)就是忙标志(1=忙,0=闲)。 - 在
write12864_com和write12864_dat函数开头,循环调用ReadStatus直到忙标志为0。 - 操作完成后,再将数据线配置回推挽输出模式(用于写数据)。
这会使驱动代码复杂度增加,但时序更标准,且能适应高速MCU。
6.2 实现图形显示功能
ST7920控制器除了字符模式,还有图形显示模式(扩展指令集)。要显示图形或自定义字符,需要:
- 发送
0x34命令切换到扩展指令集。 - 发送
0x36命令打开图形显示。 - 通过设定图形显示RAM(GDRAM)地址,然后连续写入数据来绘制图形。每个点对应一个bit,操作较为繁琐,但可以实现任意图案。 这需要你深入了解GDRAM的地址结构(分为上下半屏)和位映射关系。
6.3 优化GPIO操作速度
我们目前使用GPIOD->ODR直接操作整个端口,并使用位带操作控制单个引脚。对于STM32F1,这已经很快。如果想极致优化,可以考虑:
- 使用
GPIOx->BSRR和GPIOx->BRR寄存器进行原子性的位设置和清除操作,速度更快。 - 如果数据线在同一个GPIO端口的连续引脚上,可以使用
GPIOx->ODR = (GPIOx->ODR & 0xFF00) | data;这样的写法,一次操作完成数据更新。 - 将延时函数从毫秒级优化为微秒级,甚至使用硬件定时器来产生精确的延时。
6.4 设计更友好的显示API
当前的write12864_string函数只能显示字符串。可以封装更多实用函数:
LCD_ClearLine(u8 line): 清除指定行。LCD_Printf(u8 line, u8 col, const char *fmt, ...): 类似printf的格式化输出,方便显示变量。LCD_DrawPixel(u8 x, u8 y): 画点函数(需在图形模式下)。LCD_DrawLine,LCD_DrawRectangle等基础图形函数。
这些扩展会让你的液晶驱动库变得非常易用,可以直接应用到未来的各种项目中。
7. 项目总结与资源链接
通过这个项目,我们完成了一个从零开始的STM32驱动12864液晶屏(并行模式)的完整过程。核心要点包括:理解ST7920的并行接口时序、利用STM32的位带特性实现高效GPIO操作、编写符合时序要求的底层读写函数、设计正确的初始化序列、以及处理常见的显示问题。
关键经验总结:
- 硬件是基础: 接线务必准确,电源和对比度要调好。
- 时序是关键: 严格按照数据手册的时序要求编写代码,初期用保守的、较长的延时。
- 初始化要耐心: 上电延时和重复发送初始化命令是避免玄学问题的重要手段。
- 调试要科学: 善用万用表、逻辑分析仪,并采用分步测试法定位问题。
- 代码要模块化: 将底层驱动、硬件抽象、应用API分离,方便维护和移植。
这套驱动代码我已经在实际项目中稳定使用。它不依赖于复杂的HAL库,只依赖最基础的寄存器定义头文件,因此移植到其他STM32系列(如F0, F4)甚至其他ARM Cortex-M芯片上,也只需要修改GPIO和时钟配置部分,核心的时序逻辑和液晶操作命令是完全通用的。
最后,强烈建议你手头备一份ST7920控制器中文数据手册,里面包含了所有命令集的详细说明、时序参数和内存映射图,是开发过程中最重要的参考资料。网上很容易搜到PDF版本。有了它,你就能解锁这块液晶屏的全部功能,而不仅仅是显示几行文字。