手把手教你点亮LCD12864:从零实现显示
一块“老屏”的现代生命力
你有没有在某个老旧的温控器、电子秤或工业仪表上,看到过那样一块灰白底色、能显示汉字和简单图形的屏幕?它不炫彩,也不触控,却总能在断电重启后立刻工作,风吹日晒也毫不罢工——这很可能就是LCD12864。
尽管OLED和TFT彩屏早已铺天盖地,但在抗干扰要求高、需要长期稳定运行的工业场景里,这种“古董级”液晶模块依然坚挺。为什么?因为它够稳、够省、够皮实。
更重要的是,对嵌入式开发者来说,驱动一块LCD12864,是真正意义上“点亮第一块屏”的成人礼。它不像TFT动辄需要DMA+FSMC+显存管理,也不会像OLED那样因烧屏而提心吊胆。它的通信协议清晰、控制逻辑透明,是理解并行接口、时序控制、显存映射的最佳入门实践。
今天,我们就来手把手带你把这块经典屏幕从“黑屏”变成“有字有图”,并解决你在实际项目中最可能踩到的坑。
我们要对付的是谁?——认识你的对手
先别急着写代码,搞清楚你面对的是什么芯片,比任何库都重要。
市面上常见的LCD12864大多由两种控制器驱动:
-KS0108 / HD61202:无内置字库,纯图形模式,适合自绘界面;
-ST7920(本文主角):带GB2312中文字库,支持文本+图形双模式,开发友好度爆表。
✅ 本文以ST7920 控制器 + 带中文字库版 LCD12864为对象展开讲解。如果你买回来接上去只能显示方块或乱码,请先确认是不是这个型号。
它的核心能力一句话说清:
“我是一块分辨率为128×64像素的点阵屏,你可以用我显示两行每行八个汉字,也可以画一幅64×64的小图,而且我不需要额外存储字模。”
这就意味着:你想打印“你好世界”,只需要发送0xC4, 0xE3, 0xB0, 0xC4, 0xCA, 0xC0, 0xC9, 0xCF这些GB2312编码即可,不用自己打包字库进Flash。
硬件怎么连?引脚一个都不能错
LCD12864通常有20个引脚(部分简化版16脚),最关键的几个如下:
| 引脚 | 名称 | 功能说明 |
|---|---|---|
| 1 | VSS | 地(GND) |
| 2 | VDD | 电源(5V) |
| 3 | Vo | 对比度调节电压(接电位器中间抽头) |
| 4 | RS | 寄存器选择:0=命令,1=数据 |
| 5 | R/W | 读/写控制:一般只写设为低 |
| 6 | E | 使能信号,上升沿锁存数据 |
| 7~14 | DB0~DB7 | 8位数据总线 |
| 15~16 | BLA/BLK | 背光正负极(可串电阻或PWM调光) |
📌关键提醒:
- Vo 引脚决定了你能看见内容的关键!必须通过一个10kΩ电位器接在 VDD 和 GND 之间,调节中间电压至约 -2V ~ -4V(相对于VDD),才能看到清晰的字符。
- 若使用3.3V MCU(如STM32、ESP32),注意其IO是否兼容5V输入。若不兼容,建议加电平转换芯片或改用串行模式。
通信原理:E脉冲的艺术
ST7920 的并行通信看似简单,实则处处是坑。核心在于E引脚的时序控制。
每次传输一个字节,流程如下:
1. 设置 RS 和 R/W 表明当前操作类型;
2. 将8位数据放到 DB0~DB7 上;
3. 拉高 E → 等待至少0.45μs → 拉低 E;
4. 等待指令执行完成(不同指令延迟不同);
这个“拉高-拉低”的过程叫做Enable Pulse,就像敲门一样告诉LCD:“数据来了,快锁住!”
如果 E 脉冲太短,数据没被采样;如果两次操作间隔不够,前一条指令还没执行完,就会导致初始化失败或乱码。
所以,微秒级延时函数至关重要。
显存结构:你在往哪写?
很多人以为写数据就是直接显示,其实不然。LCD内部有一套显存管理系统,理解它才能精准定位内容。
DDRAM:字符显示区(Text Mode)
- 共有两行,每行可显示16个字符(共32字节);
- 地址映射如下:
Line 0: 0x80 ~ 0x8F (X=0~15) Line 1: 0x90 ~ 0x9F (X=0~15)比如你要在第1行第3列显示字符,就先发命令0x82(即0x80 + 2)设置地址指针,再写入数据。
GDRAM:图形显示区(Graphic Mode)
- 大小为 64×64 bit = 512 字节;
- 按页组织:Page 0~7,每页64字节,对应8行像素(64px高度 ÷ 8 = 8行);
- 每个字节控制8个纵向像素(bit7在上,bit0在下);
要绘制图形,需先进入扩展指令集,设置GDRAM地址,然后逐页写入位图数据。
初始化为何要发三次0x30?
这是新手最困惑的问题之一。
ST7920 上电后处于未知状态,必须通过一组特定序列唤醒其8位并行模式。手册规定:
在系统上电后,连续发送三次
Function Set = 0x30,确保控制器进入8位基本指令集模式。
哪怕你只打算用一次,这三个0x30也少不得。顺序如下:
HAL_Delay(50); // 上电延时 LCD_WriteCommand(0x30); // 第一次同步 HAL_Delay(5); LCD_WriteCommand(0x30); // 第二次 HAL_Delay(5); LCD_WriteCommand(0x30); // 第三次 HAL_Delay(1);之后才能继续配置其他参数,否则后续命令无效。
这就像叫醒一个睡迷糊的人,得连喊三声名字才睁眼。
实战代码:基于STM32的并行驱动实现
下面是一份经过验证、可在STM32F103C8T6上运行的驱动代码,重点优化了速度与稳定性。
#include "stm32f1xx_hal.h" // 控制引脚定义 #define LCD_RS_PIN GPIO_PIN_0 #define LCD_RW_PIN GPIO_PIN_1 #define LCD_EN_PIN GPIO_PIN_2 #define LCD_PORT GPIOA // 数据端口:PA8~PA15 #define LCD_DATA_PORT GPIOA #define LCD_DATA_MASK 0xFF00U // 微秒延时(依赖DWT计数器) void delay_us(uint16_t us) { uint32_t start = DWT->CYCCNT; uint32_t cycles = (SystemCoreClock / 1000000) * us; while ((DWT->CYCCNT - start) >= cycles); // 注意符号 } // 写命令 void LCD_WriteCommand(uint8_t cmd) { HAL_GPIO_WritePin(LCD_PORT, LCD_RS_PIN, GPIO_PIN_RESET); // 命令 HAL_GPIO_WritePin(LCD_PORT, LCD_RW_PIN, GPIO_PIN_RESET); // 使用BSRR一次性写入高8位 LCD_DATA_PORT->BSRR = LCD_DATA_MASK << 16; // 清除数据位 LCD_DATA_PORT->BSRR = (uint32_t)(cmd & 0xFF) << 8; // 写入数据 HAL_GPIO_WritePin(LCD_PORT, LCD_EN_PIN, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(LCD_PORT, LCD_EN_PIN, GPIO_PIN_RESET); // 关键指令需额外延时 if ((cmd & 0x0F) == 0x01 || (cmd & 0x0F) == 0x02) { // 清屏 HAL_Delay(2); } else { delay_us(37); // 典型指令执行时间 } } // 写数据 void LCD_WriteData(uint8_t data) { HAL_GPIO_WritePin(LCD_PORT, LCD_RS_PIN, GPIO_PIN_SET); // 数据 HAL_GPIO_WritePin(LCD_PORT, LCD_RW_PIN, GPIO_PIN_RESET); LCD_DATA_PORT->BSRR = LCD_DATA_MASK << 16; LCD_DATA_PORT->BSRR = (uint32_t)(data & 0xFF) << 8; HAL_GPIO_WritePin(LCD_PORT, LCD_EN_PIN, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(LCD_PORT, LCD_EN_PIN, GPIO_PIN_RESET); delay_us(37); }初始化函数详解
void LCD_Init(void) { // 使能GPIOA时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_DBGMCU_CLK_ENABLE(); DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 启用DWT周期计数器 // 配置控制引脚 GPIO_InitTypeDef gpio = {0}; gpio.Pin = LCD_RS_PIN | LCD_RW_PIN | LCD_EN_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(LCD_PORT, &gpio); // 配置数据引脚 PA8~PA15 gpio.Pin = LCD_DATA_MASK; HAL_GPIO_Init(LCD_PORT, &gpio); HAL_Delay(50); // 上电稳定 // 发送三次0x30进入8位模式 LCD_WriteCommand(0x30); HAL_Delay(5); LCD_WriteCommand(0x30); HAL_Delay(5); LCD_WriteCommand(0x30); HAL_Delay(1); // 功能设置:启用扩展指令集(用于图形模式) LCD_WriteCommand(0x34); // 关闭扩展指令集,回到基本指令集 LCD_WriteCommand(0x30); // 显示开,光标关,闪烁关 LCD_WriteCommand(0x0C); // 清屏 LCD_WriteCommand(0x01); HAL_Delay(2); // 输入模式:地址自动加1,不移屏 LCD_WriteCommand(0x06); }📌技巧提示:
-0x34是开启扩展指令集,用于后续进入GDRAM绘图;
- 初始化完成后记得切回0x30回到基本指令集;
-0x06设置地址自增,这样连续写数据会自动移到下一个位置,无需反复设置地址。
定位与输出:让文字出现在该出现的地方
// 设置光标位置 (x: 0~15, y: 0~1) void LCD_GotoXY(uint8_t x, uint8_t y) { uint8_t addr = 0x80 + x; if (y == 1) addr += 0x40; LCD_WriteCommand(addr); } // 打印字符串(支持ASCII和GB2312双字节汉字) void LCD_PrintString(char *str) { while (*str) { char ch = *str++; LCD_WriteData(ch); // 如果是中文首字节(0xA1~0xF7),跳过次字节(已在同一字符内) // 注意:此处假设编译器已将中文转为GB2312编码 } }使用示例:
LCD_Init(); LCD_GotoXY(0, 0); LCD_PrintString("Hello World!"); LCD_GotoXY(0, 1); LCD_PrintString("你好世界");⚠️ 注意事项:
- 必须保证源文件保存为 GB2312 编码,或者在IDE中启用中文编码支持;
- 若使用UTF-8,需额外进行编码转换(推荐使用工具预生成十六进制码流);
图形显示:画出你的第一个像素
想画图?得先进入GDRAM模式。
// 进入图形显示模式 void LCD_EnableGraphicMode(void) { LCD_WriteCommand(0x36); // 扩展指令集 + 开启GDRAM } // 关闭图形模式 void LCD_DisableGraphicMode(void) { LCD_WriteCommand(0x30); // 回到基本指令集 } // 绘制64x64全白图像(测试用) void LCD_DrawWhiteScreen(void) { LCD_EnableGraphicMode(); for (uint8_t page = 0; page < 8; page++) { LCD_WriteCommand(0x80); // 列地址=0 LCD_WriteCommand(0x80 | page); // 页地址=page for (int i = 0; i < 64; i++) { LCD_WriteData(0xFF); // 每个字节8个点全亮 } } LCD_DisableGraphicMode(); }📌 提示:
- GDRAM按“页-列”寻址,先设页(Y方向8页),再设列(X方向64列);
- 每次写入一个字节,控制垂直方向8个像素(高位在上);
- 实际应用中建议使用PC端取模软件生成图形数组,直接烧录。
常见问题排查清单
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 屏幕全黑/全白 | Vo电压不对 | 调节电位器,观察出现灰条 |
| 无任何显示 | 未完成三次0x30 | 检查初始化顺序与时序 |
| 显示乱码 | 数据线接反或松动 | 查线序,尤其是DB0~DB7是否一一对应 |
| 汉字变方框 | 未使用GB2312编码 | 检查编译环境或手动输入十六进制码 |
| 卡顿严重 | 每条指令都HAL_Delay(10) | 改为差异化延时(清屏2ms,其余37μs) |
| 背光不亮 | BLA未供电或限流过大 | 加220Ω电阻接5V,或用PWM控制亮度 |
工程级设计建议
别止步于“能亮”,真正的嵌入式开发要考虑长期可靠性。
✅ 电源设计
- 使用AMS1117-5.0等LDO提供干净5V;
- 在VDD引脚旁加0.1μF陶瓷电容 + 10μF电解电容退耦;
- 背光单独供电,避免影响逻辑电路。
✅ 电平兼容处理
- 3.3V MCU驱动5V设备?可用TXB0108或MOS管电平转换电路;
- 或干脆切换到串行SPI模式(仅需两根线)。
✅ PCB布局要点
- 数据线尽量等长,减少串扰;
- 控制线远离晶振、SWD、电机驱动等噪声源;
- 模块背面接地层完整,提升抗干扰能力。
✅ 软件架构升级
- 添加本地显存缓存,避免频繁重刷;
- 封装API:
lcd_clear(),lcd_draw_pixel(x,y),lcd_update(); - 支持菜单系统、页面切换、定时刷新任务。
它还有未来吗?
有人问:“都2025年了,还玩这玩意儿?”
答案是:当然有。
在以下场景中,LCD12864仍是首选方案:
- 工业PLC操作面板:不怕电磁干扰,十年不坏;
- 农业传感器终端:野外供电难,功耗低于1mA;
- 教学实验箱:成本不到20元,学生人人可做;
- 设备替换维修:老机器坏了屏,新OLED根本塞不进去。
更进一步,你可以:
- 把它接入 FreeRTOS,做多任务UI;
- 用状态机实现设置菜单;
- 结合 rotary encoder 编码器做旋钮交互;
- 移植轻量GUI框架如uGUI或自制组件库。
最后一句真心话
当你第一次看到“你好世界”四个汉字稳稳地出现在那块小小的灰屏上时,你会明白:
技术的魅力不在炫技,而在掌控。
LCD12864没有华丽的动画,但它教会你什么是时序、什么是显存、什么是底层控制。它是通往复杂系统的起点,也是工程师成长路上的一块里程碑。
现在,去点亮你的第一块屏吧。
如果你在调试过程中遇到问题,欢迎留言交流——那些年我们都被Vo电压折磨过。