从零开始玩转 STM32 + 串口字符型LCD:不只是“打印Hello World”
你有没有遇到过这样的场景?
项目做了一半,突然发现MCU的GPIO快被外设占满了——按键、传感器、通信模块……结果连一个1602 LCD都接不上,因为传统的并行驱动要占用整整6根数据线。
这时候,如果告诉你:只要一根TX线,就能让STM32轻松驱动一块字符屏,你会不会觉得有点神奇?
这并不是什么黑科技,而是很多老工程师早就用熟了的方案——串口字符型LCD + STM32 USART。它不炫酷,但极其实用;它不算最快,但绝对够稳。更重要的是,它能让你在资源紧张时从容不迫,在调试阶段快速出效果。
今天我们就来系统地拆解这套“嵌入式入门神装”,带你从硬件连接到软件实现,一步步构建属于你的高效人机交互界面。
为什么是“串口”字符屏?先搞清楚这个选择背后的逻辑
说到字符显示,大多数人第一反应还是HD44780控制器的1602或2004液晶屏。这类屏幕便宜、常见,资料多,但它有一个致命缺点:并行接口太吃IO。
标准接法下,你需要至少6个GPIO(RS、RW、E + D4~D7),还要严格遵循初始化时序和忙状态检测。一旦你的MCU引脚紧张,比如用的是LQFP48甚至更小封装的STM32F103C8T6,那就真的“寸土寸金”。
而串口字符型LCD的出现,就是为了解决这个问题。它的本质是什么?
它是一个“会自己干活”的智能终端。
你在外面看到的只是一个RX引脚和电源,内部却藏着一颗协议解析芯片(比如SC8813、MAX3232搭配单片机做桥接),能把UART收到的数据自动翻译成LCD指令或字符写入显存。你只需要像发AT命令一样,往串口送几个字节,屏幕上就出来了内容。
那么问题来了:它是怎么做到“即插即用”的?
我们以常见的DFRobot Serial LCD或GY-213模块为例,它们通常基于以下结构设计:
- 外部通信:UART(TTL电平,3.3V/5V兼容)
- 内核控制:内置微控单元或专用ASIC,运行固件
- 显示后端:兼容HD44780时序的驱动逻辑
- 功能扩展:支持清屏、光标定位、背光调节、自定义字符等
这意味着——你再也不用手动写E脉冲、判断BF标志位了。那些曾经让我们熬夜查手册的底层操作,全都被封装进了模块内部。
你可以把它想象成一个“对讲机”:你说一句,它显示一行。简单、直接、可靠。
硬件怎么接?两根线搞定的事情别整复杂
这是整个系统最让人安心的部分:接线极其简单。
| STM32 | 串口LCD模块 |
|---|---|
| PA9 (USART1_TX) → | RX |
| GND —— | GND |
| 3.3V / 5V —— | VCC |
就这么三根线(VCC可选外部供电),完事。
⚠️但有几个细节必须注意:
- 电平匹配:虽然多数串口LCD标称支持3.3V~5V,但如果使用5V供电的模块,请确认其RX是否兼容3.3V TTL输入。如果不支持,建议加一级电平转换(如TXS0108E)或改用3.3V版本。
- 共地是关键:没有共地,就没有通信。哪怕你用了隔离电源,也要确保数字地相连。
- 电源噪声:LCD背光电流较大(尤其白底蓝字款),容易干扰MCU。推荐在VCC-GND之间并联一个10μF电解电容 + 0.1μF陶瓷电容滤波。
如果你追求更高可靠性,还可以启用模块的“应答模式”(部分型号支持ACK反馈),通过STM32的RX引脚监听回传信息,实现简单的通信校验。
软件怎么写?HAL库几行代码就能点亮屏幕
接下来进入实战环节。我们以STM32F1系列 + HAL库为例,展示如何快速完成初始化与基本控制。
第一步:配置USART1(使用CubeMX更方便)
UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 9600; // 必须与LCD模块一致! huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX; // 只发送,不接收 huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; HAL_UART_Init(&huart1); }📌重点提醒:
- 波特率默认通常是9600,但也可能是115200,请查阅模块说明书。
- 如果你不确定,默认先试9600。
- 模式设为UART_MODE_TX即可,因为我们不需要读取LCD状态。
第二步:封装几个常用函数
// 发送字符串(阻塞方式) void LCD_Print(const char* str) { HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY); } // 清屏指令(示例:某模块格式为 0x55 0x01) void LCD_Clear(void) { uint8_t cmd[] = {0x55, 0x01}; HAL_UART_Transmit(&huart1, cmd, 2, HAL_MAX_DELAY); } // 设置光标位置(row: 0~1, col: 0~15) void LCD_SetCursor(uint8_t row, uint8_t col) { uint8_t cmd[4] = {0x55, 0x03, col, row}; // 厂商自定义协议 HAL_UART_Transmit(&huart1, cmd, 4, HAL_MAX_DELAY); }这些函数看起来很简单,但正是这种“简洁”,体现了串口LCD的核心价值:把复杂的交互变成简单的API调用。
比如你想在第二行第一个位置显示温度:
char buf[16]; sprintf(buf, "Temp: %.1f°C", temperature); LCD_SetCursor(1, 0); LCD_Print(buf);运行效果立竿见影,非常适合原型验证和教学演示。
进阶玩法:别再用阻塞发送了,试试中断和DMA
上面的例子用了HAL_UART_Transmit配合HAL_MAX_DELAY,好处是代码简单,坏处也很明显:主循环会被卡住。
假设你要每秒刷新一次温度,同时还要处理按键、采集ADC、看门狗喂狗……这时候如果串口传输时间太长(比如波特率低、数据量大),系统响应就会变慢。
怎么办?上非阻塞机制!
方法一:中断方式发送(适合短消息)
uint8_t tx_complete = 1; void LCD_Print_IT(const char* str) { if (tx_complete) { tx_complete = 0; HAL_UART_Transmit_IT(&huart1, (uint8_t*)str, strlen(str)); } } // 在 main.c 中重写回调函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { tx_complete = 1; } }这样,CPU发出启动信号后就可以立即返回,继续执行其他任务,等传输完成再通过中断通知你。
✅ 适用场景:频繁更新的小段文本,如实时数据显示。
方法二:DMA方式(适合批量发送)
如果你要发送较长的内容(比如菜单列表、状态摘要),可以进一步升级到DMA:
// 初始化时开启DMA通道 __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx); void LCD_Print_DMA(const char* str) { HAL_UART_Transmit_DMA(&huart1, (uint8_t*)str, strlen(str)); }DMA的优势在于完全解放CPU,即使是115200波特率下发送几十字节也不会影响主程序流畅性。
🔧 提示:记得在CubeMX中开启DMA请求,并配置优先级,避免与其他DMA任务冲突。
实际应用场景:不止是“显示几个字”
别小看这块小小的字符屏,它在真实项目中的用途远比你想得多。
场景1:温控仪界面
while (1) { float temp = Read_DS18B20(); uint8_t heater_status = Get_Heater_State(); char line1[17], line2[17]; sprintf(line1, "Set:%.1fC Now:%.1fC", target_temp, temp); sprintf(line2, "Heater:%s ", heater_status ? "ON" : "OFF"); LCD_SetCursor(0, 0); LCD_Print(line1); LCD_SetCursor(1, 0); LCD_Print(line2); HAL_Delay(500); }无需RTOS,无需GUI框架,一个while循环搞定基础交互。
场景2:调试助手
在开发初期,串口打印可能被占用(用于日志输出),这时可以用串口LCD作为独立的状态监视器:
- 第一行:系统运行时间
- 第二行:当前模式 / 错误码
相当于一个“物理层”的debug面板,断电也不丢信息。
场景3:简易菜单系统
结合一个旋转编码器或几个按键,就能做出可交互的参数设置菜单:
> Set Temp Limit Calibrate Sensor View History Exit每次按键切换选项,旋转编码器调整数值,确认后写入Flash保存。成本不到50元,功能却不输专业HMI。
常见坑点与避坑指南
再好的技术也有“雷区”。以下是新手最容易踩的几个坑:
❌ 坑1:波特率不匹配,发了等于没发
现象:屏幕无反应,或者乱码。
原因:模块出厂波特率可能是115200,而你代码里写的是9600。
✅ 解决方法:
- 查手册确认默认波特率
- 或尝试常用值逐一测试
- 高级用户可通过指令修改并固化到EEPROM
❌ 坑2:忘记共地,通信失败
现象:偶尔能收到,大部分时间没反应。
原因:电源地没接通,形成浮空通信。
✅ 解决方法:务必确保GND连在一起,最好使用同一电源系统。
❌ 坑3:背光太亮导致复位
现象:屏幕一闪一灭,MCU频繁重启。
原因:LCD背光瞬间电流过大,拉低系统电压。
✅ 解决方法:
- 单独给LCD供电
- 加大电源电容(100μF以上)
- 使用PWM调光降低平均功耗
❌ 坑4:中文乱码 or 特殊符号异常
现象:本该显示℃的地方变成方块或其他字符。
原因:串口LCD仅支持ASCII及部分扩展字符集(如CGROM预定义图形)。
✅ 解决方法:
- 不要发送UTF-8编码的中文
- 如需特殊符号,使用模块支持的自定义字符功能(最多8个)
- 或换用带字库的图形屏(如TFT)
总结一下:这套方案到底适合谁?
不是所有场合都适合用串口字符型LCD,但它确实填补了一个非常重要的空白地带:
| 适用场景 | 推荐程度 |
|---|---|
| 教学实验、课程设计 | ⭐⭐⭐⭐⭐ |
| 快速原型验证 | ⭐⭐⭐⭐⭐ |
| 工业设备状态显示 | ⭐⭐⭐⭐☆ |
| 成本敏感型产品 | ⭐⭐⭐⭐ |
| 需要丰富图形/动画 | ⭐☆(不适合) |
| 需要中文显示 | ⭐⭐(受限) |
它的最大优势从来不是性能,而是“省心”二字。
你不需要研究LCD初始化流程,不需要处理忙标志,不需要管理显存刷新策略。你只需要知道:“我要在哪一行显示什么内容”,然后发出去就行。
对于初学者来说,这是一种极佳的正向反馈机制——改一行代码,屏幕立刻变化,成就感满满。
对于工程师而言,它是一种高效的资源平衡手段——牺牲一点点带宽,换来大量宝贵的GPIO和开发时间。
如果你正在做一个小型嵌入式项目,又苦于没有合适的显示方案,不妨试试这块“低调的实力派”——串口字符型LCD。配上STM32的USART,你会发现,原来人机交互也可以这么简单。
正如一位资深工程师所说:“最好的技术,往往不是最复杂的那个,而是让你忘了它存在的那个。”
你现在就可以打开Keil,新建一个工程,接上屏幕,打出第一句“Hello Embedded World”。
也许下一个项目的灵感,就从这一行字开始了。
欢迎在评论区分享你的使用经验或遇到的问题,我们一起交流进步。