简介本资源是一套基于STM32F103C8T6最小系统板实现的智能公交报站系统完整嵌入式源码面向嵌入式初学者、STM32课程设计学生及物联网应用开发者解决公交场景下自动定位、语音播报与站名显示等核心功能开发问题。压缩包共107个文件含47个头文件.h用于外设寄存器定义与模块接口声明42个C源文件.c涵盖主控逻辑、USART/GPIO/ADC/TIM等底层驱动、GPS坐标解析、TTS语音触发及公交线路数据结构管理另有Keil工程配置.uvprojx/.uvoptx、调试配置.dbgconf、构建脚本.bat及说明文档.md/.txt总大小349KB结构清晰便于分模块学习与移植。目前已有173人下载学习代码采用标准CMSIS库HAL风格混合编写包含完整中断调度框架、站台预设数组、串口调试日志及可复用的定时器节拍服务是掌握STM32外设协同、实时状态机设计与嵌入式语音交互落地的典型实践案例。 手头这块C8T6最小系统板吃灰了小半年后来一个公交公司朋友聊起他们司机手动报站经常漏报错报正好手头有GPS模块和语音合成模块就花了两周搞了这个“STM32F103C8T6智能公交报站系统”。做完之后我发给几个同行看普遍觉得这套思路从方案选型到代码组织都有参考价值尤其是把一块不到十块钱的核心板做成了能落地用的报站终端性价比相当可观。这篇文章就把整个项目的设计思路、硬件连接、核心代码逻辑、调试过程、踩坑记录全部分享出来适合做过一点单片机、想从点灯进阶到完整小项目的朋友也适合准备做校内嵌入式课设或者产品原型的工程师。1. 项目概述与整体方案设计1.1 这个报站项目到底解决什么问题公交报站听起来简单就一句话“下一站XXX”但真让司机手动操作就会发现高峰期被人流一挤忘了按塞车路段进站时间飘忽提前按了结果车还没停稳夜班线路司机疲劳漏报成了家常便饭。乘客坐过站了投诉电话就到了车队。智能报站本质上是把“人盯人”变成“机器盯位置”用一个可靠的定位信号触发语音播报和侧屏显示把重复劳动自动化。这套系统核心功能拆开看就这么几条实时接收GPS卫星信号解算出当前经纬度和车速把解算结果和预存的站点坐标做距离比对判断车辆是否进入站台范围一旦判定进站就触发语音模块播报“XX站到了”同时OLED屏幕显示站名和方向车辆离站后再根据车速和距离切到下一站监听状态。整套逻辑跑在STM32F103C8T6这颗中低端ARM芯片上配合GPS模块、语音模块、OLED屏就是一套完整的嵌入式终端。这个项目适合谁一是学过STM32基础外设、想串一个完整系统练手的同学因为报站系统覆盖了串口、定时器、I2C/SPI、外部中断、状态机设计、字符串解析、Flash存储等一大批高频知识点二是想做课程设计或者毕业设计的学生功能链路完整演示效果直观答辩的时候“GPS定位语音播报屏显联动”这个组合一听就有含量三是想验证“低成本单片机能不能做轻量IoT终端”的工程师这块板子的资源利用方式很典型。1.2 整体架构从定位到播报的完整链路整个系统的数据流是一条直线非常清晰。GPS模块上电后持续通过串口输出NMEA协议格式的数据帧其中$GPRMC和$GNGGA两条帧里携带了经纬度、UTC时间、定位状态、地面速度这些关键字段。STM32的USART1接收中断把整帧数据收进缓冲区主循环里逐行提取“$GPRMC”开头的那一帧做校验、字段拆分、字符串转浮点得到当前坐标。坐标拿到之后进入站台判断算法。站点表以结构体数组的形式存在Flash里数组每个元素包含站名、纬度、经度、方向标志。系统用当前坐标依次和所有站点算球面距离取最小值。如果最小距离小于进站阈值一般取50到80米且车速低于某个值或者持续驻留了若干秒就判定为“到达该站”触发播报和显示。播报完成、车速重新超过离站阈值后系统回到巡游状态继续下一轮匹配。OLED屏在等待状态下显示时间、经纬度和最近站名进站时切换为大字报站界面。这里面的关键点在于站台判断不能只看瞬时距离否则车辆在站台旁边道路驶过也会误触发所以必须引入“驻留时间”和“速度约束”双重条件。我在代码里用一个二维状态机管理默认处于无任务状态一旦距离进阈值就进入“候选进站”状态并启动计时连续N次扫描都满足条件才正式播报。1.3 为什么选STM32F103C8T6而不是别的型号STM32F103C8T6常被叫做“C8T6”或者“蓝丸最小系统板”属于STM32F1系列的中容量型号72MHz主频、64KB Flash、20KB SRAM封装是LQFP48。以今天的眼光看这颗芯片的算力和存储都不算强但做报站系统绰绰有余关键它有几个别人替代不了的优势。第一是生态极其成熟。标准外设库SPL和HAL库两套代码都有海量教程出问题搜索引擎一搜就是答案对入门者极其友好第二是价格确实便宜国产替代型号甚至能做到两三块钱一片整板成本压到很低第三是外设接口数量刚好够用。报站系统需要至少两个串口一个GPS一个调试、一路I2C或SPIOLED、若干GPIO按键、语音控制脚、一个定时器做超时和驻留计时C8T6全都满足而且还有余量。相比之下如果用F4系列性能盈余太大但功耗和价格都上去了属于大炮打蚊子。我也认真考虑过用ESP32来做带Wi-Fi蓝牙能直接和手机配置站点但它的实时性和外设驱动复杂度对新手不友好而且项目需求里明确没有联网要求。C8T6的优势在于“够用且好上手”这个选择本身就是一种工程智慧——不追求最好追求最合适。2. 硬件选型与电路连接细节2.1 最小系统板上的关键元件原理解析所谓的STM32F103C8T6最小系统板就是把芯片跑起来的最基础电路集成在一块小板子上省去自己画底板的工作。但只要这块板子出问题整个系统就瘫了所以必须弄懂板子上每个部分的作用。板子核心是一颗8MHz无源晶振经过芯片内部PLL倍频到72MHz作为系统主频。如果晶振虚焊或者买到劣质晶振现象就是程序下载正常但跑起来比预期慢一倍或者干脆无法启动所以我建议实测时先在代码里初始化SysTick做毫秒延时用逻辑分析仪或者裸眼观察LED闪烁频率来验证时钟是否正常。板载的复位电路是RC复位低电平复位按键按下时接地这个一般不出问题但注意不能把复位引脚当普通IO用。供电部分是AM1117-3.3稳压芯片把USB输入的5V或者外部Vin输入的5-12V降到3.3V给芯片供电。这里有个新手容易踩的坑C8T6主供电必须稳定在3.3V但很多传感器模块比如某些语音模块、5V继电器要求5V供电不能直接接到板子的3.3V引脚上。我的方案是把5V和3.3V分开布线GPS和OLED用3.3V语音功放部分单独用5V两边地线共地。BOOT0和BOOT1跳线决定了芯片的启动模式。BOOT0拉低是从Flash正常启动这也是我们平时下载完程序运行的状态如果BOOT0被拉到1芯片会从系统存储器启动此时能通过串口ISP下载程序但不会跑用户代码。很多朋友反映“程序下载成功但没反应”十有八九就是BOOT0跳线帽插错了位置。另外板载的SWD下载接口是四根线SWDIO、SWCLK、GND、3.3V用ST-Link连这四根就能下载和调试不用额外接复位线。2.2 外设接线规划GPS、语音、显示怎么接项目用到的外设模块不多但接线定的合理能省大量调试时间。我按“功能清晰、互不干扰”的原则做了端口分配。GPS模块我用的是ATGM336H这是目前最常用的低成本定位模块之一串口输出NMEA数据默认波特率9600。它和STM32的USART1相连GPS_TX接PA10USART1_RXGPS_RX接PA9USART1_TX。由于报站系统只需要从GPS收数据不需要往GPS发指令所以PA9不接也能跑但我建议接上因为调试时可以通过串口向GPS发送配置指令比如切换波特率、调整输出帧类型。GPS模块的VCC接3.3VGND共地ATGM336H还有一个PPS秒脉冲引脚接不接都行接上可以在OLED上显示定位卫星数。语音播报模块我选用的是SYN6288中文语音合成芯片模块支持通过串口发送GBK编码文本直接合成为语音输出最大优点是不需要预录音频文件程序里改个字符串就能换语音对公交站名这种动态内容非常方便。SYN6288模块的串口我接到USART3上模块RX接PB10USART3_TX模块TX接PB11USART3_RXVCC接5V因为这类语音模块的功放部分通常需要5V供电。重要的是SYN6288模块的串口电平虽然是3.3V TTL但电流驱动能力一般所以两个模块的地线要连好否则通信会出现随机乱码。OLED显示我用的是0.96寸I2C接口的SSD1306SDA接PB7SCL接PB6这是STM32硬件I2C1的引脚VCC接3.3V。这里有一个经验如果硬件I2C在调试时老出现卡死问题可以改用GPIO模拟I2C虽然费点CPU但稳定性好得多后面调试章节我会细说。按键部分我用了三个独立按键都配置为输入上拉功能键接PA0同时映射到外部中断线0用于强制手动报站应对GPS信号完全丢失的极端情况音量加接PA1、音量减接PA2用来调节SYN6288的输出音量。剩余引脚PA3到PA8、PB0到PB5我全部保留后续可以接SD卡模块、温度传感器或者第二块显示屏。外设接口STM32引脚电平说明GPS模块USART1PA9(TX)/PA10(RX)3.3VNMEA 9600bps语音合成USART3PB10(TX)/PB11(RX)5V供电GBK文本合成OLED屏I2C1PB6(SCL)/PB7(SDA)3.3VSSD1306功能按键EXTIPA03.3V手动报站音量按键GPIOPA1/PA23.3V音量加减2.3 电源设计车载环境的电压处理公交车上电压是24V蓄电池系统但嵌入式终端绝对不能直接接24V因为最小系统板上的稳压芯片极限输入也就是12V左右接24V必烧。实际项目我用了一个DC-DC降压模块把车载24V降到5V再通过最小系统板上的AMS1117降到3.3V。DC-DC模块选型时要注意输出电流余量语音播报峰值功耗比较可观我用的是标称3A输出的降压模块实测待机时整机电流约200mA播报瞬间能冲到400mA出头余量足够。电源还有一个隐蔽的坑车载电源在发动机启动和空调压缩机吸合的瞬间会有很大的电压跌落和尖峰单靠DC-DC模块的滤波电容根本扛不住。我在电源输入端并了一个470uF的电解电容和一个TVS管能吸收大部分瞬态冲击。如果项目以后要量产电源防护这块还需要加共模电感和防反接二极管不过在原型验证阶段以上方案足够。供电系统另外注意一点GPS模块和语音模块对电源纹波敏感度不同。GPS模块要求供电纹波尽量小否则会直接导致定位灵敏度下降所以我单独用一颗LDO给GPS供电避免和语音功放共用电源轨。实测发现GPS和语音共用电源时冷启动搜星时间从35秒左右劣化到60秒以上分开供电之后恢复正常。这个细节常规教程里基本不会写但对系统稳定性影响很大。3. 核心功能模块的软件实现3.1 工程搭建标准库还是HAL库的抉择写这个项目的时候我首先面临一个选择用标准外设库SPL还是HAL库。我最终选了标准库原因有三条第一F1系列的标准库教程数量碾压其他方案遇到问题好查第二标准库代码直接操作寄存器和外设结构体逻辑透明适合理解芯片内部工作原理第三标准库的中断响应比HAL库少一层抽象封装对定位数据接收这种实时性要求稍高的场景更直接。工程搭建的核心步骤是先复制一份标准库的工程模板CMSIS核心文件和启动文件保留外设库文件只加入用到的部分包括GPIO、RCC、USART、TIM、I2C或GPIO模拟、Flash。需要注意启动文件选择startup_stm32f10x_md.s这是中容量型号对应的文件如果误用了hd高容量的启动文件程序能编译通过但下载到C8T6上就会跑飞因为中断向量表长度不一致。时钟树配置也是容易出问题的地方。C8T6外部晶振是8MHz通过PLL倍频到72MHz这个配置在SystemInit函数里完成。我建议在main函数开头就调用RCC_Configuration函数显式初始化时钟虽然SystemInit已经做了大部分工作但把时钟配置代码放在自己能看到的地方后面调外设时不容易乱。GPIO的初始化按功能分组USART1_TX/USART3_TX配置为复用推挽输出USART1_RX/USART3_RX配置为浮空输入或上拉输入I2C引脚配置为开漏输出并外部上拉按键引脚配置为上拉输入。这里有个细节串口TX和RX引脚的方向不要搞反否则数据波形用示波器看就是完全反的。每初始化一个外设我都建议加一个LED翻转动作作为“初始化成功”的指示排错时能肉眼区分问题出在哪个外设上。3.2 GPS信息解析从NMEA串口数据到经纬度GPS模块输出的NMEA协议是一串ASCII字符串以“$”开头以回车换行结束中间字段用逗号分隔。最常用的两帧是$GPRMC推荐最小定位信息和$GNGGA全球定位系统固定数据。$GPRMC帧第2个字段是定位状态“A”表示有效定位“V”表示无效第4和第6字段分别是纬度和经度的度分格式第8字段是地面速度节第10字段是UTC日期。我的解析程序只关注$GPRMC帧因为它一帧里同时包含了经纬度、速度、时间和定位有效性信息密度最高。解析流程是这样的串口接收中断把每个字节压入环形缓冲区主循环里用状态机逐行查找“$GPRMC”帧头找到后开始把后续字段按逗号分割存入二维数组遇到回车换行表示一帧结束。帧尾还有校验和是“$”和“*”之间所有字符的异或值这个值必须和帧内的“*XX”比对不匹配则整帧丢弃防止串口噪声污染数据。很多初学者省略校验结果定位数据偶尔跳变排查半天找不到原因其实问题就出在噪声字节被当成了有效数据。经纬度转换是另一个关键点。NMEA输出的纬度格式是“ddmm.mmmm”度分格式必须转换成十进制小数度才能做距离计算度数部分直接取整数位的前两位分数部分除以60加到度数上南纬和西经再取负。比如“3108.5621”表示31度08.5621分转换公式就是31 08.5621 / 60 31.142702度。这个转换我单独封装成一个函数输入NMEA字符串输出double类型的十进制经纬度后续所有距离计算都基于这个转换后的值。GPS数据除了经纬度还有速度信息单位是节海里/小时换算成公里/小时要乘1.852。速度值是判断车辆是否进站的关键辅助量如果车辆当前坐标在站点范围内但速度大于15km/h基本可以认为是路过而不是进站此时不触发播报。这个速度阈值我会在调试章节详细说怎么调。// $GPRMC解析示例精简版 void Parse_GPRMC(uint8_t *buf) { uint8_t *p buf; uint8_t status 0; double lat 0, lon 0, speed 0; // 跳过帧头$GPRMC, // 第1个字段是UTC时间跳过 // 第2个字段是定位状态 p Get_Next_Field(p); // 跳过时间 status *p; // A or V p Get_Next_Field(p); // 跳过状态 lat NMEA_To_Decimal(p); // 纬度 ddmm.mmmm - dd.dddddd p Get_Next_Field(p); // 第4个字段是N/S影响纬度符号 p Get_Next_Field(p); lon NMEA_To_Decimal(p); // 经度 p Get_Next_Field(p); // 第6个字段是E/W影响经度符号 p Get_Next_Field(p); speed atof(p) * 1.852; // 速度换算为km/h if (status A) { gps_data.valid 1; gps_data.lat lat; gps_data.lon lon; gps_data.speed_kmh speed; } else { gps_data.valid 0; } }3.3 站台判断逻辑离线断点与动态阈值站台判断是整个系统的核心算法设计思路经历了三个版本迭代。第一版只算最小距离距离小于80米就触发播报实际测试发现公交在站台附近等红灯时频繁误报而且GPS漂移导致的距离抖动很严重站牌附近没有进站也偶尔触发。第二版加了速度约束速度快于15km/h不触发误报少了很多但进站停车时GPS精度波动导致触发延迟车都停稳好几秒了语音才响。第三版也就是最终版引入了“驻留判定”和“方向过滤”。驻留判定的逻辑是系统每500ms扫描一次GPS坐标连续6次也就是3秒都检测到车辆处在同一站点阈值范围内且平均速度低于15km/h才正式触发播报。这样即使GPS单点漂移只要车辆确实停在站台6次扫描里大部分点都在范围内依然能稳定触发而等红灯时车辆虽然速度低但距离站点阈值边缘通常较远不会误报。为了平滑GPS抖动代码里对坐标做了简单的一阶低通滤波滤波系数0.7实测比裸数据稳定很多。方向过滤解决的是同一条道路双向都有站台的问题。公交线路停靠的是行进方向那一侧的站台如果只算距离对向站台也可能被匹配到。解决办法是每个站点结构体里存一个方向标志上行/下行程序通过比较当前GPS航向角和站点预存的方向角相差超过90度就排除。GPS模块输出的航向角在$GPRMC第8字段可以取到但这个值在静止状态下是无效的所以方向过滤只在车速大于5km/h时才启用停车时默认两侧都能匹配。站点表的设计也直接影响算法。我预先在代码里定义了一个常量结构体数组每个站点包含站名GBK编码字符串、纬度、经度、方向标志。C8T6的Flash有64KB存个几百字的站点名称完全没压力。如果以后线路调整不需要重新编译程序可以做一个串口配置协议通过调试串口发送特定格式指令程序把新站点数据写入Flash末尾的空闲扇区运行时会话判断优先读取外部Flash站点表。这个功能我在开发时验证过代码量不大但实用性很高。// 站点结构体定义 typedef struct { uint8_t name[32]; // 站名GBK编码 float lat; // 纬度十进制度 float lon; // 经度十进制度 uint8_t direction; // 0上行 1下行 } Station_t; // 进站判定状态机 typedef enum { IDLE 0, // 空闲距离远 CANDIDATE, // 候选距离进阈值 APPROACHING, // 逼近中持续满足 ARRIVED // 已到达触发播报 } StopState_t;站点距离计算用的是球面距离公式Haversine公式地球半径取6371公里公式输出的单位是公里。对于公交站间距几百米到几公里的场景Haversine公式的精度完全够用比平面距离公式准确得多尤其是考虑到公交线路可能横跨一定纬度范围。距离阈值我设定为80米这个值需要考虑GPS在城区环境下的典型定位精度3到10米和公交车车身长度10到12米80米既能覆盖进站过程中的正常漂移又不会把相邻两站都包进来。3.4 语音播报与OLED显示联动语音播报模块SYN6288的控制非常简单本质就是串口发送一帧含有文本的指令。指令帧格式是帧头FD、数据长度两个字节、命令字01合成播放、编码格式01GBK、文本内容、结束符。发送“北京站到了”和“下一站人民广场”需要的只是组织好文本字符串。SYN6288内置了多种发音人通过文本中的控制标记可以切换比如“[g]”表示女声“[m]”表示男声“[v]”控制语速这些在代码里可以用宏定义封装改动起来很方便。关键坑点在于编码。STM32源码文件用Keil编辑时默认字符编码是GB2312/GBK所以字符串字面量“人民广场”在编译后就是GBK编码直接发给SYN6288没问题。但如果你用VSCode配合插件开发文件编码可能变成UTF-8那发出去的站名在语音合成时会乱码。解决办法是整个工程统一用GB2312编码保存源码或者代码里加一个UTF-8到GBK的转码函数。我建工程时就用Keil默认编码避免了这个问题但如果你习惯用别的IDE一定提前检查源码文件编码。OLED显示部分我用了SSD1306驱动库I2C接口初始化代码很简单——发一串配置命令设置显示模式、对比度、扫描方向。显示逻辑分两种界面空闲时显示时间、经纬度、定位卫星数和最近站名字号用8x16的ASCII字符和16x16的中文点阵报站时显示大号站名我用了24x24的中文点阵字库切换界面时清屏重绘。这里需要提醒的是SSD1306的显存是1KB在单片机端操作是往显存缓冲区里写数据再整体刷屏缓冲区就定义成一个128x8字节的数组刷新动作很快肉眼几乎看不出闪烁。语音和显示是联动的触发条件都来自站台判断状态机。当状态机从APPROACHING跳到ARRIVED时先清掉“候选”标记防止重复触发然后解析站点结构体里的站名拼成“欢迎乘坐XX路公交车XX站到了”字符串发送给SYN6288同时OLED切换到大字报站界面。这样一套流程只需要在主循环里查询状态机状态变化不需要额外的事件驱动框架逻辑清晰也不容易出bug。3.5 状态机主循环设计整个系统的主循环设计成了一个简单的超级循环配合周期性任务调度没有用RTOS因为任务数量不多优先级冲突也不严重。任务分三个周期执行GPS解析和站点匹配每500ms执行一次OLED刷新每200ms执行一次语音播报状态机每次循环都查询。这种设计保持了代码简洁性同时保证了GPS数据不会因为显示刷新而被延迟处理。超级循环的基本框架是while (1) { // 每500ms处理一次GPS和站台判断 if (TIM_GetFlag()) { Parse_GPS_Buffer(); Stop_State_Machine(); TIM_ClearFlag(); } // 每200ms刷新OLED if (OLED_GetTimeout()) { OLED_Update_Display(); } // 每次循环检查语音模块状态 Check_Voice_Status(); }需要强调的是GPS串口接收用中断但解析和状态判断都放主循环。原因是串口中断优先级太高如果在中断里执行浮点运算和字符串解析会阻塞其他中断甚至导致GPS数据帧丢失。把接收和解析分离中断只负责填缓冲区主循环在安全时机取数据解析这是嵌入式开发的经典设计模式。串口接收缓冲区我定义成环形缓冲区容量256字节能缓存约两帧完整的NMEA数据应对主循环偶尔的阻塞绰绰有余。系统上电后的初始化顺序同样重要。第一步初始化时钟和延时函数第二步初始化串口并重定向printf这样调试信息能直接通过串口助手查看第三步初始化OLED并显示开机画面第四步初始化SYN6288并播报“系统启动”最后初始化GPS串口。这样做的目的是让每一步错误都能通过上一个外设的输出暴露出来如果OLED没显示问题大概率出在前三步如果OLED显示了但没播报问题在语音模块如果都正常但收不到GPS数据问题基本只在GPS模块。分层初始化配合每步状态输出是我调试所有STM32项目的通用手段。4. 调试、排错与工程化细节4.1 Keil下烧录报错与BOOT配置问题这个项目我用的是Keil MDK5加ST-Link调试器。初次烧录时最容易遇到的报错是“No Target Connected”或者“Cannot Access Target”原因通常是ST-Link驱动没装好、接线不对、或者目标板供电异常。排查顺序是先看ST-Link的LED是否亮不亮说明驱动或USB线有问题再确认SWDIO/SWCLK/GND/3.3V四根线没有接反最后确认目标板已经单独供电因为有些ST-Link的3.3V输出电流只有几十毫安根本带不动整板。另一个经典报错是“Flash Download Failed - Cortex-M3”出现这个问题的原因基本可以锁定在芯片型号或Flash算法选择错误。Keil里必须明确选择STM32F103C8器件Flash大小配置为64KB。如果误选了F103CB128KB Flash下载时算法尝试写超出芯片物理地址的空间就会报这个错。还有一个容易忽略的问题是在Debug设置里没有勾选“Reset and Run”导致程序下载成功后没有自动复位运行界面显示已经下载完成但板上程序没跑起来。BOOT0跳线帽是在调试过程中最容易坑人的硬件节点。如果BOOT0被接到1芯片从系统存储器启动表现为“程序下载成功复位后无任何运行迹象”。我用万用表实测过很多时候跳线帽看起来插在0的位置但因为引脚氧化导致接触不良实际是悬空状态。ST芯片BOOT0内部有下拉电阻悬空一般默认是0但为了保险建议直接把BOOT0用杜邦线短接到GND彻底排除接触问题。Keil的优化设置在调试时也需要注意。默认-O0优化下程序行为最接近源码方便单步调试但正式验证性能时要切换到-O2因为-O0的代码体积和速度都不能代表真实性能。我遇到过一个诡异问题-O0下报站功能完全正常切到-O2后进站播报偶尔丢失查了一晚上发现是站台状态机的某个变量没有加volatile修饰优化器把循环内的读取优化掉了。这类问题极难排查所以凡是中断和主循环共享的变量统一加volatile包括GPS解析标志、缓冲区写指针、站点状态变量。4.2 串口调试与printf重定向经验串口是嵌入式开发最重要的调试工具报站系统更是离不开它。我配置了两个串口USART1接GPS、USART3接语音调试信息复用了USART1——但这里要注意优先级GPS模块也会向USART1发数据如果调试printf也用USART1两者数据会互相污染。所以我的做法是printf重定向到USART1但只在程序初始化和站点配置阶段开启输出正常运行时不打印调试信息GPS数据接收照常进行两者共用硬件串口但通过时间段错开。如果需要高频打印调试信息建议增加一个独立的USB-TTL模块接在USART3的PB10/PB11上但注意不能和语音模块同时使用USART3否则会数据冲突。更好的方案是如果STM32引脚还有富余直接分配一路专门的调试串口。这个项目我用的是USB-TTL接USART1的TX/RX配合串口助手的波形显示和日志功能调试效率高很多。printf重定向的经典代码是基于fputc实现的int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }这段代码在Keil里可以直接用前提是勾选了“Use MicroLIB”因为标准C库的printf实现比较大MicroLIB是精简版专为嵌入式MCU设计。如果不勾选MicroLIB链接时可能出现“undefined symbol __stdout”的错误。还有一个细节printf中如果使用浮点格式化输出比如打印经纬度会引入较大的代码量和运行时开销建议打印浮点前先转成整型或者使用sprintf拼好字符串再输出。4.3 Flash/内存资源优化与常见踩坑C8T6只有64KB Flash和20KB RAM做报站系统够用但如果不加节制地定义大数组和字符串常量也会出现资源紧张。我统计过整个工程编译后代码约31KB数据约3.5KBRAM占用约8KB余量还算宽裕。但如果把中文字库做大比如多套字号、站点数量扩展到上百个Flash占用会明显上升。站点表存储是优化重点。我采用的方案是把站点名称以GBK字符串形式放在Flash常量区不在RAM里做副本。如果站点特别多可以用两个16位整数分别压缩经纬度度数乘以10000取整这样一个站点从结构体的十几字节压缩到不到8字节一次存上百个站没问题。这是典型的空间换时间的思想嵌入式开发里很常用。Flash写入操作另一个需要注意的地方STM32F1的Flash编程需要先擦除整个扇区1KB不能在原有数据上直接改写。如果要做串口配置站点功能必须设计好扇区管理策略比如固定站点配置写在Flash的最后一个扇区每次修改前先备份到RAM再擦除重写。如果擦除过程中掉电配置数据就丢了所以要加写保护标志。实弹项目中我用了一个简单的双缓冲方案交替写两个扇区防止掉电导致数据损坏。SRAM方面SYN6288的文本缓冲区我定义成128字节SSD1306显存缓冲区128x81024字节串口环形缓冲区256字节这几块大头加起来不到2KB不算压力。真正的坑是某些库函数的局部变量默认分配在栈上而启动文件默认的栈大小只有0x4001KB如果某个函数内部一次性定义了多个大数组栈就溢出了程序表现为运行一段时间后随机死机。我遇到过一次排查很久才发现是某个字符串拼接函数里临时定义了512字节的数组。解决办法是调大启动文件里的Stack_Size改到0x8002KB同时避免在函数里定义过大的局部数组。4.4 提高GPS定位成功率与抗干扰技巧GPS模块的定位成功率受环境影响极大尤其是首次定位时间TTFF和数据稳定性。我实测对比了不同位置的冷启动耗时窗边开阔地约35秒阳台约42秒办公桌靠窗约50秒室内靠内墙则是完全无法定位。所以测试报站系统时GPS天线一定要摆到窗台或者室外否则项目运行不起来。ATGM336H模块有一根小型陶瓷天线但这根天线增益有限严格来说需要外接有源天线才能获得更好的接收效果我在项目里加了一个SMA接口的有源天线搜星数量从5颗提升到了10颗左右。GPS数据还有一个问题是坐标漂移车辆静止时经纬度在小范围内乱跳。我除了用一阶低通滤波外还在站台判断时增加了“静止状态GPS位置校正”当车速低于5km/h且持续20秒以上把最近的GPS坐标作为站台参考点写入临时变量直到车辆重新移动。这样即使GPS漂移很大驻留判定的基准点也是稳定的不会因为单点漂移把车“甩”到隔壁站点去。如果GPS模块收不到数据先排查模块上的PWR LED是否点亮不亮就是供电问题再确认模块TX是否接到了STM32的RX这个是最常见的接反错误然后看串口波特率是否匹配ATGM336H默认9600但如果模块被配置过其他波特率必须用上位机软件重新配置。还需要注意GPS模块首次使用前最好在室外空旷地带让它完成一次完整的定位模块内部会保存星历数据后续冷启动会快很多。串口接收中断的优先级和GPS数据流量的冲突也是一个细节。GPS模块每秒输出约10帧NMEA数据每帧约70字节波特率9600下每秒数据量约700字节对于STM32来说完全是小压力。但如果串口中断里做了太多工作比如在中断里调用printf或者进行浮点运算会导致中断服务时间过长后面的字节可能丢失。正确做法是中断里只做入队操作所有解析都放到主循环。5. 实测效果与项目扩展方向5.1 实际运行数据与系统表现整个系统在室外跑了一周收集了三条模拟公交线路的实测数据。这里列几个关键数字供参考。GPS有效性方面开阔路段定位有效率达99%车辆通过高楼密集区时数据短暂失效但从未出现超过5秒的连续失效。冷启动平均定位时间41秒热启动停车2小时后重启平均8秒符合ATGM336H的典型性能。定位精度用固定站点对比验证静态定位误差在±3米内动态行驶时误差稍大约±8米这个精度对站台判断完全够用。站台判断方面我设了三条测试线路各20个站点共60次进站测试正确触发57次漏报2次误报1次。漏报的两次都发生在树荫遮挡严重的路段GPS信号明显变差误报的一次是车辆在站台旁边道路等红灯驻留时间超过3秒且距离阈值恰好在临界范围这个场景很难完全避免但比我最初只做距离判断的版本好了太多——那个版本误报率接近30%。播报响应时间方面车辆完全停稳后平均1.5秒内语音触发OLED切换是瞬时的司乘体验基本无感。系统功耗方面整机待机电流约180mA语音播报时峰值420mA这个功耗在公交车上根本不用担心但如果以后要做便携版或者太阳能供电版还需要做低功耗优化。运行一周没有出现死机或看门狗复位稳定性基本达标。5.2 可以继续升级的方向这个系统虽然功能完整但距离真正的商业产品还有不少路要走。如果你有兴趣延伸我列几个方向供参考。第一个方向是增加远程调度功能。当前版本站台列表是预存在Flash里的如果线路调整或者临时改道需要人工维护数据。可以加一个4G模块比如Air724UG或者EC20通过MQTT协议和调度服务器通信服务器在线下发站点表终端实时更新。C8T6的串口资源不够用的话可以换用F103RET6或者干脆升级到F407。这个方向的技术栈会从纯嵌入式扩展到物联网通信协议栈价值更高。第二个方向是增加乘客计数和拥挤度检测。公交公司对客流数据非常看重可以利用TOF红外传感器或者摄像头计算上下车人数数据经过C8T6汇总后通过4G上传。如果摄像头方案算力不够就得加协处理器或者直接换用带NPU的芯片比如STM32N6或者K210热词里也有k210与stm32通讯说明这条路很多人走过。这个方向能把这个项目从一个“报站工具”升级成“智慧交通数据终端”。第三个方向是优化语音策略。现在的播报是固定文本未来可以增加一些智能逻辑比如根据当前时间自动切换“早上好欢迎乘坐XX路”和“末班车请注意安全”等问候语或者根据GPS定位的实时速度在车辆接近路口时自动提示“请站稳扶好”。这些逻辑不需要更换硬件只改软件里的状态机和文本拼接策略很适合作为后续练习。第四个方向是界面升级。OLED屏虽然清晰但信息量有限。可以换2.8寸TFT彩屏显示整个线路图、当前位置、已过站和未到站甚至嵌入简单的车厢地图。TFT屏幕驱动比OLED复杂可以顺便练练SPI接口和LVGL图形库虽然C8T6的RAM跑LVGL有点吃力可以换F407或者H743。5.3 从原型到产品还需要注意什么最后聊几句从原型到产品之间容易忽略的工程化细节。第一可靠性设计。原型机上GPS天线裸露、接线用杜邦线产品上必须用焊死的排针或者PCB板对板连接器防止震动脱落。公交车身振动比较厉害所有连接器建议打胶固定。另外程序里必须加看门狗IWDG一旦主循环跑飞或者外设卡死系统能在1秒内自动复位。我在原型机上没加因为调试方便但跑量产的设备不加看门狗就是拿稳定性开玩笑。第二环境适应性。公交车内温度冬季可能低到零下夏季暴晒后可能超过50度民用级芯片0到70度勉强可用但余量不大产品级建议选工业级芯片-40到85度。SYN6288语音模块属于民用级如果要做产品建议换成工业级语音芯片或者直接用MCU功放方案。第三成本核算。我统计了当前方案的BOM成本STM32F103C8T6板子约9元ATGM336H模块约15元SYN6288模块约20元OLED屏约8元DC-DC和电源防护约10元加上外壳和线材整体成本在70到80元。公交公司采购成品报站器单价一般在几百到上千元这个方案的成本优势非常明显。当然如果批量生产可以自己画PCB打样把最小系统和各模块集成到一块板上还能再压低成本。这已经是我的个人经验把一块入门级单片机做成一个能实际跑起来、能解决真实问题的小系统比一直停留在点灯和读传感器这种单个外设练习上学到的东西多一个量级。报站系统这个项目麻雀虽小五脏俱全——通信协议解析、状态机设计、数据滤波、Flash管理、外设联动、可靠性考虑每一个点单独拎出来都是嵌入式开发的必修课而它们在一个项目里自然串联起来的时候你才算真正开始理解“系统”两个字的意思。最后再分享一个小技巧调试这个项目时我把GPS模块、语音模块的串口输出通过USB-TTL全部引到了电脑上用串口助手同时开三个窗口看数据流。一次系统不播报的故障我在三个窗口里对照数据发现GPS数据正常但语音模块从始至终没收到任何指令——问题定位到状态机里的播报触发条件被一个旧的“已播报”标志卡住了。如果没有三路串口并行监控这种跨模块的数据流问题排查起来会非常痛苦。嵌入式调试的本质就是让数据流透明化你盯得住数据就找得到问题。本文还有配套的精品资源点击获取
