资讯详情

资讯详情

STM32移植TencentOS tiny MQTT组件实现物联网云接入

1. 项目概述与整体设计思路1.1 为什么要做这个移植物联网设备接入云端MQTT几乎是绕不开的一个协议。它轻量、基于发布订阅模型、支持QoS分级非常适合资源受限的MCU设备。嵌入式开发中怎么让设备稳定地连上MQTT服务器收发消息是无数开发者想搞清楚的问题。如果自己从零写一套MQTT协议栈先不说工作量光是处理CONNECT报文、心跳保活、重连机制、主题订阅这些逻辑就能耗掉好几个星期。而且自己写的协议栈很难保证在各种异常网络环境下都能稳定运行——掉线重连、消息丢包、服务器主动断开这些场景全都需要大量打磨。所以这次选择了一条更务实的路线直接使用TencentOS tiny操作系统自带的MQTT组件把它移植到STM32开发板上。TencentOS tiny是腾讯开源的物联网实时操作系统内核代码量精简通信组件齐全关键是有完整的MQTT协议栈实现API封装也足够干净。移植完成后业务逻辑只需要关注数据的发布和订阅协议细节全部由组件处理。这里要提前说清楚整个项目的最终目标和应用场景。移植完成后STM32能做的事情包括通过MQTT协议连接远程服务器周期性上报温度、湿度、开关状态等数据订阅云端下发的指令主题根据消息内容控制LED、电机、继电器等外设支持掉线自动重连消息丢失处理适配弱网环境在TencentOS tiny的多任务调度下MQTT通信与传感器采集、外设控制并行运行互不干扰文章的受众其实很明确已经熟悉STM32基础开发GPIO、串口、定时器这一级别想接触物联网操作系统和网络通信但还不清楚RTOS里跑网络协议这条路该怎么走的人。文章会从开发环境搭建讲到最终验证完整展示整个移植过程。1.2 方案选型背后的核心考量先讲讲为什么选这套组合而不是其他方案。第一个问题是为什么不直接用裸机网络协议栈很多STM32项目的网络通信都是靠裸机while循环轮询来处理的。这种方式对于简单的TCP透传够用但一旦涉及MQTT保活、QoS重传、多任务调度这些复杂逻辑裸机轮询的实时性缺陷就暴露出来了。MQTT客户端需要周期性发送PINGREQ报文维持连接需要响应服务器下发的数据还要在应用层同时处理传感器采集任务——单靠一个超循环代码会变成一锅粥。第二个问题是为什么选TencentOS tiny而不是FreeRTOSFreeRTOS当然也能干这件事市面上也大量使用。但区别在于TencentOS tiny的定位就是物联网操作系统它把网络协议栈、MQTT客户端、OTA升级这些物联网场景常见组件都做进了仓库里而且是官方维护、测试过的。用FreeRTOS的话你需要自己找MQTT客户端库自己适配网络接口不少坑需要自己趟。TencentOS tiny这套东西已经把路铺好了移植的工程量集中在处理板级差异上效率会高很多。第三个问题是网络接入方式怎么选STM32板子本身没有网口想要连上网络通常有两条路板载以太网接口比如W5500、LAN8720等外扩以太网芯片外接WiFi模组比如ESP8266、ESP32之类这次选择的是通过以太网接口连接用lwIP协议栈做网络支撑。原因有两个一是以太网连接稳定调试方便没有WiFi信号干扰这个问题干扰变量二是TencentOS tiny的适配层中lwIP的支持已经非常成熟基本就是配置网络接口参数的事。整个技术栈的架构可以用一个简单的分层来描述硬件层STM32主控 以太网物理层收发器内核层TencentOS tiny操作系统负责任务调度、内存管理、信号量同步网络层lwIP协议栈提供TCP/IP网络能力协议层TencentOS tiny的mqtt_client组件封装MQTT协议逻辑应用层业务代码完成数据采集、主题发布、指令处理这种分层结构的好处是每一层都是可替换的。以后想换成WiFi接入只需要改网络层适配想换云端平台只要改服务器地址和主题命名协议层和应用层不用大动。2. 开发环境准备与工程搭建2.1 硬件与软件清单先把这个项目需要的所有东西列个表方便照单准备类别项目说明硬件STM32开发板选用带以太网接口的板型主控为STM32F407系列硬件以太网PHY芯片板上自带的LAN8720通过RMII接口连接主控硬件网线连接路由器和开发板保证在同一局域网内硬件PC用于开发调试需要联网访问MQTT服务器软件Keil MDK5STM32开发的主流IDE配合AC5或AC6编译器软件STM32CubeMXST公司官方配置工具用于生成初始化代码软件TencentOS tiny源码从官方仓库克隆包含内核和各组件软件串口调试助手查看开发板日志输出关于硬件补充说一句STM32F407是带以太网MAC控制器的型号配合外部的PHY芯片就能实现以太网通信。开发板上LAN8720是常见的搭配组合。如果你手里的板子没有以太网那需要先解决网络接入问题比如通过ESP8266转WiFi这个方案我们后面在常见问题部分单独展开。2.2 工程目录结构与TencentOS tiny的添加方式拿到TencentOS tiny源码后首先需要把它的核心部分整理到自己的工程里。不要直接把整个仓库全部塞进工程那样文件太多太乱编译也慢。这篇移植里我实际用到的是几个核心目录kernel/TencentOS tiny内核源码包含任务管理、内存管理、调度器arch/架构相关代码比如Cortex-M4的上下文切换、SysTick适配cmsis-os/CMSIS-RTOS接口层部分项目用不到可以裁剪掉net/网络相关内容包含lwIP适配层components/connectivity/mqtt/MQTT客户端组件这是这次最核心的部分在Keil工程里我采用的添加方式是分组管理Group方式每一个模块对应一个Group内核组把kernel/下的core、sched、mm等子目录的源码全部加进去架构组加入arch/arm/arm-v7m/下的port文件网络组加入lwIP相关的源文件和TencentOS tiny的net适配层MQTT组加入mqtt_client组件的所有源码应用组自己写的业务代码这种分组方式的优势很直观编译报错时能通过错误定位到具体是哪个模块的问题做裁剪配置时也能清晰地看到每个模块的组成知道删掉哪个文件会影响到什么。2.3 核心配置文件的关键参数说明TencentOS tiny的几乎所有内核配置都在tos_config.h这个文件里。这个文件是裁剪的关键里面有大量的宏定义开关。我挑几个这次移植中影响比较大的参数来说#define TOS_CFG_CPU_ARCH TOS_CPU_ARCH_ARM_V7M #define TOS_CFG_CPU_CLOCK 168000000UL // 根据实际主频修改 #define TOS_CFG_CPU_TIMER_FREQ 1000UL // 系统节拍频率1kHz #define TOS_CFG_TASK_POOL_SIZE 0 // 不开启任务池用动态创建模式 #define TOS_CFG_MMHEAP_EN 1 // 开启内存堆MQTT客户端需要动态分配 #define TOS_CFG_MMHEAP_POOL_SIZE 20 * 1024 // 内存池大小必须足够大 #define TOS_CFG_MUTEX_EN 1 // 互斥锁多任务共享资源会用到 #define TOS_CFG_MSG_EN 1 // 消息队列任务间通信需要 #define TOS_CFG_TIMER_EN 1 // 软件定时器MQTT心跳函数需要 #define TOS_CFG_NET_EN 1 // 开启网络组件总开关 #define TOS_CFG_MQTT_EN 1 // 开启MQTT组件总开关最值得注意的地方是内存堆的大小。MQTT客户端在建立连接、发布消息、处理订阅回调时都会动态分配内存尤其是接收缓冲区、发送缓冲区和主题过滤器的分配非常吃内存。我第一次按默认配置跑的时候一连接MQTT服务器直接宕机排查半天发现就是堆内存不够用。最终把TOS_CFG_MMHEAP_POOL_SIZE调到20KB后整个系统才稳定运行。还有一点要特别讲系统时钟节拍频率。默认值是1000Hz也就是1ms一个tick这个频率对物联网场景够用。如果你初始化了但系统没有跑起来优先检查这个值和TOS_CFG_CPU_CLOCK是否和实际硬件匹配SysTick中断能不能正确触发。3. 网络接口适配与lwIP移植3.1 网络硬件初始化流程TencentOS tiny本身不带以太网MAC驱动它需要配合底层驱动程序来实现网络数据收发。STM32F407内部的以太网MAC控制器需要做的工作包括GPIO复用配置、DMA描述符初始化、PHY芯片的MDIO访问等。在移植中这一步是通过STM32CubeMX生成的HAL库代码来完成的。配置流程是这样的在CubeMX中使能ETH外设选择RMII接口模式PHY地址根据硬件原理图填写常见的LAN8720地址是0生成初始化代码后把MX_ETH_Init()调用添加到网络初始化流程中配置PHY芯片的复位引脚确保上电时序正确完成MAC层初始化后还需要验证物理连接状态。我踩过一个坑开发板插上网线后LED灯不亮PHY的link状态读出来一直不对。查了很久最后发现是PHY芯片复位引脚没有操作LAN8720一直没有正常启动。这个复位的动作lwIP的low_level_init函数里需要额外处理一下——拉低引脚一段时间再拉高完成后才能正常读取PHY寄存器。3.2 TencentOS tiny如何与lwIP对接lwIP是一个独立的TCP/IP协议栈它和RTOS之间需要一层适配。TencentOS tiny官方已经提供了这层适配代码。它的原理并不复杂本质是这几件事信号量把lwIP里需要的sys_sem_new、sys_sem_signal等锁操作映射到TencentOS tiny的信号量API内存把lwIP的mem_malloc对接到底层的内存池管理线程为lwIP的tcpip_thread创建独立的系统任务负责处理网络事件在代码层面这体现在tos_net.c这个适配文件里。当你在配置文件中开启了TOS_CFG_NET_EN后系统的初始化流程会自动注册网络组件。我只需要在应用初始化时手动添加一个网络设备把这个设备与lwIP的netif结构绑定就能宣告TencentOS tiny已经可以上网了。这个过程中最关键的一个参数是IP地址的获取方式。开发环境里建议先使用静态IP减少变量。比如开发板IP地址192.168.1.100子网掩码255.255.255.0网关地址192.168.1.1用静态IP调试的好处是后续做MQTT连接测试时能快速定位是网络层问题还是协议层问题不会被DHCP分配超时之类的问题干扰。等整套流程跑通之后再改成DHCP动态获取IP体验一下更灵活的工作方式。3.3 网络连通性验证在开始MQTT移植之前必须先把网络通路打通。这一步非常关键不然MQTT代码写得再好也白搭——TCP连接都建立不起来。通过网络调试工具进行验证是一个好办法。我当时的做法是开发板通过网络发送一个UDP广播包或者ping请求在PC上用抓包工具观察。更直接的方法是在开发板上运行一段简单的TCP客户端代码连接PC上开启的TCP服务器端口看能否正常发送和接收数据。这里我也要提醒一个问题调试时PC防火墙经常会把连接拦掉。如果发现代码逻辑完全正确但TCP连接就是失败先去检查防火墙是否放行了对应端口。4. MQTT客户端组件的集成与配置4.1 使能MQTT组件TencentOS tiny的组件不是默认全部编译进去的需要通过配置文件打开。前面已经在tos_config.h的片段里看到了两个关键的宏#define TOS_CFG_NET_EN 1 #define TOS_CFG_MQTT_EN 1但光配置这两个还不够编译时还需要把MQTT组件对应的源文件包含进工程。在TencentOS tiny的目录结构下MQTT组件的代码位于components/connectivity/mqtt/大多数情况下直接全部添加即可不需要做切割。如果你是从GitHub仓库拉的最新代码建议先看下这个目录下的README或者源码注释因为不同分支的组件代码依赖会有细微差异。常见的做法是保证工程里同时包含MQTT组件的c文件和头文件路径。4.2 MQTT客户端初始化流程分析MQTT客户端的初始化逻辑非常清晰核心步骤可以用一段代码来描述#include tos_mqtt.h // MQTT客户端句柄 mqtt_client_t mqtt_client; // 连接参数 mqtt_client_config_t mqtt_config; static void mqtt_init_and_start(void) { // 1. 协议名称配置3.1协议版本写法是MQTT mqtt_config.mqtt_client_id stm32-device-001; mqtt_config.mqtt_user_name test_user; mqtt_config.mqtt_password test_password; mqtt_config.mqtt_conn_timeout_ms 3000; mqtt_config.mqtt_alive_time_ms 60 * 1000; // 心跳保活60秒 mqtt_config.mqtt_clean_session 1; mqtt_config.mqtt_version MQTT_VERSION_3_1_1; // 2. 设置服务器IP与端口 mqtt_config.mqtt_host 192.168.1.50; // 换成你的MQTT Broker地址 mqtt_config.mqtt_port 1883; // 3. 初始化客户端 mqtt_client_init(mqtt_client, mqtt_config); // 4. 连接到服务器 mqtt_client_connect(mqtt_client); }有几个参数的取值逻辑这里展开讲一下Client ID是必须唯一标识设备的。如果在同一台Broker上多个客户端用同一个Client ID连接前面的那个客户端会被强制挤下线。测试时这个参数看着不起眼但很容易在设备反复重连时引起混淆。心跳保活时间mqtt_alive_time_ms要合理配置。这个值决定了MQTT客户端多久发一次PINGREQ报文服务器如果在1.5倍这个时间内没有收到任何报文就会认为连接已断开。如果设备处于频繁上报数据的场景数据报文本身就在维持连接心跳间隔可以设长些如果设备只是被动等待指令心跳间隔就相应设置短一些。太长的保活时间会导致掉线后服务端不能及时感知太短的保活时间会白白增加网络流量。clean_session的设置决定了会话状态的处理方式。设置为1就是每次连接都是全新会话不保留历史订阅简单直接适合大多数数据上报场景。设置为0时如果设备掉线重连断线期间服务器积攒的消息会在重连后继续推给设备不过QoS为0的消息依旧不被保证。从专业角度说MQTT协议的QoS机制值得再多说几句。QoS 0是最多交付一次消息发出后不确认、不重发适合环境数据上报这类丢一两帧无所谓的场景QoS 1是至少交付一次通过PUBACK确认保障消息送达但可能出现重复QoS 2是恰好一次通过四步握手确保不重不漏代价是性能开销最大。在嵌入式低带宽场景下默认用QoS 0或者对关键控制指令用QoS 1是合理的组合。实际测试时我的传感器数据用了QoS 0设备控制指令用了QoS 1既保证了效率也兼顾了控制类消息的可靠性。4.3 主题订阅与发布函数的调用示例完成连接之后剩下就是业务编程了。拿STM32最典型的上报场景——周期采集温湿度并上报云端以及接收云端指令控制LED——具体代码示例是这样的// 订阅回调函数处理云端下发的指令 void subscribe_callback(void *client, mqtt_message_t *msg) { if (strncmp((char *)msg-payload, LED_ON, 6) 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 可以追加一句应答反馈 mqtt_client_publish(mqtt_client, device/001/ack, led_on, 6, 0); } else if (strncmp((char *)msg-payload, LED_OFF, 7) 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } } void mqtt_test_task(void) { // 订阅云端指令主题 mqtt_client_subscribe(mqtt_client, device/001/cmd, 0, subscribe_callback); while (1) { // 读取传感器数据省略具体读取代码 int temperature read_temperature(); char payload[32]; sprintf(payload, {\temp\:%d}, temperature); mqtt_client_publish(mqtt_client, device/001/data, payload, strlen(payload), 0); tos_task_delay(5000); // 每5秒上报一次 } }从这段代码可以看到mqtt_client_subscribe函数把主题和回调函数绑定在一起一旦服务器推送消息到该主题回调函数就会在MQTT组件的线程上下文里被调用。处理主题消息时要特别注意不要在回调里做耗时操作比如在回调里加一个HAL_Delay会导致MQTT线程被阻塞心跳保活也发不出去连接就容易被服务端断开。正确做法是把消息内容拷贝出来通过消息队列或者信号量通知业务任务去处理。4.4 用户任务如何创建与运行TencentOS tiny上跑MPTT我上面用MQTT就行这里提到的MPTT是输入内容里的同义词实际指的就是MQTT业务通常是先创建连接然后创建独立的业务任务。任务的定义方式如下k_task_t app_task; uint8_t app_task_stack[2048]; static void app_task_entry(void *arg) { mqtt_init_and_start(); // 初始化并连接MQTT mqtt_test_task(); // 执行业务循环 } void application_init(void) { tos_task_create(app_task, app, app_task_entry, NULL, app_task_stack, sizeof(app_task_stack), 4); }这里有个任务栈大小的细节。栈的大小取决于业务代码的调用深度如果任务里用了sprintf这种格式化函数、连接了调试打印、再调用MQTT发布栈消耗会在几百字节到一千多字节。建议直接给2KB起步调试稳定后再裁剪。任务栈设得太小会出现诡异的运行崩溃而且这种崩溃往往难以复现排查效率极低。5. 调试过程与问题排查实战5.1 网络层问题的定位方法在这类移植项目中网络问题是占比最高的故障来源。我实际调试中遇到过的情况基本可以用一个检查顺序来定位。第一步查看网口指示灯是不是亮的不亮说明物理链路有问题确认亮灯后第二步检查IP地址是否配置正确能不能ping通网关第三步如果ping不通检查PHY配置和RMII引脚复用。下面直接上一段我在调试中用来排查网络问题的示例代码在这里打印lwIP提供的网络状态信息void network_status_report(void) { struct netif *net_if netif_default; if (net_if NULL) { printf([NET] no default netif\r\n); return; } printf([NET] link %d, ip: %s\r\n, netif_is_link_up(net_if), ipaddr_ntoa(net_if-ip_addr)); }实际调用时如果netif_is_link_up返回0那首先应该排查的就是网线或者PHY链路而不是去翻MQTT代码。这类先分清是哪一层的问题的思路在面对多层协议栈组合时非常关键。5.2 MQTT连接失败排查流程如果网络已经通但MQTT连接不上问题可能出在这些环节现象可能原因排查动作连接超时服务器地址/端口错误核对Broker的IP和1883端口确保服务器在同一网络可达连接被拒绝用户名/密码错误在PC上用MQTT客户端工具验证账号有效性连接被挤掉线Client ID重复改用一个唯一标识排查是否有其他设备占用连接不稳定心跳时间过短加大mqtt_alive_time_ms减少PINGREQ报文触发频率连上即断开clean_session0且会话冲突先将clean_session设为1测试PC上的MQTT客户端工具是我排查问题的利器。它能快速帮你验证是不是我这边开发板的问题。如果PC客户端用同样的参数能正常连接、订阅、发布那问题大概率在开发板的配置或者代码上如果PC也连不上那问题就在Broker端或者网络环境。5.3 内存不足导致的运行异常再讲一个更隐蔽的问题内存不足。TencentOS tiny的内存堆是有上限的MQTT组件本身在运行时创建缓冲区、处理消息时会不断消耗内存。如果堆设置太小可能出现的情况是代码编译下载后一切看起来正常但连接一段时间后系统突然卡死看门狗也不喂了串口打印全部停掉。排查这个问题有一个很实用的方法调用TencentOS tiny的内存统计API打印当前堆的剩余量k_err_t err; k_mmheap_info_t info; err tos_mmheap_info_get(info); if (err K_ERR_NONE) { printf([MEM] free: %d, used: %d\r\n, info.free_size, info.used_size); }在MQTT连接前后、运行一段时间后分别打印一次内存使用情况如果free_size在持续下降那就是有内存泄漏或者分配了不释放的缓冲区。大多数情况下就是MQTT组件接收到了意想不到的报文格式或者订阅回调处理逻辑里分配了内存但忘记释放。定位到是哪个操作导致的内存增长后要么修正逻辑要么给出更大的堆空间预期。5.4 关于WiFi模组方案的补充说明如果你手里的开发板没有以太网接口项目一样可以继续推进只是网络接入层需要换成WiFi方案。最常见的做法是使用ESP8266类模组把它配置成透明传输模式把AT指令封装成一层驱动接口。这层接口向上对接lwIP的SLIP拨号方式或者直接对接一个socket适配层。不过要注意这种方式会在底层的传输效率上有一定折损也存在缓冲区管理的额外负担。如果网络时延要求不高、数据量不大做演示和原型验证是完全可行的。这里我的建议很直接如果条件允许优先用以太网方案做通整个流程跑通后再去尝试WiFi方案。网络问题Debug的复杂度是成倍增高的WiFi环境的信号干扰、路由器重启、DHCP延迟都会让调试问题变量增多不利于学习。6. 功能验证思路与个人经验总结6.1 一个简易的端到端验证方案移植完成后总归要拿出让人信服的跑通了证据。我当时采用了一套简单的验证方案整个过程一气呵成。先在PC端启动一个本地的MQTT Broker服务也可以用云端公共测试服务器但本地环境变量的因素少很多然后打开PC上的MQTT客户端工具同时订阅开发板上报数据的主题。开发板启动你能在客户端工具里按5秒一次的频率收到JSON或纯文本格式的传感器数据。然后在客户端工具中向开发板订阅的命令主题推送一条LED_ON指令此时开发板上的板载LED应立刻点亮同时开发板会发出一个应答消息到另一个主题。在PC客户端上能看到这个应答消息那就说明订阅-回调-处理-发布这条链路已经全部打通了。这套验证方案的思路很值得推广先验证单向通设备上报再验证双向通下行控制上行应答把通道一分为二来看出问题时定位非常清楚。6.2 真实踩坑过程的小结回顾整个项目几个让我印象最深的问题值得专门留在最后第一使能宏的遗漏问题。不知道是什么心态人总是倾向于默认源码拉下来编译烧录就能跑结果第一次编译后MQTT相关函数根本找不到引用。后来才发现原因是宏开关没打开组件源码没有参与编译。其实对新上手的人来说工程的组件能出现在编译目录里这件事就先要确认好如果在Keil的工程文件里根本没见着tos_mqtt.c这个文件先去查配置开关。第二内存池大小的问题。这个问题前面已经提过再补充一句如果系统中有内存碎片化的现象即使总剩余内存看起来够用也可能分配不出一块连续的缓冲区给MQTT消息。这属于RTOS使用中一个典型隐患关键还是要把内存堆设得足够宽裕。第三任务优先级的问题。MQTT组件的内部线程优先级直接影响到消息处理的实时性。我最初把MQTT线程优先级设得比业务任务低结果服务器下发指令后业务任务高优先级反复占用CPUMQTT线程迟迟得不到调度消息处理延迟非常高。调整优先级策略之后指令响应速度立刻恢复正常。这个问题容易被忽视但实际影响很大。第四强烈建议在工程里保留串口日志。特别是近期在做网络方面调试的朋友日志是唯一能让你在黑盒里看到芯片运行状态的手段。不是所有问题都能靠Debugger下断点解决——网络报文是异步的断点会暂停整个芯片但服务器可不会暂停等待等你恢复运行时TCP会话可能已经断了。在关键流程上加上日志输出很多问题能直接打出来不说就一目了然。6.3 项目后续可扩展方向这个项目本身跑通后我认为它的价值绝不止能连上MQTT发个消息这样简单。后续可以做的方向有很多比如在TencentOS tiny的OTA组件基础上实现固件的远程升级把多个设备接到同一台Broker做设备之间的消息互通在God视角把采集到的数据汇总到云端数据库做可视化分析再或者做一套完整的物模型——按照设备上报属性的规范格式实现标准化的数据交互。而且从学习的角度讲通过这个项目把一条完整链路走通后STM32RTOS网络协议栈这套组合拳已经打下来了以后再接触其他物联网操作系统、其他通信协议、其他云端平台时都能举一反三很轻松地触类旁通。关于这个移植项目我最想对读者说的还是那句话嵌入式开发没有捷径但一定有更聪明的方法。选对路子把工程化的方法学扎实比盲目埋头造轮子有意义得多。希望这篇实操记录能帮你少踩几个坑。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →