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天线馈点阻抗失配,通过在模块底部涂覆三防漆解决。这提醒我们:嵌入式系统设计必须考虑环境应力,不能只关注功能实现。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐