1 整体架构(目录/分层)
MultiMenuDHT11/
├─ Core/
│ ├─ Src/
│ │ ├─ main.c
│ │ ├─ menu/ ← 多级菜单引擎(与显示解耦)
│ │ │ ├─ menu_core.c
│ │ │ ├─ menu_items.c ← 把“DHT11 子模块”挂进菜单树
│ │ ├─ dht11/ ← 独立驱动子模块
│ │ │ ├─ dht11.c
│ │ │ ├─ dht11.h
│ │ ├─ oled/ ← OLED 驱动(I²C 中断+DMA 双缓冲)
│ │ ├─ app/
│ │ │ ├─ app_dht11.c ← 把驱动封装成“菜单可用接口”
├─ Drivers/…(CubeMX 生成,略)
2 硬件连接(极简)
DHT11 STM32
DATA ────▶ PB12(普通 GPIO,开漏+上拉)
VCC ───▶ 5 V / 3.3 V 均可
GND ───▶ GND
OLED I²C 已占 PB6/PB7(CubeMX 默认配置,100 kHz)。
3 DHT11 子模块(dht11.h / dht11.c)
要点
- 纯 HAL GPIO 位带操作,不依赖寄存器,方便移植到任何系列。
- 提供两种粒度 API:dht11_read_raw() → 返回 5 字节数组dht11_read_float() → 返回 float 温度/湿度,带校验和错误码
- 内部 1-Wire 时序全部用 DWT->CYCCNT 做 us 级延时,误差 <0.5 µs(72 MHz)。
- dht11.h
#ifndef __DHT11_H
#define __DHT11_H
#include "stm32f1xx_hal.h"
typedef struct {
float temp; // °C
float humi; // %RH
uint8_t ok; // 1=success
} DHT11_Result_t;
void dht11_init(GPIO_TypeDef *port, uint16_t pin);
DHT11_Result_t dht11_read_float(void);
#endif
dht11.c(核心片段)
c复制
static GPIO_TypeDef *dht11_port;
static uint16_t dht11_pin;
static void DHT11_DelayUs(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock/1000000);
while((DWT->CYCCNT - start) < ticks);
}
static uint8_t DHT11_ReadBit(void){
while(!HAL_GPIO_ReadPin(dht11_port, dht11_pin)); // 等 50 us 低结束
DHT11_DelayUs(30); // 30 us 采样窗口
return HAL_GPIO_ReadPin(dht11_port, dht11_pin);
}
DHT11_Result_t dht11_read_float(void){
uint8_t buf[5]={0};
DHT11_Result_t r={0};
/* 起始+响应+40 bit 读入 */
...
if(buf[4] != (buf[0]+buf[1]+buf[2]+buf[3])){ r.ok=0; return r;}
r.humi = buf[0] + buf[1]*0.1f;
r.temp = buf[2] + buf[3]*0.1f;
r.ok = 1;
return r;
}
4 菜单引擎(menu_core.h / menu_core.c)
设计思路
每个节点都是一个结构体;
typedef struct menu_item{
const char *name;
void (*enter)(void); // 按“OK”键进入
void (*refresh)(void); // 每 200 ms 自动刷新,显示实时数据
struct menu_item *child; // 子菜单链表
struct menu_item *next; // 兄弟节点
} menu_item_t;
- 用“上下键”遍历兄弟,“OK”进入子节点,“BACK”返回父节点。
- refresh() 可以让任意页面“活”起来——正好给 DHT11 实时刷值。
5 把 DHT11 挂进菜单(app_dht11.c)
/* 菜单页:实时温湿度 */
static void page_live_refresh(void){
DHT11_Result_t r = dht11_read_float();
char line[32];
oled_clear();
if(r.ok){
snprintf(line,32,"T:%.1f C H:%.1f%%", r.temp, r.humi);
}else{
strcpy(line,"DHT11 Error");
}
oled_draw_str(0,2,line,Font_8x16);
oled_update();
}
menu_item_t menu_dht11_live = {
.name = "Live Data",
.enter = NULL,
.refresh = page_live_refresh,
.child = NULL,
.next = &menu_dht11_setTh
};
/* 菜单页:设定报警阈值 */
static void page_setTh_enter(void){
/* 简易数值调节界面,略 */
}
在 menu_items.c 里再把 menu_dht11_live 挂到根节点“Sensor”下,
用户操作路径:
主界面 → Sensor → DHT11 → Live Data(实时刷新)
→ Set Threshold(可调上下限)6 主循环与低功耗
main.c
int main(void){
HAL_Init();
SystemClock_Config();
MX_I2C1_Init(); // OLED
MX_GPIO_Init(); // 按键 + DHT11
DWT->CYCCNT_EN = 1; // 使能 DWT 计数器
dht11_init(GPIOB, GPIO_PIN_12);
oled_init();
menu_init(&menu_root);
while(1){
key_t k = key_scan(); // 非阻塞
menu_navigate(k);
if(menu_current()->refresh)
menu_current()->refresh();
HAL_PWR_EnterSLEEPMode(PWR_REGULATOR_ON, PWR_SLEEPENTRY_WFI);
}
}
7 运行效果(OLED 128×64)
Live Data 页面每 200 ms 更新一行:
T:23.7 °C H:51.2 %
若拔掉传感器,立即显示 DHT11 Error,回插后 1 s 内自动恢复。
在“Set Threshold”节点把阈值写进 MCU 内部 Flash 最后一页,掉电保存。
利用 refresh() 钩子做越界报警:温度>30 °C 蜂鸣器 Beep

开源社区
