1. 从零搭建物联网节点:为什么选这套组合
搞嵌入式物联网开发的朋友,手里大概率都有一块正点原子的STM32F103开发板,也囤过几个ESP8266模块。这两个东西单独玩都不难,但要把它们串起来,再接入一个云平台实现远程监控,中间涉及的知识点就多了——串口通信、AT指令、协议适配、云端配置,每一步都有坑。
我这次做的项目,就是用正点原子的STM32F103工程模板作为基础框架,通过ESP8266模块连接机智云,实现设备数据上报和远程控制。选这套组合的原因很实际:STM32F103是ARM Cortex-M3内核,主频72MHz,资源对于中小型物联网节点绰绰有余;ESP8266成本低、资料多,AT指令集成熟;机智云提供了自动代码生成工具,能省掉大量协议对接的重复劳动。三者搭配,属于性价比和开发效率都比较均衡的方案。
这篇文章适合谁看?如果你已经会用Keil或者STM32CubeIDE写简单的GPIO和串口程序,对AT指令有基本概念,但还没完整跑通过一个“MCU+WiFi模块+云平台”的链路,那这篇内容就是为你准备的。我会从工程模板的裁剪开始,一步步讲到云端数据点配置、代码生成、串口驱动适配、配网调试,最后分享几个我在实测中踩过的坑和排查方法。整个过程不需要你精通TCP/IP协议栈,但需要你有耐心跟着步骤走。
注意:本文涉及的云平台操作均基于通用物联网平台的标准流程,具体界面可能随版本更新有所变化,核心逻辑不变。
2. 工程模板的裁剪与串口资源规划
2.1 正点原子模板里哪些东西该留、哪些该删
正点原子的STM32F103工程模板通常包含大量外设驱动:LCD、触摸屏、SD卡、红外、温湿度传感器等等。如果你直接拿来用,编译出来的固件体积会很大,而且很多初始化代码会占用你需要的串口和定时器资源。我的做法是新建一个干净的工程目录,只保留最核心的几个文件。
具体来说,保留SYSTEM文件夹下的delay、sys、usart三个子文件夹。delay用于毫秒和微秒级延时,AT指令的发送和等待响应离不开它;sys提供系统时钟配置和中断管理;usart是串口驱动的基础。HARDWARE文件夹下只保留你实际用到的外设驱动,比如LED和按键,其他全部移除。CORE和STM32F10x_FWLib是启动文件和标准外设库,必须保留。
裁剪完成后,在Keil的工程目标选项中,把不用的.c文件从工程树里移除,同时检查stm32f10x_conf.h里是否还使能了不需要的外设头文件。这一步能显著减少编译时间和固件体积。我实测下来,裁剪后的工程编译出来大约占用Flash 20KB左右,给后续的协议代码留足了空间。
2.2 串口分配:调试口和通信口必须分开
这是很多新手容易忽略的一点。STM32F103通常有3个USART,我建议这样分配:
| 串口 | 用途 | 波特率 | 引脚 |
|---|---|---|---|
| USART1 | 调试打印,连接电脑串口助手 | 115200 | PA9/PA10 |
| USART2 | 连接ESP8266模块 | 115200 | PA2/PA3 |
| USART3 | 预留,可接其他传感器 | 9600 | PB10/PB11 |
为什么调试口和通信口要分开?因为ESP8266在配网和通信过程中会输出大量状态信息,如果你把调试信息也混在同一个串口上,日志会非常混乱,排查问题时根本分不清哪条是模块返回的,哪条是你自己打印的。分开之后,USART1专门用来看程序运行状态,USART2专门和ESP8266对话,互不干扰。
在代码层面,你需要为USART2单独写一个发送函数和接收中断服务函数。正点原子的usart.c里默认只初始化了USART1,你需要复制一份初始化代码,把寄存器基址改成USART2,引脚配置改成PA2(TX)和PA3(RX),并开启USART2的接收中断。接收中断里把收到的字节存入一个环形缓冲区,供AT指令解析函数读取。
2.3 环形缓冲区的实现细节
AT指令的响应是不定长的,而且可能分多次到达。如果你在中断里直接处理字符串,很容易因为处理时间过长导致后续字节丢失。所以必须用一个环形缓冲区来暂存接收数据。
我定义了一个大小为512字节的缓冲区,用head和tail两个索引来管理读写位置。中断服务函数里只做一件事:把接收到的字节写入buffer[head],然后head = (head + 1) % BUFFER_SIZE。主循环里的AT指令解析函数则从tail位置开始读取,直到tail == head为止。这样即使主循环在处理其他任务,串口数据也不会丢失。
提示:缓冲区大小要根据你实际通信的数据量来定。如果只是收发AT指令和少量传感器数据,512字节足够;如果要传固件升级包,建议扩大到2KB以上。
3. 机智云数据点配置与代码生成
3.1 数据点定义:先想清楚你要传什么
在机智云平台上创建一个新产品,选择“WiFi/移动网络”接入方式,然后开始定义数据点。数据点是云端和设备之间约定的数据单元,每个数据点有名称、标识名、读写类型、数据类型和取值范围。
以我做的环境监测节点为例,我定义了以下几个数据点:
| 数据点名称 | 标识名 | 读写类型 | 数据类型 | 备注 |
|---|---|---|---|---|
| 温度 | Temperature | 只读 | 数值 | 范围-20~80,精度0.1 |
| 湿度 | Humidity | 只读 | 数值 | 范围0~100,精度0.1 |
| LED开关 | LED_OnOff | 可写 | 布尔 | 0关1开 |
| 上报周期 | Report_Interval | 可写 | 数值 | 单位秒,范围5~300 |
定义数据点时要注意:标识名一旦确定就不要随意修改,因为生成的代码里会用到这个字符串作为变量名。读写类型决定了设备是只能上报还是也能接收控制指令。数据类型影响云端解析方式,数值类型可以设置精度和步长,布尔类型就是简单的0和1。
3.2 自动生成代码的目录结构解读
机智云的代码生成工具会输出一个压缩包,解压后包含Gizwits、Utils、User等文件夹。Gizwits文件夹是协议核心,里面有gizwits_protocol.c和gizwits_product.c两个关键文件。Utils里是数据点处理相关的工具函数。User里是用户回调函数的框架。
你需要把这些文件添加到你的Keil工程中。注意,机智云生成的代码默认使用一个串口和ESP8266通信,你需要根据自己实际的串口分配,修改gizwits_protocol.h里的UART_PORT宏定义,把它指向USART2。同时,生成的代码里会调用一个uartWrite函数来发送数据,你需要把这个函数替换成你自己写的USART2发送函数。
3.3 数据上报和事件处理的回调机制
机智云的协议栈采用回调机制。当云端下发控制指令时,协议栈会解析出对应的数据点,然后调用gizwitsEventProcess函数。你需要在这个函数里根据dataPoint_t结构体的内容来执行具体操作,比如检测到LED_OnOff为1就点亮LED,为0就熄灭。
数据上报则更简单。你只需要定期更新dataPoint_t结构体里的变量值,然后调用gizwitsHandle函数,协议栈会自动把变化的数据打包上报。上报周期可以通过云端配置,也可以在设备端控制。我通常设置一个定时器,每5秒调用一次gizwitsHandle,这样既能保证数据实时性,又不会过于频繁地占用网络。
注意:
gizwitsHandle函数内部有状态机,不要在中断里调用它。放在主循环里,配合一个标志位来触发。
4. ESP8266的AT指令驱动与配网流程
4.1 ESP8266的固件版本与AT指令集差异
ESP8266的AT指令集在不同固件版本之间有差异,这一点必须提前确认。我手里这个模块出厂固件是AT 0.21版本,支持AT+CWJAP连接路由器、AT+CIPSTART建立TCP连接、AT+CIPSEND发送数据。但有些老版本固件用的是AT+CWJAP=后面直接跟SSID和密码,新版本则要求SSID和密码用逗号分隔并加引号。
我的建议是:先用USB转TTL工具把ESP8266接到电脑上,用串口助手发AT+GMR查看版本号,然后发AT+CWMODE=1设置为Station模式,再发AT+CWJAP="你的WiFi名","你的WiFi密码"测试能否连上路由器。这一步在电脑上先跑通,能排除很多硬件和接线问题。
4.2 用状态机管理AT指令的发送与等待
在STM32上驱动ESP8266,最忌讳的就是用delay死等。因为WiFi连接路由器可能需要几秒钟,如果你在等待期间什么都不做,看门狗可能会复位,而且用户体验也很差。正确的做法是用状态机。
我定义了几个状态:WIFI_IDLE、WIFI_SEND_AT、WIFI_WAIT_OK、WIFI_CONNECTING、WIFI_CONNECTED、WIFI_ERROR。主循环里根据当前状态决定下一步动作。比如在WIFI_SEND_AT状态,发送AT+CWJAP指令后切换到WIFI_WAIT_OK,并启动一个超时计数器。如果在规定时间内收到了OK,就进入WIFI_CONNECTED;如果收到ERROR或者超时,就进入WIFI_ERROR,等待一段时间后重试。
这种非阻塞的方式让MCU在等待WiFi连接的同时还能处理按键扫描、传感器读取等任务。实测下来,从发送连接指令到成功获取IP地址,大约需要3到5秒,具体取决于路由器响应速度。
4.3 与机智云服务器的TCP连接建立
WiFi连上路由器后,下一步是连接机智云的服务器。机智云会为你的产品分配一个服务器地址和端口号,这些信息在产品的“基本信息”页面可以找到。通常是一个域名加端口,比如mqtt.gizwits.com:1883或者类似的TCP地址。
你需要用AT+CIPSTART="TCP","服务器地址",端口号来建立连接。这里有个细节:ESP8266的AT指令不支持域名解析的情况很少,但如果你用的是IP地址,直接填IP即可。连接成功后模块会返回CONNECT和OK。然后你需要发送AT+CIPMODE=1进入透传模式,再发AT+CIPSEND开始透传。透传模式下,你从USART2发给ESP8266的所有数据都会被原样发送到服务器,服务器返回的数据也会从USART2出来。
提示:透传模式虽然方便,但退出透传需要发送
+++,而且前后要有至少1秒的静默时间。如果你的程序逻辑比较复杂,建议用非透传模式,每次发送前用AT+CIPSEND=长度指定数据长度,这样控制更精确。
5. 联调过程中最容易卡住的几个环节
5.1 串口乱码:波特率、时钟和电平匹配
串口乱码是最常见的问题,原因通常有三个:波特率不一致、系统时钟配置错误、电平不匹配。正点原子的模板默认使用外部8MHz晶振,经过9倍频后得到72MHz系统时钟。如果你在sys.c里修改了时钟配置,USART的波特率寄存器值会跟着变,导致实际波特率偏离设定值。
排查方法:先用示波器或者逻辑分析仪测一下USART2的TX引脚波形,看看一个位的时间宽度是否等于1/115200秒,大约是8.68微秒。如果偏差超过5%,通信就会不稳定。另外,ESP8266的IO电平是3.3V,STM32F103的USART引脚也是3.3V,直接连接没问题。但如果你用了5V的USB转TTL工具调试,中间必须加电平转换电路,否则可能损坏ESP8266。
5.2 配网失败:路由器设置和信号强度的隐形影响
有时候AT指令返回OK,但就是连不上路由器。我遇到过几次,最后发现是路由器开了AP隔离,或者WiFi密码里包含特殊字符。ESP8266的AT指令对密码中的逗号和引号敏感,如果你的密码里有这些字符,需要转义处理。
另一个容易被忽略的是信号强度。ESP8266的板载天线增益有限,如果路由器在另一个房间,信号强度低于-80dBm时连接会非常不稳定。你可以在连接前发AT+CWJAP?查询当前信号质量,或者用AT+CWLAP扫描周围热点,看看目标路由器的RSSI值。如果太弱,考虑加一个外置天线或者调整模块位置。
5.3 云端收不到数据:数据点绑定和上报格式检查
设备显示已连接服务器,但云端看不到数据,这种情况通常是数据点没有正确绑定。机智云的协议要求设备上报的数据必须包含在已定义的数据点列表中,而且格式要符合协议规范。如果你在代码里直接发送自定义的JSON字符串,云端会直接丢弃。
排查步骤:先在机智云的“设备日志”页面查看是否有原始数据上报记录。如果有记录但解析失败,说明数据格式有问题;如果完全没有记录,说明TCP连接虽然建立了,但数据没有真正发出去。这时候要检查gizwitsHandle函数的调用频率,以及dataPoint_t结构体里的变量是否真的被更新了。我遇到过一次,是因为定时器中断里更新了变量,但主循环里的gizwitsHandle没有及时读取,导致上报的还是旧值。
5.4 设备频繁掉线:心跳间隔与路由器DHCP租期
设备运行一段时间后自动掉线,过一会儿又重连,这种反复横跳很让人头疼。原因可能是心跳间隔设置得太长,路由器或者服务器认为设备已经离线,主动断开了连接。机智云默认的心跳间隔是180秒,但有些路由器会在60秒无数据时断开TCP连接。
我的做法是把心跳间隔改成30秒,同时在设备端加一个TCP连接状态检测。如果连续三次心跳没有收到服务器响应,就主动断开重连。另外,路由器的DHCP租期也会影响,如果租期到了而没有及时续约,IP地址会变化,导致TCP连接失效。建议在路由器里给ESP8266的MAC地址绑定一个静态IP,彻底避免这个问题。
6. 从能跑到好用:稳定性优化的几个实操技巧
6.1 看门狗与异常复位处理
物联网设备经常部署在无人值守的环境,程序跑飞了必须能自动恢复。STM32F103内置独立看门狗和窗口看门狗,我通常用独立看门狗,在主循环里定期喂狗。但要注意,如果WiFi连接状态机卡在某个状态超过看门狗超时时间,设备会复位。这其实是好事,复位后重新初始化往往能恢复正常。
不过,频繁复位会影响用户体验。所以我在状态机里加了超时重试机制:每个状态最多停留10秒,超时后自动切换到错误处理状态,尝试重新初始化ESP8266。只有连续三次重试都失败,才让看门狗复位。这样既保证了自恢复能力,又避免了不必要的重启。
6.2 数据缓存与断线续传
网络不稳定的时候,传感器数据不能丢。我在SRAM里开辟了一个小型的循环队列,用来缓存最近的上报数据。当TCP连接正常时,队列里的数据按顺序发送;当连接断开时,数据继续入队,但不出队。等连接恢复后,先把队列里的历史数据补发,再发送最新数据。
这个队列的大小要根据你的上报频率和预期断线时长来定。假设每5秒上报一次,每次数据20字节,断线10分钟就是120条数据,大约2.4KB。STM32F103有64KB SRAM,拿出4KB做缓存完全没问题。实现时注意队列的读写指针要原子操作,避免中断和主循环同时访问导致数据错乱。
6.3 固件升级的预留设计
产品定型后,难免要修复bug或者增加功能。如果每次都要把设备拆下来接下载器,那就太麻烦了。机智云支持OTA升级,但需要你在代码里预留升级接口。具体来说,就是在gizwitsProduct.c里实现gizwitsOtaInit和gizwitsOtaProcess函数,并在主循环里调用。
OTA升级的原理是:云端把新固件分片下发,设备收到后写入Flash的指定区域,全部接收完成后跳转到Bootloader进行固件替换。这个过程需要你对STM32的Flash编程和中断向量表偏移有一定了解。如果暂时用不上,至少要把Flash空间规划好,给Bootloader留出足够的区域,免得以后想加OTA时发现空间不够。
6.4 日志分级与远程诊断
调试阶段,串口打印越详细越好;产品部署后,日志太多反而影响性能。我建议在代码里实现一个简单的日志分级机制,用宏定义控制输出级别。比如LOG_LEVEL_ERROR只打印错误,LOG_LEVEL_INFO打印关键状态变化,LOG_LEVEL_DEBUG打印所有AT指令交互。
更进一步,可以把错误日志通过MQTT上报到云端的一个专用数据点。这样设备部署在外地,你也能在云端看到它什么时候掉线、什么时候重连、错误码是什么。我实测下来,这个远程诊断功能至少帮我节省了80%的现场排查时间。
7. 我在这个项目里踩过的几个真实坑
第一个坑是串口接收中断的优先级。我一开始把USART2的中断优先级设得比SysTick还高,结果AT指令解析函数在SysTick中断里被调用时,又触发了USART2中断,导致栈溢出。后来把USART2的抢占优先级设为1,SysTick设为0,问题解决。记住:中断优先级数值越小,优先级越高,但不要把所有中断都设成最高优先级。
第二个坑是ESP8266的供电。ESP8266在发射WiFi信号时瞬间电流能达到300mA,如果直接用STM32开发板上的3.3V稳压器供电,电压会被拉低到2.8V以下,导致模块复位。我的解决方案是给ESP8266单独用一个低压差稳压器,输入接5V,输出3.3V,并且并联一个470uF的电解电容来缓冲电流波动。改完之后,模块再也没有无故重启过。
第三个坑是机智云的数据点精度设置。我在云端把温度数据点的精度设成了0.1,但代码里上报的是整数。结果云端显示的温度永远是0。后来发现,协议要求上报的数值必须乘以10再取整,比如25.3度要上报253。这个细节在机智云的文档里写得很隐蔽,我翻了好久才找到。所以,定义数据点时一定要看清楚精度和步长的含义,代码里做好单位换算。
第四个坑是AT指令的响应超时。ESP8266在连接路由器时,如果密码错误,会一直重试而不返回ERROR。我的状态机原本设了10秒超时,但模块重试了30秒才返回失败。后来我把超时时间改成35秒,并且在超时后主动发送AT+CWQAP断开当前连接,再重新开始配网流程。这样即使密码错了,也能在35秒内进入错误处理,而不是无限等待。
8. 后续可以继续折腾的方向
这套基础框架跑通之后,你可以往几个方向扩展。一是增加更多传感器,比如PM2.5、二氧化碳、光照强度,把数据点定义得更丰富,做一个完整的环境监测站。二是加入本地控制逻辑,比如温度超过阈值自动打开风扇,不依赖云端指令,提高响应速度。三是研究低功耗模式,用STM32的Stop模式配合ESP8266的睡眠模式,让设备用电池也能跑几个月。
如果你对协议底层感兴趣,可以尝试不用机智云的自动生成代码,自己实现MQTT协议栈。这样虽然工作量大,但能完全掌控通信过程,也更容易适配其他云平台。不过对于大多数应用场景来说,用现成的协议栈把产品快速做出来,才是更务实的选择。毕竟,能把东西跑通、稳定运行,比什么都重要。