简介:STM32 单片机通过 ESP8266 接入机智云平台的完整工程资料,采用标准函数库(非 HAL 库)实现,适合熟悉寄存器操作、追求底层灵活性的嵌入式开发者。资源包共 153 个文件,解压后约 53MB,涵盖 38 个 .h、37 个 .c 源文件,以及 bmp 图片、conf/ini 配置文件、bin 固件、Keil 工程文件(uvprojx/uvoptx)等,另附 APK 客户端示例、AT 固件、PDF 和 readme 文档,便于系统性对照学习。已有 1109 人下载学习,可用于快速搭建 STM32+ESP8266 的物联网实验平台,理解标准库下 UART 初始化、AT 指令交互、Wi-Fi 配网以及与机智云平台的数据上行和控制下行流程。配套的 GAgent 固件覆盖 16Mbit/32Mbit 等不同 Flash 容量,并带有密钥与配置示例,能够帮助开发者规避常见连接问题,适合课程设计、毕业设计或物联网产品原型开发。
1. 平台发下来的是 HAL 库,标准函数库工程要的是协议栈而不是整个模板
看到“平台生成的是 HAL 库”这句话,很多人第一反应是把手里的标准函数库工程推倒重来。其实机智云接入这件事,核心不在 HAL 还是标准库,而在 ESP8266 里跑的固件和你 STM32 上的串口数据组织方式。ESP8266 刷上 GAgent 固件后,TCP、TLS、云端长连接这些活全在 8266 上干完了,STM32 要做的只是按固定帧格式通过串口收发数据。平台生成 HAL 库工程是一回事,你自己维护多年的标准库工程是另一回事,两者真正有差异的只有串口收发、毫秒计时这三个底层接口,协议解析那几百行代码完全可以原样搬过来。下面按“剥协议栈、改标准库接口、跑通上报下发、抓帧验证”的顺序,给一套能在 Keil MDK 5 下直接落地的手工移植路线,适合手上有存量标准库工程、又不想为其换血的开发者。
2. 先搞清楚 ESP8266 跑 GAgent 固件、STM32 跑协议栈的分工
很多人拿到样例会习惯性地把 ESP8266 当成 AT 模块,发 AT 指令去连网,这套思路接机智云会立刻卡住。机智云的典型接入结构里,ESP8266 负责网络侧所有事情,MCU 只和它保持一个串口协议对话,方向搞对,后面移植才有意义。
2.1 GAgent 固件装进 ESP8266 后,它替你干了哪些活
机智云接入分 SoC 方案和 MCU+WiFi 方案。标题里 ESP8266 只是连接平台,所以走的是 MCU+WiFi:ESP8266 刷 GAgent 固件,它内部已经封装了 WiFi 配网、连接路由器、云端认证、心跳维持、数据透传。STM32 不需要知道任何 TCP 或 MQTT 细节,只需要在收到 GAgent 发来的帧之后,取出数据点事件做业务处理,再把业务数据按帧格式回发。
GAgent 固件和 STM32 之间是串口通信,常见参数如下:
| 参数 | 常见默认值 | 说明 |
|---|---|---|
| 波特率 | 9600 | 部分固件支持 115200,必须和固件配置一致 |
| 数据位 | 8 | 几乎都是 8 数据位 |
| 停止位 | 1 | 无奇偶校验 |
| 逻辑电平 | 3.3V TTL | 不能直接接 5V 单片机串口,需要电平转换 |
容易踩的坑是把乐鑫原厂 AT 固件当成 GAgent 用。AT 固件和 GAgent 在相同引脚上一上电就狂发乱码或者毫无反应,因为它俩的启动流程和串口输出内容完全不同。GAgent 上电后会主动发送“握手包”之类的协议帧,而不是等着你发 AT 指令,这个差异用串口助手一眼就能看出来。
2.2 平台生成 HAL 库工程,为什么协议栈层却和库无关
机智云代码生成器给 STM32 的模板一般是 STM32CubeMX 结构,所以拿到手是 HAL 库,但你在 Keil 里打开工程会发现,真正和芯片强相关的代码只集中在串口驱动、延时和毫秒 tick 三处。协议栈的解析、组帧、状态机是纯 C 逻辑,不直接调用HAL_UART_Transmit这类函数,而是调用你实现的外发接口。
以常见文件结构为例,移植时按这张表对待:
| 文件 | 作用 | 移植处理方式 |
|---|---|---|
| gizwits_protocol.c / .h | 协议帧解析、CRC 校验、命令处理 | 原样复制,不要改动 |
| gizwits_product.c / .h | 产品密钥宏、数据点初始化、用户事件回调 | 复制后修改宏和回调内容 |
| gizwits_transport.c / .h | MCU 与 GAgent 的串口收发中转 | 只保留接口骨架,内部换成标准库实现 |
| 串口硬件驱动文件 | USART 初始化和中断处理 | 完全用你工程里标准库的写法替换 |
理解了这层关系,你就明白 HAL 库模板只是协议栈的“宿主”,协议栈本身依赖的是你提供进来的四个能力:能发字节、能收字节、能拿当前毫秒数、能延时。标准函数库工程里只要把这四个能力补齐,跑起来的效果和 HAL 工程没有任何区别,云端的报文都是一样的。
2.3 上报、下发、配网三条链路的真实流向
上报链路是:传感器读数填进currentDataPoint对应的数据点字段,然后调gizwitsReportData(),协议栈把数据点按产品定义序列化成帧,通过发送接口交给 8266,8266 再推到云端。这个链路里最容易被忽略的是上报频率,数据点上报间隔不要太激进,一般 1 秒左右一次足够,过快的上报会把 8266 和云端的连接刷出问题。
下发链路反过来:App 操作产生命令,云端推给 8266,8266 从串口发出帧,你的串口接收中断把字节逐个喂给协议栈,协议栈解析后触发userEventProcess回调,你在回调里根据事件类型去开继电器、调速、改亮度。配网则是独立的第三条路,代码里触发 AirLink 或 SoftAP,手机 App 把 WiFi 账号密码通过广播或者热点模式交给 8266,配网成功后 GAgent 会以特定事件帧通知 MCU,常见做法是在这个事件里点亮一个 LED 提示用户。
3. 把 HAL 库工程里的机智云协议栈搬进标准函数库工程
平台生成的是 HAL 库,你要交付出标准函数库版本,最稳妥的不是一行行翻译 HAL API,而是把生成工程里协议栈文件整个拷进自己的标准库工程,再局部替换底层接口。这一章给出硬件连接、文件搬运和关键代码三个步骤。
3.1 先做硬件接线,别让电平差异变成第一道事故
STM32 和 ESP8266 之间只需要三根线加一个共地。以 STM32F103C8T6 为例,USART1 的 PA9 接 8266 的 RXD,PA10 接 8266 的 TXD,两边的 GND 必须连在一起,否则串口波形完全没有参考电平,现象是 8266 能启动但 8266 收到的全是乱码。ST 的大部分主流型号串口是 3.3V TTL 电平,和 ESP8266 模块电平一致,可以直接连。
如果用的是 5V 供电的板子而非 3.3V 核心板,建议加一级电平转换,或者用分压电阻把 5V 侧 TX 降到 3.3V 再进 8266。8266 的 RXD 引脚耐压并不高,长期接 5V 有概率烧模块,这个坑在批量交付时尤其要早发现。供电也别用 STM32 核心板上的3.3V引脚硬带 8266,8266 启动瞬间电流接近 300mA,很多开发板上的 LDO 扛不住,表现为上电后灯闪一下就灭。常见做法是单独给 8266 一路 3.3V 电源或选择带稳压和天线的现成模组底板。
3.2 从生成工程里挑该复制的文件,并在标准库工程建好分组
先在 Keil 里建一个空的标准库工程,芯片包选好对应型号,工程选项里定义USE_STDPERIPH_DRIVER和STM32F10X_MD,再把标准外设库的src、inc路径加进 C/C++ Include Paths,这是标准函数库新建工程的基本盘。然后把机智云生成工程中Gizwits目录的内容整体拷贝到自己的工程目录。
在 Keil 里新建一个分组叫Gizwits,把gizwits_product.c、gizwits_protocol.c、gizwits_transport.c加进去,头文件路径指到对应目录。打开gizwits_product.h,把里边的产品密钥宏替换成你自己产品页面上分配的GIZWITS_PRODUCT_KEY和GIZWITS_PRODUCT_SECRET,这两处如果保留成模板值,会出现设备始终连接不上、或连上了无法绑定产品的诡异问题。修改时注意一下宏名在不同版本里的差异,以生成工程里实际注释标注为准。
| 检查点 | 操作方法 | 出错后果 |
|---|---|---|
| 芯片启动文件 | 确认用的是标准库配套的startup_stm32f10x_md.s | 启动即跑飞 |
| 宏定义 | 加上USE_STDPERIPH_DRIVER | 外设头文件报错 |
| 密钥替换 | 换成自己产品的密钥 | 无法入网 |
| 中断函数名 | 和启动文件保持一致 | 串口中断不触发 |
3.3 用标准库重写串口发送、接收中断和 1ms 心跳
协议栈对外发数据的接口一般是gizwits_transport.c里的某个函数,它接收一个字节缓冲区,你在这里直接操作标准库的USART_SendData循环发送。标准库发送一个字节有两个标志位可以等,发送数据寄存器空TXE和发送完成TC,严谨的做法是等TC,保证最后一个字节真正从移位寄存器发出,而不是只进了数据寄存器:
#include "stm32f10x.h" void giz_uart_send_bytes(uint8_t *buf, uint16_t len) { for (uint16_t i = 0; i < len; i++) { USART_SendData(USART1, buf[i]); // 写入发送数据寄存器 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待移位寄存器发完 } }这里USART_SendData只是把数据放进数据寄存器,如果不管TC标志就连着发下一字节,高频下发命令时会丢最后一个或几个字节。接收方向,标准库的中断处理和 HAL 的写法差异很大,HAL 把HAL_UART_IRQHandler包了一层,标准库要求直接在中断服务函数里读状态寄存器,再读数据寄存器:
void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t byte = (uint8_t)USART_ReceiveData(USART1); // 先读SR再读DR,自动清RXNE extern int8_t gizPutData(uint8_t *pData, uint16_t len); // 以生成代码中的原型为准 gizPutData(&byte, 1); // 把新收到的字节交给协议栈 } }按标准函数库要求,读USART_ReceiveData会顺带清除RXNE标志,所以在中断里不要画蛇添足再去手动清标志。gizPutData的原型不同版本有差异,有的接收单字节参数,有的接收指针和长度,照生成工程里的声明去适配即可。移植完成一个实用自检点是:在这个中断函数里加一个计数变量,每进入一次加一,配网时用 debugger 观察计数值跳动,能立刻判断中断有没有被使能。
毫秒 tick 是另一个高频坑点。HAL 工程里有HAL_GetTick(),标准库工程没有这个函数,你需要在SysTick_Handler里维护一个全局毫秒计数。先调用SysTick_Config(SystemCoreClock / 1000)配置 1ms 中断,再实现以下三个东西:
static volatile uint32_t g_ms_ticks = 0; void SysTick_Handler(void) { if (g_ms_ticks < 0xFFFFFFFF) { g_ms_ticks++; } } uint32_t get_ms_ticks(void) { return g_ms_ticks; } void delay_ms(uint16_t ms) { uint32_t start = get_ms_ticks(); while ((get_ms_ticks() - start) < ms) { // 等待,不开放中断调度 } }SysTick_Handler这个名字必须和启动文件保持一致,启动文件里叫SysTick_Handler你就不能改成别的名字。如果你的标准库工程里原本已经写了原子级的delay_us定时器,要检查它是否占用了SysTick。协议栈要求的是一个单调递增的毫秒计数器,很多移植失败的现象是协议栈超时判断错乱,在 Debug 窗口里看g_ms_ticks是否增长就能定位问题。
4. 在标准函数库工程里跑通数据点上报和命令下发
接口层替换完,剩下的事情在两处:一是把产品数据点填进上报结构体,二是在事件回调里响应平台下发的命令。这两个动作都在gizwits_product.c里完成,它会被协议栈周期调用,不要在里面做阻塞式延时。
4.1 数据点映射与回调函数的最小改动
以智能插座为例,产品上定义了两个数据点:一个布尔型开关switch_1,一个整型电量power。在userHandle()里持续刷新要上报的值,该函数会被周期性调用,上报周期由协议栈控制:
void userHandle(void) { if (get_ms_ticks() - last_report_ms >= 1000) // 每1秒上报一次,避免冲刷云端 { currentDataPoint.value.switch_1 = read_relay_state(); // 读取实际继电器状态 currentDataPoint.value.power = read_power_meter(); // 读取计量芯片数值 gizwitsReportData(0); // 0表示非告警上报 last_report_ms = get_ms_ticks(); } }上报不是越勤越好。云端有频率限制,机械性高频上报会被风控丢弃或触发限流,一般 1 秒即可。currentDataPoint的字段名里带上产品定义的前缀,比如value.switch_1,这些字段名在gizwits_product.h里由生成器自动生成,不要自己去猜。上报前先确认对应传感器已经稳定,避免把瞬时抖动值直接推上云。
命令下发走的是userEventProcess回调,平台下发的每个命令都会到这里。通常事件类型什么含义,直接看生成代码里的枚举,常见的是WIFI_STATION_CONNECTED、WIFI_GOT_IP、以及 ACTION 型命令事件:
void userEventProcess(eventInfo_t *eventInfo, dataPoint_t *dataPoint) { switch (eventInfo->event) { case EVENT_switch_1: control_relay(dataPoint->value.switch_1); // 执行开关动作 break; case WIFI_GOT_IP: turn_on_net_led(); // 连上路由器,点亮状态灯 break; default: break; } }EVENT_switch_1这类宏由生成器根据数据点名字生成,每个产品不同。回调里只做事,不调delay_ms,因为协议栈的状态机在回调返回后还要继续推进,在里面阻塞会直接拖垮整个通信。如果执行动作耗时较长,比如继电器吸合需要 50ms,可以把动作丢到外部状态机去处理,回调只置一个执行标志位。
4.2 主循环里调用 gizwitsHandle 的正确姿势
整个协议栈的驱动靠gizwitsHandle推进,它要做协议解析超时判断、状态机轮询和心跳维护。很多人在while(1)里直接抄例程,却发现一加自己的业务代码就掉线,原因是主循环被业务阻塞,gizwitsHandle没得到及时调度。主循环的写法应该是高频轮询,避免长任务拦断:
int main(void) { system_init(); // 时钟、GPIO、USART1、SysTick 初始化 gizwitsInit(); // 协议栈初始化,注册产品信息 while (1) { gizwitsHandle(); // 协议栈轮询,越快越频繁越好 update_led_status();// 业务任务,必须简短 if (read_config_key() == KEY_PRESSED) { gizwitsSetMode(SOFTAP_MODE); // 按键触发配网 } } }gizwitsHandle()的执行时间很短,但要求被反复调用,所以主循环里不要出现delay_ms(1000)这种粗粒度延时,等待逻辑一律用非阻塞计时实现。配网触发一般接一个 GPIO 按键,短按进入 SoftAP 模式,长按清空配置,这些模式常量在生成头文件里会有定义。业务任务如果必须做耗时操作,拆成状态机分帧执行,保证gizwitsHandle在 10ms 级别至少被调用一次。
4.3 配网流程和云端状态核对
配网有两种常见方式。AirLink 适合初次使用,App 把 WiFi 名和密码以特定编码广播出去,8266 在混杂模式下收帧解析,缺点是部分路由器会过滤广播包,成功率不稳定。SoftAP 模式是 8266 自己发一个热点,手机连上这个热点后把 WiFi 信息写入,这种方式成功率最高,也是调试期优先用的方式。启动 SoftAP 后,8266 会有一个独立 WiFi 热点出现,名称带设备标识,手机连上去在机智云 App 里完成配置。
配网成功与否不要靠猜,看串口日志。GAgent 和 STM32 之间的串口线上的帧是可见的,把 USB 转 TTL 同时挂到 TX/RX 线上抓包,协议帧头通常是一段固定字节,如FF FF开头。配网成功后能看到设备状态类型的事件帧,手机 App 端设备会从“离线”变成“在线”,这时候向 App 下发一次开关命令,观察EVENT_switch_1分支里的执行结果,链路就是通的。下表是对照判断:
| 观察点 | 正常表现 | 异常指向 |
|---|---|---|
| 8266 指示灯 | 配网中快闪,配网成功后慢闪或常亮 | 密钥不对或路由器信号弱 |
| 串口日志 | 出现周期性的状态上报帧 | 长时间无帧则协议栈没跑起来 |
| App 设备在线 | 显示在线 | gizwitsHandle 没被调用 |
| 下发命令 | 设备执行动作 | 事件宏与字段名不匹配 |
5. 移植完成后最容易踩的三个坑:灯灭、只收一次、抓不到帧
前几章解决了能不能跑起来,最后这章解决跑起来之后现场反馈最多的三个症状。每个症状背后基本都能对应到一处具体代码,而不是玄学。
5.1 ESP8266 上电灯灭:先查固件、供电和波特率
“机智云配置是 esp8266 灯灭了”这句话是调试群里出现频率最高的描述。灯灭分两种:上电从头到尾不亮,这是供电问题,8266 没启动;上电闪了一下然后灭,多半是启动电流拉垮了稳压源,或者 GAgent 固件没刷进去,模块进入了异常状态,常见做法是先用 USB 转 TTL 单独给 8266 供电和刷机,确认模组自己能稳定运行,再接回 STM32。刷好 GAgent 后不要用 AT 固件测试脚本去发指令,两者波形完全不同。波特率也常被忽略,GAgent 固件默认 9600,但如果你之前刷过 115200 的配置,STM32 侧还是 9600,就会表现为 8266 收得到但 MCU 解析全错,抓串口波形看到的全是乱码。
5.2 串口中断只收一次:多半是标志位和使能逻辑没配对
“串口中断接收只收一次”在 HAL 和标准库工程里都会碰到,原因不同。HAL 工程常见的是HAL_UART_Receive_IT一次只能接收一个字节,接收完必须重新调用一次,忘了重调用就永远只接一个字节。而标准库工程从 HAL 模板迁移过来时,常见的错误是在中断里顺手调用了USART_ITConfig(USART1, USART_IT_RXNE, DISABLE)想清标志,结果下次再没有使能它的地方,于是中断只触发一次。标准库不需要主动清 RXNE,读数据寄存器就是清标志。另外检查NVIC_Init是否配置了USART1_IRQn并使能,还要确认启动文件里的中断函数名没有拼错,比如把USART1_IRQHandler写成了USART1_HANDLER,这种情况编译不报错,但中断永远不进来。
5.3 抓不到帧:把监听点放在 MCU 与 8266 之间而不是云端
排查到最后层面的手法是抓协议帧。云端上看不到帧细节,你应该直接监听 STM32 的 TX 引脚和 ESP8266 的 RX 引脚。USB 转 TTL 和逻辑分析仪都行,用串口助手以十六进制显示,抓到的帧会以FF FF这样的头字节开始,中间是长度、命令字、序列号和数据区。如果抓不到任何帧,先确认 USB 转 TTL 的 RX 接在了 MCU 的 TX 上,且两边共地;如果抓到了帧但没有周期上报,说明gizwitsHandle没被调用或userHandle卡死;如果上报周期异常快,检查get_ms_ticks有没有被复位过。抓帧这个动作建议直接写成一个固定操作流程:上电后 3 秒内看到握手帧,配网后看到状态帧,云端下发后看到命令帧,三个节点确认完,整个移植链路基本没有隐藏问题。
本文还有配套的精品资源,点击获取