STM32+ESP8266+DHT11物联网温湿度上云实战
1. 系统架构与硬件连接原理
在嵌入式物联网系统中,传感器数据上云并非简单的“采集-发送”线性流程,而是一个涉及多层级时序协同、总线资源分配与协议栈状态管理的工程系统。本项目以战舰V3开发板(STM32F103ZET6)为核心控制器,通过USART3与ESP8266 Wi-Fi模块通信,完成DHT11温湿度数据向OneNet云平台的HTTP上报。整个链路需严格遵循物理层电气特性、协议栈状态机约束及MCU外设时钟树配置逻辑。
1.1 开发板与Wi-Fi模块的物理层对接
战舰V3开发板右侧预留的六针Wi-Fi接口,并非标准UART直连设计,而是针对正点原子专用模块的引脚镜像适配。其标称的RXD/TXD标识存在功能反转:开发板侧RXD实为TX(发送端),TXD实为RX(接收端)。这种设计源于模块厂商对信号流向的定义惯例——当模块作为从设备接入主控时,其TXD引脚需连接主控的RXD引脚。因此实际连接必须遵循 交叉接法 :
| 开发板引脚 | 功能定义 | 连接目标 | 电气要求 |
|---|---|---|---|
| PA9 (USART1_TX) | 调试串口输出 | 串口调试助手 | 3.3V TTL电平 |
| PB10 (USART3_TX) | Wi-Fi模块发送通道 | ESP8266 RXD | 3.3V TTL,需限流电阻 |
| PB11 (USART3_RX) | Wi-Fi模块接收通道 | ESP8266 TXD | 3.3V TTL,需上拉至3.3V |
| 3.3V电源 | 模块供电 | ESP8266 VCC | 电流能力≥250mA |
| GND | 公共地 | ESP8266 GND | 必须单点接地 |
特别注意:ESP8266模块在AT指令模式下,TXD引脚输出为开漏结构,需在PB11外部添加4.7kΩ上拉电阻至3.3V,否则接收电平不稳定。开发板自带的3.3V电源轨可直接使用,但若模块工作在透传模式且并发数据量大,建议改用独立LDO供电以避免MCU复位。
1.2 DHT11传感器的电气特性与接口约束
DHT11采用单总线协议,其数据引脚兼具输入/输出功能,对MCU GPIO有特殊要求:
-
驱动能力
:MCU需提供至少5mA灌电流能力以满足DHT11低电平维持需求
-
上拉电阻
:必须在DATA引脚与3.3V间接入5.1kΩ上拉电阻(非开发板默认值)
-
电压兼容性
:DHT11支持3.3V~5.5V供电,但数据电平与MCU IO电压必须匹配。当开发板使用3.3V逻辑时,VCC必须接3.3V而非5V,否则高电平可能超出STM32F103的VIH(max)=0.7×VDD=2.31V阈值,导致通信失败。
战舰V3开发板将GPIOG11(PG11)引出至LED区域,该引脚属于APB2总线,时钟频率72MHz,满足DHT11时序要求(数据位采样窗口±1μs)。连接方式如下:
- PG11 → DHT11 DATA(经5.1kΩ上拉至3.3V)
- 3.3V → DHT11 VCC
- GND → DHT11 GND
若需更换引脚,必须确保新引脚所属GPIO端口已使能时钟(RCC->APB2ENR),且配置为推挽输出/浮空输入复合模式——初始化阶段输出低电平启动通信,后续切换为浮空输入读取数据。
1.3 USB通信与固件烧录链路
开发板左下角的USB-B接口连接CH340G USB转串口芯片,其TXD/RXD分别映射至MCU的PA2/PA3(USART2)。此链路承担双重职责:
-
调试信息输出
:通过printf重定向至USART2,在串口调试助手中显示温度/湿度值及AT指令交互日志
-
固件烧录通道
:利用STM32内置Bootloader,通过DTR/RTS信号触发复位并进入系统存储器启动模式
烧录关键参数设置:
-
波特率
:115200bps(Bootloader固定速率)
-
DTR控制
:勾选”DTR pin control”,DTR下降沿触发复位
-
RTS控制
:勾选”RTS pin control”,RTS高电平维持Bootloader模式
-
起始地址
:0x08000000(Flash起始地址)
若烧录失败,需检查Windows设备管理器中CH340是否识别为COM端口,驱动版本是否为v3.4以上。旧版驱动在Win10/11中常出现端口丢失问题。
2. STM32固件工程结构解析
本项目采用HAL库构建,工程目录结构体现典型的嵌入式分层设计思想。
User
文件夹存放应用层逻辑,
Drivers
包含HAL库及底层驱动,
Middlewares
集成ESP8266和DHT11中间件。理解各模块职责边界是后续扩展的基础。
2.1 主函数初始化流程的时序逻辑
main.c
中的
MX_GPIO_Init()
、
MX_USART1_UART_Init()
等函数调用顺序不可随意调整,其背后是严格的硬件依赖关系:
1.
GPIO初始化优先
:所有外设引脚模式配置必须在对应外设时钟使能前完成,否则寄存器写入无效
2.
时钟树配置前置
:
SystemClock_Config()
必须在任何外设初始化前执行,它配置了SYSCLK=72MHz、HCLK=72MHz、PCLK1=36MHz、PCLK2=72MHz,直接影响USART波特率计算精度
3.
串口初始化次序
:USART1(调试串口)需在ESP8266初始化前就绪,否则AT指令响应日志无法输出;USART3(Wi-Fi串口)必须在DHT11初始化后初始化,因为DHT11通信会占用CPU时间片,延迟Wi-Fi模块AT指令同步
关键代码段分析:
// MX_GPIO_Init()中PG11配置
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 初始化为推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
// 后续在DHT11驱动中动态切换模式
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_11, GPIO_PIN_SET); // 拉高总线
HAL_GPIO_Mode_t mode = GPIO_MODE_INPUT; // 切换为浮空输入
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct); // 重新初始化
2.2 DHT11驱动的时序实现机制
DHT11单总线协议要求MCU精确控制高低电平持续时间。HAL库的
HAL_Delay()
因调度开销无法满足微秒级精度,故驱动采用
NOP循环+SysTick计数器
混合方案:
// 启动信号:主机拉低80μs,再拉高80μs
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_11);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_11, GPIO_PIN_RESET);
for(uint16_t i=0; i<320; i++) __NOP(); // 320×25ns≈8μs,需校准
// 数据位采样:DHT11拉低50μs后,检测高电平持续时间
// 50μs低电平期间:等待GPIO_PIN_SET事件
while(HAL_GPIO_ReadPin(GPIOG, GPIO_PIN_11) == GPIO_PIN_RESET) {
if(++timeout > 1000) return ERROR_TIMEOUT; // 防死锁
}
// 采样高电平宽度:记录SysTick计数值差
uint32_t start = HAL_GetTick();
while(HAL_GPIO_ReadPin(GPIOG, GPIO_PIN_11) == GPIO_PIN_SET) {
if(HAL_GetTick() - start > 10) return ERROR_DATA; // 超过10ms判错
}
uint32_t high_width = HAL_GetTick() - start;
该实现规避了HAL_Delay的系统滴答中断干扰,但需注意:若在FreeRTOS环境下,SysTick已被RTOS接管,必须改用
HAL_GetTick()
配合超时判断,而非直接读取SysTick->VAL寄存器。
2.3 ESP8266中间件的状态机设计
ESP8266模块固件内置TCP/IP协议栈,其AT指令交互本质是状态机驱动。中间件
esp8266.c
定义了6个核心状态:
-
ESP_IDLE
:等待用户触发连接
-
ESP_AT_TEST
:发送”AT”检测模块响应
-
ESP_WIFI_CONNECT
:执行”AT+CWJAP”接入热点
-
ESP_TCP_CONNECT
:建立TCP连接至OneNet服务器
-
ESP_HTTP_SEND
:构造HTTP POST报文并发送
-
ESP_WAIT_RESP
:等待服务器返回”SEND OK”或”ERROR”
每个状态转换需严格校验响应字符串。例如检测Wi-Fi连接成功:
// 解析"AT+CWJAP?"响应
if(strstr(recv_buffer, "WIFI GOT IP")) {
esp_state = ESP_TCP_CONNECT;
} else if(strstr(recv_buffer, "FAIL")) {
esp_state = ESP_WIFI_CONNECT; // 重试
} else {
// 超时处理:发送"AT+RST"硬复位
HAL_UART_Transmit(&huart3, (uint8_t*)"AT+RST\r\n", 8, 100);
}
关键约束:AT指令必须以
\r\n
结尾,响应超时时间设为2000ms(模块冷启动时首条AT响应可能达1500ms)。
3. OneNet云平台通信协议实现
OneNet HTTP协议要求严格遵循RESTful规范,数据上传采用POST方法,请求体为JSON格式。ESP8266中间件需在
esp8266_send_data()
函数中动态构造符合平台要求的数据包。
3.1 HTTP请求报文结构
OneNet设备API要求请求头包含三个必需字段:
-
api-key
:设备密钥,长度32位十六进制字符串
-
Content-Type
:必须为
application/json
-
Content-Length
:JSON报文体字节数
典型POST请求示例:
POST /devices/587628234/datapoints HTTP/1.1
api-key: 4Xe9YtQzKpLmNcRvBjSfGhDwIuOaEiYn
Host: api.heclouds.com
Content-Type: application/json
Content-Length: 42
{"datastreams":[{"id":"temperature","datapoints":[{"value":25.3}]},{"id":"humidity","datapoints":[{"value":45.6}]}]}
中间件通过
snprintf()
动态生成JSON:
char json_buf[256];
int len = snprintf(json_buf, sizeof(json_buf),
"{\"datastreams\":["
"{\"id\":\"%s\",\"datapoints\":[{\"value\":%.1f}]},"
"{\"id\":\"%s\",\"datapoints\":[{\"value\":%.1f}]}]}"
, "temperature", temp_val, "humidity", humi_val);
3.2 设备认证与连接复用机制
OneNet要求每次HTTP请求携带设备密钥,但密钥不应硬编码在固件中。本项目采用编译期宏定义:
// 在stm32f1xx_hal_conf.h中定义
#define ONENET_API_KEY "4Xe9YtQzKpLmNcRvBjSfGhDwIuOaEiYn"
#define ONENET_DEVICE_ID "587628234"
为降低网络开销,中间件实现TCP连接复用:
- 首次上传时建立TCP连接(
AT+CIPSTART="TCP","api.heclouds.com",80
)
- 后续上传复用同一连接,仅发送HTTP请求
- 连接空闲60秒后自动关闭(
AT+CIPCLOSE
),避免OneNet服务端连接泄漏
3.3 错误恢复策略
网络环境不可靠,需设计多级容错:
-
AT指令级
:单条指令超时重发3次,第4次触发模块复位
-
TCP级
:连接失败时切换APN(
AT+CWAUTOCONN=1
开启自动重连)
-
HTTP级
:收到400/401错误码时,重新获取设备token(需实现OneNet鉴权接口)
关键恢复代码:
if(strstr(recv_buffer, "401 Unauthorized")) {
// 触发设备密钥刷新流程
esp_state = ESP_AUTH_REFRESH;
memset(recv_buffer, 0, sizeof(recv_buffer));
}
4. 本地LCD数据显示扩展实现
战舰V3开发板集成1.44寸TFT LCD(ST7735驱动),SPI接口速率可达20MHz。扩展本地显示需在原有工程中添加LCD驱动层,并重构数据流。
4.1 LCD硬件接口与SPI配置
LCD通过SPI2连接MCU:
- PA1 → SPI2_SCK(时钟)
- PA2 → SPI2_MISO(未使用,悬空)
- PA3 → SPI2_MOSI(数据)
- PA4 → TFT_CS(片选)
- PA5 → TFT_DC(数据/命令选择)
- PA6 → TFT_RST(复位)
SPI2初始化关键参数:
-
SPI_BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4
(主频72MHz→18MHz,满足ST7735最大20MHz)
-
SPI_FirstBit = SPI_FIRSTBIT_MSB
(高位先行)
-
SPI_CRCPolynomial = 7
(CRC校验禁用)
4.2 图形库移植与内存优化
ST7735分辨率为128×128,全屏帧缓存需32KB(128×128×2字节),超出STM32F103ZET6的20KB SRAM。故采用
行缓冲+DMA传输
方案:
- 分配256字节行缓存(128像素×2字节)
- 使用DMA1_Channel5传输每行数据
- 屏幕刷新时逐行更新,避免全屏闪烁
关键初始化代码:
// 配置DMA传输
hdma_spi2_tx.Instance = DMA1_Channel5;
hdma_spi2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi2_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_spi2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
HAL_DMA_Init(&hdma_spi2_tx);
4.3 温湿度数据显示界面设计
在
main.c
主循环中插入LCD刷新逻辑:
while (1) {
// 1. 采集DHT11数据
if(dht11_read_data(&temp, &humi) == DHT_OK) {
// 2. 更新LCD显示
lcd_fill_screen(BLACK);
lcd_draw_string(10, 10, "TEMP:", WHITE, BLACK, 2);
lcd_draw_number(80, 10, (int)temp, WHITE, BLACK, 2);
lcd_draw_string(10, 40, "HUMI:", WHITE, BLACK, 2);
lcd_draw_number(80, 40, (int)humi, WHITE, BLACK, 2);
// 3. 上传至OneNet
esp8266_send_data(temp, humi);
}
HAL_Delay(2000); // 2秒采集周期
}
字体渲染采用点阵字模,数字显示使用16×16 ASCII字库,温度/湿度单位使用自定义图标(如℃符号用16×16位图)。
5. 工程调试与常见故障排除
实际开发中80%问题源于硬件连接与配置不一致。以下为高频故障的定位方法论。
5.1 串口通信失效诊断
当串口调试助手无输出时,按以下步骤排查:
1.
硬件层
:用万用表测量PA2引脚对地电压,正常应为3.3V(空闲态),发送数据时电压在0~3.3V跳变
2.
驱动层
:检查
huart2.Init.BaudRate
是否为115200,
huart2.Init.WordLength
是否为UART_WORDLENGTH_8B
3.
中断层
:确认NVIC中USART2_IRQn是否使能,
HAL_UART_IRQHandler()
是否被正确调用
4.
应用层
:验证
printf
重定向函数是否将字符写入
huart2
而非其他串口
典型错误:在
main.c
中误将
huart1
用于调试输出,但实际硬件连接的是PA2/PA3(USART2)。
5.2 DHT11数据读取失败根因分析
DHT11返回
ERROR_TIMEOUT
的三大原因:
-
上拉电阻失效
:测量PG11对地电阻,应为5.1kΩ。若为无穷大则电阻虚焊
-
时序偏移
:在
dht11_delay_us()
函数中,
__NOP()
数量需根据实际晶振校准。72MHz系统下,1μs需约28个NOP(72M/1000000×1.3周期)
-
电源噪声
:DHT11对电源纹波敏感,用示波器观测VCC引脚,纹波应<50mVpp。若超标,需在VCC-GND间并联10μF电解电容+100nF陶瓷电容
5.3 ESP8266连接OneNet失败处理
连接失败时,首先捕获AT指令交互日志:
// 在esp8266_uart_receive()中添加日志
HAL_UART_Receive(&huart3, recv_buffer, sizeof(recv_buffer), 100);
printf("ESP RX: %s\r\n", recv_buffer); // 输出至调试串口
常见错误码解析:
-
ERROR
:AT指令语法错误,检查指令末尾是否遗漏
\r\n
-
NO CHANGE
:Wi-Fi模块未扫描到热点,确认手机热点名称无隐藏字符(如中文、空格)
-
CLOSED
:TCP连接被服务器拒绝,检查OneNet设备ID是否在平台启用状态
-
SEND FAIL
:HTTP请求体格式错误,用在线JSON校验工具验证生成的JSON有效性
6. 光敏电阻扩展与LED闭环控制
在现有系统基础上增加光敏电阻(GL5528)和LED控制,构成环境光自适应系统。此扩展需新增ADC采集、PWM输出及控制算法。
6.1 光敏电阻硬件接口设计
GL5528为光敏电阻,阻值范围10kΩ(亮)~1MΩ(暗)。采用分压电路接入ADC1_IN0(PA0):
- PA0 → 分压点(GL5528与10kΩ电阻串联)
- VCC → 10kΩ电阻另一端
- GND → GL5528另一端
ADC配置关键参数:
-
Resolution = ADC_RESOLUTION_12B
(12位精度)
-
DataAlign = ADC_DATAALIGN_RIGHT
(右对齐,低位补零)
-
SamplingTime = ADC_SAMPLETIME_55CYCLES_5
(55.5个ADC周期,满足1MHz采样率)
6.2 LED亮度PWM控制实现
开发板LED连接于PB0(TIM3_CH3),需配置TIM3为PWM模式:
// TIM3初始化
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71; // 72MHz→1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 999; // 1kHz PWM频率
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim3);
// 配置PB0为TIM3_CH3
sConfig.OCMode = TIM_OCMODE_PWM1;
sConfig.Pulse = 500; // 初始占空比50%
sConfig.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfig, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
6.3 光控算法设计
采用比例-积分(PI)控制算法,避免LED亮度突变:
#define LIGHT_TARGET 2000 // 目标ADC值(对应环境光强度)
int16_t light_error = LIGHT_TARGET - adc_value;
static int32_t integral = 0;
integral += light_error;
if(integral > 32767) integral = 32767;
if(integral < -32768) integral = -32768;
int16_t pwm_duty = (int16_t)(0.6 * light_error + 0.05 * integral);
if(pwm_duty > 1000) pwm_duty = 1000;
if(pwm_duty < 0) pwm_duty = 0;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, pwm_duty);
该算法使LED亮度随环境光增强而减弱,形成负反馈闭环。比例系数0.6确保响应速度,积分系数0.05消除稳态误差。
7. 实际项目经验与避坑指南
在多个工业环境部署此类系统后,总结出以下关键实践:
7.1 电源完整性设计要点
战舰V3开发板的3.3V LDO(AMS1117)在Wi-Fi模块瞬时峰值电流(200mA)下易发生压降。实测当ESP8266发送数据时,3.3V跌落至2.9V,导致DHT11通信失败。解决方案:
- 在ESP8266 VCC引脚就近放置47μF钽电容+100nF陶瓷电容
- 将DHT11 VCC改接开发板5V电源,DATA引脚通过电平转换器(TXB0104)连接PG11
7.2 AT指令超时的动态校准
不同批次ESP8266固件响应时间差异可达±300ms。硬编码超时值(如2000ms)在高温环境下易失效。改进方案:
// 首次上电时执行AT指令响应时间标定
uint32_t timeout_base = 1500;
for(int i=0; i<3; i++) {
HAL_UART_Transmit(&huart3, (uint8_t*)"AT\r\n", 4, 100);
uint32_t start = HAL_GetTick();
while(!strstr(recv_buffer, "OK") && (HAL_GetTick()-start < 3000)) {
HAL_UART_Receive(&huart3, &ch, 1, 10);
strncat(recv_buffer, &ch, 1);
}
timeout_base = max(timeout_base, HAL_GetTick()-start + 200);
}
7.3 云平台数据流绑定技巧
OneNet平台中,若修改代码中数据流ID(如将”temperature”改为”temp”),需同步更新平台配置:
- 进入设备详情页 → 数据流管理 → 删除原数据流
- 点击”添加数据流”,输入新ID(区分大小写)
- 在数据可视化中,编辑仪表盘组件 → 数据源 → 选择新数据流
切勿直接修改数据流名称,否则历史数据将丢失。建议在代码中定义宏:
#define STREAM_TEMP "temperature"
#define STREAM_HUMI "humidity"
便于统一维护。
我在实际项目中遇到过最棘手的问题是Wi-Fi模块在潮湿环境中连续工作72小时后,AT指令响应延迟从200ms增至1800ms。最终发现是PCB板受潮导致ESP8266天线馈点阻抗失配,通过在模块底部涂覆三防漆解决。这提醒我们:嵌入式系统设计必须考虑环境应力,不能只关注功能实现。
更多推荐
所有评论(0)