1. 项目概述与环境搭建
最近在做一个温湿度监测的小项目,用STM32驱动DHT11传感器采集数据,同时在OLED和LCD1602两种屏幕上显示。这个方案特别适合需要双屏对比显示的场合,比如实验室环境监测或者智能家居控制面板。下面我就把整个实现过程详细分享一下。
先说说硬件选型。我用的主控是STM32F103C8T6,也就是常说的"蓝莓派",性价比超高。传感器选用DHT11,虽然精度比不上DHT22,但对于一般室内监测完全够用。显示部分选了两种屏:OLED用的是0.96寸I2C接口的SSD1306,LCD1602是经典的16x2字符液晶屏。
开发环境方面,我习惯用STM32CubeMX配合Keil MDK。CubeMX可以快速完成引脚配置和初始化代码生成,特别适合我这种懒人。硬件连接时要注意:
- DHT11的数据线接在PB5
- LCD1602的RS、RW、EN分别接PB0、PB1、PB2,数据线PA0-PA7
- OLED的SCL和SDA接PB6和PB7
接线时有个小技巧:给DHT11的数据线加个4.7K上拉电阻,这样信号更稳定。LCD1602的对比度调节端VL记得接电位器,不然可能显示不清。
2. DHT11传感器驱动开发
DHT11是单总线通信的温湿度传感器,虽然数据手册看起来简单,但实际调试时发现时序要求很严格。传感器每次传输40bit数据,包含湿度整数、湿度小数、温度整数、温度小数和校验和。
关键是要实现精确的微秒级延时。我用的是SysTick定时器实现的延时函数:
void delay_us(uint16_t cnt) {
uint8_t i;
while(cnt--){
for(i=0;i<10;i++){}
}
}
传感器通信分三步:主机发送开始信号、传感器响应、数据传输。开始信号要保持至少18ms低电平:
void dht11Start() {
DHT_GPIO_PB5_INIT(GPIO_MODE_OUTPUT_PP);
DHT_HIGH;
DHT_LOW;
HAL_Delay(20); // 保持低电平18ms以上
DHT_HIGH;
DHT_GPIO_PB5_INIT(GPIO_MODE_INPUT);
// 等待传感器响应
while(DHT_VALUE); // 等待传感器拉低
while(!DHT_VALUE); // 等待传感器拉高
while(DHT_VALUE); // 等待传感器准备数据
}
数据读取时要特别注意时序判断。'0'和'1'的区别在于高电平持续时间不同:
for(i=0; i<5; i++){ // 读取5字节
for(j=0; j<8; j++){ // 每字节8位
while(!DHT_VALUE); // 等待上升沿
delay_us(40); // 延时40us后检测电平
if(DHT_VALUE == 1){ // 高电平表示'1'
while(DHT_VALUE); // 等待高电平结束
tmp = (tmp << 1) | 1;
} else { // 低电平表示'0'
tmp = tmp << 1;
}
}
dht11Data[i] = tmp;
}
实际测试中发现,环境电磁干扰可能导致数据错误,所以我在代码中增加了校验和检查:
if(dht11Data[0] + dht11Data[1] + dht11Data[2] + dht11Data[3] != dht11Data[4]) {
return ERROR_CODE; // 校验失败返回错误
}
3. LCD1602显示实现
LCD1602是并行接口的字符液晶,有8位和4位两种工作模式。我选择8位模式,虽然多用几个IO口,但时序更简单。首先定义控制引脚:
#define RS_GPIO_Port GPIOB
#define RW_GPIO_Port GPIOB
#define EN_GPIO_Port GPIOB
#define RS_GPIO_PIN GPIO_PIN_0
#define RW_GPIO_PIN GPIO_PIN_1
#define EN_GPIO_PIN GPIO_PIN_2
初始化过程要严格按照时序进行,特别注意延时时间:
void lcd1602Init() {
HAL_Delay(15); // 上电延时15ms
writeCmd(0x38); // 功能设置:8位接口,2行显示
HAL_Delay(5);
writeCmd(0x38); // 再次发送确保稳定
writeCmd(0x08); // 显示关闭
writeCmd(0x01); // 清屏
HAL_Delay(2); // 清屏需要较长时间
writeCmd(0x06); // 入口模式:地址递增,不移位
writeCmd(0x0C); // 显示开,无光标
}
显示字符串的函数要注意地址设置。LCD1602第一行地址从0x80开始,第二行从0xC0开始:
void lcd1602ShowData(char row, char col, char *str) {
if(row == 1) {
writeCmd(0x80 + col - 1);
} else {
writeCmd(0xC0 + col - 1);
}
while(*str) {
writeData(*str++);
}
}
调试时遇到一个坑:如果直接快速连续发送指令,LCD可能来不及处理。解决方法是在每个指令后加5ms延时,或者检查忙标志。我选择了简单粗暴的延时方式,虽然效率低但代码简单。
4. OLED显示驱动开发
OLED我选用的是SSD1306驱动的0.96寸屏,通过I2C接口通信。相比LCD1602,OLED显示效果更好,但驱动也更复杂。
首先配置I2C接口。在CubeMX中启用I2C1,PB6(SCL)和PB7(SDA):
void MX_I2C1_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
}
OLED初始化命令序列比较长,建议直接使用厂家提供的初始化代码:
void oledInit() {
oledWriteCmd(0xAE); // 关闭显示
oledWriteCmd(0xD5); // 设置时钟分频
oledWriteCmd(0x80); // 建议值
oledWriteCmd(0xA8); // 多路复用比例
oledWriteCmd(0x3F); // 63
// ... 其他初始化命令
oledWriteCmd(0xAF); // 开启显示
}
显示字符需要先定义字模。我使用PCtoLCD2003软件生成了ASCII字符集点阵数据:
const unsigned char oledFont[] = {
0x00,0x00,0x00,0x00,0x00, // 空格
0x00,0x00,0x5F,0x00,0x00, // !
// ... 其他字符点阵数据
};
字符显示函数要处理分页和列地址:
void oledShowChar(char row, char col, char ch) {
unsigned int i;
// 上半部分
oledWriteCmd(0xB0 + row*2 - 2); // 设置页地址
oledWriteCmd(((col*8) & 0x0F) | 0x00); // 列地址低4位
oledWriteCmd((((col*8)>>4) & 0x0F) | 0x10); // 列地址高4位
for(i=(ch-32)*10; i<(ch-32)*10+5; i++) {
oledWriteData(oledFont[i]);
}
// 下半部分
oledWriteCmd(0xB0 + row*2 - 1);
// ... 同上
for(i=(ch-32)*10+5; i<(ch-32)*10+10; i++) {
oledWriteData(oledFont[i]);
}
}
5. 系统整合与优化
把三个模块整合起来,主循环中每隔1秒读取一次传感器数据,然后刷新两个屏幕:
while(1) {
if(HAL_GetTick() - lastRead > 1000) {
lastRead = HAL_GetTick();
if(readDht11Data() == SUCCESS) {
formatDisplayString();
// LCD1602显示
lcd1602ShowData(1, 1, tempStr);
lcd1602ShowData(2, 1, humiStr);
// OLED显示
oledClean();
oledShowString(1, 1, "Temp:");
oledShowString(1, 6, tempStr);
oledShowString(3, 1, "Humi:");
oledShowString(3, 6, humiStr);
}
}
}
为了提高稳定性,我做了几点优化:
- 增加传感器数据校验,连续3次读取失败则重置硬件
- 显示内容加入单位符号和分隔线,提高可读性
- 为OLED添加了简单的动画效果,数据更新时有刷新提示
- 使用RTOS的任务调度管理传感器读取和显示更新
电源管理方面,发现DHT11和LCD1602工作时电流较大,于是增加了软件开关控制:
void powerOnSensors() {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); // 打开电源MOSFET
HAL_Delay(50); // 等待电源稳定
}
void powerOffSensors() {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}
这个项目花了我差不多一个周末的时间,主要卡在DHT11的时序调试上。后来用逻辑分析仪抓波形才发现,我的延时函数在72MHz主频下不够精确。调整后一切正常,现在设备已经连续运行一周多,数据稳定可靠。

189

被折叠的 条评论
为什么被折叠?



