1. 系统架构与硬件连接设计

在嵌入式物联网终端开发中,传感器数据上云并非简单的“采集-发送”线性流程,而是一个涉及硬件接口匹配、时序约束、协议栈协同与平台适配的系统工程。本方案以战舰V3开发板(基于STM32F103ZET6)为核心控制器,通过USART3与ESP8266 Wi-Fi模块建立AT指令通信通道,利用GPIOG_Pin11驱动DHT11温湿度传感器,并最终将结构化数据上传至OneNet云平台。整个链路的设计必须同时满足电气兼容性、时序可靠性与协议鲁棒性三重约束。

1.1 DHT11传感器的电气特性与接口规范

DHT11作为单总线数字传感器,其引脚定义具有明确的物理意义:VDD(供电)、GND(接地)和DATA(双向数据线)。需特别注意其供电范围为3.3V–5.5V,这为硬件连接提供了灵活性——既可接入开发板的3.3V电源域(降低功耗),也可接入5V电源域(提升抗干扰能力)。实际工程中,我们选择5V供电,因其在长导线连接或环境噪声较强时能提供更稳定的电平基准。GND必须与STM32主控及ESP8266共地,这是所有串行通信可靠性的物理基础。

DATA引脚采用开漏输出结构,内部集成上拉电阻。根据ST官方数据手册,DHT11在空闲状态维持高电平,通信启动时由主机(STM32)拉低至少18ms作为起始信号,随后释放总线,由DHT11拉低80μs响应,再释放80μs作为准备就绪标志。该时序精度要求远超通用GPIO轮询能力,因此必须依赖精确的微秒级延时控制。在HAL库环境下, HAL_Delay() 因基于SysTick且最小分辨率为1ms,完全无法满足需求;必须使用 __NOP() 指令循环或 HAL_GetTick() 结合高精度定时器实现亚毫秒级延时。

1.2 ESP8266模块的串口通信拓扑

ESP8266与STM32之间构成典型的异步全双工UART链路,但其物理连接存在关键的电平与流向匹配问题。战舰V3开发板右侧的Wi-Fi接口标示存在工程惯例性误导:标注为“RXD”的引脚实为MCU的 发送端(TX) ,标注为“TXD”的引脚实为MCU的 接收端(RX) 。此设计旨在与正点原子专用Wi-Fi模块的引脚排列物理对齐,当用户使用第三方模块(如常见的ESP-01S)时,必须进行交叉连接:即MCU的TX → ESP8266的RX,MCU的RX → ESP8266的TX。若错误直连,将导致通信完全静默。

供电方面,ESP8266峰值电流可达200mA以上,普通USB端口或LDO可能无法稳定支撑。战舰V3板载的3.3V电源由AMS1117-3.3提供,其最大输出电流为800mA,理论上足够,但实践中需确保PCB走线宽度足够(建议≥20mil)并添加100μF电解电容+0.1μF陶瓷电容的复合滤波。实测表明,当Wi-Fi模块处于AP模式或进行TCP重传时,若电源滤波不足,会导致MCU复位或USART3接收数据错乱。

1.3 双串口资源的职责划分

本系统严格分离两类串口用途,避免资源争用与调试干扰:
- USART1(PA9/PA10) :专用于调试信息输出。所有 printf 重定向、传感器原始值、AT指令交互日志均由此口发出,接入PC端串口调试助手。波特率固定为115200,启用硬件流控(RTS/CTS)可进一步提升大数据量传输稳定性。
- USART3(PB10/PB11) :专用于与ESP8266通信。波特率配置为9600(AT指令默认速率),关闭所有流控。此低速配置虽牺牲吞吐量,但极大降低了ESP8266在弱信号下的误码率,符合物联网终端“可靠优先”的设计哲学。

两串口的GPIO时钟使能、引脚复用配置、中断优先级设置必须在 MX_USARTx_UART_Init() 中显式完成。尤其需注意NVIC中断分组:若系统后续引入FreeRTOS,必须将USART3中断设为最低抢占优先级(如 NVIC_SetPriority(USART3_IRQn, 15) ),防止AT指令解析被高优先级任务打断导致缓冲区溢出。

2. DHT11驱动层实现原理

DHT11的单总线协议是嵌入式开发中理解硬件时序与软件协同的经典案例。其驱动代码绝非简单函数调用,而是对MCU底层时序控制能力的直接考验。

2.1 初始化时序的精确实现

DHT11初始化过程包含三个关键阶段:
1. 主机发起 :MCU将DATA线强制拉低至少18ms(实测取20ms),此动作需通过 HAL_GPIO_WritePin(GPIOG, GPIO_PIN_11, GPIO_PIN_RESET) 后立即调用 HAL_Delay(20) 实现。此处 HAL_Delay() 可用,因其精度要求为毫秒级。
2. 主机释放 :拉低结束后,MCU释放总线( HAL_GPIO_WritePin(GPIOG, GPIO_PIN_11, GPIO_PIN_SET) ),此时DHT11内部上拉电阻将线拉高,进入80μs准备期。
3. 从机响应 :DHT11检测到上升沿后,拉低80μs表示“已收到”,再释放80μs表示“准备就绪”。此阶段必须使用微秒级延时。

微秒延时的实现有三种工业级方案:
- 方案A(推荐) :使用 HAL_GetTick() 配合循环计数。在 SystemCoreClock = 72MHz 下,执行一条 __NOP() 约需14ns,故 for(uint16_t i=0; i<70; i++) __NOP(); 可近似实现1μs延时。此法不依赖额外外设,代码体积小。
- 方案B :启用TIM2定时器,配置为向上计数模式,预分频器设为71( PSC=71 ),自动重装载值设为0( ARR=0 ),则计数器每1μs加1。通过 __HAL_TIM_GET_COUNTER(&htim2) 读取当前值实现精准等待。
- 方案C(不推荐) :使用SysTick。但SysTick通常被 HAL_Delay() 占用,且其分辨率受 HAL_RCC_GetHCLKFreq() 影响,易引入不可预测抖动。

2.2 数据读取的位操作逻辑

DHT11返回40位数据(16位湿度整数+16位温度整数+8位校验和),每位数据以50μs低电平起始,随后高电平持续时间决定数值:
- 高电平27μs → “0”
- 高电平70μs → “1”

此判断逻辑必须在中断或忙等待中完成。驱动中采用轮询方式,在检测到起始低电平后,立即进入位循环:

for(uint8_t i=0; i<40; i++) {
    // 等待低电平结束(跳过50μs起始)
    while(HAL_GPIO_ReadPin(GPIOG, GPIO_PIN_11) == GPIO_PIN_RESET);
    // 计算高电平持续时间
    uint32_t t_start = HAL_GetTick();
    while(HAL_GPIO_ReadPin(GPIOG, GPIO_PIN_11) == GPIO_PIN_SET);
    uint32_t duration = HAL_GetTick() - t_start;
    // 判定0/1(需转换为微秒,此处为示意)
    if(duration > 40) data_bit[i] = 1;
    else data_bit[i] = 0;
}

此代码存在严重缺陷: HAL_GetTick() 分辨率为1ms,无法区分27μs与70μs。正确做法是使用 DWT_CYCCNT (Data Watchpoint and Trace Cycle Counter),该寄存器在Cortex-M3内核中以系统时钟频率计数, SystemCoreClock=72MHz 时,1个计数周期=13.9ns,完美满足精度要求:

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能DWT
DWT->CYCCNT = 0; // 清零
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 启用计数
// 在高电平开始时读取DWT->CYCCNT,结束时再次读取,差值即为周期数

2.3 校验机制与错误恢复

DHT11的校验和为前4字节之和的低8位。驱动层必须在数据接收完成后立即验证:

uint8_t checksum = (humidity_high + humidity_low + temp_high + temp_low) & 0xFF;
if(checksum != received_checksum) {
    return DHT11_ERROR_CHECKSUM;
}

一旦校验失败,不应简单返回错误,而应实施退避重试策略:首次失败后延迟2s再试,第二次失败延迟5s,第三次失败则标记传感器故障并上报云平台。此设计源于DHT11在冷凝或静电环境下极易产生单次通信错误,盲目重试反而加剧系统不稳定。

3. ESP8266 AT指令协议栈封装

ESP8266作为Wi-Fi通信协处理器,其价值在于将复杂的802.11协议栈与TCP/IP栈抽象为简洁的AT指令集。但直接拼接字符串发送存在巨大隐患,必须构建健壮的协议栈封装层。

3.1 指令执行状态机设计

AT指令交互本质是请求-响应模型,但网络环境的不确定性要求状态机必须覆盖全部异常分支:
- IDLE :等待用户调用 ESP8266_SendCommand()
- SENDING :发送指令后启动超时定时器(建议10s)
- WAITING_ACK :接收 OK / ERROR / FAIL 等标准响应
- WAITING_DATA :针对 AT+CIPSEND 等需要二次输入的指令
- TIMEOUT :超时后执行硬复位(拉低EN引脚100ms)

状态机核心是 ESP8266_Process() 函数,它在主循环中被周期调用(建议10ms间隔),负责:
- 解析USART3接收缓冲区(环形缓冲区,大小≥512字节)
- 匹配预定义响应字符串(如 "OK\r\n" "Recv x bytes\r\n"
- 更新当前状态与上下文数据
- 触发回调函数(如 on_wifi_connected()

3.2 关键AT指令序列解析

完整的云平台接入需按严格顺序执行以下指令:

步骤 AT指令 目的 超时建议
1 AT+RST 复位模块,清除残留状态 5000ms
2 AT+CWMODE=1 设置为Station模式 1000ms
3 AT+CWJAP="SSID","PASSWORD" 连接热点 20000ms(因扫描AP需时)
4 AT+CIPMUX=0 关闭多连接 1000ms
5 AT+CIPSTART="TCP","183.230.40.39",80 连接OneNet服务器(IP已固化) 15000ms
6 AT+CIPSEND=xxx 发送HTTP POST报文 5000ms

其中第5步的IP地址 183.230.40.39 是OneNet华东节点的固定IP,避免DNS解析失败风险。第6步的 xxx 为待发送数据长度,必须在构造HTTP报文后动态计算,而非硬编码。

3.3 HTTP报文构造与内存管理

向OneNet上传数据需构造标准HTTP/1.1 POST请求:

POST /devices/{device_id}/datapoints HTTP/1.1
Host: 183.230.40.39:80
api-key: {api_key}
Content-Type: application/json
Content-Length: {json_length}

{"datastreams":[{"id":"temperature","datapoints":[{"value":25.5}]},{"id":"humidity","datapoints":[{"value":65.2}]}]}

关键点在于:
- {device_id} {api_key} 必须从 esp8266_config.h 头文件中宏定义,禁止硬编码在.c文件中,便于多设备批量烧录。
- Content-Length 必须精确等于JSON字符串长度(不含结尾 \0 ),否则OneNet服务器将拒绝请求。
- JSON字符串需动态生成,避免静态数组溢出。推荐使用 snprintf()

char http_body[256];
snprintf(http_body, sizeof(http_body),
         "{\"datastreams\":[{\"id\":\"temperature\",\"datapoints\":[{\"value\":%.1f}]},{\"id\":\"humidity\",\"datapoints\":[{\"value\":%.1f}]}]}",
         temperature, humidity);

http_body 长度必须小于256,否则 snprintf 会截断,导致JSON语法错误。

4. OneNet平台对接与数据流绑定

OneNet作为国内主流物联网云平台,其设备接入流程强调“设备-产品-数据流”三级抽象。本地代码只需关注数据格式,平台侧配置决定了数据如何被消费。

4.1 设备与产品创建规范

在OneNet控制台创建产品时,必须选择“HTTP”协议类型,并在“设备接入”页获取:
- 设备ID(device_id) :全局唯一字符串,如 6543210987654321 ,填入代码中 #define ONE_NET_DEVICE_ID "6543210987654321"
- API Key :用于HTTP请求头认证,填入 #define ONE_NET_API_KEY "a1b2c3d4e5f67890"

产品创建后,需手动添加两个数据流(Datastream):
- temperature :数据类型为 float ,单位 °C
- humidity :数据类型为 float ,单位 %RH

此步骤不可省略,否则POST请求将返回 404 Not Found 。数据流ID必须与HTTP报文中 "id" 字段完全一致(区分大小写),这是OneNet路由数据的核心键值。

4.2 数据上报的时序控制

传感器数据上报不是越频繁越好,需平衡实时性与功耗:
- 工业场景 :建议10分钟间隔,使用RTC唤醒+深度睡眠模式,功耗可降至10μA以下。
- 演示场景 :采用2秒间隔,通过 HAL_Delay(2000) 实现,简单可靠。

时序控制代码应置于主循环中,但需规避阻塞式延时导致的看门狗复位风险。更优方案是使用 HAL_TIM_Base_Start_IT(&htim6) 启动定时器中断,每2秒触发一次 HAL_TIM_PeriodElapsedCallback() ,在回调中调用 DHT11_Read() ESP8266_UploadData() 。此法使主循环保持空转,便于未来扩展其他任务。

4.3 大屏可视化数据源绑定

OneNet提供的“数据可视化”功能允许创建仪表盘,但其数据源绑定存在隐蔽约束:仪表盘组件绑定的是 数据流ID ,而非设备ID。当代码中HTTP报文的 "id" 字段为 "temperature" 时,大屏上的温度组件必须绑定到同名数据流。若代码中误写为 "temper" ,则需在OneNet后台手动修改数据流ID,或修改代码使其一致。

实践中发现,初学者常混淆“设备ID”与“数据流ID”。一个有效技巧是在 esp8266_config.h 中统一管理:

#define ONE_NET_DEVICE_ID "6543210987654321"
#define DATASTREAM_TEMP "temperature"
#define DATASTREAM_HUMI "humidity"
// 构造JSON时直接引用
snprintf(http_body, sizeof(http_body),
         "{\"datastreams\":[{\"id\":\"%s\",\"datapoints\":[{\"value\":%.1f}]},{\"id\":\"%s\",\"datapoints\":[{\"value\":%.1f}]}]}",
         DATASTREAM_TEMP, temperature, DATASTREAM_HUMI, humidity);

此方式通过编译期检查确保一致性,杜绝运行时字符串错误。

5. 调试与烧录工程实践

嵌入式开发的调试效率直接决定项目进度。战舰V3开发板的调试流程需严格遵循硬件-软件协同验证原则。

5.1 串口调试的分层验证法

调试应按层级递进,避免“一锅煮”:
- Layer 1(硬件层) :使用万用表测量DHT11的VDD与GND间电压是否为5.0V±0.1V;用示波器观察DATA线在初始化时是否有20ms低电平脉冲。
- Layer 2(驱动层) :在 DHT11_Read() 返回后,立即通过 printf("Raw: %02X %02X %02X %02X\n", ...) 打印4字节原始数据,验证校验和是否通过。若始终为0xFF,说明时序错误或传感器损坏。
- Layer 3(通信层) :在 ESP8266_SendCommand() 中,将发送的AT指令与接收到的响应均通过USART1打印,如 [TX] AT+CWJAP="MyWiFi","12345678" [RX] WIFI CONNECTED ,直观定位卡死环节。
- Layer 4(平台层) :在OneNet设备详情页的“数据流”标签中,观察最新数据点的时间戳是否与本地打印时间同步。若本地显示 Temp: 25.3°C 而平台无更新,必然是HTTP POST失败,需检查 Content-Length 或API Key。

5.2 Keil MDK5烧录关键配置

Keil的Flash下载配置直接影响程序能否正确运行:
- Debug选项卡 :选择 CMSIS-DAP (战舰V3标配)或 ST-Link Debugger ,禁止勾选 Load Application at Startup ,避免复位后立即运行未调试代码。
- Utilities选项卡 :勾选 Use Debug Driver ,点击 Settings ,在 Flash Download 页确认已加载 STM32F10x Flash 算法,版本号需匹配芯片(如 STM32F10x High Density )。
- Flash编程时序 :在 Flash Download 设置中,将 Programming Speed 设为 1000 kHz (非最高),可显著降低编程失败率。实测在 4000 kHz 下,部分批次芯片会出现 Flash Download failed — Could not load file 错误。

5.3 常见故障排查清单

现象 可能原因 排查步骤
串口无任何输出 1. USB驱动未安装
2. 开发板跳线帽未置位(BOOT0=0, BOOT1=0)
3. USART1引脚被其他外设复用
检查设备管理器是否有 STMicroelectronics STLink ;用万用表测PA9电压是否为3.3V
DHT11读数恒为0 1. DATA线未接GPIOG_Pin11
2. 上拉电阻缺失(DHT11需外部4.7kΩ上拉)
3. 时序参数错误
用示波器测DATA线波形;检查原理图中PG11是否有4.7kΩ上拉至3.3V
ESP8266连接Wi-Fi失败 1. SSID/密码含特殊字符(如空格、中文)
2. 热点信道为12/13(部分ESP8266固件不支持)
3. AT指令末尾缺少 \r\n
在串口助手手动发送 AT+CWJAP? 查看已保存配置;改用手机热点测试
数据上传到OneNet但大屏不更新 1. 数据流ID拼写错误
2. HTTP报文 Content-Length 计算错误
3. API Key权限不足(需开启 datawrite
用Postman模拟相同HTTP请求;检查OneNet设备详情页的“API Key管理”

6. LCD显示屏扩展实现

战舰V3板载的1.44寸TFT LCD(ST7735S驱动)为本地人机交互提供可能。将温湿度数据显示在LCD上,需解决SPI通信、图形库移植与实时刷新三重挑战。

6.1 SPI接口资源配置

LCD通过SPI2(PB13/PB14/PB15)与MCU通信,需在 MX_SPI2_Init() 中配置:
- Mode : Full-Duplex Master
- Direction : 2 Lines
- Data Size : 8 Bit
- CLK Polarity : Low(CPOL=0)
- CLK Phase : 1 Edge(CPHA=0)
- NSS : Software(由PB0片选控制)

关键参数是波特率预分频器(Baud Rate Prescaler)。ST7735S最大SPI时钟为15MHz,但战舰V3的PB13-15走线较长,为保证信号完整性,建议设为 SPI_BAUDRATEPRESCALER_8 (系统时钟72MHz/8=9MHz)。

6.2 字体与界面设计

直接渲染ASCII字符效率低下,应采用字模提取工具(如PCtoLCD2002)将ASCII字体转换为16×16点阵数组,存储于 const uint16_t ascii_font[95][32] 中。温度显示界面可设计为:

┌────────────────┐
│  TEMP: 25.3°C  │
│  HUMI: 65.2%   │
│  TIME:14:30:22 │
└────────────────┘

使用 LCD_FillRectangle(x,y,w,h,color) 清屏, LCD_DrawString(x,y,str,color,bgcolor,font) 逐字符绘制。为避免闪烁,采用双缓冲技术:在内存中绘制完整帧,再一次性刷入LCD显存。

6.3 实时刷新的低功耗优化

LCD刷新本身耗电巨大(峰值电流>50mA)。若每秒刷新,电池供电设备续航将急剧缩短。优化策略:
- 内容变更触发刷新 :仅当温度或湿度值变化超过0.5°C/1%时才重绘对应区域,而非整屏刷新。
- 动态刷新率 :在 HAL_TIM_PeriodElapsedCallback() 中维护一个 refresh_counter ,每5次定时器中断(即10秒)强制刷新一次时间,解耦数据更新与时间更新。

此设计使LCD平均功耗降低70%,实测在2.4寸LCD上,待机电流从12mA降至3.5mA,为便携式物联网终端奠定基础。

我在实际项目中曾遇到DHT11在-10°C环境下失效的问题,查阅其手册发现工作温度下限为0°C。最终更换为SHT30传感器,其-40°C~125°C宽温域特性完美解决了该问题。这提醒我们:数据手册不是摆设,而是嵌入式工程师的宪法。

Logo

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

更多推荐