第5章从传感器讲到了实际案例,5.7节的GPS定位是整个章节里最完整的一个——从芯片特性、GPIO复用、UART采集,一路到NMEA解析和Flash存储,几乎串起了嵌入式开发的所有环节。我手上没有KP-IOT开发板,但正因为脱开了硬件,反而可以把注意力放在软件链路的设计思路上。我对这一章感兴趣的原因是做过gps的相关项目。
L80RE这颗芯片本身也挺有意思。冷启动35秒,但启用EASY技术后缩到15秒。EASY的思路是预存星历数据——说白了就是提前告诉芯片卫星大概在哪,不用从头搜。这个「用存储换时间」的思路在后面看Flash存储设计时又会碰到,算是一条贯穿全章的暗线。
从芯片到串口:采集这一层
L80RE通过GPIO-05和GPIO-06复用为UART功能,以9600波特率持续吐出NMEA 0183报文。GpsGetData每次只读1个字节,初看呢可能会觉得效率有点低,但仔细想想其实有道理——NMEA帧长度不固定,以换行符结尾,逐字节读配合一个简单的状态机就能精确截帧,不用预判帧长,也不用担心一次读到半帧。
osDelay(100)的注释写的是sleep 1000ms,这说明OS的tick周期是10ms。这个延迟出现在串口没数据可读的时候,不是GPS的更新频率。L80RE默认1Hz输出,也就是每秒一帧,这个频率来自芯片规格而不是代码里的delay——代码只是在等数据来。在很多嵌入式场景里,「低效」的逐字节反而是最稳妥的。一次性读取整帧看似高效,但帧边界对不齐时更难处理。
从串口到帧:解析这一层
process_gps_data实现了一个逐字节状态机:碰到$开始累积,碰到换行就认为一帧结束,然后strstr查找GPRMC标识,找到就交给NMEAAnalysisGPRMC去提取经纬度。流程不算复杂,但嵌套的if层级不少——先验帧头,再验帧尾,再验协议标识,层层筛选。
度分转度的算法是我觉得这一章最巧妙的地方。NMEA报文里的经度格式是ddmm.mmmm,原代码没有直接用atof转浮点,而是先转成整数temp,再用整数除法和取余来拆分度和分。这是有道理的:Hi3861上的float是32位单精度,ddmm.mmmm这种大数字直接做浮点运算可能会有截断误差,走整数运算就绕开了这个问题。
我自己的Python实现里直接用了float()——64位双精度没有这个顾虑。数学上取度加分除以60是完全一样的。很多嵌入式C里的「笨办法」其实是对硬件约束的诚实回应,不能简单理解为写法落后。
NMEA报文尾部其实有*hh格式的XOR校验和,但代码里似乎跳过了校验直接信任串口数据。在野外电磁环境复杂的地方,这个跳过可能会让定位偶尔出错。当然,如果串口本身足够可靠,省掉校验确实能省一些计算量——这是个工程取舍
从帧到存储:持久化这一层
两个线程通过Flash实现了间接协作。GpsUartTask采集到有效数据就写Flash,GpsFlashTask定期从Flash读取并显示。这可以看作一种生产者-消费者模式,Flash充当了中间介质。用Flash做消息队列有点重,每次读写都要操作硬件,但考虑到嵌入式环境里没有现成的消息队列组件,这也许是当时最直接的选择。
Flash分区的管理方式:先调hi_get_usr_partition_table拿到分区起始地址和大小,再用GPS_ENQUEUE宏写入数据,写的同时更新头部的写指针。互斥锁hi_mux_pend/hi_mux_post保护并发写。这种「分区表加写指针加互斥锁」的组合,在没有文件系统的设备上算是一种轻量的存储抽象。
如果设备要长期跑呢,Flash写入是线性递增的,写满之后没有回绕。短期救援问题不大,但如果设备长期部署,写满后新数据就存不进去了。我自己的实现里试了一下环形缓冲区的写法——写指针到头就回绕,加一个结束标记让读取端知道发生了回绕。不知道真机上Flash的擦写寿命是否允许这样做,但至少逻辑上是通的。
两条线并行:线程模型的设计思路
把采集和展示拆到两个线程,我觉得是这个系统最值得琢磨的地方。GPS采集受限于串口和卫星更新率,是I/O密集型;数据处理和显示是CPU密集型。两者节奏不同,放在一起的话显示逻辑可能会阻塞采集。拆开之后各跑各的,Flash做中间人——思路很清楚。
不过两个线程之间只靠Flash间接通信也意味着每次读位置都要访问Flash。Flash读写有寿命限制,速度也不快。如果改成消息队列或者共享内存传递最新一帧位置,可能更轻量。当然这要看具体的硬件资源——如果RAM紧张,Flash也许是唯一选择。这种设计权衡在读的时候挺值得反复想的:什么时候用重但稳的方案,什么时候该换轻的。
我试着写了什么
因为手边没有开发板,我写了一份Python模拟实现来验证自己的理解。不是照着书本翻译C到Python,而是把硬件依赖替换成模拟器——GPS芯片行为、串口输出、Flash分区都用Python对象模拟,软件架构保持和书本一致。
几个关键的对应关系:osThreadNew创建两个线程,我用了threading.Thread;逐字节状态机解析NMEA帧,思路完全一样;Flash分区存储,我加了一个环形回绕。另外还试着模拟了一些书本没涉及但真实存在的场景——冷启动时卫星逐步锁定、位置有CEP精度范围内的漂移、偶尔信号被遮挡短暂失锁。
代码放在了后面,这里只贴一小段模拟卫星锁定的逻辑,算是整个模拟器里最像硬件的部分:
def simulatelock_progress(self): elapsed = time.time() - self._init_time # 实际L80RE冷启动约15s(EASY启用), # 这里为演示加速,缩到3s+5s if elapsed < 3: self.satellites = 0 self.locked = False elif elapsed < 8: self.satellites = min(int(elapsed), 4) self.locked = self.satellites >= 4运行起来长什么样
跑起来之后能看到双线程协作的过程:前三秒模拟冷启动没有定位,之后逐步锁定卫星开始输出坐标,消费者线程每隔几秒从Flash读取一次生成报告。下面是一段实际输出:
[GpsUartTask] 定位: 22.54312N, 114.05791E | 速度: 2.8节[GpsFlashTask] 当前坐标: 22.54313N, 114.05794E Flash存储: 4 条记录 | 使用: 267/20480 bytes书本和我的实现大致怎么对应
整理了一张表,方便对照着看两边各自做了什么:
书本的C代码 | 我的Python模拟 | 在做什么 |
osThreadNew(GpsUartTask) | Thread(target=_producer_task) | 采集线程 |
osThreadNew(GpsFlashTask) | Thread(target=_consumer_task) | 读取线程 |
IoTUartRead(idx, buf, 1) | chip.generate_nmea()逐字符 | 逐字节采集 |
process_gps_data() | GPSDataProcessor.process_byte() | 帧状态机 |
NMEAAnalysisGPRMC() | NMEAParser.parse_gprmc() | NMEA解析 |
gps_data_save_flash() | FlashSimulator.write() | Flash存储 |
hi_mux_pend/post | threading.Lock | 互斥锁 |
这一章读完,最直接的感受是:一个嵌入式定位系统看起来简单——不就是读串口、解字符串、存Flash嘛——但每一步背后都有硬件约束在驱动设计决策。逐字节读不是因为偷懒,是因为帧长不固定;整数运算不是因为不会用浮点,是因为32位float会丢精度;Flash当消息队列不是因为不知道消息队列更好,是因为RAM不够。
这些看起来笨的选择凑在一起,反而构成了一个能在小设备上稳定跑的系统。脱开硬件写Python模拟的时候,我反而比看代码时更清楚地感受到了这些约束的存在——因为Python里什么都不缺,精度够、内存够、不需要操心Flash寿命,所以当你主动去模拟这些约束时,才会意识到它们在原系统里是怎样一层一层地塑形了整个架构。
接下来想试试把GPS定位数据通过网络传出去——正好第6章讲了TCP/UDP,如果把5.7的坐标用6.3的socket发到远端,就算是一个最小但完整的物联网链路了。
