基于STM32G030与移远EC800MCNGB的自行车定位器:低功耗设计、GPS/MQTT通信与3D打印外壳全解析
最近有朋友想给自己的爱车加装一个定位器,市面上成品要么功能单一,要么续航不行,要么价格不菲。于是,我决定自己动手做一个。这个项目麻雀虽小,五脏俱全,用到了STM32单片机、4G Cat.1通信模组、GPS定位、低功耗设计、MQTT物联网协议,甚至还自己画了PCB、设计了3D打印外壳。今天,我就把这个项目的完整开发过程分享出来,手把手教你从零打造一个属于自己的、可远程查看的自行车定位器。
这个教程非常适合想深入学习嵌入式物联网开发的初学者。你将学到如何将不同的硬件模块组合成一个完整的产品,如何编写低功耗程序,如何处理GPS数据,以及如何通过MQTT与手机App通信。跟着做下来,你对一个完整嵌入式项目的开发流程会有非常清晰的认识。
1. 项目整体方案与硬件选型
做项目的第一步,永远是先想清楚要做什么,以及用什么来做。咱们这个自行车定位器的核心需求很简单:定时获取车辆位置,并通过网络发送到手机App上,同时要省电,用电池能撑很久。
1.1 核心硬件选型
基于以上需求,我选择了以下核心硬件:
主控芯片:STM32G030C8T6
- 为什么选它?首先,它是ARM Cortex-M0+内核,性能足够处理我们的逻辑。其次,它功耗很低,支持多种低功耗模式,非常适合电池供电场景。最后,它价格便宜,内存和Flash也够用(64KB Flash,8KB RAM),性价比极高。
通信与定位模组:移远EC800M-CNGB
- 为什么选它?这是一个“二合一”模组,它既是4G Cat.1通信模组(支持MQTT协议),又内置了GPS/北斗定位功能。用这一个模组,就解决了联网和定位两大核心问题,省去了外接GPS模块的麻烦,也简化了电路设计。它采用LCC封装,方便焊接。
供电方案:锂电池 + 升压/降压电路
- 定位器需要移动使用,必须用电池。我选择了一块2000mAh的锂电池。由于4G模组和GPS对供电电压有要求,我用了SY7069这颗DC-DC升压芯片,确保无论电池电压高低,都能稳定输出3.9V给模组。单片机则通过一颗LDO(XC6206)从3.9V降压到3.3V供电。
其他关键器件:
- 霍尔传感器:用于实现“磁铁靠近立即定位”的功能,无需物理按键,更防水。
- EEPROM (AT24C02):用于存储用户设置的休眠时间等配置,断电不丢失。
- 电平转换芯片:因为STM32是3.3V电平,而EC800M模组的串口是1.8V电平,直接连接可能通信不稳定,所以需要一颗电平转换芯片(如TXS0102)来“翻译”电平。
1.2 系统工作原理
整个系统的工作流程,我用下面这张图来帮你理解:
定位器端工作流程:
- 休眠:设备大部分时间处于最低功耗的
STANDBY模式,只有RTC(实时时钟)在运行。 - 唤醒:有两种唤醒方式:
- 定时唤醒:RTC闹钟时间到,唤醒单片机。
- 立即唤醒:磁铁靠近霍尔传感器,触发外部中断唤醒单片机。
- 工作:唤醒后,单片机执行以下操作:
- 给4G模组上电。
- 通过串口发送AT指令,让模组连接网络、获取GPS定位。
- 解析GPS数据,并将其从原始的WGS84坐标系转换成国内地图常用的GCJ-02坐标系。
- 通过MQTT协议,将转换后的坐标发送到云端服务器。
- 从模组获取网络时间,校准单片机的RTC,并设置下一次唤醒的闹钟。
- 再休眠:所有任务完成后,单片机控制MOS管切断4G模组电源,然后自己再次进入
STANDBY模式,等待下一次唤醒。
手机端(App):App订阅了MQTT服务器的特定主题。当定位器发布坐标信息到这个主题时,App就能实时收到并在地图上显示出来。
这样,一个完整的“采集-发送-显示”的物联网链路就打通了。
2. 核心软件设计与代码解析
硬件是骨架,软件才是灵魂。下面我们深入代码,看看各个关键功能是如何实现的。
2.1 低功耗管理与唤醒机制
省电是这个项目的重中之重。STM32G030的STANDBY模式功耗极低(微安级),但在此模式下,所有寄存器、内存内容都会丢失,程序会从main函数开始重新执行。因此,我们需要在进入休眠前,保存好状态(比如休眠时长),并在唤醒后根据标志位判断是哪种唤醒方式,从而执行不同的流程。
关键代码:进入STANDBY模式
// 使能唤醒引脚(对应霍尔传感器连接的引脚) __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // 设置RTC闹钟,用于定时唤醒 void Set_Rtc_AlarmTime(uint8_t hour, uint8_t minute, uint8_t second, uint8_t day) { // ... 配置RTC闹钟结构体NewAlarm ... if (HAL_RTC_SetAlarm_IT(&hrtc, &NewAlarm, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } } // 所有任务完成后,进入休眠 printf("STM32进入standby休眠模式\r\n"); HAL_PWR_EnterSTANDBYMode(); // 进入standby模式注意:进入
STANDBY模式后,之前的调试器连接会断开,再次下载程序需要先给芯片断电再上电,或者使用复位按钮。这是正常现象。
唤醒后的处理:在main函数开始,我们可以通过检查RTC的唤醒标志或GPIO的状态来判断唤醒源,从而决定是执行定时定位流程,还是立即定位流程。
2.2 与4G模组通信:AT指令的收发与解析
单片机通过串口2(USART2)与EC800M模组通信,发送AT指令来控制它。AT指令的回复长度不固定,但通常以\r\n结尾。为了高效、可靠地接收,我使用了“DMA + 串口空闲中断”的方式。
- DMA:像是一个“自动搬运工”,串口收到数据后,DMA会自动把数据搬到我们指定的内存缓冲区,完全不需要CPU参与。
- 空闲中断:当串口线上超过一个字节的时间没有新数据,就认为一帧数据发送完了,此时产生中断。
两者结合,CPU只在收到完整一帧数据时才被中断通知,去处理数据,效率非常高。
关键代码:AT指令发送与接收框架
// 定义一个结构体来管理串口2的状态 typedef struct { uint8_t Usart2DMARecBuffer[500]; // DMA接收缓冲区 uint8_t Usart2RecBuffer[500]; // 数据处理缓冲区 uint16_t UsartDMARecLen; // 本次接收到的数据长度 uint8_t UsartRecFlag; // 接收完成标志位 } Usart2_TypeDef; Usart2_TypeDef Usart2type; // 串口空闲中断回调函数(HAL库) void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart->Instance == USART2) { Usart2type.UsartDMARecLen = Size; // 保存接收长度 Usart2type.UsartRecFlag = 1; // 设置标志位,通知主循环处理 // 注意:这里不要处理数据,主循环中处理 // 重新启动DMA接收,准备下一帧数据 HAL_UARTEx_ReceiveToIdle_DMA(&huart2, Usart2type.Usart2DMARecBuffer, 500); } } // 主循环中检查并处理数据 if(Usart2type.UsartRecFlag == 1) { Usart2type.UsartRecFlag = 0; // 1. 将DMA缓冲区的数据拷贝到处理缓冲区 memcpy(Usart2type.Usart2RecBuffer, Usart2type.Usart2DMARecBuffer, Usart2type.UsartDMARecLen); // 2. 解析数据,例如判断是否包含"OK", "+QGPSLOC"等关键字 if(strstr((char *)Usart2type.Usart2RecBuffer, "+QGPSLOC")) { // 处理GPS数据 gps_location_flag = 1; } // 3. 清空DMA缓冲区,为下次接收做准备 memset(Usart2type.Usart2DMARecBuffer, 0x00, sizeof(Usart2type.Usart2DMARecBuffer)); }封装AT指令发送函数:为了使用方便,我封装了一个发送AT指令并等待回复的函数。
uint8_t EC800MCN_Send_Cmd(char *cmd, char *replay, uint16_t waittime) { uint8_t times = 3; // 重试3次 do { printf("发送指令: %s\r\n", cmd); HAL_UART_Transmit(&huart2, (uint8_t *)cmd, strlen(cmd), HAL_MAX_DELAY); HAL_Delay(waittime); // 等待模组响应 if(Usart2type.UsartRecFlag == 1) { // 检查回复中是否包含期待的字符串,如"OK" if(strstr((char *)Usart2type.Usart2RecBuffer, replay)) { return 1; // 成功 } } times--; } while(times > 0); return 0; // 失败 } // 使用示例:开启GPS EC800MCN_Send_Cmd("AT+QGPS=1\r\n", "OK", 2000);2.3 GPS数据获取、解析与坐标转换
这是项目的核心功能之一。EC800M模组内置GPS,我们只需要发送正确的AT指令就能获取定位信息。
1. 获取原始GPS数据
// 1. 开启GPS功能 EC800MCN_Send_Cmd("AT+QGPS=1\r\n", "OK", 2000); // 2. 获取定位信息,参数0表示立即返回(如果已定位) printf("AT+QGPSLOC=0\r\n"); HAL_UART_Transmit(&huart2, (uint8_t *)"AT+QGPSLOC=0\r\n", strlen("AT+QGPSLOC=0\r\n"), HAL_MAX_DELAY); HAL_Delay(1000); // 等待返回模组会返回一串数据,例如:+QGPSLOC: 082805.0,2232.7218,N,11356.1533,E,1.0,45.0,2,200.32,0.0,0.0,060623,06。我们需要从中提取经纬度。
2. 解析经纬度字符串返回的经纬度格式是“度分”格式(DDMM.MMMM)。例如2232.7218,N表示北纬22度32.7218分。
void extractCoordinatesAndConvert(const char *gpsInfo, double *latitude, double *longitude) { char *token = strtok((char *)gpsInfo, ","); int index = 0; while (token != NULL) { if (++index == 2) { // 第二个字段是纬度(如 2232.7218,N) *latitude = atof(token); // 转换为浮点数,得到2232.7218 // 移除后面的'N'字符 token[strcspn(token, "NSEW")] = '\0'; } else if (index == 3) { // 第三个字段是经度(如 11356.1533,E) *longitude = atof(token); token[strcspn(token, "NSEW")] = '\0'; break; } token = strtok(NULL, ","); } }3. 将“度分”格式转换为“十进制度”格式我们需要把2232.7218这样的数据转换成22.545363(22 + 32.7218/60)这样的十进制格式,方便计算。
double ddmmToDecimal(double ddmm) { int degrees = (int)ddmm / 100; // 取出“度”的部分,如22 double minutes = ddmm - (degrees * 100.0); // 取出“分”的部分,如32.7218 return degrees + minutes / 60.0; // 转换为十进制 } // 使用 double lat_ddddd = ddmmToDecimal(lat_ddmm); // lat_ddmm是2232.7218 double lng_ddddd = ddmmToDecimal(lng_ddmm);4. 坐标系统转换(WGS84 -> GCJ-02)GPS设备获取的经纬度是基于WGS84坐标系(国际标准)。但国内的地图服务(如高德、腾讯)为了国家安全,使用的是加密后的GCJ-02坐标系(俗称“火星坐标”)。不转换的话,位置在地图上会有几百米的偏移。
转换算法比较复杂,但网上有成熟的C语言版本。我们直接使用即可。
// 此函数判断坐标是否在中国境内(境外无需转换) int out_of_china(double lng, double lat) { return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55); } // WGS84转GCJ02的核心函数 void wgs84_to_gcj02(double wgLat, double wgLon, double *pgLat, double *pgLon) { if (!out_of_china(wgLon, wgLat)) { // ... 具体的转换计算(涉及三角函数和一系列系数) *pgLat = wgLat + dLat; *pgLon = wgLon + dLon; } else { // 中国境外,直接返回原坐标 *pgLat = wgLat; *pgLon = wgLon; } } // 使用转换后的坐标 double gcjLat, gcjLon; wgs84_to_gcj02(lat_ddddd, lng_ddddd, &gcjLat, &gcjLon);2.4 通过串口1进行调试与配置
为了方便调试和设置参数,我留出了串口1(USART1)连接到电脑(或通过USB转TTL模块)。通过这个串口,可以发送简单的指令来控制设备。
| 输入指令 | 功能说明 |
|---|---|
| RESET | 复位单片机,重新运行程序 |
| SLEEP | 强制单片机立即进入休眠模式(相当于关机) |
| Mx | 设置定时休眠的分钟数,例如M10表示10分钟 |
| Sx | 设置定时休眠的秒数,例如S30表示30秒 |
| SET | 设置为持续定位模式(每次唤醒都定位上传) |
| CANCEL | 设置为单次定位模式(只定位一次,之后休眠) |
| HELP | 打印所有可用的指令说明 |
指令处理代码片段:
// 在串口1的空闲中断处理中 if(strstr((char *)Usart1type.Usart1RecBuffer,"RESET")) { printf("设备重启!\r\n"); NVIC_SystemReset(); // 系统复位 } else if(strstr((char *)Usart1type.Usart1RecBuffer,"SLEEP")) { printf("设备进入待机模式!\r\n"); HAL_PWR_EnterSTANDBYMode(); // 进入休眠 } else if(strstr((char *)Usart1type.Usart1RecBuffer,"M")) { // 解析"M"后面的数字,如M15 char *numPart = (char *)Usart1type.Usart1RecBuffer + 1; int minutes = atoi(numPart); // 将分钟数保存到EEPROM EE_WriteBytes(&minutes, SLEEP_MINUTE_ADDR, 2); printf("休眠时间设置为:%d 分钟\r\n", minutes); } // ... 其他指令处理这样,在项目组装好、封闭到外壳里之后,我们依然可以通过这个串口来调试和修改关键参数,非常方便。
3. 硬件电路设计要点
光有代码跑不起来,硬件电路是基础。这里我挑几个关键部分讲讲设计时的考虑。
3.1 电源管理电路
电源是稳定运行的基石。整个系统有两路主要供电:
- 主电源(3.9V):由SY7069 DC-DC升压芯片提供。无论锂电池电压是3.7V还是4.2V,它都能输出稳定的3.9V,直接供给耗电大户——4G模组EC800M。
- 单片机电源(3.3V):由XC6206 LDO降压芯片从3.9V降压得到。单片机和外围传感器(如霍尔)用这路电。
为什么用升压芯片?因为4G模组在发射信号时瞬时电流很大(可能超过500mA),如果电池电压降低,LDO可能无法提供足够的电压和电流,导致模组重启或通信失败。升压芯片能保证电压始终稳定。
3.2 4G模组控制与电平转换
模组电源控制:为了极致省电,在单片机休眠时,需要彻底断开4G模组的供电。我使用了一个PMOS管来实现这个开关。当单片机引脚输出高电平(或高阻态)时,PMOS关闭,模组断电;当引脚输出低电平时,PMOS导通,模组得电。
电平转换:STM32的IO口是3.3V电平,而EC800M模组的串口引脚是1.8V电平。直接连接可能导致通信失败或损坏芯片。我使用了TXS0102这类双向电平转换芯片,自动适配两边电压,确保通信稳定。
3.3 天线与接口设计
- 4G天线:选用标准的弹簧天线,安装在PCB边缘,确保信号良好。
- GPS天线:选用35x35mm的有源陶瓷天线。**“有源”**意味着天线内部集成了低噪声放大器(LNA),能显著增强微弱的卫星信号,提高定位速度和精度。记得按照模组手册,给天线供电脚提供3.0V左右的电压。
- 调试接口:引出了SWD下载口和串口1,方便程序下载和调试。
- 霍尔传感器:放在PCB边缘,这样磁铁从外部靠近就能触发,实现无接触唤醒。
4. 结构设计与组装心得
产品最终要能用,外壳和结构设计很重要。
4.1 3D打印外壳
我用3D建模软件设计了一个54x54x54mm的方形外壳。设计要点:
- 防水:上壳设计成“盖子”状,扣在下壳上,接缝处尽量高,减少雨水渗入的可能。
- 固定:下壳底部嵌入了一个1/4英寸的滚花螺母。这是相机支架的标准接口,可以直接拧在自行车手机支架、运动相机支架上,安装拆卸非常方便。
- 开孔:只在下壳侧面开了一个Type-C充电/调试口的孔,并用防水胶塞堵住,最大化保证密封性。
4.2 堆叠组装
为了节省空间,我采用了三层堆叠的结构:
- 顶层:主控PCB板,包含STM32、4G模组、天线等所有电子部件。
- 中层:2000mAh锂电池,用双面胶或泡棉固定在上下板之间。
- 底层:充电/调试小板,集成了Type-C接口、充电芯片TP4056和USB转串口芯片CH340N。通过排线与顶层主板连接。
三层之间用长的塑料螺柱和螺丝固定,结构紧凑且牢固。
4.3 功耗实测与续航估算
功耗是检验低功耗设计成败的唯一标准。我用电流表实测了两种状态下的电流:
- 工作状态(模组上电、搜网、定位、上传):平均电流约90mA。这个过程持续时间较短,每次约30-60秒。
- 休眠状态(模组断电,单片机在STANDBY模式):平均电流约160µA(0.16mA)。
续航估算: 假设每天定位上传4次,每次工作1分钟(0.0167小时)。
- 工作耗电:90mA * 0.0167h * 4次/天 = 6 mAh/天
- 休眠耗电:0.16mA * 24h = 3.84 mAh/天
- 总耗电:约 10 mAh/天
- 电池容量:2000 mAh
- 理论续航:2000 / 10 ≈200天(超过6个月)
当然,这是理想情况,实际使用中信号搜索、定位难度会影响工作电流和时间,但续航一两个月是完全可以期待的。
5. 实际使用与注意事项
项目做完,最终要投入使用。这里有几个实用的提醒:
- 定位技巧:第一次定位,或者长时间未使用后,最好在室外空旷处进行。GPS天线要朝上,上方不要有金属物体严重遮挡。
- 手机App:你需要一个能连接MQTT服务器的安卓App来接收和显示位置。你可以自己写一个简单的,或者使用现成的物联网调试App,配置好服务器地址、主题(Topic)即可。
- MQTT服务器:你需要一个MQTT服务器作为中转。可以使用公共的测试服务器(如
broker.emqx.io),但对于长期使用的产品,建议自己搭建或使用可靠的云服务商(如阿里云、腾讯云物联网平台)。 - 防水:外壳虽有一定防水能力,但不要浸泡或长时间淋暴雨。Type-C口的防水塞一定要塞紧。
- 磁铁唤醒:准备一小块强磁铁(如钕铁硼磁铁),靠近设备侧面的霍尔传感器位置(可以贴个标记),设备就会立即唤醒并上报一次位置,非常方便。
这个项目从硬件选型、电路设计、PCB绘制、软件编程到结构组装,涵盖了一个完整嵌入式产品开发的全流程。希望这份详细的解析能为你打开一扇门,不仅仅是学会做一个定位器,更是掌握一种解决问题的思路和方法。嵌入式开发乐趣就在于,你能亲手将代码和电路,变成实实在在、能解决生活问题的智能设备。