以下是对您提供的博文内容进行深度润色与结构重构后的技术文章。整体风格更贴近一位资深嵌入式工程师在技术社区中自然分享的经验总结:语言精炼、逻辑递进、去AI痕迹、重实战细节,同时强化了教学性与可复用性。全文已删除所有模板化标题(如“引言”“总结”),改用更具引导力和场景感的层级标题;关键知识点融入叙述流中,避免割裂;代码与表格保留并增强注释;结尾不设总结段,而以一个开放性的工程思考收束,符合真实技术博客的表达习惯。
一根TX线点亮一块LCD:STM32驱动串口字符屏的实战手记
去年调试一款电池供电的环境监测终端时,我遇到个典型困境:MCU是STM32G474,资源吃紧——ADC、I²C温湿度传感器、LoRa模块全占满,只剩两个空闲GPIO。客户却坚持要在面板上加一行状态显示:“温度:23.5℃”。
并行LCD?11根线直接劝退;SPI LCD?得额外加电平转换+驱动库;I²C?市面上便宜的I²C转接板故障率偏高,产线不敢用。
最后选了一块JHD162A-UART——TTL电平、9600bps默认波特率、3.3V兼容、20ms内完成初始化。从焊接到显示“OK”,不到一小时。
这件事让我重新意识到:最朴素的接口,往往藏着最扎实的工程价值。
今天这篇笔记,就聊聊怎么用STM32把串口字符型LCD真正“用熟”,而不是仅仅“点亮”。
它不是普通LCD,而是一台“协议翻译机”
市面上标着“UART接口”的字符屏,比如DFRobot的LCD1602-Serial、Newhaven的NHD-0216K3Z,或者国产常见的JHD162A-UART,它们内部都藏着一个“黑盒子”:前端是标准UART接收器,后端是HD44780兼容控制器。你发过去的不是像素点,而是一条条带语义的指令——就像给一个只会说中文的助手发微信:“清屏”“光标移到第1行第5列”“显示‘ERR’”。
这个设计带来三个本质变化:
- 物理层极简:仅需TX/RX/GND三线,VCC走5V(多数模组不支持3.3V直接供电,这点务必查手册!);
- 软件层解耦:MCU不用管LCD内部时序(比如忙标志BF检测),只要把字节按顺序“扔”过去,剩下的交给模组自己消化;
- 调试层透明:用CH340或逻辑分析仪直连TX线,看到的就是纯ASCII或0xFE开头的指令帧——没有SPI的CPOL/CPHA纠结,也没有I²C的地址仲裁烦恼。
但正因它是个“翻译机”,就特别在意你说话的方式:语速(波特率)、停顿(帧间隔)、用词(指令格式)稍有偏差,它就可能装听不懂。
波特率不是设了就行,而是要“对得上”
STM32的USART可以轻松配出9600、19200、115200等任意波特率,但LCD模组的UART接收器没那么精密。实测几款主流型号发现:
| 模组型号 | 出厂默认波特率 | 实测容差范围 | 备注 |
|---|---|---|---|
| JHD162A-UART | 9600 | ±8% | 在115200下丢包率飙升 |
| NHD-0216K3Z | 9600 | ±5% | 帧间隔<1ms即乱码 |
| DFRobot LCD1602 | 9600 | ±3% | 支持AT指令切换波特率 |
⚠️血泪教训:某次量产固件烧录后整批LCD无显示,查了两天,最后发现是产线编程器把MCU的USART2时钟源从APB1换成了APB2,导致实际波特率漂移了6.7%——刚好卡在JHD162A的容忍边缘。
所以我的做法是:
- 初期开发一律用9600bps(兼容性最好);
- 在MX_USART2_UART_Init()之后,强制加HAL_Delay(20),确保LCD电源稳定后再发第一条指令;
- 若必须用高速率(如动态刷新滚动字幕),先用逻辑分析仪抓TX波形,用游标测出实际位宽,反推真实波特率,再微调USARTDIV。
✅ 小技巧:用示波器测TX空闲态为高电平,一个完整数据帧(10位:1起始+8数据+1停止)的时间就是波特率倒数。比如测出1042μs ≈ 9600bps。
指令不是乱发的,帧与帧之间要有“呼吸感”
串口LCD的指令集非常精简,常见操作基本靠0xFE开头的两字节指令搞定:
| 指令序列 | 功能 | 执行时间 | 注意事项 |
|---|---|---|---|
0xFE 0x01 | 清屏 | ~1.6ms | 必须等待完成再发下一条 |
0xFE 0x40 | 设置DDRAM地址为0x00 | — | 光标归位,常用于清屏后 |
0xFE 0x0C | 显示开 + 光标关 | — | 最常用的状态设置 |
0xFE 0x0E | 显示开 + 光标开 | — | 闪烁频率由模组内部决定 |
0xFE 0x51 | 软复位(部分型号) | ~15ms | 比断电重启更优雅 |
但问题来了:这些指令发得太“急”,LCD会直接忽略。
原因在于模组内部UART后面还连着一个HD44780控制器,它执行清屏、地址设置等操作需要时间。而它的“忙”并不通过RX线反馈给你——它是静默的。
所以我在代码里从不裸用HAL_UART_Transmit()链式发送:
// ✅ 安全可靠的指令序列发送(带硬间隔) void LCD_SendCmd(const uint8_t *cmd, uint8_t len) { for (uint8_t i = 0; i < len; i++) { HAL_UART_Transmit(&huart2, (uint8_t*)&cmd[i], 1, 10); // 关键:每字节后强制延时,覆盖最严苛型号要求(如NHD-0216K3Z) HAL_Delay(2); } } // 示例:清屏 + 光标归位 + 显示开启 LCD_SendCmd((uint8_t[]){0xFE, 0x01}, 2); // 清屏 HAL_Delay(2); // 等待清屏完成 LCD_SendCmd((uint8_t[]){0xFE, 0x40}, 2); // 地址归零 LCD_SendCmd((uint8_t[]){0xFE, 0x0C}, 2); // 显示开,光标关💡 这里的
HAL_Delay(2)不是拍脑袋:它既满足≥1ms的最小帧间隔,又留出1ms安全裕量,还能掩盖不同编译优化等级下的指令执行抖动。
发字符串?别总用阻塞式,DMA才是生产力
显示静态文本(如“System Ready”)用上面的指令序列没问题,但若要做实时数据显示——比如每秒刷新一次温湿度——频繁调用HAL_UART_Transmit()会让CPU一直卡在发送等待里。
这时该轮到DMA出场了。
STM32G4的USART支持独立的TX DMA通道。配置要点就三条:
1. 在CubeMX中勾选USART2的DMA Transmit,并设置优先级为High;
2. 发送缓冲区必须是静态分配(不能是栈上局部变量),否则DMA运行时变量已被释放;
3. 启动DMA后,不要立刻修改缓冲区内容,需等传输完成中断(TC)或查询HAL_UART_GetState()。
我封装了一个即插即用的打印函数:
// ✅ 非阻塞式字符串打印(推荐用于动态刷新) static uint8_t lcd_tx_buf[64]; // 静态缓冲区,最大64字符 void LCD_Print(const char *str) { uint16_t len = strlen(str); if (len >= sizeof(lcd_tx_buf)) len = sizeof(lcd_tx_buf) - 1; memcpy(lcd_tx_buf, str, len); lcd_tx_buf[len] = '\0'; HAL_UART_Transmit_DMA(&huart2, lcd_tx_buf, len); // 此刻CPU可自由处理ADC采样、LoRa收发等任务 }配合一个简单的状态机,就能实现“温度:24.3℃”的平滑刷新,CPU占用率从35%降到不足2%。
乱码、不显示、光标抽风?先看这三处
在十多个项目中,90%的LCD异常都能归结为以下三类,按优先级排查:
🔌 电平不匹配——最容易被忽视的“硬伤”
- STM32G4的USART2_TX默认是3.3V,但JHD162A-UART标称输入高电平阈值为3.5V(VCC=5V时)。实测3.3V信号在长线或噪声环境下会被识别为低电平。
- ✅ 解法:要么换用3.3V兼容型号(如某些Newhaven模组),要么在TX线上加一颗1kΩ上拉电阻到5V(简单有效),或上TXS0108E(批量生产推荐)。
⚡ 电源纹波过大——工业现场的隐形杀手
- LCD控制器对电源噪声极其敏感。曾有个客户现场,LCD在电机启动瞬间频繁乱码,万用表测VCC纹波仅80mV,但示波器一看——高频毛刺峰值超400mV。
- ✅ 解法:在LCD VCC引脚就近并联4.7μF钽电容 + 100nF陶瓷电容,且GND走线必须短而粗,与MCU共地时采用单点连接(避免地弹)。
🕒 帧间隔违规——写代码时最容易“想当然”
- 很多人以为“发完0xFE就立刻发0x01”,殊不知模组刚收到0xFE还在解析指令头,下一字节就到了,状态机直接错乱。
- ✅ 解法:所有
0xFE XX指令对之间,强制插入HAL_Delay(2);对耗时指令(如清屏、软复位),延时加到5ms。
PCB上这两厘米,决定了你能不能按时交样
硬件设计常被软件工程师忽略,但在LCD这种模拟+数字混合器件上,布局就是稳定性底线:
- 走线长度 ≤ 10cm:USART2的TX/RX尽量走表层,避开DC-DC电感、电机驱动MOSFET开关节点;
- 包地处理:TX/RX线下方铺完整GND铜皮,两侧加GND过孔围住(类似差分线包地);
- 去耦电容紧贴LCD引脚:4.7μF X5R电容的焊盘中心到LCD VCC引脚距离 ≤ 2mm;
- 背光控制单独走线:LED+通过限流电阻接GPIO,LED-接地,避免背光电流干扰信号地。
有一次我们PCB初版LCD在高温箱里工作半小时后开始闪屏,最后发现是背光回路与RX线平行走线8cm,热胀冷缩导致耦合加剧。改版后加地屏蔽,问题消失。
写在最后:它为什么还没被淘汰?
有人会问:OLED都用SPI/I²C了,TFT也白菜价了,为什么还要讲这种“古董级”的串口LCD?
因为真实世界里的产品,从来不是参数表上的最优解,而是约束条件下的最稳解。
- 它不需要你懂SPI时序、I²C地址、FSMC总线;
- 它不怕电源波动,不挑MCU型号,连51单片机都能30分钟搞定;
- 它让一个实习生也能看懂逻辑分析仪波形,快速定位问题;
- 它的固件升级只需一条AT指令,产线工人用串口助手就能批量配置。
所以当我看到团队新人第一次用HAL_UART_Transmit()发出“HELLO”并在LCD上亮起时,那种“原来嵌入式也没那么玄乎”的眼神,比任何高性能指标都让我踏实。
如果你也在做一个需要快速验证、成本敏感、长期可靠的小型HMI项目——不妨试试,只用一根TX线,点亮你的第一块LCD。
👇 如果你在DMA传输中遇到发送不完整、或自定义字符写入失败的问题,欢迎在评论区留言,我们可以一起抓波形、看寄存器、翻手册——这才是嵌入式最本真的样子。