资讯动态

STM32F103ZET6结合ESP8266的温湿度光照度采集与WiFi上报实战

发布时间:2026/9/10 8:37:54 来源:尧图企业网站定制
简介这是一份基于 STM32F103ZET6 的智慧农业物联网项目源码面向嵌入式开发者、物联网爱好者以及电子设计竞赛或毕业设计使用者。项目以 STM32 为主控借助 ESP8266 接入腾讯物联网平台通过 MQTT 协议完成光照度BH1750与温湿度等环境数据的采集和上报能直观展示设备端到云端的通信链路。压缩包共 117 个文件大小约 19.62MB包含 Keil MDK 工程文件.uvproj/.uvopt、C 语言源码.c/.h、编译中间文件.o/.d/.crf、烧录文件.hex/.axf、图片及说明页面目录结构清晰便于按模块查阅和二次开发。目前已有 9276 人学习下载代码中覆盖 esp8266 驱动、MQTT 报文组包、OLED 显示、BH1750 光照采集等关键模块并保留完整工程配置与编译产物适合对照博客步骤逐步复现、调试验证或移植到同类 MCU 平台。对希望短时间搭建设备连云原型、理清 MQTTWi-Fi 物联网框架的读者来说是一份较完整的参考工程。1. 智慧农业项目里的数据链路STM32F103ZET6、光照度、温湿度与 ESP8266 各自做什么一个温室大棚监测盒往地里插三根探头屏幕和云端同时跳数。你把这个标题拆开看其实是四条主线STM32F103ZET6 负责把传感器数据读进来光照度模块和温湿度模块负责物理量到电信号的转换ESP8266 负责把整理好的数据搬上网络。很多刚接触基于 stm32 的温湿度检测的人会把注意力全放在“读传感器”上但实际项目里最花时间的反而是 ESP8266 这条链路上电时序、AT 响应、掉线重连都比 DHT11 的 40 bit 时序更磨人。这套项目适合两类人一是手上正好有 STM32F103ZET6 开发板想跑通光照度、温湿度和 WiFi 上报的入门者二是想把这套采集逻辑改造成定时上报、带断线补偿的工程原型的人。读完你会知道每个引脚干什么、每段初始化在做什么、数据从传感器到远端需要经过哪些步骤。2. 从接线到例程STM32F103ZET6 与光照度、温湿度传感器的接口设计2.1 引脚分配光照度、温湿度、ESP8266 各占哪几个引脚我推荐一套最常见也最省事的组合温湿度用 DHT11光照度用 BH1750WiFi 用 ESP8266-01S 或 ESP-12F。这三者的接口类型完全不同正是这套项目的学习价值所在。引脚分配如下表。这里给出的是我常用的一套映射你也可以按板子调整只要注意 DHT11 的数据线要选能手动切换输出输入状态的引脚BH1750 占用一组 I2CESP8266 占用一组空闲串口即可。外设信号类型STM32F103ZET6 引脚说明DHT11单总线开漏PB5外部接 4.7kΩ 上拉到 3.3VBH1750I2C开漏PB6(SCL)、PB7(SDA)两个引脚都要上拉 4.7kΩADDR 接地ESP8266UART3.3V TTLPA2(TX)、PA3(RX)PA2 接 ESP8266 RXDPA3 接 ESP8266 TXDESP8266 使能GPIO 输出PB4控制 CH_PD拉高后模块才工作系统状态灯GPIO 输出PC13按 1s 闪烁表示 WiFi 未连上先看电源。STM32F103ZET6 开发板上通常有 AMS1117-3.3输出电流在几百毫安以内。单独带 STM32 和 BH1750 没问题但 ESP8266 在发射瞬间电流可以冲到 300mA 以上如果 WiFi 模块和 STM32 共用同一个板载 LDO会出现 3.3V 被拉低、DHT11 或 BH1750 偶发读不到的问题。常见做法是 ESP8266 单独用一块低压差稳压器或者用 USB 口的 5V 转 3.3V 模块供电地线仍然共地。这是很多 esp8266 开发板与 stm32 通信案例里被跳过的一步。电平也要注意。ESP8266 的 GPIO 不是 5V 耐压。STM32F103ZET6 的 PA2、PA3 如果配置成推挽输出高电平是 3.3V可以直接连。为了保险可以在 STM32 TXD 到 ESP8266 RXD 之间串一个 1kΩ 电阻防止误配置成 5V 时损坏模块。2.2 初始化顺序先时钟再 GPIO最后外设拿到项目代码后先看 SystemClock_Config 和 GPIO_Init。STM32F103ZET6 的 GPIOB、GPIOA、GPIOC 都要开时钟I2C1 开时钟USART2 开时钟。没有时钟就没有任何外设这是从其他平台转过来的人最容易漏的地方。__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE();GPIO 初始化时DHT11 的 PB5 我建议直接配成开漏输出因为 DHT11 靠主机拉低启动信号之后要把总线释放给从机。BH1750 的 I2C 引脚同样用开漏输出带上拉。USART2 的 TX 配复用推挽RX 配浮空输入。GPIO_InitTypeDef GPIO_InitStruct {0}; // PB5 先做开漏输出外部上拉由 4.7k 提供 GPIO_InitStruct.Pin GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct);GPIO_MODE_OUTPUT_OD的含义是开漏输出。DHT11 通信过程中PB5 一会儿要输出低电平产生启动信号一会儿要读取总线状态。开漏输出配合外部上拉可以在不切换模式的情况下完成这两件事写 0 拉低总线写 1 释放总线。读取时直接读 IDR 寄存器反映的引脚电平。I2C1 配置成 100kHz 标准模式就够了。BH1750 支持到 400kHz但 F103 的 I2C 外设临界情况比较多慢一点更可靠。USART2 的波特率我一般设 1152008 位数据、1 位停止位、无校验对应 ESP8266 AT 固件的默认设置。huart2.Instance USART2; huart2.Init.BaudRate 115200; huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; huart2.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart2);HwFlowCtl必须关闭。ESP8266 的 AT 固件默认不用硬件流控打开 RTS/CTS 后反而会因为接线不对卡死。OverSampling保持 16配合 72MHz 主频115200 的波特率误差在可接受范围。2.3 软件 I2C 与硬件 I2C 的选择F103 的 I2C1 外设存在一些容易触发总线忙的状态调试时经常需要复位。对于光照度这类数据量很小的传感器我一般直接写软件模拟 I2C只用两个 GPIO 逐位翻转时钟和数据不吃资源也不怕硬件 I2C 卡死。这样工程里少依赖一个外设也便于把 BH1750 换到其他引脚上。如果需要用硬件 I2C初始化前最好先调用__HAL_RCC_I2C1_FORCE_RESET()再调用__HAL_RCC_I2C1_RELEASE_RESET()否则调试器停下再启动时容易报 BUSY。数据量大或需要频繁读取 BH1750 时硬件 I2C 确实省 CPU但农业采集频率通常低于 1Hz软件 I2C 完全够用。3. 传感器读数不用猜光照度与温湿度的采集代码和误差处理3.1 DHT11 温湿度读取40bit 时隙和校验和DHT11 是很多基于 stm32 的温湿度检测项目会用的入门传感器。它一次返回 40 bit格式是湿度整数、湿度小数、温度整数、温度小数、校验和总共 5 字节。DHT11 的小数位通常为 0但读取时仍然要以 5 字节为单位处理最后一位等于前四位相加才算成功。通信时序分两段。主机先把总线拉低 18ms 以上然后释放DHT11 会先拉低 80us再拉高 80us之后开始数据位。每个数据位由 50us 低电平和一段高电平组成高电平持续 26~28us 表示 070us 左右表示 1。测量这段高电平的宽度是所有 DHT11 驱动代码的核心。#define DHT_PORT GPIOB #define DHT_PIN GPIO_PIN_5 #define DHT_READ() HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN) // 等待引脚电平变为 level超时返回 1 static uint8_t dht_wait(uint8_t level, uint16_t timeout_us) { uint16_t t 0; while (DHT_READ() level) { if (t timeout_us) return 1; delay_us(1); } return 0; } uint8_t DHT_ReadRaw(uint8_t data[5]) { uint8_t i, j; uint16_t w; // 发起开始信号拉低 20ms再释放 HAL_GPIO_WritePin(DHT_PORT, DHT_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT_PORT, DHT_PIN, GPIO_PIN_SET); delay_us(30); // DHT11 应答先低 80us再高 80us if (dht_wait(GPIO_PIN_SET, 100)) return 1; if (dht_wait(GPIO_PIN_RESET, 100)) return 2; for (i 0; i 5; i) { data[i] 0; for (j 0; j 8; j) { // 等待数据位低电平结束再量高电平宽度 if (dht_wait(GPIO_PIN_RESET, 100)) return 3; w 0; while (DHT_READ() GPIO_PIN_SET) { if (w 100) return 4; delay_us(1); } data[i] 1; if (w 40) data[i] | 1; } } if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) return 5; // 校验失败直接丢弃本次数据 return 0; }dht_wait(GPIO_PIN_SET, 100)的第二个参数是超时时间单位 us。响应阶段只有约 80us给到 100us 就能分辨有没有接传感器。读数据位时高电平宽度小于 40us 记为 0大于 40us 记为 1这个阈值在 26~28us 和 70us 之间留得很宽只要延时函数接近 1us判断就不会出错。delay_us要自己实现HAL 没有现成的微秒延时。常见做法是用 DWT 计数器或者一个简单的 for 循环校准。不要在延时函数里做太复杂的事DHT11 的时序要求不高但要求整体等待时间稳定可预期。3.2 BH1750 光照度读取一次指令加两次字节BH1750 是数字光照度传感器输出单位是 luxI2C 接口。它的使用比 DHT11 简单得多写一个测量指令等转换完成读两个字节合并后除以 1.2 就是照度值。常见的测量指令有 0x10 连续 H 分辨率、0x20 一次 H 分辨率。农业场景通常每几十秒采一次样用 0x20 单次测量更省电但代码里要注意每次测量前重新发送指令。#define BH1750_ADDR 0x23 // ADDR 引脚接地时的地址 static void bh1750_write_cmd(uint8_t cmd) { soft_i2c_start(); soft_i2c_send_byte((BH1750_ADDR 1) | 0); // 低 bit 表示写 soft_i2c_send_byte(cmd); soft_i2c_stop(); } static int bh1750_read_lux(uint16_t *lux) { uint8_t hi, lo; // 单次 H 分辨率测量 bh1750_write_cmd(0x20); HAL_Delay(150); // 转换时间最大 120ms留余量 soft_i2c_start(); soft_i2c_send_byte((BH1750_ADDR 1) | 1); // 低 bit 表示读 hi soft_i2c_read_ack(); lo soft_i2c_read_nack(); soft_i2c_stop(); *lux ((uint16_t)hi 8) | lo; *lux *lux / 1.2; // 高分辨率模式 1lx/bit实际值是原始值除以 1.2 return 0; }(BH1750_ADDR 1) | 0是 I2C 的 8 位地址转换方式。BH1750 的 7 位地址是 0x23 或 0x5C取决于 ADDR 引脚接低还是接高。如果模块上带了 0x23 和 0x5C 两个焊盘接错就读不到器件。soft_i2c_read_ack()和soft_i2c_read_nack()分别表示读完一个字节后主机回 ACK 还是 NACK最后一个字节必须回 NACK否则传感器会一直占用总线。3.3 数据校验与周期采样把 DHT11 和 BH1750 的数据合成一个结构体传给上层使用是我在工程里的常用做法。这样上报代码不用关心传感器怎么读只处理数据。typedef struct { int16_t temperature; // 单位 0.1℃ uint16_t humidity; // 单位 0.1% uint16_t lux; } SensorData_t; uint8_t Sensor_Collect(SensorData_t *out) { uint8_t raw[5]; uint16_t lux; if (DHT_ReadRaw(raw) ! 0) return 0; if (bh1750_read_lux(lux) ! 0) return 0; out-temperature (int16_t)raw[2] * 10; out-humidity (uint16_t)raw[0] * 10; out-lux lux; return 1; }DHT11 的采样周期必须大于 1s。如果你发现读第一次能成功、第二次总是超时基本就是两次读取间隔太短。BH1750 的照度值在日光灯下会有轻微波动遮阳网移动时波动更大。可以做一次简单的一阶滤波。static uint16_t lux_filt 0; lux_filt lux_filt ? (lux_filt * 7 lux_raw * 3) / 10 : lux_raw;系数 7:3 表示保留 70% 历史值、30% 新值。采样频率越高历史权重可以越大如果希望灯光开关后响应更快改成 5:5。DHT11 自带的 ±2℃、±5%RH 误差无法通过滤波消除做精确控制要换 SHT30 或其他工业传感器。4. ESP8266 做主传用 AT 指令把温湿度和光照度发到远端4.1 上电、AT 测试与串口中断接收ESP8266 上电后先用 RST 引脚做一次硬件复位再发AT\r\n测试模块是否响应。复位后至少要等 1s早期 AT 固件启动较慢立即发指令会丢失第一个数据。HAL_GPIO_WritePin(ESP_RST_PORT, ESP_RST_PIN, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(ESP_RST_PORT, ESP_RST_PIN, GPIO_PIN_SET); HAL_Delay(1000); HAL_UART_Transmit(huart2, (uint8_t *)AT\r\n, 4, 1000);如果你刚开始看 esp8266 入门教程会发现几乎所有教程都从这一步开始。区别在于教程里是人手动在串口助手里输入 AT工程里要让 STM32 自动读取返回值。我通常用串口 1 字节中断把收到的内容放进环形队列然后写一个esp_wait_for(OK, 2000)函数去扫描队列。uint8_t rx_byte; HAL_UART_Receive_IT(huart2, rx_byte, 1); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { ringbuf_push(rx_byte); HAL_UART_Receive_IT(huart2, rx_byte, 1); } }ringbuf_push是一个环形队列写入函数队列长度在 256 字节以上的话应付 AT 响应完全够用。不要在主循环里用HAL_UART_Receive阻塞等一个不定长的响应那样所有业务都会被串口卡住。4.2 发送过程ATCWJAP 之后接 ATCIPSEND连接 WiFi 主要用三条指令。第一条设置模块为 station 模式第二条连接路由器第三条建 TCP 连接。指令字符串末尾必须带\r\n。esp_send_cmd(ATCWMODE1\r\n, OK, 500); esp_send_cmd(ATCWJAP\SSID\,\password\\r\n, WIFI GOT IP, 10000); esp_send_cmd(ATCIPSTART\TCP\,\192.168.1.100\,8080\r\n, CONNECT OK, 5000);ATCWMODE1表示 station 模式只让 ESP8266 去连接外部路由器不开热点。ATCWJAP的等待时间要放宽到 10s因为模块要扫描信道、认证、获取 DHCP 地址。某些固件返回的是WIFI CONNECTED有些返回WIFI GOT IP更保险的做法是等收到OK后再用ATCWJAP?查询当前连接的 SSID 来确认。建好 TCP 后发送数据要分两步先发ATCIPSEND长度收到后再发数据内容。长度必须和实际发送字节数一致。void esp8266_upload(const SensorData_t *data) { char json[64]; char cmd[32]; uint16_t len; len snprintf(json, sizeof(json), {\t\:%d.%d,\h\:%d.%d,\l\:%u}\r\n, >nc -lk 8080python3 -c import socket ssocket.socket() s.bind((0.0.0.0,8080)) s.listen(1) c,_s.accept() print(c.recv(1024)) nc -lk 8080的意思是监听本机 8080 端口并持续输出。Python 脚本更适合验证 JSON 格式它打印收到的原生字节能直接看到t、h、l三个字段有没有被空格或\0污染。如果收到的字节数比ATCIPSEND声明的长度少问题多半出在snprintf和发送函数的长度参数不一致。最后把逻辑分析仪或示波器探针放到 STM32 的 PA3 上观察 UART 波形里有没有完整的数据位。看波形能直接区分是 STM32 根本没发送还是 ESP8266 收到但没回比反复改代码猜原因快得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价