串口字符型LCD驱动实战:用STM32打造高效简洁的人机交互
你有没有遇到过这样的窘境?项目快完成了,却发现MCU的GPIO几乎被占满——传感器、按键、通信模块……唯独少了块显示屏来展示结果。传统的并行接口1602液晶需要至少6根控制线,而你的STM32F103C8T6只剩下两个可用引脚。
别急,这篇文章就是为你准备的。我们不讲大道理,只解决一个实际问题:如何用一根线,让STM32驱动字符型LCD显示数据。
答案是:串口字符型LCD + UART通信。这不仅是个“救急”方案,更是一种现代嵌入式设计思维的体现——把复杂性交给外设,主控只负责发指令。
为什么现在还要用字符型LCD?
在OLED和TFT彩屏泛滥的今天,有人会问:“都2025年了,谁还用1602?”
但现实是,在工业控制柜里、在温湿度记录仪上、在实验室自制的数据采集器中,字符型LCD依然随处可见。
原因很简单:
- 够稳定:没有操作系统,不怕死机;
- 功耗低:静态显示电流不到1mA;
- 成本低:批量单价可压到5元以内;
- 看得清:宽视角、高对比度,阳光下也能读数;
- 寿命长:无背光衰减问题(可更换LED背光);
更重要的是,当它加上“串口”二字后,突然变得灵活起来——不再需要占用大量IO资源,也不再需要手写时序代码。
串口字符型LCD到底是什么?
你可以把它理解为一块“自带大脑”的液晶屏。传统HD44780控制器需要你严格按照时序发送命令和数据,而串口版则在其基础上增加了一个“翻译官”——通常是一颗小MCU或专用ASIC芯片。
这块模块内部集成了:
- UART接收单元
- 协议解析引擎
- 字符生成ROM(CGROM)
- DDRAM显存管理
- 背光控制逻辑
你只需要通过TX线发送字符串,比如"Hello World",它就能自动显示出来。想换行?发个\n就行。要清屏?发个特殊命令字节即可。
常见型号如DFRobot的Serial LCD Display、Waveshare的UART/I2C LCD1602,支持16x2、20x4等标准尺寸,兼容ASCII字符集,部分还支持自定义字符和滚动显示。
它是怎么工作的?从一帧数据说起
假设你想在屏幕上显示字母A,STM32会通过UART发送一个字节:0x41。
这个过程背后发生了什么?
- STM32将
0x41写入USART的TDR寄存器; - 硬件自动添加起始位(0)、8位数据、无校验、1位停止位,形成10位帧;
- 数据以设定波特率(如9600bps)逐位输出到PA9(TX)引脚;
- 串口LCD模块的RX引脚接收到这一串电平变化;
- 内部芯片判断这是一个普通可打印字符,于是将其写入DDRAM地址计数器指向的位置;
- LCD控制器根据DDRAM内容刷新屏幕,最终你看到了“A”。
整个过程对开发者完全透明,你甚至不需要知道HD44780的初始化流程。
那如果想执行命令呢?比如清屏、设置光标位置?
这类模块通常采用“转义前缀”机制。例如,发送0xFE表示接下来的一个字节是命令:
| 操作 | 发送数据序列 |
|---|---|
| 清屏 | 0xFE, 0x01 |
| 光标归家 | 0xFE, 0x02 |
| 设置光标位置第1行第3列 | 0xFE, 0x82 |
不同厂商的具体命令略有差异,但基本思路一致:用一个引导字节进入“命令模式”。
为什么选STM32?UART不只是“能用”
很多人以为UART就是“最简单的外设”,但在STM32上,它的能力远超想象。
以最常见的STM32F103系列为例,它的USART不仅仅是异步收发器,而是一个功能完整的通信协处理器:
✅ 高精度波特率发生器
STM32的UART波特率由APB总线时钟分频得到。使用外部8MHz晶振+PLL倍频至72MHz后,计算9600bps的误差小于0.5%,远低于±2%的RS-232标准容差,确保长期稳定通信。
✅ 支持DMA传输
你想连续发送一条32字节的消息?不用CPU一个个轮询等待。配置DMA通道后,只需启动一次传输,后续数据自动搬移,期间CPU可以去处理ADC采样或其他任务。
✅ 中断机制完善
TXE(Transmit Data Register Empty):每发完一个字节触发,适合小量数据;TC(Transmission Complete):整批数据发送完成后通知;IDLE中断:检测到总线空闲,可用于帧同步;
这些特性让你既能做实时响应,又能实现非阻塞通信。
✅ 引脚重映射自由
默认USART1的TX是PA9,但如果PA9已被占用怎么办?可以通过AFIO重映射到PB6!这种灵活性在PCB布线紧张时尤为关键。
实战配置:从零开始点亮第一行文字
下面我们以STM32F103C8T6 + HAL库为例,一步步完成串口LCD驱动配置。
第一步:硬件连接
| STM32 | 串口LCD模块 |
|---|---|
| PA9 (TX) | RX |
| GND | GND |
| 3.3V | VCC |
⚠️ 注意事项:
- 若LCD模块标注为5V逻辑输入,请务必加入电平转换电路(如使用TXS0108E或电阻分压);
- 推荐在VCC与GND之间并联一个0.1μF陶瓷电容,抑制电源噪声;
- 不建议直接使用USB转TTL模块供电,可能导致共地干扰。
第二步:CubeMX快速配置
打开STM32CubeMX,进行如下设置:
- RCC→ HSE Crystal/Ceramic Resonator(启用外部晶振)
- Clock Configuration→ SYSCLK = 72MHz
- USART1 Mode→ Asynchronous + TX Only
- GPIO Settings
- PA9: USART1_TX → Output Level: Push-Pull, Speed: Low
- NVIC Settings→ Enable USART1 Global Interrupt(可选)
生成代码前记得开启HAL_UART_MODULE_ENABLED宏定义。
第三步:核心驱动代码实现
#include "main.h" #include <stdio.h> #include <string.h> UART_HandleTypeDef huart1; void UART_LCD_Init(void) { // 使能时钟 __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置PA9为复用推挽输出 GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_9; gpio.Mode = GPIO_MODE_AF_PP; // 复用功能 gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); // UART参数配置 huart1.Instance = USART1; huart1.Init.BaudRate = 9600; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_ONLY; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } } // 直接打印字符串 void LCD_Print(const char* str) { HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY); } // 发送控制命令(0xFE + cmd) void LCD_Command(uint8_t cmd) { uint8_t cmd_buf[2] = {0xFE, cmd}; HAL_UART_Transmit(&huart1, cmd_buf, 2, HAL_MAX_DELAY); }就这么简单。没有复杂的延时函数,也没有手动翻转电平的操作。
让它真正跑起来:一个温度监控示例
假设我们要做一个简易温控器,读取DS18B20温度值并在LCD上显示。
int main(void) { HAL_Init(); SystemClock_Config(); UART_LCD_Init(); // 初始化显示 LCD_Print("Initializing..."); HAL_Delay(1000); LCD_Command(0x01); // 清屏 HAL_Delay(2); while (1) { float temp = DS18B20_ReadTemperature(); // 假设有此函数 char buffer[16]; sprintf(buffer, "Temp: %.1f°C", temp); LCD_Command(0x80); // 移动光标到第一行首地址 LCD_Print(buffer); HAL_Delay(500); // 刷新间隔 } }运行效果:
------------------ |Temp: 25.3°C | | | ------------------你会发现,整个过程中你根本没有关心“什么时候拉高E引脚”、“是否满足40ns建立时间”这类底层细节。这就是协议封装的价值。
常见坑点与调试秘籍
别以为接上线就万事大吉。以下是新手最容易踩的几个坑:
❌ 波特率不匹配
最常见故障现象:乱码、部分字符缺失、偶尔正常。
✅ 解法:
- 双方必须统一波特率(推荐9600或19200);
- 检查STM32系统时钟是否正确配置为72MHz;
- 使用示波器抓取TX波形,测量周期验证实际波特率;
❌ 电源不稳定导致复位
某些模块在上电瞬间会闪屏或乱码。
✅ 解法:
- 在LCD模块端单独加100μF电解电容 + 0.1μF陶瓷电容;
- 主控与LCD共地要牢固,避免形成地环路;
❌ 命令执行延迟未处理
例如清屏命令(0xFE, 0x01)可能需要1.5ms才能完成,若立即发送新内容,会导致丢失。
✅ 解法:
LCD_Command(0x01); HAL_Delay(2); // 给足响应时间❌ 3.3V vs 5V电平兼容问题
虽然很多“5V”模块能识别3.3V高电平,但并非全部可靠。
✅ 解法:
- 查阅模块手册确认VIH最小值;
- 如要求 > 0.7×VCC(即3.5V),则必须加电平转换;
- 或直接选购支持3.3V~5V宽压输入的模块(越来越多厂商提供此类产品);
进阶玩法:不只是“显示器”
你以为这只是个被动接收数据的终端?其实它可以更智能。
🔄 支持双向通信(带反馈)
有些高级模块允许你查询当前状态,比如:
// 查询光标当前位置 uint8_t query_pos[] = {0xFE, 0x06}; HAL_UART_Transmit(&huart1, query_pos, 2, HAL_MAX_DELAY); uint8_t resp; HAL_UART_Receive(&huart1, &resp, 1, 100); // 接收回传数据有了反馈机制,就可以实现菜单导航、用户输入确认等功能。
⏱️ 内建定时刷新
部分模块支持“自动更新”模式,主控只需发送一次模板:
"Time:%t Date:%d"模块内部定时获取RTC时间并刷新显示,极大减轻主控负担。
🔗 多设备级联
通过地址标识(类似I²C),可在同一串口总线上挂多个LCD,分别显示不同信息:
// 选择设备1 LCD_SelectDevice(1); LCD_Print("Node 1 Online"); // 切换到设备2 LCD_SelectDevice(2); LCD_Print("Battery: 80%");总结:这不是妥协,而是进化
回到最初的问题:为什么要用串口字符型LCD?
因为它代表了一种现代嵌入式系统的设计哲学:
把复杂留给外设,把简洁留给主控。
过去我们习惯于“一切尽在掌握”,亲手操控每一个时序脉冲。但现在,随着外设智能化程度提高,我们应该学会“放手”。
一根TX线,解放了6~10个GPIO;
一个UART接口,换来清晰直观的状态反馈;
一段简洁API,支撑起未来扩展的可能性。
这不仅是技术选择,更是思维方式的升级。
如果你正在做一个紧凑型项目,又不想放弃本地显示功能,那么串口字符型LCD + STM32的组合,绝对值得你放进工具箱。
你在项目中用过串口LCD吗?有没有遇到奇葩兼容性问题?欢迎在评论区分享你的经验!