用LCD1602把传感器数据“说”出来:一个看得见的温湿度监测系统
你有没有过这样的经历?调试一个温湿度采集项目时,串口打印一堆数字来回滚,眼睛都快看花了,却还是搞不清当前环境到底有多湿、多热。这时候要是有个小屏幕,直接告诉你“温度:25°C,湿度:60%”,是不是瞬间清爽了?
别急着上OLED或TFT彩屏——成本高、功耗大、驱动复杂。在很多对价格敏感的小型嵌入式设备里,真正扛大梁的,其实是一个看起来有点“复古”的家伙:LCD1602字符液晶屏。
今天我们就来做一个实战案例:让LCD1602实时显示DHT11采集到的温湿度数据。不讲虚的,从硬件连接到代码逻辑,再到常见坑点和优化技巧,手把手带你把“传感器→单片机→显示屏”这条链路打通。
为什么选LCD1602?它真的还值得用吗?
先别急着嫌弃它“老土”。虽然现在满大街都是彩色触摸屏,但在实际工程中,LCD1602依然有它的不可替代性。
我们来看一组对比:
| 特性 | LCD1602 | OLED | TFT |
|---|---|---|---|
| 成本 | < ¥5 | ~¥15 | > ¥30 |
| 功耗(不含背光) | ~1mA | ~0.05mA | ~50–100mA |
| 接口方式 | 并行GPIO(4/8位) | I2C/SPI | SPI/RGB并行 |
| 显示内容 | 固定ASCII字符 | 图形+文字 | 全彩图形+GUI |
| 自定义能力 | 支持8个自定义字符 | 完全自由绘图 | 支持复杂界面 |
看出门道了吗?
如果你只需要显示几行固定格式的文字,比如:
Temp: 26 C Humi: 58 %那完全没必要为了一朵“云”买下整片天空。LCD1602结构简单、驱动直接、稳定性强,特别适合教学实验、工业控制面板、农业监测节点这类对可靠性要求高、预算有限的场景。
更重要的是——它不需要操作系统,不用跑RTOS,甚至不用DMA,STM32、Arduino、51单片机都能轻松驾驭。
硬件怎么接?一张图搞定
我们这个系统的主角有三个:
- 主控芯片:STM32F103C8T6(蓝 pill 开发板)
- 传感器:DHT11 温湿度模块
- 显示器:LCD1602 字符屏(带HD44780控制器)
引脚连接一览
| LCD1602引脚 | 功能说明 | 连接到MCU引脚 | 备注 |
|---|---|---|---|
| VSS | GND | GND | 必接 |
| VDD | VCC (5V) | 5V | 必接 |
| V0 | 对比度调节 | 可调电阻中间抽头 | 建议接10kΩ电位器 |
| RS | 寄存器选择 | PA0 | 高=数据,低=指令 |
| RW | 读写控制 | GND | 通常只写不读,直接接地 |
| E | 使能信号 | PA2 | 上升沿触发 |
| D4–D7 | 数据线(低4位) | PB4–PB7 | 4位模式 |
| A / K | 背光电源 | 5V 或 PWM 控制 | 可串联限流电阻 |
⚠️ 注意:DHT11的数据线建议通过一个10kΩ上拉电阻接到VCC,并尽量缩短走线以减少干扰。
这样一共用了7个GPIO(RS、E + D4~D7),比8位模式省了两个IO,性价比拉满。
软件怎么写?分三步走
整个程序的核心流程非常清晰:
初始化LCD → 显示启动提示 → 循环读取DHT11 → 成功则更新显示,失败则报错我们一步步拆解。
第一步:让LCD1602“醒过来”
LCD1602上电后不能马上干活,必须按照严格的时序进行初始化,尤其是切换到4位工作模式这一步,稍有差池就会黑屏无响应。
关键步骤如下:
- 上电延时15ms
- 发送0x03三次(确保进入8位模式)
- 发送0x02(切换至4位模式)
- 配置显示参数:两行、5x7点阵、开显示、关光标
下面是基于STM32 HAL库的简化实现:
void LCD_Init(void) { delay_ms(15); LCD_CTRL_PORT->BRR = RS_PIN; // 指令模式 LCD_SendNibble(0x03); delay_ms(5); LCD_SendNibble(0x03); delay_us(150); LCD_SendNibble(0x03); LCD_SendNibble(0x02); // 正式进入4位模式 LCD_WriteCommand(0x28); // 4位数据,2行显示,5x7字体 LCD_WriteCommand(0x0C); // 开显示,关光标 LCD_WriteCommand(0x06); // 地址自动+1,画面不动 LCD_WriteCommand(0x01); // 清屏 delay_ms(2); }其中LCD_SendNibble()是核心函数,负责发送半字节数据并触发E脉冲:
void LCD_SendNibble(uint8_t nibble) { uint32_t temp = LCD_DATA_PORT->ODR & 0xFF0F; // 清除D4-D7 temp |= (nibble & 0x0F) << 4; LCD_DATA_PORT->ODR = temp; LCD_CTRL_PORT->BSRR = EN_PIN; // E上升沿 delay_us(1); LCD_CTRL_PORT->BRR = EN_PIN; // E下降沿 delay_ms(1); }初始化完成后,就可以愉快地输出文字了。
第二步:从DHT11手里“抢”数据
DHT11是典型的单总线器件,通信靠“握手+打拍子”完成。整个过程需要精确控制毫秒和微秒级延时。
基本流程如下:
- MCU拉低数据线至少18ms,唤醒DHT11;
- DHT11回应一个80μs的低电平+80μs的高电平;
- 开始传输40位数据,每位以50μs低电平开头,高电平长短区分0和1;
- 高电平持续26–28μs → ‘0’
- 高电平持续70μs左右 → ‘1’
下面是读取函数的关键逻辑:
int DHT11_Read(void) { uint8_t i, j, data = 0; // 设置PA3为推挽输出 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 启动信号:拉低18ms以上 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, RESET); delay_ms(18); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, SET); delay_us(30); // 释放总线,等待响应 // 切换为输入模式 GPIO_InitStruct.Mode = GPIO_MODE_INPUT; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 等待DHT11拉低(应答开始) wait_timeout(!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3), 100); // 应答低 wait_timeout(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3), 100); // 应答高 wait_timeout(!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3), 100); // 数据0开始 // 读取40位数据 for (i = 0; i < 5; i++) { data = 0; for (j = 0; j < 8; j++) { while (!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3)); // 等待低电平结束 delay_us(40); // 进入高电平后延迟40us判断 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3)) { data |= (1 << (7 - j)); } while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3)); // 等待该位结束 } dht11_data[i] = data; } // 校验和检查 if (dht11_data[4] == (dht11_data[0]+dht11_data[1]+dht11_data[2]+dht11_data[3])) { return 1; } return 0; }📌 提示:
wait_timeout(condition, max)是一个带超时保护的等待宏,防止死循环。
第三步:把数据显示出来,还不许闪!
很多人第一次做联动显示时都会犯同一个错误:每次刷新前先清屏。结果就是——屏幕疯狂闪烁,用户体验极差。
问题出在哪?LCD_WriteCommand(0x01)不仅清屏,还会归零地址指针,导致整个画面重绘,视觉上就是“闪一下”。
✅ 正确做法:局部刷新 + 补空格防残留
我们要做的不是“重画整个屏幕”,而是“只改变该变的部分”。
比如这一行:
Humi: 60 %下次变成:
Humi: 8 %如果不处理,屏幕上会留下一个“0”,变成“Humi: 80 %”——这就是典型的字符残留。
解决办法很简单:写完数字后手动加个空格覆盖旧字符。
void LCD_PrintHumidity(uint8_t humi) { LCD_SetCursor(1, 0); // 第二行第0列 LCD_PrintStr("Humi:"); LCD_PrintNum(humi); LCD_WriteData(' '); // 清除可能残留的% LCD_WriteData('%'); }同理,温度也可以封装成类似函数。
再配合一个状态机制,避免无效刷新:
uint8_t retry = 0; while (retry < 3 && !DHT11_Read()) { retry++; delay_ms(1000); } if (retry >= 3) { LCD_DisplayError("Sensor Error"); } else { LCD_PrintTemperature(dht11_data[2]); LCD_PrintHumidity(dht11_data[0]); }刷新频率控制在每2秒一次即可,既保证实时性,又延长传感器寿命(DHT11建议采样间隔≥1秒)。
实战中的那些“坑”,我都替你踩过了
你以为写完代码就能稳定运行?Too young。下面这些经验,全是血泪教训总结出来的。
❗ 坑一:DHT11偶尔读不出数据
现象:有时成功,有时失败,重启就好。
原因:单总线对时序极其敏感,稍微一点延迟偏差就会导致误判。
解决方案:
- 使用__NOP()内联汇编代替delay_us()提高精度;
- 在中断密集的系统中,临时关闭中断;
- 加10kΩ上拉电阻增强信号质量;
- 优先使用外部晶振而非内部RC。
❗ 坑二:LCD对比度调不好,要么全黑要么全白
现象:调了半天电位器,还是看不清。
真相:V0脚电压决定了对比度,理想范围是0.5V~1.5V之间。
推荐方案:
- 不用手动电位器,改用DAC输出1V左右;
- 或者用固定分压电路(如4.7k + 1k电阻);
- 工业级应用可加入温补电路,因为液晶特性受温度影响较大。
❗ 坑三:背光太亮费电,晚上刺眼
改进思路:
- 将背光正极通过N-MOS管连接到MCU的PWM引脚;
- 白天全亮,夜间降为30%亮度;
- 甚至可以结合光敏电阻自动调节。
还能怎么升级?让它更聪明一点
别以为这只是个“玩具级”项目。这个基础架构完全可以扩展出更多实用功能:
🔹 加个按键,切换显示模式
- 按一下:显示温湿度
- 再按:显示历史最高/最低值
- 长按:进入校准模式
🔹 接Wi-Fi上传数据
- 用ESP-01S模块将数据发到MQTT服务器;
- 手机APP随时查看记录;
- 结合继电器实现自动除湿/加热控制。
🔹 自定义图标提升体验
LCD1602支持8个自定义字符,我们可以画一个小小的“水滴”💧表示湿度,“太阳”☀️表示温度:
// 自定义“水滴”图案 uint8_t droplet[8] = { 0b00100, 0b01010, 0b01010, 0b01010, 0b01010, 0b10001, 0b10001, 0b01110 }; LCD_CreateChar(0, droplet); // 存入CGRAM位置0 LCD_WriteData(0); // 显示该图标从此你的显示不再是冷冰冰的字母,而是有了“表情”的交互界面。
写在最后:小屏幕,大世界
也许你觉得LCD1602已经“过时”了,但我想说的是:技术没有过时,只有是否适用。
在这个追求“炫酷UI”的时代,我们反而更需要回归本质——用最简单的方案解决最真实的问题。
当你在一个偏远农田的监测站里,看到一块小小的LCD屏稳稳地写着“Temp: 28°C, Humi: 72%”,而整个系统靠太阳能供电一年都不用维护时,你会明白:
有时候,最朴素的显示,才是最有力量的信息传递。
掌握LCD1602与传感器联动的技术,不只是学会了一个外设驱动,更是建立起一种系统思维:如何让物理世界的变化,被人类感知。
这正是嵌入式开发的魅力所在。
如果你也在做类似的项目,欢迎留言交流经验。或者告诉我你想下一个点亮什么传感器?PM2.5?土壤湿度?我们可以一起把它“显示”出来。