STM32F103+EC800-4G多传感器上云实战:GNSS定位与ONENET双向通信
2026/9/13 20:00:17 网站建设 项目流程

简介:本资源是一套面向嵌入式物联网开发者的STM32F103单片机实战项目例程,聚焦于GNSS定位与多传感器数据采集、4G远程传输及云平台双向通信,适用于高校课程设计、毕业设计及工程师快速原型开发。压缩包共245个文件,含40余个C/H源码与头文件(实现串口驱动、EC800-4G模块AT指令解析、GNSS数据解析、ONENET MQTT协议接入及控制指令响应)、40余个编译中间文件(o/crf/d)与4个可执行镜像(hex/axf),辅以BMP效果截图(如实时地图定位、多传感器上传可视化)和KEIL工程配置文件(uvprojx/uvoptx/sct),整体8.42MB,结构完整、注释详尽。已有2242人学习下载,提供从硬件接线定义、芯片型号适配说明到传感器扩展指引的全流程参考,代码可直接编译运行,显著降低物联网终端开发门槛。

1. 为什么用 STM32F103 + EC800-4G 做 GNSS+多传感器上云,不能只靠“AT 指令手册”就开干?

很多工程师拿到“STM32F103 单片机连接 EC800-4G 模块采集 GNSS 定位数据和多组传感器数据上传到 ONENET 云平台并接收控制指令”这个需求时,第一反应是翻 EC800 的 AT 指令集、查 ONENET 的 HTTP/MQTT 接入文档、抄几段 HAL 库串口收发代码——结果卡在第 3 步:GNSS 数据解析乱码、传感器时间戳错位、ONENET 返回 401 或 400、控制指令收不到或解析失败。根本原因在于,这不是“单点通信”,而是一个四层耦合系统:底层硬件时序(STM32F103 串口 DMA 与中断优先级)、模组状态机(EC800 的注册/附着/网络就绪/PPP 拨号/AT 命令队列)、协议栈语义(GNSS NMEA-0183 字段校验与 UTC 时间同步、ONENET 设备鉴权与 JSON 数据体结构)、应用逻辑调度(多传感器采样周期对齐、GNSS 定位有效性判断、指令缓存与执行隔离)。尤其当接入光电传感器、胎压监测传感器、霍尔传感器等不同电气特性与协议的设备时,STM32F103 的 GPIO 配置、ADC 采样精度、I2C/SPI 总线仲裁、CAN 总线同步跳跃宽度(SJW)设置都会直接影响数据一致性。本文不讲“如何点亮 LED”,而是按真实项目节奏,从 EC800-4G 在 STM32F103 上的稳定拨号开始,逐层打通 GNSS 解析、多传感器融合、ONENET 可靠上传与双向指令闭环。

2. 用 STM32F103 HAL 库 + EC800-4G 实现稳定 PPP 拨号与网络就绪检测

EC800-4G 模块不是即插即用的 USB 网卡,它需要 STM32F103 主动完成网络注册、APN 配置、PPP 拨号三阶段才能获得 IP 地址。常见错误是跳过注册状态轮询直接发AT+CGATT?,或在未确认+CGATT: 1时强行拨号,导致ERRORNO CARRIER。必须建立状态机驱动的 AT 交互流程。

2.1 STM32F103 与 EC800-4G 的硬件连接与串口初始化要点

EC800-4G 通过 UART2(PA2/PA3)与 STM32F103C8T6 连接,需注意三点:

  • 电平匹配:EC800 是 3.3V TTL 电平,直接接 STM32 引脚,无需 MAX3232;
  • 电源设计:EC800 发射峰值电流达 2A,必须用低 ESR 电容(≥470μF)紧贴模块 VCC 引脚,否则拨号瞬间复位;
  • RESET 与 POWERKEY 控制:POWERKEY(高电平持续 1.5s)用于开机,RESET(低电平脉冲)用于硬复位,二者不可共用 GPIO,建议 POWERKEY 接 PB0(推挽输出),RESET 接 PB1(开漏+上拉)。
// stm32f103_hal_init.c:关键串口配置(UART2) huart2.Instance = USART2; huart2.Init.BaudRate = 115200; // EC800 默认波特率,勿用 9600(易丢帧) huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; HAL_UART_Init(&huart2); // 启用 RX 中断 + IDLE 中断(捕获完整 AT 响应) __HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE); __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);

提示:禁用UART_OVERSAMPLING_8,EC800 对时序敏感,16 倍过采样更可靠;IDLE 中断用于检测一帧数据结束(无空闲线状态),比超时判断更精准。

2.2 EC800-4G 网络就绪状态机实现(含超时与重试)

EC800 的网络状态需按顺序确认:+CREG: 1,1(已注册到 LTE 网络)→+CGREG: 1,1(已附着)→+CGACT: 1,1(PDP 上下文激活)→AT+CIICR(PPP 拨号)→AT+CIFSR(获取 IP)。任一环节失败需回退重试,且每次 AT 命令必须带\r\n并等待OKERROR

// ec800_network.c:状态机核心逻辑(简化版) typedef enum { EC800_STATE_POWER_ON, EC800_STATE_WAIT_CREG, EC800_STATE_WAIT_CGREG, EC800_STATE_WAIT_CGACT, EC800_STATE_PPP_DIAL, EC800_STATE_GET_IP, EC800_STATE_READY } ec800_state_t; static ec800_state_t current_state = EC800_STATE_POWER_ON; static uint32_t state_timer = 0; void ec800_task(void) { switch(current_state) { case EC800_STATE_POWER_ON: if (ec800_power_on()) { // 拉高 POWERKEY 1.5s current_state = EC800_STATE_WAIT_CREG; state_timer = HAL_GetTick(); } break; case EC800_STATE_WAIT_CREG: if (HAL_GetTick() - state_timer > 30000) { // 30s 超时 current_state = EC800_STATE_POWER_ON; // 复位重试 break; } if (ec800_check_creg()) { // 解析 "+CREG: 1,1" current_state = EC800_STATE_WAIT_CGREG; state_timer = HAL_GetTick(); } break; // ... 后续状态类似,省略 case EC800_STATE_READY: // 网络就绪,启动 GNSS 和传感器任务 break; } }

注意:AT+CGDCONT=1,"IP","CMNET"中 APN 必须与运营商匹配(中国移动为CMNET,电信为CTNET),错误 APN 导致+CGACT: 0,1AT+CIICR后必须立即发AT+CIFSR,否则 IP 不刷新。

3. 解析 EC800-4G 内置 GNSS 数据与多传感器数据融合策略

EC800-4G 模块内置 u-blox M8 GNSS 芯片,支持 NMEA-0183 协议输出$GPGGA$GPRMC等语句。但直接解析原始 NMEA 存在三大陷阱:校验和错误(*XX后换行符缺失)、UTC 时间与本地时区错位、定位无效时GGAFix quality字段为 0 却被误传。同时接入多路传感器(如 I2C 的 BME280 温湿度气压、SPI 的 ADXL345 加速度、模拟输入的 MQ-2 烟雾传感器)时,必须解决采样时序冲突。

3.1 GNSS NMEA 数据的健壮解析与有效性过滤

EC800 通过AT+CGNSINF获取 GNSS 信息(返回 CSV 格式),或启用AT+CGNSURC=1让模块主动推送 NMEA 语句。后者更实时,但需处理乱序和粘包。推荐使用AT+CGNSINF定时查询(每 5s 一次),因其返回结构化字段,规避 NMEA 解析风险。

// gnss_parser.c:AT+CGNSINF 返回示例 // +CGNSINF: 1,1,20230520123456.000,39.9042,116.3975,123.45,1.2,2.3,3,10,1.5,2.0,3.1 // 字段含义:有效标志,定位状态,UTC时间,纬度,经度,海拔,水平精度,垂直精度,定位模式,卫星数,PDOP,HDOP,VDOP typedef struct { uint8_t valid; // 1=有效 uint8_t fix; // 1=GPS,2=GLONASS,3=GPS+GLONASS uint32_t utc_time; // 秒级时间戳(需转为 Unix 时间) float latitude; // 度 float longitude; // 度 float altitude; // 米 float h_acc; // 水平精度(米) } gnss_data_t; bool parse_cgnsinf(const char* resp, gnss_data_t* out) { char* p = strstr(resp, "+CGNSINF:"); if (!p) return false; p += 10; // 用 strtok_s 分割(HAL 库不自带,需自行实现或用 sscanf) int fields[13]; if (sscanf(p, "%d,%d,%*d,%d,%d,%f,%f,%f,%d,%d,%f,%f,%f", &fields[0], &fields[1], &fields[2], &fields[3], &fields[4], &out->altitude, &out->h_acc, &out->h_acc, &fields[8], &fields[9], &out->h_acc, &out->h_acc, &out->h_acc) != 13) { return false; } out->valid = fields[0]; out->fix = fields[1]; // UTC 时间字段为 "YYYYMMDDHHMMSS",需转换为 Unix 时间戳(此处省略具体转换) out->latitude = fields[2] / 100.0f + (fields[2] % 100) / 60.0f; // DDDMM.MMMM → DD.DDDD out->longitude = fields[3] / 100.0f + (fields[3] % 100) / 60.0f; return (out->valid == 1 && out->fix >= 1); // 仅当定位有效且有卫星参与才接受 }

提示:AT+CGNSINF返回的经纬度是度分格式(如3954.2522表示 39°54.2522′),需转换为十进制度;h_acc字段单位为米,若 >10 则视为定位漂移,应丢弃该帧。

3.2 多传感器数据采集时序协调与总线仲裁

STM32F103 的 ADC、I2C、SPI 共享 AHB/APB 总线,高频采样易引发冲突。例如:BME280 I2C 读取耗时约 5ms,若此时 ADC 触发 DMA 传输,可能造成 I2C SCL 被拉低超时。解决方案是时间片轮询 + 优先级标记

传感器类型接口采样周期关键参数优先级
GNSSUART5svalid==1高(触发上传)
BME280I2C1s温度/湿度/气压
ADXL345SPI100msX/Y/Z 加速度高(振动告警)
MQ-2ADC500ms模拟电压值
// sensor_manager.c:基于 SysTick 的时间片调度 #define SENSOR_TASK_INTERVAL_MS 100 static uint32_t last_sensor_tick = 0; void sensor_task(void) { uint32_t now = HAL_GetTick(); if (now - last_sensor_tick < SENSOR_TASK_INTERVAL_MS) return; last_sensor_tick = now; static uint8_t sensor_idx = 0; switch(sensor_idx) { case 0: // GNSS 查询 if (ec800_is_network_ready()) { ec800_send_at("AT+CGNSINF\r\n"); } break; case 1: // BME280 读取 bme280_read_data(&bme_data); break; case 2: // ADXL345 读取 adxl345_read_xyz(&acc_data); break; case 3: // MQ-2 ADC 采样 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); mq2_value = HAL_ADC_GetValue(&hadc1); break; } sensor_idx = (sensor_idx + 1) % 4; }

注意:ADC 采样前需确保hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT(默认),且HAL_ADC_GetValue()返回 12-bit 值(0–4095),MQ-2 输出电压需通过Vout = (mq2_value / 4095.0f) * 3.3f换算;I2C 读取 BME280 时,务必检查HAL_I2C_GetState(&hi2c1) == HAL_I2C_STATE_READY,避免总线忙时强行操作。

4. 构建 ONENET 平台设备接入与双向数据通道(HTTP POST + MQTT 订阅)

ONENET 支持 HTTP 和 MQTT 两种接入方式。HTTP 适合小数据量、低频上传(如 GNSS 定位点),MQTT 适合实时指令下发(如远程重启、传感器阈值修改)。但 STM32F103 RAM 仅 20KB,无法运行完整 MQTT 客户端,必须精简实现。

4.1 ONENET HTTP 设备注册与 APIKey 生成(非 SDK 方式)

ONENET 不再提供旧版 SDK,所有设备需通过APIKey 鉴权。步骤:登录 ONENET 控制台 → 创建产品 → 添加设备 → 在设备详情页点击“生成 APIKey” → 复制 32 位字符串。该 Key 用于 HTTP Header 的api-key字段,不可硬编码在固件中,应通过 AT 命令从外部写入 Flash。

// onenet_http.c:构造 GNSS 数据上传请求 // POST /devices/{device_id}/datapoints HTTP/1.1 // Host: api.heclouds.com // api-key: YOUR_APIKEY_HERE // Content-Type: application/json const char* http_post_template = "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" "%s"; char json_buf[256]; snprintf(json_buf, sizeof(json_buf), "{\"datastreams\":[" "{\"id\":\"gnss\",\"datapoints\":[{\"at\":\"%u\",\"value\":{\"lat\":%.6f,\"lng\":%.6f,\"alt\":%.1f}}]}," "{\"id\":\"env\",\"datapoints\":[{\"at\":\"%u\",\"value\":{\"temp\":%.1f,\"humi\":%.1f,\"pres\":%.0f}}]}" "]}", time_now, gnss.lat, gnss.lng, gnss.alt, time_now, bme.temp, bme.humi, bme.pres); int json_len = strlen(json_buf); char http_req[512]; snprintf(http_req, sizeof(http_req), http_post_template, "your_device_id", "your_apikey_here", json_len, json_buf); // 通过 UART2 发送 HTTP 请求(需 TCP 连接已建立) ec800_send_tcp_data(http_req, strlen(http_req));

提示:at字段必须为 Unix 时间戳(秒级),ONENET 拒绝毫秒级;datastreams数组中每个id必须与 ONENET 平台创建的数据流 ID 严格一致(区分大小写);HTTP Body 长度必须精确计算,否则服务器返回400 Bad Request

4.2 基于 AT+MQTT 指令的轻量级 MQTT 订阅与指令解析

EC800-4G 支持AT+MQTT指令集,无需移植 MQTT 库。关键步骤:AT+MQTTUSERCFG(配置 clientID/user/pwd)→AT+MQTTCONN(连接 ONENET MQTT 服务器mqtt.heclouds.com:1883)→AT+MQTTSUB(订阅/devices/{device_id}/cmd主题)。

// mqtt_control.c:指令接收与解析 // ONENET 下发指令格式:{"cmd":"reboot","para":{"delay":10}} // 订阅后,EC800 会推送:+MQTTRCV: "topic","payload_length" void handle_mqtt_recv(const char* topic, const char* payload) { if (strcmp(topic, "/devices/your_device_id/cmd") == 0) { cJSON* root = cJSON_Parse(payload); if (!root) return; cJSON* cmd_obj = cJSON_GetObjectItem(root, "cmd"); if (cmd_obj && cmd_obj->valuestring) { if (strcmp(cmd_obj->valuestring, "reboot") == 0) { cJSON* para = cJSON_GetObjectItem(root, "para"); int delay = 0; if (para && cJSON_GetObjectItem(para, "delay")) { delay = cJSON_GetObjectItem(para, "delay")->valueint; } HAL_Delay(delay * 1000); NVIC_SystemReset(); // 执行重启 } else if (strcmp(cmd_obj->valuestring, "set_threshold") == 0) { // 解析阈值参数,更新本地变量 } } cJSON_Delete(root); } }

注意:AT+MQTTUSERCFGuser为设备 ID,pwd为 APIKey;AT+MQTTSUB的 QoS 必须设为 1(至少一次),否则指令可能丢失;ONENET MQTT 服务器要求 TLS 加密,但 EC800-4G 默认不支持,需确认模块固件版本支持AT+MQTTUSERCFG=0,1,...(开启 SSL)。

5. STM32F103 上 ONENET 数据上传可靠性增强与指令执行安全隔离

真实场景中,网络抖动、ONENET 限流、传感器异常会导致数据丢失或指令误执行。必须加入重传机制、本地缓存、指令签名验证。

5.1 基于环形缓冲区的本地数据缓存与指数退避重传

ONENET HTTP 接口有频率限制(每分钟最多 60 次),若 GNSS 每 5s 上传一次(12 次/分钟)+ 环境数据每 1s 上传(60 次/分钟),必然触发限流。解决方案:将待上传数据存入 Flash 环形缓冲区(如 STM32F103C8T6 的 64KB Flash,划出 4KB 专用区),上传失败时按 2^N 秒退避(1s, 2s, 4s, 8s...)重试,成功后擦除对应扇区。

// flash_cache.c:简易环形缓冲区(地址 0x0800F000 开始,4KB) #define CACHE_START_ADDR 0x0800F000 #define CACHE_SIZE 4096 #define RECORD_SIZE 128 typedef struct { uint32_t timestamp; // Unix 时间戳 uint8_t type; // 0=GNSS, 1=ENV, 2=ACC uint8_t data[124]; // 序列化后的 JSON 片段 } cache_record_t; static uint16_t cache_head = 0; static uint16_t cache_tail = 0; bool cache_write(const cache_record_t* rec) { if ((cache_head + sizeof(cache_record_t)) % CACHE_SIZE == cache_tail) { return false; // 满 } uint32_t addr = CACHE_START_ADDR + cache_head; HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, rec->timestamp); HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr+4, rec->type); for (int i = 0; i < 124; i++) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr+5+i, rec->data[i]); } HAL_FLASH_Lock(); cache_head = (cache_head + sizeof(cache_record_t)) % CACHE_SIZE; return true; } void cache_upload_task(void) { if (cache_tail == cache_head) return; // 空 uint32_t addr = CACHE_START_ADDR + cache_tail; cache_record_t rec; rec.timestamp = *(uint32_t*)addr; rec.type = *(uint8_t*)(addr+4); memcpy(rec.data, (void*)(addr+5), 124); if (onenet_http_post(&rec)) { // 上传成功 // 擦除该记录所在扇区(需按 1KB 扇区擦除) HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(&eraseInitStruct, &SECTOR_ERROR); HAL_FLASH_Lock(); cache_tail = (cache_tail + sizeof(cache_record_t)) % CACHE_SIZE; } else { static uint8_t retry_count = 0; if (HAL_GetTick() - last_retry_time > (1U << retry_count) * 1000) { retry_count = (retry_count < 5) ? retry_count + 1 : 5; last_retry_time = HAL_GetTick(); } } }

提示:Flash 编程前必须调用HAL_FLASH_Unlock(),擦除后需HAL_FLASH_Lock()AT+CGATT?返回+CGATT: 0时禁止上传,避免无网络重试耗电。

5.2 控制指令执行前的安全校验与白名单机制

ONENET 指令通道是开放的,任何拥有 APIKey 的人都能下发命令。必须增加两道防线:

  1. 指令签名:ONENET 下发指令时,在para中加入sig字段(HMAC-SHA256(apikey+cmd+timestamp)),STM32 用相同算法校验;
  2. 白名单指令:只允许rebootset_ledupdate_threshold等预定义动作,拒绝format_flashexec_shell等危险指令。
// control_security.c:指令白名单与签名验证 const char* allowed_commands[] = {"reboot", "set_led", "update_threshold"}; #define ALLOWED_CMD_COUNT 3 bool is_command_allowed(const char* cmd) { for (int i = 0; i < ALLOWED_CMD_COUNT; i++) { if (strcmp(cmd, allowed_commands[i]) == 0) { return true; } } return false; } bool verify_command_signature(cJSON* root) { cJSON* sig_obj = cJSON_GetObjectItem(root, "sig"); if (!sig_obj || !sig_obj->valuestring) return false; // 构造待签名字符串:apikey+cmd+timestamp cJSON* cmd_obj = cJSON_GetObjectItem(root, "cmd"); cJSON* ts_obj = cJSON_GetObjectItem(root, "timestamp"); if (!cmd_obj || !ts_obj) return false; char sign_str[128]; snprintf(sign_str, sizeof(sign_str), "%s%s%ld", "your_apikey_here", cmd_obj->valuestring, ts_obj->valueint); // STM32F103 无硬件 SHA,用软件库(如 mbedtls)计算 HMAC uint8_t expected_sig[32]; mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), (const unsigned char*)"your_apikey_secret", 16, (const unsigned char*)sign_str, strlen(sign_str), expected_sig); // 与 sig_obj->valuestring 比较(Base64 编码) return hmac_compare_base64(sig_obj->valuestring, expected_sig, 32); }

注意:timestamp字段必须由 ONENET 服务端注入,防止重放攻击;mbedtls_md_hmac需提前移植 mbedtls 到 STM32F103,编译时关闭不必要模块以节省空间;白名单指令列表应存储在 Flash 中,支持 OTA 更新。

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

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

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

立即咨询