做车载定位、手持GPS或者二轮车追踪这类产品开发的工程师,这几年应该都被同一件事折磨过——STM32F103的交期和价格。我在一个户外定位终端项目里就被它卡过脖子:芯片交期一拖就是二十多周,价格翻了不止一倍,整套BOM成本直接失控。后来把主控换成了国芯思辰平台主推的国产32位高性能MCU,同样的Cortex-M3内核、72MHz主频,引脚和大部分外设资源跟STM32F103对齐,硬件板子几乎没怎么改,GPS数据照常跑,成本却下去了一大截。这篇文章就把这次替换项目的完整过程拆出来:从选型思路、最小系统设计,到GPS模块接线、软件串口实现,再到NMEA协议解析和调试现场踩过的坑,给正准备做国产化替代或GPS平台开发的朋友一个可直接参考的完整方案。
先说清楚这篇文章适合谁看:如果你正在用STM32F103做定位类产品,想换国产MCU但担心迁移工作量;或者你刚接触GPS平台开发,想知道主控该怎么选、GPS模块怎么接、数据怎么解,那这篇内容就是按你的需求写的。我会把参数计算过程、接线表、排查思路都放出来,尽量做到你拿着文章就能在自己的板子上复现。
1. 替换选型:为什么是国产32位MCU,而不是其他方案
1.1 从一次芯片供应危机说起
2021年前后那次全球范围的MCU缺货潮,相信不少同行都记忆犹新。我们当时有一个便携式GPS logger的项目,主控选的就是STM32F103C8T6,用量虽然不算特别大,但属于那种"离了它就转不动"的核心物料。缺货最严重的时候,代理商的回复要么是"排期20周以上",要么是"现货价格翻了3倍还拿不到量"。项目要按时交付,不能等,于是我开始认真评估国产替代方案。
选型的时候,我把市面上主流的国产M3内核MCU都过了一遍:GD32、APM32、HC32、AT32等都在考虑范围内。最终通过国芯思辰渠道拿到了一款主推的国产32位MCU,它的关键规格相当讨喜:Cortex-M3内核、主频最高72MHz、Flash做到128KB到256KB可选、SRAM 20KB起步,GPIO和复用功能基本对齐STM32F103,连封装都是LQFP48引脚兼容。这意味着我原来的PCBLayout不用大改,硬件成本和时间成本都能压到最低。
还有一个很现实的考量:供货和价格。国产芯片的产能调度更灵活,通过国芯思辰这类平台直接对接原厂资源,交期可以压缩到4到8周,单价相比缺货时的STM32F103有明显优势。对于一个要量产交付的产品来说,供应链确定性有时候比性能参数更重要。
1.2 GPS平台对MCU的真实需求拆解
很多工程师第一次做GPS产品,容易走进"主控性能越高越好"的误区。实际上GPS平台对MCU的要求非常固定,捋清楚之后你会发现STM32F103这个级别的芯片其实刚刚好,这也正是它多年占据中低端定位市场的原因。
第一是串口资源。GPS模块(比如u-blox NEO-M8N)通过UART输出NMEA数据,至少占用1路硬件串口。如果你的产品还要接调试串口、蓝牙模块或者4G Cat.1模组,那就需要2到3路UART。STM32F103的USART1/2/3正好够用,但如果你像我一样想额外引出两路串口给传感器,就可能要动用软件串口方案,这个我后面会详细讲。
第二是定时器资源。做软件串口需要定时器;做PPM信号捕获(比如接遥控器接收机)需要定时器输入捕获;做PPS秒脉冲同步也需要捕获通道。所以选型时一定要确认定时器数量够不够,别光看主频。
第三是低功耗需求。手持GPS设备对功耗非常敏感,MCU的睡眠模式和唤醒时间很关键。我当时选的这颗国产MCU支持Sleep和Stop模式,Stop模式下电流可以做到微安级别,GPS模块在待机时断电,整机静态功耗就能控制住。
第四是电源逻辑。GPS模块、MCU、SD卡存储、LED指示灯都工作在3.3V,系统的电源设计可以统一,不用额外做电平转换。这一点对简化BOM很有利。
把需求拆完之后,我的结论是:一颗72MHz主频、具备3路以上串口、定时器数量充足、支持低功耗模式的国产M3内核MCU,就是GPS平台的最优解。与其盲目追求高端芯片,不如把资源用在刀刃上。
2. 硬件设计实战:最小系统搭起来,GPS模块接好线
2.1 最小系统电路:电源、晶振、复位、下载口
替换芯片之后的硬件设计,我建议从最小系统开始验证。虽然引脚兼容,但毕竟不同厂家的芯片在电气特性上会有细微差异,先把最小系统跑通,再往上挂GPS模块,调试会顺畅很多。
电源部分,我用的是5V供电输入,板载一颗AMS1117-3.3LDO转3.3V。这里有个明显的坑:NEO-M8N模块的峰值电流可以达到80mA以上(有源天线馈电时),GPS模块启动瞬间会有电流冲击。如果LDO选得太小或者输入输出压差不足,模块会反复重启,表现就是定位数据时断时续。我自己的板子在3.3V输出端放了两个10uF陶瓷电容并联,再加一个100nF高频去耦,实测模块启动电压跌落控制在50mV以内。
晶振电路用的是8MHz主晶振加两颗22pF负载电容。这里要注意:国产MCU的晶振驱动能力跟ST原厂可能有差异,如果起振困难,把负载电容换成18pF或者把晶振换成低ESR型号通常能解决。RTC如果需要独立走时,可以加一颗32768Hz的晶振,不过GPS模块本身有UTC时间输出,很多场景下可以省掉这颗RTC晶振。
复位电路沿用ST经典的10k上拉加0.1uF对地电容,NRST引脚内部已经有弱上拉,这颗电阻可选。BOOT0和BOOT1引脚直接接地,避免浮空导致启动异常。下载调试口接SWD标准4线:SWDIO、SWCLK、GND、3.3V,我额外加了一颗10k上拉到SWDIO,保证下载器的稳定性。整套最小系统跑起来之后,用LED闪烁程序验证GPIO,再用RTT或者串口打印验证内核时钟,确认无误再进入GPS模块调试。
2.2 NEO-M8N模块接线:电平匹配与有源天线馈电是重灾区
GPS模块部分,我使用的是u-blox NEO-M8N,这是市面上最常见的GPS模组之一,支持GPS、GLONASS、北斗等卫星系统双频接收,默认波特率9600,默认输出NMEA协议。接线关系看下表。
| 信号 | MCU引脚 | 方向 | 说明 |
|---|---|---|---|
| VCC | 3.3V | - | 模块供电,必须稳定3.3V |
| GND | GND | - | 共地,必须与MCU同地 |
| TXD | PA10 (USART1_RX) | 模块->MCU | GPS模块发送数据到MCU接收 |
| RXD | PA9 (USART1_TX) | MCU->模块 | MCU发送配置指令给模块 |
| PPS | PB0 (可选) | 模块->MCU | 秒脉冲输出,可做时间同步 |
| RF_IN | 有源天线 | - | 天线馈电由模块内部3V提供 |
接线截图在网上搜"NEO-M8N接线"能搜到很多,但大家最容易忽略的是两个点。
第一个是TXD和RXD的电平匹配。NEO-M8N是3.3V逻辑电平,不能直接接5V MCU的IO。我选的国产MCU本身就是3.3V供电,所以这个问题天然规避了。如果你用的是5V单片机,RXD方向必须加电平转换或者用电阻分压,否则长期运行会烧模块引脚。
第二个是有源天线馈电。天线座的RF_IN引脚模块内部一般已经有3V馈电,但部分国产天线模块或者延长线可能会影响馈电。我做的第一版PCB因为天线走线过长且没有做好阻抗匹配,导致定位灵敏度很差,搜星数一直在6颗以下。后来把天线走线改短、增加走线宽度、远离高频开关电源,搜星数直接提升到12颗左右。另外,天线尽量放在板边或独立区域,远离MCU晶振和DC-DC电感,这些干扰源对GPS信号的影响比你想象的大得多。
3. 串口不够用怎么办:定时器手写一个软件串口
3.1 触发场景:三路串口需求遇上两路硬件UART
这个项目原本的串口规划是这样的:USART1接GPS模块,USART2接调试串口。后来客户加了一个需求,要外接一个串口输出的温湿度传感器,而且传感器协议是Modbus RTU,必须独占一路UART。这下USART1和USART2都被占了,USART3虽然存在,但它和SPI2、I2C2部分复用引脚,这些引脚已经被其他功能占用。硬件上重新分配引脚会推翻Layout,代价太大。
这个时候软件串口就成了最务实的方案:用一颗定时器加两个GPIO,模拟出一个半双工的UART口,跑9600波特率,足够应付温湿度传感器这种小数据量的交互。
3.2 软件串口实现原理与定时器参数计算
软件模拟UART的本质,就是按位周期的节奏,用GPIO电平变化来表示串口波形。UART的8N1格式,一帧数据由10个位组成:1个起始位(低电平)、8个数据位(LSB在前)、1个停止位(高电平)。波特率9600表示每秒传输9600个位,每个位周期是1/9600秒,约104.17微秒。
定时器的重载值计算在这里很关键。我使用的国产MCU定时器时钟来自APB1总线,默认配置下是72MHz。为了得到9600Hz的定时中断频率,重载值计算公式是:
重载值 = 定时器时钟 / (预分频 + 1) / 波特率 - 1
如果预分频设为0,则重载值 = 72,000,000 / 9600 - 1 = 7499。也就是说,定时器每计数7500个脉冲溢出一次,中断周期正好是104.17微秒,和9600波特率的位周期完全对齐。这里注意,如果使用预分频会产生累计误差,传输大量字节时可能累积出位错位。所以软件串口务必使用高精度配置,不要图省事加预分频。
发送方向的逻辑比较简单:先把TX引脚配置为推挽输出,拉低引脚产生起始位,然后启动定时器。第一个定时器中断到来时发送第0位数据(LSB),之后每个中断发送下一位,直到第7位发送完毕,最后一位中断发送停止位(高电平),然后关闭定时器。发送过程参考下面的代码结构。
void SoftUART_SendByte(uint8_t byte) { tx_byte = byte; tx_bit_index = 0; TX_PIN_LOW(); // 起始位 TIM_Cmd(SOFT_TIM, ENABLE); } void SOFT_TIM_IRQHandler(void) { if (tx_bit_index < 8) { if (tx_byte & (1 << tx_bit_index)) { TX_PIN_HIGH(); } else { TX_PIN_LOW(); } tx_bit_index++; } else { TX_PIN_HIGH(); // 停止位 TIM_Cmd(SOFT_TIM, DISABLE); } }接收方向稍微麻烦一点:RX引脚配置为外部中断下降沿触发。GPS模块或传感器模块空闲时TX线保持高电平,数据帧起始位到来时会产生下降沿,外部中断被触发。此时启动定时器,并在中断里通过状态机方式,在每一位的中点采样RX引脚电平。
这里有一个实操细节:起始位下降沿触发后,我们不能立刻采样第一个数据位,而是要等1.5个位周期,也就是从下降沿开始算,等数据位0到达中点附近再采样。后续每一位则间隔一个完整位周期采样。这样做是因为UART是异步协议,接收方只能通过起始沿同步,所以要在每一位最稳定的中点读取电平,避开跳变区域。
void EXTI_RX_IRQHandler(void) { // 下降沿触发,起始位到来了 rx_bit_index = 0; rx_sample_delay = 1; // 跳过起始位,先等待1.5位周期再采样 TIM_SetCounter(SOFT_TIM, 0); TIM_Cmd(SOFT_TIM, ENABLE); } void SOFT_TIM_IRQHandler(void) { if (rx_bit_index == 0) { // 这是起始沿后的第一个中断,点位1.5位周期后,直接采样第一位 rx_byte = 0; if (RX_PIN_READ()) rx_byte |= 0x01; rx_bit_index++; } else if (rx_bit_index < 8) { if (RX_PIN_READ()) rx_byte |= (1 << rx_bit_index); rx_bit_index++; } else { // 采样停止位,关闭定时器 TIM_Cmd(SOFT_TIM, DISABLE); // rx_byte 就是完整接收的字节 SoftUART_RxHandler(rx_byte); } }3.3 软件串口的三个稳定性关键点
第一,定时器中断优先级必须设为最高或次高。如果软件串口位周期被其他中断打扰,比如默认的SysTick滴答中断或硬件串口中断,采样点就会偏移,导致数据收错。我在项目里把软件串口用的TIM3中断优先级设成了0(最高优先级组别下),实测接收一百万个字节的误码率在1个以内。
第二,接收方向务必使用外部中断采样起始沿。有人偷懒用轮询检测起始位,但主循环执行时间不确定,轮询频率跟不上位周期,数据很容易丢。外部中断加定时器,中断驱动才是可靠方案。
第三,波特率最好固定9600,不要轻易尝试115200。软件串口的定位误差跟定时器精度、中断响应时间有关,9600波特率下位周期104微秒,哪怕中断响应有1-2微秒的抖动,累计误差也不容易超界。但115200下位周期只有8.7微秒,中断抖动占比太大,软件串口基本不可用,该上硬件串口还得上。
4. GPS数据处理:从NMEA原始帧到经纬度坐标
4.1 NMEA协议关键语句解读
GPS模块输出的NMEA 0183协议是一种纯文本协议,每条语句以$开头,以回车换行结尾,字段间用逗号分隔。定位信息主要来自GGA和RMC两条。
看一条实际的GGA语句:
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47依次解析:123519是UTC时间12:35:19;4807.038,N是北纬48度07.038分;01131.000,E是东经11度31.000分;1表示GPS定位有效(0为无效);08是参与定位的卫星数;0.9是HDOP水平精度因子;545.4,M是海拔高度;46.9,M是大地水准面高度差。末尾*47是校验和。
再看RMC语句,它包含了更多速度、航向和时间信息:
$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A其中A表示定位状态(A=有效,V=无效),022.4是速度(节),084.4是航向角,230394是日期。对移动定位产品来说,RMC比GGA更常用,因为速度和航向都在一条语句里。
不过现在很多模块同时接收GPS和北斗信号,输出语句会变成$GNGGA、$GNRMC,前缀从GP变成GN,解析逻辑要兼容这两种前缀,否则北斗/GPS双模模式下会漏掉一半的数据。我踩过这个坑,后面在状态机判断里把"$GPGGA"和"$GNGGA"都列入白名单才解决问题。
4.2 在国产MCU上高效解析GPS数据的实现思路
GPS模块以固定周期(默认1秒)输出多条语句,MCU需要逐字节接收并缓存,然后从缓存中提取有效帧。推荐使用环形缓冲区:串口接收中断把字节压入缓冲区,主循环或者定时器周期性检查缓冲区中是否存在完整的一帧。
完整解析流程可以这样拆解:
第一步,找帧头。扫描缓冲区中的$字符,确认其后跟随的前5个字符属于需要解析的语句类型(GPGGA、GNRMC、GPGSV等)。帧头匹配不上就继续扫描,避免把两条语句粘在一起解析。
第二步,校验和验证。NMEA校验和是帧头之后所有字符的异或结果,按十六进制表示在*之后。计算方法是:从$之后的第一个字符到*之前最后一个字符,逐个累加异或,将结果与*后面的两位十六进制数比较。不一致就丢弃整帧,等待下一帧。这一步看似多余,却能在强干扰环境下拦截大量损坏数据,我实测下来,开启校验后解析出的坐标异常率降低了90%以上。
第三步,字段分割与提取。对通过校验的字符串按逗号索引取值。GGA语句里,逗号分隔的字段索引顺序是固定的,比如GGA的第2个字段是时间,第3和第4个字段是纬度和南北纬标志,第5和第6个字段是经度和东西经标志。C语言里可以用指针遍历字符串,逐个逗号替换为字符串结束符来切分字段,也可以用sscanf按格式提取。在MCU上推荐前者,效率更高。
第四步,坐标格式转换。GPS模块输出的是度分格式:4807.038表示48度07.038分。要转成十进制小数度,公式是:
十进制度 = 度 + 分 / 60纬度越界时要处理负数情况:南纬S和西经W标志位需要给结果取负。这个转换直接决定了后续距离计算和地图显示的准确性,不要漏掉方向判断。
第五步,有效标志判断。GGA语句的定位质量字段(第6个字段)为0时,后面的经纬度数据不可信,要直接丢弃或标记为无效。有些场景下会要求至少有3颗有效卫星才更新定位结果,我在项目里把判断条件设在定位质量字段非0且卫星数大于等于4,过滤掉刚开机时的半冷启动不稳定数据。
我把上面这套解析逻辑封装成了一个函数,主循环只要周期性调用,就能拿到干净的坐标结构体:
typedef struct { uint8_t fix_quality; uint8_t sat_num; float lat; // 十进制纬度(北正南负) float lon; // 十进制经度(东正西负) float altitude; float speed; // 节,1节 = 1.852 km/h } GPS_Info_Typedef;这个结构体直接贯穿整个应用层,往屏幕显示、SD卡存储、4G上传都很方便。
5. 调试现场实录:常见问题与排查思路
5.1 串口收不到数据或数据乱码
GPS模块上电后,如果串口助手收不到任何数据,先从供电查起。NEO-M8N启动瞬间电流较大,很多面包板或杜邦线接触不良会导致模块反复复位,表现为LED闪烁但不输出数据。更换粗一点的导线或者直接在PCB上焊接,问题马上消失。
确认供电正常后,检查TXD和RXD是否接反。GPS模块TXD接MCU的RX,RXD接MCU的TX,这个低级错误非常常见。接反后模块正常工作,但MCU收不到任何字节,示波器上看RX引脚有波形但数据全是FF。
硬件没问题再看波特率。虽然模块默认9600,但如果模块被别人配置过,比如改成4800或者115200,你的代码还是按9600收,数据就会乱码。解决方法是先串口助手用9600、4800、38400、115200分别尝试,或者用u-center软件连接模块重新配置波特率并保存到flash,我一般直接在初始化代码里给模块发一遍UBX配置指令,确保波特率是预期的值。
5.2 搜星慢、卫星数少、定位漂移
GPS模块的搜星速度和卫星数很大程度上取决于天线。有源天线比无源天线在室内测试环境下优势明显,但必须确认天线馈电正常。我遇到过一款天线因为射频插座虚焊,导致天线实际没通电,搜星数始终停在0到2颗,重新焊接后立刻恢复到8颗以上。
天线摆放位置同样关键。不要把GPS天线放在金属外壳正中间,不要靠近MCU和DC-DC转换器。我做了个对比测试:同样的模块,天线在板子角落且远离晶振时,冷启动搜星时间约35秒;天线靠近电源电感时,冷启动时间延长到90秒以上,热启动也经常丢星。这个区别非常直观,Layout阶段就把天线位置定好,比事后软件优化有用得多。
定位漂移问题,尤其是静态漂移,除了多路径效应,还要检查NMEA语句中的定位质量字段。城市峡谷或者高架桥下,模块输出的坐标虽然显示有效,但实际漂了十几米。解决思路是在应用层加一个简单的速度滤波:当模块输出的速度低于0.5节且卫星数少于6颗时,把定位结果标记为可疑,并结合前后两秒的坐标跳变幅度做平滑。这种方法治标不治本,但对于大多数定位显示场景已经够用。
5.3 国产MCU替换后的隐蔽兼容性问题
迁移到国产MCU以后,大部分代码可以直接复用,但有两个隐蔽坑值得专门提醒。
第一个是GPIO复用和重映射的差异。虽然引脚兼容,但部分复用功能的重映射选项、AFIO配置寄存器的地址分配,不同厂家的芯片可能有细微差别。我的项目里USART3的引脚重映射配置就跟STM32F103原厂方式不同,导致串口数据怪异。解决方法是认真看国产芯片的数据手册和外设库函数,不要想当然认为完全一样。
第二个是Flash和掉电保存的差异。STM32F103的Flash擦写和读取有标准库函数,国产MCU的库函数接口虽然尽量对齐,但底层擦除时间、掉电时Flash操作的可靠性仍有区别。我在做设备的配置参数掉电保存时,出现过写入后立刻掉电导致配置丢失的情况,后来增加了写入重试和双备份机制才稳定下来。
此外,国产MCU的ADC输入阻抗、GPIO驱动能力、定时器计数方式等电气参数跟ST原厂存在细微差异,涉及模拟采样和高精度定时得重新标定。我们项目里电池电压检测用的是ADC,替换前3.50V显示正确,替换后同一个分压电阻网络读数偏移了0.12V,后来校准了VREF和分压系数才恢复正常。这些参数不在"引脚兼容"的承诺范围里,只能靠自己实测。
6. 最后说几句实在话
整个项目做下来,我的核心体会是:国产MCU替代STM32F103这件事,难的不是替代本身,而是盲目乐观之后的细节撞墙。引脚兼容不等于一切兼容,电气参数、外设寄存器、复位时序、Flash操作这些底层行为都要自己跑一遍测试验证。但反过来,只要把最小系统、串口、定时器、ADC这几个核心外设测透了,应用层移植就是改改头文件的事,替换成本远没有想象中高。
这套方案不只是用在GPS平台上,同样的MCU基础上加一个4G模组上报定位数据,或者加一个LoRa模组做区域追踪,架构都可以直接沿用。软件串口这个技巧更通用,我后来好几个项目资源紧张时都用它救过急,建议你把它当作一个常备技能存下来。如果你正在做类似的国产化替换或者GPS定位项目,欢迎在实际调试中多试多记,有些坑自己踩过一遍比看十篇文章都管用。