简介:这套压缩包是一例面向嵌入式初学者的STM32物联网实战工程,主控采用STM32系列单片机,通信侧使用移远RM500U 5G模块,传感器选用AHT20,采集温湿度数据后通过MQTT协议上传至OneNET平台,完成从硬件驱动、网络接入到云端显示的全链路参考实现。工程基于Keil HAL库开发,源码按模块划分,注释清晰,管脚接线定义均标注在代码中,适合学习5G模组AT指令、MQTT报文交互以及物联网项目调试方法。压缩包内共972个文件,大小13.38MB,以c源码和h头文件为主体,另含启动文件、链接脚本、Keil和IAR工程文件、STM32CubeMX的ioc配置,以及最终编译生成的hex、axf等镜像文件,便于直接烧录与验证。丰富的编译中间文件可以帮助读者分析构建过程,配合代码注释理解各子模块关系。该工程目前已有617人学习/下载,作者还针对J-Link/ST-Link选择、芯片型号与Flash容量调整给出了提醒,可按自身硬件做适配,也可替换其他传感器进行二次开发,是较好的课设、毕设或产品预研参考。
1. 5G模组串起STM32到OneNET的完整链路,这个项目在解决什么
一个STM32开发板、一块移远RM500U 5G模组、一颗AHT20温湿度传感器,外加OneNET物联网平台的MQTT接入,这条链路看起来像是把多个成熟模块拼在一起,实际做起来却会卡在不少隐性的地方。最常见的情况是模组的AT指令已经回了OK,5G信号也显示满格,但平台上就是看不到一条数据点。真正难的从来不是5G这个名词,而是OneNET改造版MQTT的鉴权格式、RM500U的拨号附着流程、以及AHT20在I2C总线上的时序细节这三件事互相咬合。这篇文章把这三段拆开讲清楚,从硬件接线、AT指令到上报代码,每一步都给出可以直接抄的参数和排错思路。适合手里正好有RM500U或是想用5G模组做物联网采集项目的开发者,尤其是卡在OneNET上数这一步的人。
2. 硬件链路与初始化:确认RM500U和AHT20都在STM32能管控的范围里
2.1 RM500U用UART还是USB虚拟串口对接STM32
RM500U作为移远的5G模组,对外接口非常丰富,USB、PCIe、UART都有。和STM32单片机对接,常见做法是用UART串口发AT指令,而不是走USB。原因在于USB接口在RM500U上会枚举出多个虚拟串口,不同的AT口、数据口、日志口混在一起,在MCU侧处理起来需要引入USB Host控制器,画蛇添足。RM500U的UART接口支持AT指令和PPP拨号两种数据通道,波特率默认可以是115200,硬件上只需要两个引脚交叉连接。
接线参考如下表:
| RM500U引脚 | STM32引脚 | 说明 |
|---|---|---|
| UART_TX (Pin 56附近) | USARTx_RX | 模组发送,MCU接收 |
| UART_RX (Pin 55附近) | USARTx_TX | 模组接收,MCU发送 |
| GND | GND | 共地 |
| VCC 4.0V~4.4V | 外部DC-DC供电 | 峰值电流2A以上,不能直接用3.3V LDO |
| PWRKEY | GPIO输出 | 拉低至少500ms触发开机 |
| RESET | GPIO输出或悬空 | 低电平有效,上电初期建议拉高 |
这里有一个容易被忽视的地方:RM500U的UART电平是1.8V,不是常见的3.3V TTL。多数STM32开发板的串口号称兼容3.3V逻辑,直接接上去虽然能通讯,但长期运行时可能出现偶发乱码。稳妥的做法是在RM500U的UART_RX/TX和STM32之间加一个电平转换芯片,比如TXS0108或简单的二极管电平匹配电路。如果你手边的RM500U是带开发底板的版本,底板一般已经把电平转到3.3V,此时直接接STM32没问题。
2.2 模组上电时序:为什么AT指令回不了OK
RM500U不是一颗简单的无线芯片,它内部跑了完整的Linux操作系统,上电到AT指令可用通常需要10秒到20秒,这和ESP8266那种上电几百毫秒就能工作的模组完全是两个节奏。正式项目里,STM32在复位后不能立刻发AT,要先给模组留出启动时间。
我一般会在代码里做这样一个流程:模组PWRKEY拉低500ms松开,然后循环发送不带回车的AT字符,每500ms一次,最多等到30秒。当收到OK响应时,才认为模组就绪。这个机制的代码逻辑很简单,但能避免项目前期把大量时间浪费在"为什么AT没反应"上。另外注意,RM500U冷启动时电流会冲高到2A以上,如果用USB线供电的STM32开发板直接给模组供电,电压跌落会导致模组反复重启,现象就是AT偶尔通偶尔不通。给RM500U单独配一个可输出3A的DC-DC模块是省心方案。
2.3 STM32侧的外设分配与CubeMX参数
AHT20是I2C接口传感器,RM500U走UART,主控只需要两个外设:一个I2C、一个USART。在STM32CubeMX里把I2C速率设为100kHz标准模式,AHT20在400kHz下也能工作,但信号线稍长或者上拉电阻偏大时容易读数飘,100kHz最稳。USART设置为115200 8N1,打开中断,关闭硬件流控。RM500U的UART接口不支持RTS/CTS硬件流控,这里务必关闭,否则模组会等流控信号而一直不吐数据。
外设初始化完成后,别忘了处理延时问题。STM32标准库和HAL库的HAL_Delay在系统时钟配置错时会明显偏短或偏长,会直接影响AHT20读取时序。用CubeMX配置外部高速晶振HSE,把系统时钟倍频到最高,再回头测一次延时是否准确。很多人AHT20读出的温度偏高,排查到最后发现是HAL_Delay实际只延时了十分之一。
3. RM500U拨号与网络附着:AT指令把5G通道真正跑通
3.1 建立数据通道的最小AT指令序列
RM500U支持移远统一AT指令集,和EC20、RM500Q等模组的指令风格一致,熟悉4G模块的人可以直接上手。上电后依次输入下面的命令,每一条都等到OK再发下一条。
AT AT+CPIN? AT+CSQ AT+COPS? AT+CGDCONT=1,"IP","cmiot" AT+CGACT=1,1 AT+CGPADDR=1 AT+QPING=1,"183.230.40.39"逻辑说明:AT探活;AT+CPIN?查SIM卡是否识别,返回READY才能继续;AT+CSQ看信号强度,数值代表接收信号电平,大于15基本能正常工作;AT+COPS?确认运营商注册状态,返回里的第二个字段如果是0到3之间,表示已经注册上网络。AT+CGDCONT=1设置PDP上下文,APN要填当前SIM卡运营商实际使用的值,比如电信的ctnet、联通的wonet,移动物联网卡常见cmiot。AT+CGACT=1,1激活PDP上下文,这一步成功了才有IP地址。最后AT+CGPADDR=1查询分配到的IP,并用AT+QPING验证到OneNET平台的网络通路。
参数说明:AT+CGDCONT=1,"IP","cmiot"中的第一个1是PDP上下文编号,IP指IPv4,cmiot是APN。如果这张卡是定向流量卡,APN一定要填对,否则5G虽然注册上了但任何TCP连接都建立不起来,这是物联网卡最常见的问题。AT+QPING=1,"183.230.40.39"中的1也是上下文编号,后面的IP是OneNET MQTT接入地址,能收到字节回复说明模组的网络侧已经通了。
3.2 5G和LTE的附着差异:别被CSQ值骗了
RM500U支持NSA和SA两种5G组网模式,也向下兼容4G。实际环境中,5G信号强度很好但不代表数据通道就通畅,尤其在NSA组网下,模组需要同时维持5G和LTE两条链路。检查是否真的工作在5G网络下,用AT+QNWPREFCFG="mode_pref",NR5G强制优先5G,再看AT+QNWINFO回显的network mode是不是NR5G。
一个实际的坑是:在室内测试时,5G信号显示满格,但AT+CGACT=1,1返回ERROR。这种情况多数是当前小区的5G载波没有配置上行资源,或者SIM卡的DNN配置不允许该APN走5G承载。此时把网络模式改为AT+QNWPREFCFG="mode_pref",LTE,用LTE把数据业务先跑通,项目联调期间效率更高。5G模组不是必须工作在5G网络,关键在于数据业务链路是否可用。
3.3 拨号方式选择:AT指令自带协议栈还是外部PPP
RM500U提供了两种上云路径。一种是模组内部完成TCP/IP协议栈,MCU只管发AT指令,比如AT+QHTTP、AT+QMTOPEN等,这种方式MCU开销最小。另一种是走PPP拨号,把模组当成一个网卡,数据经过PPP链路后由MCU侧去处理TCP/IP。在单片机项目里,前者更常用,原因很简单:STM32的RAM和Flash有限,跑一个完整TCP/IP协议栈再叠加MQTT压力不小,而模组自带协议栈处理这些绰绰有余。后面的MQTT接入全部基于移远的socket AT指令展开。
4. OneNET MQTT鉴权与数据帧:把AHT20数值送进物联网平台
4.1 OneNET的MQTT不是标准MQTT,差在鉴权
OneNET平台提供MQTT接入能力,但它的第一版接入方式对MQTT协议做了一层特殊封装,网上流传的mqtt协议详解大多是从标准MQTT讲起,直接套到OneNET上会连不上。差别主要体现在CONNECT报文的三个核心字段上:标准MQTT中clientId、username、password是用户自己定义的;OneNET则规定clientId填产品ID,username填设备ID,password填设备接入的apiKey。这三个信息在OneNET控制台创建产品和设备后都能找到,apiKey类似设备的访问令牌,丢了可以重新生成。
连接参数常见是MQTT地址183.230.40.39,端口6002。R5模组侧的AT指令写法是:
AT+QMTOPEN=0,"183.230.40.39",6002 AT+QMTCONN=0,"产品ID","设备ID","apiKey"逻辑说明:AT+QMTOPEN建立到OneNET的TCP连接,0是socket连接编号,后面是服务器地址和端口。AT+QMTCONN发送MQTT CONNECT报文完成接入,三个字符串依次对应OneNET要求的clientId、username、password。需要注意,OneNET对apiKey的大小写敏感,从控制台复制时不要加多余的换行或空格。
参数说明:如果AT+QMTCONN返回ERROR,大概率不是网络问题,而是产品ID、设备ID、apiKey三者不匹配。可以先自查一遍ID中间是否混入空格,再把apiKey重新生成一次。
4.2 上行数据报文的topic与JSON格式
OneNET MQTT接入中,设备上报数据固定发布到sys/$dp这个topic,平台才会把数据解析成数据流。payload是JSON格式,结构如下:
{"datastreams":[{"id":"temp","datapoints":[{"value":25.3}]},{"id":"hum","datapoints":[{"value":68.5}]}]}其中id要和控制台产品里已经创建的数据流名称一致。如果平台的数据流叫temperature,而JSON里写temp,上报会成功但平台侧显示不出折线图,这是排查时最容易漏掉的一环。value可以是float,平台端展示时保留一位小数。
模组侧发布这条数据用AT+QMTPUB指令:
AT+QMTPUB=0,0,0,0,"sys/$dp","{\"datastreams\":[{\"id\":\"temp\",\"datapoints\":[{\"value\":25.3}]}]}"逻辑说明:AT+QMTPUB的参数里,第一个0是连接编号,第二个0是消息编号,第三个0表示QoS等级为0,第四个0表示不保留消息,然后是topic和payload。QoS实际使用时保持0即可,OneNET对QoS1的支持表现并不好,反而容易出现重复数据点。而JSON字符串里双引号在AT指令中必须用\"转义,不然模组解析指令会在第一个双引号处截断。转义是多数人第一次做AT+MQTT上报时最容易卡住的点。
4.3 更稳妥的上报状态机
把发布动作做成状态机比一次性阻塞式发送可靠得多。模组处于Onenet连接态时,每次发布前先发AT+QMTPUB,等待模组返回>提示符后再发JSON载荷。如果直接拼一条长指令发出去,有些固件版本会因为缓冲问题丢尾部数据。正确时序是:
AT+QMTPUB=0,0,0,0,"sys/$dp",60 > {"datastreams":...}其中60是payload长度,必须在发送JSON前明确告知模组。模组收到完整载荷后会返回OK表示已进入发送流程,再返回+QMTPUB: 0,0,0表示发送完成。建议在收到+QMTPUB: 0,0,0之后再做下一次采集和上报,避免多个报文在模组内部排队。
5. STM32采集AHT20并发布MQTT:完整驱动代码拆解
5.1 AHT20的I2C读取时序与温湿度换算
AHT20出厂时已经校准,MCU读取的流程是初始化、触发测量、等待、读取六字节数据。读取到的六个字节中状态字占一个字节,后面是湿度20bit和温度20bit。需要注意AHT20的I2C设备地址是0x38,左移一位后在HAL库的写函数里传入0x70作为引脚地址。
uint8_t AHT20_Measure(void) { uint8_t cmd[3] = {0xAC, 0x33, 0x00}; uint8_t raw[6] = {0}; uint32_t hum_raw, temp_raw; float hum, temp; HAL_I2C_Master_Transmit(&hi2c1, 0x70, cmd, 3, 100); HAL_Delay(80); HAL_I2C_Master_Receive(&hi2c1, 0x71, raw, 6, 100); if ((raw[0] & 0x80) != 0) { return 1; // 测量忙 } hum_raw = ((uint32_t)raw[1] << 12) | ((uint32_t)raw[2] << 4) | (raw[3] >> 4); temp_raw = (((uint32_t)raw[3] & 0x0F) << 16) | ((uint32_t)raw[4] << 8) | raw[5]; hum = (float)hum_raw * 100.0f / 1048576.0f; temp = (float)temp_raw * 200.0f / 1048576.0f - 45.0f; printf("temp=%.1f hum=%.1f\r\n", temp, hum); return 0; }逻辑说明:0xAC是AHT20的触发测量指令,后面紧跟两个参数0x33和0x00,这个是数据手册规定的固定序列。触发后芯片需要大约80ms完成测量,HAL_Delay大于这个时间才能读到有效数据。读取时先判断状态字二进制首位是否为1,1表示芯片还在忙,此时读取出来的数据是上一次的旧值。
换算公式的细节尤其要注意:AHT20的温度公式是T = -45 + 200 * raw / 2^20,而它的兄长AHT10用的是-40起算。如果把AHT10的公式套到AHT20上,读出的温度会整体偏高5度。湿度公式两端是相同的,RH = 100 * raw / 2^20。2^20对应代码中的1048576,直接用浮点运算就可以。
5.2 拼装OneNET JSON帧并走UART下发AT指令
采集到温度和湿度后,下一步就是把浮点数格式化成OneNET要求的JSON字符串。用sprintf拼出payload,再通过UART发送给RM500U。
char payload[128]; char at_cmd[160]; sprintf(payload, "{\"datastreams\":[{\"id\":\"temp\",\"datapoints\":[{\"value\":%.1f}]},{\"id\":\"hum\",\"datapoints\":[{\"value\":%.1f}]}]}", temp, hum); sprintf(at_cmd, "AT+QMTPUB=0,0,0,0,\"sys/$dp\",%d\r\n", strlen(payload)); HAL_UART_Transmit(&huart1, (uint8_t*)at_cmd, strlen(at_cmd), 1000); HAL_Delay(50); HAL_UART_Transmit(&huart1, (uint8_t*)payload, strlen(payload), 1000); HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", 2, 1000);逻辑说明:第一条AT+QMTPUB指令中的%d是payload的字节数,必须和实际发送的payload长度完全一致。%.1f控制小数点后一位,OneNET平台显示折线图时偏好一位小数,过多的有效位数反而会让Y轴刻度变得难读。发送完AT指令后不要立即发payload,加一个短延时,等待模组先返回>字符,这个字符是模组给出的可以接收数据的信号。
参数说明:HAL_UART_Transmit的最后一个参数是超时时间,这里设1000ms。如果模组因为信号不好而处理变慢,超时太短会导致payload发出去时模组还没进入接收模式,数据直接丢失。另一个隐含问题是,如果temp或hum的值是负数,%.1f会正常输出负号,OneNET数据流也接受负值,AHT20测到零下温度时不需要特殊处理。
5.3 发布完成判定与掉线重连判断
模组在发布完成后会主动回一条+QMTPUB: 0,0,0到串口。STM32的UART中断里要把这一帧完整收下来,作为发布成功的标志。对于每次采集上报,正确的闭环是:串口收到完整响应后置位一个全局标志,主循环看到标志后清零,再进行下一次采集。这个做法避免了定时采集周期到了就盲目发AT指令,结果模组还在处理上一次报文,缓冲溢出后表现为主题正常但平台长时间不更新数据。
RM500U的MQTT连接掉线后不会自动重连,判断方法是看串口是否出现+QMTSTAT: 0,1之类的URC上报,或者AT+QMTCONN?查询到的连接状态不是0。重连动作不要紧跟着掉线就执行,常见的重连策略是启动一个5秒到10秒的延时,然后重新执行AT+QMTOPEN和AT+QMTCONN,因为模组在掉线瞬间网络栈还没有完全释放,立即重连容易失败。
6. 平台折线图验证与三层排错:把问题定位到具体环节
6.1 OneNET控制台创建数据流并确认折线图时间戳
登录OneNET控制台,在产品详情页的数据流管理里手动创建temp和hum两个数据流,类型都是数值型。这里没有先建数据流或数据流名称和JSON里的id不一致,平台侧根本不会画出折线图。设备详情页打开数据展示,选择更新时间段,一分钟后应该能看到新的数据点。确认平台显示正常的关键是看时间戳有没有一直往前走,如果时间戳停留在几小时前,说明设备的上报并没有真正到达平台。
验证时可以手动在串口工具里发一条JSON,把值固定为25.0和50.0,平台折线图立刻出现这两个值,再切回STM32实测数据,就能区分问题是出在采集环节还是上报环节。OneNET折线图默认按时间聚合显示,数据点过密时看起来是一条直线,可以把时间粒度调到1分钟观察单点数据。
6.2 三层排错顺序与常见错误码对照
排查顺序按照信号链路从底层往上走,严格按照下面的顺序来,可以少走弯路:
| 层级 | 检查手段 | 常见结果 |
|---|---|---|
| 模组网络层 | AT+CSQ信号值、AT+CGACT?PDP激活状态 | 信号值过低或PDP未激活 |
| 协议接入层 | AT+QMTCONN?查询MQTT连接状态,检查CONNACK返回码 | 返回非0表示鉴权或网络参数错误 |
| 应用数据层 | 平台数据流时间戳是否为当前时间 | JSON的id与数据流名称不一致 |
error: no stm32 target found!这类报错在烧录阶段频繁出现,跟5G链路无关,常见原因是没有安装配套的STM32芯片包,或者ST-Link的SWDIO和SWCLK两根线在板子上没有接对目标芯片。先用stm32 st-link utility读一下IDCODE,能读到芯片ID再上Keil,比在烧录界面反复报错里找原因都快。
6.3 AT指令交互的调试技巧
RM500U的UART回显默认开着,收下来的每个字符都有回显,第一次看会很乱。可以在AT指令集调试时用ATE0关闭回显,串口输出会干净很多。另外RM500U有个AT+QCFG="dbgctl"之类的日志开关,不建议在正常调试时打开,它会把模组内部日志混在AT响应里输出,干扰判断。还有一点,RM500U的串口默认是本机回显模式,接线只接TX和RX不接GND时,现象往往是模组能收到AT但没有任何响应,因为信号回路不完整。排查串口通信时第一件事就是确认共地,第二件事才是换波特率。
OneNET数据流的更新频率和MQTT心跳有关,模组默认心跳间隔较长,长时间只采集不发布会让平台判定设备离线。每次完成数据上报后,同步执行一次AT+QMTPING=0来维持连接保活,这样即便数据采集周期拉长到几分钟一次,平台上的设备状态也始终显示在线。这算是整套链路里性价比最高的一个小动作,强烈建议加上。
本文还有配套的精品资源,点击获取