STM32+SIM800C 温湿度GPS数据上云OneNet:从AT指令到HTTP组包全链路详解
2026/9/11 12:03:02 网站建设 项目流程

简介:面向STM32与物联网开发者,该7z压缩包提供一个完整可运行的SIM800C+STM32接入OneNet云平台的温湿度与GPS数据上传工程,解决GPRS模组与单片机通信、数据上云等常见问题,适合具备一定单片机基础、想学习AT指令和MQTT/HTTP接入的读者。包内含87个文件,以c和h源码为主,包含uvproj工程配置、o/d/crf等编译中间文件、hex烧录文件及htm参考页面,整体约1.08MB,目录按DHT11、SIM800C、GPS、OLED等功能模块划分,层次清晰。已有3578人学习下载。借助该工程,读者可对照源码理解SIM800C的TCP连接建立、OneNet数据点上传流程,掌握STM32多串口解析GPS与传感器数据的方法,并能基于MDK工程直接编译调试。同时附带相关文档与编译产物,便于逆向阅读和排错,是物联网实战项目不错的起步范例。

1. 用 SIM800C 和 STM32 把温湿度、GPS 一起送上 OneNet,一次讲透整条链路

很多远程监测项目在选型时会卡在同一个问题上:设备摆在田里、冷链车里、工地上,那里没有 WiFi,甚至没有固定电源,数据怎么上云?这个标题给出的组合很典型——STM32 负责采集和业务逻辑,SIM800C 通过 GPRS 拨号上网,温湿度和 GPS 数据打包成 HTTP 请求发到 OneNet 平台。OneNet 是国内物联网平台里接入成本较低的一个,核心玩法是“创建设备、拿 APIKey、上报数据流”,不需要自己买服务器。这套方案经常出现在毕业设计、农业大棚监测、车辆定位原型里,而且改一改就能迁移到 NB-IoT 或 4G 模块上。下面按“平台侧准备 → 模块连网 → HTTP 组包 → STM32 采集 → 联调排错”的顺序把整条链路讲清楚。

2. OneNet 平台侧接入:产品、设备与生成 APIKey 的关系

写 STM32 代码之前,先花半小时把 OneNet 平台侧的东西准备好。否则代码写到一半,连 token 怎么放、路径长什么样都不知道,后面所有调试都会卡在 401 这类权限错误上。

2.1 先把 OneNet 的三层概念理清:产品、设备、APIKey

OneNet 的模型不复杂,四个词就够了:产品、设备、数据流、数据点。一个产品对应一类设备,比如“冷链监测终端”;产品下面挂多个设备,每个设备有唯一的设备 ID;设备下可以定义若干数据流,比如 temp、hum、lat、lng;某个时刻往数据流里写入一个值,就产生一个数据点。上报时用的鉴权信息是 APIKey,它相当于这把锁的钥匙。需要注意 APIKey 分产品级和设备级,上传数据时用设备级 APIKey 即可,权限上更收敛。

开发中最容易搞混的是“设备 ID”和“APIKey”这两个值。设备 ID 是一个数字字符串,出现在请求 URL 里;APIKey 是一个较长的令牌,放在请求头里。我把它们写在同一个结构体里,代码中统一引用,避免到处复制粘贴导致不一致。

typedef struct { char device_id[16]; // 例如 "12345678" char api_key[48]; // 例如 "AbCdeFgHijkLmN..." } onenet_config_t;

这里的 device_id 和 api_key 都是字符串,在 HTTP 请求里逐字节使用。设备 ID 可以直接从 OneNet 设备列表里看到,APIKey 在设备详情页或产品的密钥管理里生成,生成后建议立即复制保存,页面刷新后不会再次明文显示。

2.2 创建产品并生成 APIKey:多协议接入里的 HTTP 选项

OneNet 控制台的界面改过几次版本,但核心入口基本一致:登录后创建产品,产品名称随意,选择“多协议接入”,协议选 HTTP。不同版本的按钮文字可能有差异,比如新版控制台挪到了“设备接入”或“物模型”里,但你要找的本质是三个值:产品 ID、设备 ID、APIKey。

创建完产品后,在产品下添加一个设备,设备名称比如dev_001,鉴权信息可以自己填也可以自动生成。然后进入设备详情,找到 APIKey 相关选项,复制设备级 APIKey。老控制台里叫做“APIKey”,新版有些位置叫“密钥”或“鉴权信息”,原则是:哪个值能作为api-key请求头使用,哪个就是要找的 APIKey。这一步千万别拿产品的“产品ID”当设备ID用,路径不对会返回 404。

2.3 用 curl 在电脑上先验证一次数据点上报

硬件还没接之前,先用电脑验证 OneNet 接口通不通。OneNet 的数据点上报接口路径一般是/devices/{device_id}/datapoints,方式为 POST JSON。先在终端里执行下面这条命令,把设备 ID 和 APIKey 换成自己的。

curl -X POST \ "http://api.heclouds.com/devices/12345678/datapoints" \ -H "api-key: AbCdeFgHijkLmN..." \ -H "Content-Type: application/json" \ -d '{"datastreams":[{"id":"temp","datapoints":[{"value":25.6}]}]}'

请求里datastreams是数组,可以一次上报多个数据流;每个数据流下的datapoints也是数组,支持批量上报多个时间点的数据。这条命令执行后,如果返回内容里有"errno":0,说明数据已经写入 OneNet。errno非 0 时,后面的error字段会给出原因,比如 APIKey 不对、格式错误等。

字段类型说明
id字符串数据流名称,OneNet 会自动创建
value数值数据的当前值,浮点或整数均可
datapoints数组支持单个或多个数据点
datastreams数组一次上传多个数据流

curl 验证通过后,OneNet 的设备数据流里应该能看到 temp 的最新值。此时平台侧已经准备好,接下来把注意力放到 SIM800C 上。

3. SIM800C 上电、注册网络与建立 TCP 连接的 AT 指令时序

SIM800C 是一个 2G/GPRS 模块,通过串口和 STM32 通信。它的操作模式是“发 AT 指令,看返回结果”。很多人在这一步折戟,不是因为指令难,而是因为电源和时序没处理好。串口助手里能看到 AT 指令的回显,但指令不执行、返回 ERROR、或者根本没反应,大半是硬件问题。

3.1 SIM800C 与 STM32 的接线:电平、电源、PWRKEY 三个坑

SIM800C 的串口高电平大约 2.8V,STM32 的串口是 3.3V 逻辑。市面上的 SIM800C 开发板大多自带电平转换电路,可以直接和 STM32 的 USART 交叉连接:模块 TXD 接 STM32 的 RX,模块 RXD 接 STM32 的 TX。如果自己画板,不要直接把 2.8V 和 3.3V 互怼,建议加转换芯片或电阻分压。

供电是最容易出问题的地方。SIM800C 在 GSM 发射瞬间电流峰值能到 2A 左右,STM32 开发板上的 3.3V LDO 完全带不动。常见做法是给模块单独接一个 4V/2A 以上的电源,或者用 LM2596 这类降压模块从 5V 或 12V 降压到 4V。用面包板连接时还要注意杜邦线压降,线太长太细会导致模块在发射瞬间电压跌落,直接表现为“能发 AT 但一拨号就重启”。

PWRKEY 引脚决定了模块怎么开机。SIM800C 上电后默认不开机,需要把 PWRKEY 拉低大约 1.2 秒再松开。之后等 2 到 3 秒,再发 AT 指令。如果模块上的 NETLIGHT 指示灯在慢闪,说明正在搜网;如果快速闪烁(约 1Hz),说明已经注册上网络。

3.2 上电后的 AT 指令序列:同步、查信号、注册网络

模块开机后,第一步是“同步波特率”。如果 STM32 和 SIM800C 的串口波特率都是 9600 或 115200,直接发AT,返回OK就说明通信正常。有时模块刚开机没准备好,可以发几次AT,或者先发一个不带回车的空字节。

AT ATE0 AT+CSQ AT+CREG? AT+CSTT="cmnet" AT+CIICR

ATE0是关闭回显,关闭后串口输出干净很多,程序判断返回结果时不容易被回显干扰。AT+CSQ返回信号质量,格式如+CSQ: 15,0,第一项范围 0 到 31,数值越大信号越好,10 以上基本可用,99 表示无信号。AT+CREG?返回+CREG: 0,1,第二个字段为 1 表示已注册到本地网络,5 表示漫游,0 表示未注册。信号和注册都正常后,再配置 APN 并激活 GPRS。

指令期望返回含义失败时查什么
ATOK串口通信正常波特率、接线、PWRKEY 开机
AT+CSQ+CSQ: 12,0信号质量SIM 卡是否插好、天线是否接上
AT+CREG?+CREG: 0,1网络注册状态SIM 卡是否欠费、模块是否在服务区
AT+CSTT="cmnet"OK设置 APN物联网卡 APN 可能与默认不同
AT+CIICROK激活 GPRS 场景卡未开通 GPRS 数据业务

AT+CSTT="cmnet"是设置 APN。大多数手机卡和物联网卡的上网 APN 是cmnet,但部分专用卡有独立的 APN 名称,以运营商提供的参数为准。AT+CIICR是激活 GPRS 场景,类似“拨号上网”。执行成功后可以用AT+CIFSR查询模块获得的本地 IP。如果AT+CIICR返回 ERROR,先确认AT+CSQAT+CREG?都正常,再检查 APN 是否正确。

3.3 建立 TCP 连接:AT+CIICR 附着 GPRS 后连 OneNet 域名

GPRS 附着成功后,下一步是建立 TCP 连接。OneNet 老版 HTTP 接口的域名是api.heclouds.com,HTTP 端口是 80。SIM800C 支持直接通过域名建连,但需要先开启 DNS 解析功能,指令是AT+CDNSGIP=1。之后再执行建连指令:

AT+CDNSGIP=1 AT+CIPSTART="TCP","api.heclouds.com",80

正常情况下模块会输出CONNECT OK。如果返回ERRORCLOSED,大概率是 GPRS 没有附着成功,或者 DNS 解析失败。此时回头执行AT+CIFSR,拿到 IP 才能说明网络层面已通。建连成功后,SIM800C 就处于一个透明的 TCP 通道里,可以向服务器发送原始数据。

4. 组 HTTP POST 包上传 OneNet:Content-Length 是最容易翻车的点

TCP 建好之后,很多人会直接往模块里丢 JSON 字符串,然后发现 OneNet 没有任何反应。原因很简单:服务器要的是合法 HTTP 报文,不是裸 JSON。HTTP 报文的四个基本要素:请求行、请求头、空行、请求体,缺一不可。而请求头里的 Content-Length 必须和请求体字节数完全一致,这是整个 DIY 里最容易翻车的地方。

4.1 为什么不用 AT+HTTP:TCP 裸连跑 HTTP 更可控

SIM800C 固件里其实自带一套 HTTP 扩展指令,比如AT+HTTPINITAT+HTTPPARAAT+HTTPDATA。这套指令能省掉手动组 HTTP 报文的工作,但实际用起来并不省心:一次 HTTP 请求要拆成五六条指令,AT+HTTPDATA对长度参数也要求精确,出错后排查链路长。相比之下,我一般用 TCP 裸连接自己组报文,先把报文在 STM32 里拼好,通过AT+CIPSEND一次发出。这样做的好处是逻辑透明,服务器返回的原始 HTTP 响应也能直接看到,而且换用 NB-IoT 或 4G 模块时,组包代码完全不用改。

需要注意 OneNet 老版 HTTP 接口支持http://明文传输,不需要 TLS。SIM800C 的固件虽然有些版本支持 SSL,但握手过程占用资源大、容易失败,调试阶段别给自己找麻烦。等整条链路跑通,真有加密需求再考虑换带硬件加密的 4G 模块。

4.2 组装 HTTP POST 请求:先算 Content-Length 再整包发送

组包顺序有讲究:先把请求体 body 拼好,再用strlen算出 body 长度,最后把这个长度填进请求头。反过来先写头再算长度也可以,但很容易出现“头里写死了长度,body 改了一个字节忘记同步”的隐蔽 bug。

char body[256]; int body_len; body_len = sprintf(body, "{\"datastreams\":[" "{\"id\":\"temp\",\"datapoints\":[{\"value\":%.1f}]}," "{\"id\":\"hum\",\"datapoints\":[{\"value\":%.1f}]}," "{\"id\":\"lat\",\"datapoints\":[{\"value\":%.6f}]}," "{\"id\":\"lng\",\"datapoints\":[{\"value\":%.6f}]}" "]}", temp, hum, lat, lng); char header[128]; int header_len; header_len = sprintf(header, "POST /devices/%s/datapoints HTTP/1.1\r\n" "Host: api.heclouds.com\r\n" "api-key: %s\r\n" "Content-Type: application/json\r\n" "Content-Length: %d\r\n" "\r\n", device_id, api_key, body_len);

这里有几个细节。第一,Content-Length的单位是字节,不是字符数,ASCII 数值和字符串在内存里一一对应,sprintf返回的正好是字节数。第二,HTTP 行的结束符必须是\r\n,也就是回车加换行两个字节,写成\n在某些服务器上会直接报 400。第三,请求头和 body 之间有一个单独的空行,也就是最后一个\r\n,这个空行不能省。最后一行的%d对应的是body_len,如果用sizeof去算,就会把后面没用的缓冲区空间也数进去,Content-Length 偏长,服务器一直等不完请求体会超时。

4.3 AT+CIPSEND 发送流程与 0x1A 结束符

报文拼好后的发送流程是:先发AT+CIPSEND,模块返回>后,把 header 和 body 连续发出,最后发一个十六进制的0x1A作为结束标志。这个0x1A是 SIM800C 的 TCP 透传结束符,类似键盘上的 Ctrl+Z,它的作用是告诉模块“这包数据到此为止,可以封装发送了”。

USART_SendString("AT+CIPSEND\r\n"); // 等待串口收到 '>' 字符 delay_ms(50); USART_SendString(header); USART_SendString(body); USART_SendByte(0x1A); // 十六进制 0x1A,不是字符串 "1A"

初学最容易在这里踩坑的是把0x1A当成了字符串"1A"来发。字符串"1A"是两个字节:字符'1'和字符'A',对应的十六进制是0x31 0x41。而模块要的是单个字节0x1A。用串口助手手动调试时,要在 HEX 发送模式下输入1A,而不是在文本模式下输入1A再点发送。发送完这个字节后,模块正常会返回SEND OK,表示数据已经从模块发出。有时服务器响应很快,模块还没来得及打印SEND OK,直接输出CLOSED,这也是正常情况,说明服务器收到数据后主动关闭了连接。CLOSED不一定代表失败,要看后续服务器响应的内容。

4.4 用响应报文判断上传结果

SIM800C 在 TCP 模式下会把服务器返回的原始数据直接推到串口,OneNet 的响应通常长这样:

HTTP/1.1 200 OK Content-Type: application/json Content-Length: 37 {"errno":0,"error":"succ"}

只要看到HTTP/1.1 200 OKerrno为 0,数据就写入成功。errno非 0 时,重点看error字段的英文提示,比如invalid api-key就是 APIKey 错误,device not found是设备 ID 写错。收到响应后,用AT+CIPCLOSE关闭当前 TCP 连接,为下一次上报做准备。

这里还要提一个判断时序:模块返回的数据是一次性输出的一整段,而不是先输出 HTTP 状态行再等一会儿输出 body。如果你的 STM32 串口接收代码只读前几个字节就结束,可能只能拿到HTTP/1.1 200而拿不到errno。调试阶段建议把完整响应缓存下来,再按\r\n\r\n切分头部和 body。

5. STM32 端采集:DHT11 温湿度与 GPS NMEA 数据的解析

数据能发到 OneNet 之后,剩下的问题就是“采集端怎么把温湿度和 GPS 坐标变成干净的数值”。这个标题涉及两个典型外设:DHT11 是单总线协议,GPS 模块是串口 NMEA 协议。两者的坑都不在硬件上,而在时序和单位换算上。

5.1 DHT11 温湿度读取:单总线时序与校验和

DHT11 只有一根数据线,通过时序区分 0 和 1。读取一次的流程是:主机把总线拉低至少 18ms,再释放并延时 20 到 40us,此时 DHT11 会回一个 80us 的低电平响应,之后拉高 80us,然后开始输出 40 位数据。每位数据都是先拉低 50us,再拉高,高电平时间约 26 到 28us 表示 0,约 70us 表示 1。

uint8_t dht11_buf[5]; if (DHT11_Start() == 0) { for (i = 0; i < 5; i++) { dht11_buf[i] = DHT11_ReadByte(); } // 校验:前四个字节之和的低8位应等于第五个字节 if ((uint8_t)(dht11_buf[0] + dht11_buf[1] + dht11_buf[2] + dht11_buf[3]) == dht11_buf[4]) { hum = dht11_buf[0]; // 湿度整数部分 temp = dht11_buf[2]; // 温度整数部分 } }

DHT11 的温湿度数据格式是湿度整数、湿度小数、温度整数、温度小数、校验和,DHT11 的小数位通常为 0,DHT22 才有实际小数。校验和是前四个字节相加取低 8 位,校验不通过的数据直接丢弃,不要上传到云平台。

DHT11 对时序中断极其敏感。读取过程中如果发生串口中断或定时器中断,很容易导致电平跳变检测超时。在裸机环境下,读取 DHT11 时建议关闭全局中断;在 FreeRTOS 环境下,建议用临界区保护整个读取过程,或者把 DHT11 读取放在优先级较高的任务里,但仍要处理中断延迟问题。另一个常见错误是 GPIO 方向切换。DHT11 要求主机先输出模式拉低总线,再切换输入模式读取响应,如果初始化时把引脚配置成开漏输出并接了上拉电阻,也可以省去切换,但大多数代码仍然采用“输出/输入切换”的写法,逻辑更直观。

5.2 GPS NMEA 解析:GGA 语句里的经纬度不是十进制

GPS 模块的输出按照 NMEA 0183 协议,常用语句有 GGA、RMC、GSV 等。其中 GGA 语句包含定位状态、经纬度、卫星数、海拔等关键信息,适合做入网数据源。一条典型的 GGA 语句长这样:

$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

按逗号切分后,第 2 个字段是纬度4807.038,第 3 个字段是方向N,第 4 个是经度01131.000,第 5 个是方向E,第 6 个字段1表示定位状态,0表示未定位。解析时直接从缓冲区里找$GPGGA,然后逐段拆逗号。

char *p = strstr(gps_buf, "$GPGGA"); if (p) { double lat_raw, lng_raw; char lat_dir, lng_dir; int fix, sat; sscanf(p, "$GPGGA,%*s,%lf,%c,%lf,%c,%d,%d", &lat_raw, &lat_dir, &lng_raw, &lng_dir, &fix, &sat); if (fix > 0 && sat >= 4) { double lat = (int)(lat_raw / 100) + (lat_raw - (int)(lat_raw / 100) * 100) / 60.0; double lng = (int)(lng_raw / 100) + (lng_raw - (int)(lng_raw / 100) * 100) / 60.0; if (lat_dir == 'S') lat = -lat; if (lng_dir == 'W') lng = -lng; } }

sscanf%*s表示跳过时间字段,%lf读取浮点经纬度,%c读取方向。这里最关键的坑是单位换算:NMEA 里的4807.038是“度分”格式,表示 48 度 07.038 分,不是十进制的 48.07038 度。换算公式是:度取整数部分,分除以 60 后加到度上。如果直接把4807.038当十进制数上传,地图上的位置会偏出几十公里。另外南纬和西经要取负值,否则坐标会落在错误半球。

GPS 模块串口通常是 9600 波特率,和 STM32 的某一个 USART 对接。串口接收建议做成环形缓冲区,在中断里逐字节存入,主循环里按行查找 GGA 语句。如果直接在主循环里等一整行数据,模块 1 秒输出多条语句,很容易漏数据。也可以先看\n\r\n切分行,再从行里找$GPGGA,逻辑更稳。

5.3 GPS 漂移处理:先用距离阈值做一次过滤

GPS 模块的原始定位数据存在两类误差:一类是环境误差,比如城市高楼反射导致的多径效应,另一类是接收机本身的噪声,表现为静止时坐标也会缓慢漂移。针对单点定位的应用,没必要上卡尔曼滤波,一个简单的“距离阈值”方案够用。

基本思路是:每收到一个新定位点,先和上一个有效点计算距离,如果距离超过预设阈值,认为这次跳变大概率是漂移,丢弃;如果距离在阈值以内,则更新当前位置。对于静止或慢速移动的场合,阈值可以设 20 到 50 米;对于车速较高的场景,阈值要放宽,否则正常移动会被误杀。

两点距离的近似计算可以用等距圆柱公式,在几十公里范围内精度足够:

double dx = (lng2 - lng1) * cos((lat1 + lat2) / 2 * PI / 180) * 111320.0; double dy = (lat2 - lat1) * 111320.0; double dist = sqrt(dx * dx + dy * dy);

111320是地球一米对应的纬度度数换算值,经度方向需要乘以纬度的余弦值修正。这段代码在 C 里建议用double计算,避免浮点精度不足。还有一个容易被忽略的点:如果设备长时间静止,同一坐标会重复上报,OneNet 折线图看起来是一条直线,这是正常现象。可以在业务层做一个“数值变化小于阈值不上报”的判断,省流量也减少平台存储压力。

6. 从 AT 指令到 OneNet 折线图:一条链路的分段验证与排错

整条链路的调试顺序应该是“先模块、再平台、后代码”,很多人一上来就让 STM32 自动跑全流程,出问题后无从下手。更靠谱的做法是先用 PC 串口助手把 SIM800C 调通,再让 STM32 接管。

6.1 用串口助手把 SIM800C 单独调通再合代码

把 SIM800C 的串口接到 PC 的 USB 转 TTL 模块上,用串口助手操作,逐步确认:上电后发 AT 能回 OK,AT+CSQ 信号正常,AT+CIICR 能附着 GPRS,AT+CIPSTART 能连上 OneNet 域名。然后手动把第 4 章的 HTTP 报文内容复制到串口助手里,以 HEX 模式发送结束符0x1A,观察 OneNet 返回的响应。如果这一步能返回"errno":0,说明 SIM800C 和 OneNet 之间的链路完全没问题,剩下的事情都集中在 STM32 代码上。

6.2 OneNet 数据流折线图与上报频率选择

数据上传成功后,登录 OneNet 控制台,进入设备详情,打开“数据流管理”或“数据展示”,选择 temp 数据流,就能看到按时间顺序的折线图。曲线不出现时先看平台的数据时间戳:如果设备上报的数值正常但曲线断断续续,往往是因为 SIM800C 每次上报后都断开 TCP,数据虽然到达但存在秒级延迟。上报频率方面,温湿度这类缓变量建议 30 秒到 60 秒一次,GPS 轨迹应用可以 5 到 10 秒一次。OneNet 免费版对请求频率有速率限制,短时间内高频请求可能返回限流错误,使用前留意平台说明。

6.3 三个部署细节:SIM 卡 APN、GPS 翻转补丁、2G 覆盖

实际部署时还有几个容易被忽略的细节。第一,物联网卡的 APN 不一定都是cmnet,一些专网卡使用独立 APN,拿到卡后先确认运营商提供的参数,否则 AT+CSTT 执行成功但 AT+CIICR 却一直失败。第二,如果你用的是库房里的老款 GPS 模块,在 2019 年之后出现“一直搜不到星”的现象,先查 GPS 周数翻转问题。早期部分 GPS 模块固件未处理周数溢出,需要刷新版固件或更换模块,网上的解决方案常被称为 GPS 翻转补丁。第三,SIM800C 依赖 2G 网络,部分地区的 2G 覆盖已经减弱甚至退网,这类方案在产线部署前一定要到目标区域实测信号;如果覆盖不达标,把 SIM800C 换成 NB-IoT 或 4G 模块,本文里的 HTTP 组包方式和 OneNet 数据流定义可以原样迁移,改动集中在 AT 指令集上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询