STM32F103ZET6结合ESP8266的温湿度光照度采集与WiFi上报实战
2026/9/10 8:37:52 网站建设 项目流程

简介:这是一份基于 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 平台。对希望短时间搭建设备连云原型、理清 MQTT+Wi-Fi 物联网框架的读者来说,是一份较完整的参考工程。

1. 智慧农业项目里的数据链路:STM32F103ZET6、光照度、温湿度与 ESP8266 各自做什么

一个温室大棚监测盒,往地里插三根探头,屏幕和云端同时跳数。你把这个标题拆开看,其实是四条主线:STM32F103ZET6 负责把传感器数据读进来,光照度模块和温湿度模块负责物理量到电信号的转换,ESP8266 负责把整理好的数据搬上网络。

很多刚接触基于 stm32 的温湿度检测的人,会把注意力全放在“读传感器”上,但实际项目里最花时间的反而是 ESP8266 这条链路:上电时序、AT 响应、掉线重连都比 DHT11 的 40 bit 时序更磨人。这套项目适合两类人:一是手上正好有 STM32F103ZET6 开发板,想跑通光照度、温湿度和 WiFi 上报的入门者;二是想把这套采集逻辑改造成定时上报、带断线补偿的工程原型的人。读完你会知道每个引脚干什么、每段初始化在做什么、数据从传感器到远端需要经过哪些步骤。

2. 从接线到例程:STM32F103ZET6 与光照度、温湿度传感器的接口设计

2.1 引脚分配:光照度、温湿度、ESP8266 各占哪几个引脚

我推荐一套最常见也最省事的组合:温湿度用 DHT11,光照度用 BH1750,WiFi 用 ESP8266-01S 或 ESP-12F。这三者的接口类型完全不同,正是这套项目的学习价值所在。

引脚分配如下表。这里给出的是我常用的一套映射,你也可以按板子调整,只要注意 DHT11 的数据线要选能手动切换输出输入状态的引脚,BH1750 占用一组 I2C,ESP8266 占用一组空闲串口即可。

外设信号类型STM32F103ZET6 引脚说明
DHT11单总线,开漏PB5外部接 4.7kΩ 上拉到 3.3V
BH1750I2C,开漏PB6(SCL)、PB7(SDA)两个引脚都要上拉 4.7kΩ,ADDR 接地
ESP8266UART,3.3V TTLPA2(TX)、PA3(RX)PA2 接 ESP8266 RXD,PA3 接 ESP8266 TXD
ESP8266 使能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 的波特率我一般设 115200,8 位数据、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 表示 0,70us 左右表示 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 是数字光照度传感器,输出单位是 lux,I2C 接口。它的使用比 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 发送过程:AT+CWJAP 之后接 AT+CIPSEND

连接 WiFi 主要用三条指令。第一条设置模块为 station 模式,第二条连接路由器,第三条建 TCP 连接。指令字符串末尾必须带\r\n

esp_send_cmd("AT+CWMODE=1\r\n", "OK", 500); esp_send_cmd("AT+CWJAP=\"SSID\",\"password\"\r\n", "WIFI GOT IP", 10000); esp_send_cmd("AT+CIPSTART=\"TCP\",\"192.168.1.100\",8080\r\n", "CONNECT OK", 5000);

AT+CWMODE=1表示 station 模式,只让 ESP8266 去连接外部路由器,不开热点。AT+CWJAP的等待时间要放宽到 10s,因为模块要扫描信道、认证、获取 DHCP 地址。某些固件返回的是WIFI CONNECTED,有些返回WIFI GOT IP,更保险的做法是等收到OK后,再用AT+CWJAP?查询当前连接的 SSID 来确认。

建好 TCP 后,发送数据要分两步:先发AT+CIPSEND=长度,收到>后再发数据内容。长度必须和实际发送字节数一致。

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 8080
python3 -c " import socket s=socket.socket() s.bind(('0.0.0.0',8080)) s.listen(1) c,_=s.accept() print(c.recv(1024)) "

nc -lk 8080的意思是监听本机 8080 端口并持续输出。Python 脚本更适合验证 JSON 格式,它打印收到的原生字节,能直接看到thl三个字段有没有被空格或\0污染。如果收到的字节数比AT+CIPSEND声明的长度少,问题多半出在snprintf和发送函数的长度参数不一致。

最后,把逻辑分析仪或示波器探针放到 STM32 的 PA3 上,观察 UART 波形里有没有完整的数据位。看波形能直接区分是 STM32 根本没发送,还是 ESP8266 收到但没回>,比反复改代码猜原因快得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询