资讯动态

STM32公交语音报站器:原理图、PCB与代码调试全流程

发布时间:2026/10/26 1:05:33 来源:尧图企业网站定制
做公交地铁语音报站器这类STM32项目这几年在课程设计和毕业设计里一直很常见。功能链路非常清楚按键触发进站出站语音模块播报实时站名屏幕同步显示站点信息软硬件都有展示点做出来的实物效果又很直观。我最近把整套系统从头到尾完整走了一遍交付物包含程序源码、原理图、PCB和演示视频。这篇文章不打算堆砌重复的工程说明重点讲清楚我在器件选型、原理图设计、PCB排布和代码调试中遇到的关键取舍和实际踩过的坑给正在做类似项目的同学一份能直接参考的实操路径。这套系统的核心并不复杂但要做好需要把几个环节都打通先梳理报站逻辑再定器件方案然后画原理图和PCB最后写代码调试。任何一个环节出现问题都会在演示时翻车。下面我就按实际开发顺序把每一步的关键细节展开说。1. 系统整体设计先想清楚报站器到底要干什么1.1 需求拆解从真实公交报站逻辑说起做硬件项目最容易犯的毛病就是一上来就画原理图结果做到一半发现功能定义不清楚。我先花了一点时间把真实公交车上的报站流程梳理了一遍再用单片机去模拟它。真实的报站逻辑大概是这样的车辆起步后播报“欢迎乘坐XX路公交车本车开往终点站XX”行驶途中在距离下一站还有一段距离时播报“下一站XX请提前做好下车准备”进站停稳后播报“XX站到了请从后门下车”出站后再播报下一站的预告。周而复始直到终点站。放在这个STM32项目里我们不需要真的去定位车辆位置核心是用按键来模拟“行驶到站”这个动作。所以我把功能需求拆成了三块按键操作按键A模拟“已到站”按键B模拟“已离站/开启下一段行程”必要时加一个复位按键回到始发站。语音播报中文合成播报站名和提示语这是整个项目的核心亮点。信息显示OLED屏显示当前站序、当前站名、下一站名和剩余站数让演示时不仅“听得见”还能“看得见”。除了这三个基础功能后续还可以扩展自动报站、环境信息显示等项目增强点这个放到最后再说。1.2 核心器件选型语音模块是最大变量主控芯片我选的STM32F103C8T6。这颗芯片在同类项目里几乎是默认答案64KB Flash、20KB RAM跑一个报站器绰绰有余GPIO、串口、I2C、定时器这些外设也足够丰富最小系统板成本很低资料多到随手就能搜到。相比51单片机它的Flash空间更从容后面增加站点、扩展传感器时不用频繁担心容量不够。语音模块是整个项目最需要斟酌的部分我对比过三种常见方案方案原理优点缺点适用场景SYN6288语音合成模块通过串口发送GBK中文文本芯片内部TTS合成语音站名动态可变不需要预先录音串口控制简单中文必须用GBK编码容易踩编码坑语音是合成音不够自然各种站名、动态文本推荐首选DFPlayer Mini TF卡播放TF卡中预存的MP3文件音质接近真人录音播放稳定需要提前生成音频文件串口协议带校验和稍繁琐需要真人语音播报时JQ8900语音模块支持按键和串口控制可播报MP3集成度高外围电路简单内部资源不太透明灵活性和扩展性一般功能固定、不想写代码控制的场合我最后选了SYN6288。原因是报站器要求的是“动态文本播报”——站名是可以配置的用合成芯片只需要改代码里的字符串不用每次录一段新的MP3。SYN6288自身带功放可以直接接喇叭这也能少做一个功放电路。后面我会专门讲它最坑的GBK编码问题这个坑几乎每个人都会踩一次。显示模块用0.96寸OLEDSSD1306主控I2C接口。I2C比SPI少两根线用GPIO模拟也很稳定适合在原理图和PCB上省事。如果想做大一点的信息展示也可以换1.3寸或者2.4寸TFT屏但演示需求下0.96寸OLED足够清晰还省电。电源部分用USB 5V输入经过AMS1117-3.3稳压给STM32供电。语音模块和功放部分用5V直供因为TTS芯片和功放对电压要求更高直接吃5V可以保证输出功率。要注意OLED模块的供电选择很多0.96寸OLED板载LDO3.3V到5V都能工作但接线时一定要确认模块的VCC标识。1.3 架构思路为什么用状态机管理报站流程报站逻辑如果用“按下按键就播报一段语音”这种线性写法代码会越写越乱。比如用户连续按了三次按键语音还没播完这时候下一个状态应该是什么如果每次都直接打断上一次播报会出现语音错乱如果不打断又会堆叠。我采用的是经典有限状态机思路定义了四个状态待机状态系统上电初始显示起始站信息等待启动。行驶状态语音播报“本车开往XX方向下一站XX”屏幕显示站序。到站状态播放“XX站到了请下车”屏幕高亮显示当前站。离站状态播放“下一站XX请提前准备”随后回到行驶状态。每次按键只负责“产生一个事件”具体进入什么状态、播报什么内容都由状态机统一决定。这样带来的直接好处是无论按键怎么乱按系统状态都不会紊乱后续想改成自动报站只需要把“按键事件”替换成“传感器触发事件”状态机主体完全不用动。这个设计思路虽然听起来简单但在实际调试里帮我省了大量时间。2. 硬件设计原理图与PCB的实操要点2.1 STM32最小系统的原理图设计STM32F103C8T6的最小系统主要由电源、晶振、复位、BOOT和下载接口组成。我用嘉立创EDA标准版画原理图元件库直接搜“STM32F103C8T6”“AMS1117-3.3”“SWD”就能找到封装。电源部分USB座输入5V经过AMS1117-3.3输出3.3V。AMS1117输入端放一个100μF电解电容和一个100nF瓷片电容输出端放一个10μF钽电容再加100nF这样能有效抑制电源纹波。每个VDD引脚旁边都要放100nF去耦电容VDDA引脚放1μF电容和10nF电容组合。晶振部分STM32可以用内部HSI振荡器跑8MHz很多例程甚至不画晶振也能工作。但我建议还是加上8MHz外部晶振加两个20pF负载电容这样串口波特率更准确。语音模块对时序要求不算苛刻但串口波特率如果偏差超过2%偶尔就会出现乱码或丢字外部晶振能让人更省心。复位电路用经典的10kΩ上拉电阻加100nF对地电容接到NRST引脚。BOOT0引脚必须通过一个10kΩ电阻下拉到地如果悬空芯片可能启动到系统存储器而不是Flash中的程序表现为下载后运行不了。SWD下载接口引出4个引脚3.3V、GND、SWDIO、SWCLK建议再串联一个1kΩ电阻在SWDIO上做简单保护。2.2 语音模块和OLED的接口设计SYN6288模块和STM32之间是串口连接。我用的USART1PA9做TX连模块RXPA10做RX连模块TX。这里有一个很容易被忽略的点SYN6288模块的串口电平通常是3.3V TTL和STM32兼容直接连接没问题但如果买的模块是5V电平版本就必须做电平转换否则长期使用会损伤引脚。语音模块的电源和喇叭接口要留足空间。SYN6288模块板载功放喇叭接口一般是2P的排针或接线端子。选喇叭时我用了8Ω、2W的圆形小喇叭声音清晰够用。喇叭离MCU要有一定距离功放输出大电流走线也尽量不要和串口信号线平行贴在一起否则播报语音时串口容易受到干扰导致后续指令偶尔丢失。OLED用软件模拟I2C选两个普通GPIO我用PA5做SCL、PA7做SDA。为什么不选硬件I2C引脚PB6/PB7原因很现实STM32F103的硬件I2C稳定性一直被人诟病软件I2C虽然多占用CPU但在这个项目里完全够用而且可以随便换引脚。对初学者来说软件I2C遇到问题也好排查逻辑分析仪一抓就知道时序对不对。整个硬件接线可以整理成一张速查表功能STM32引脚外设引脚备注语音模块TXPA10RX仅播报时可省略语音模块RXPA9TX播报数据发送OLED SCLPA5SCL软件I2COLED SDAPA7SDA软件I2C按键APA0KEY1下拉/外部中断按键BPA1KEY2下拉/外部中断喇叭模块输出接口喇叭正负8Ω/2W2.3 嘉立创EDA绘制流程与PCB布局布线避坑原理图在嘉立创EDA里画完后直接“转换为PCB”然后进入布局布线环节。学生项目基本都是两层板这个规模两层板绰绰有余打样价格也便宜。布局的关键原则是从“使用体验”反推。按键放在板子边缘方便演示时用手指按压OLED排针座放在板子上方屏幕可以正面朝外语音模块的插座放在一侧喇叭线从这里引出SWD接口集中放一排方便烧录时插线。STM32放在板子中央晶振紧贴MCU放置距离控制在5mm以内两个负载电容对称放在晶振和MCU之间。布线时我习惯先布电源和地再布信号线。电源线加宽到1mm以上信号线使用0.3mm到0.5mm。铜箔厚度1oz的情况下1mm线宽大约能过1A电流这个项目的语音模块瞬时电流可能到500mA以上所以语音模块供电线不能太细。晶振走线要短尽量包地串口线和I2C线远离喇叭输出走线整板铺地铜并打一圈过孔连接底层地。关于机械固定孔PCB四角可以放M2螺丝孔方便后面装亚克力外壳或3D打印外壳。M2螺丝孔的钻孔直径一般是2.0mm到2.2mm如果做金属化过孔过孔外径做到2.8mm到3.0mm也就是环宽留0.4到0.5mm即可。立创EDA生成Gerber后直接下单板厚选1.6mm颜色看个人喜好一般绿色打样最便宜。2.4 焊接与硬件调试要点PCB到手后不要急着全焊我习惯按模块分批焊接、逐级验证。先焊电源部分USB座、AMS1117、滤波电容上电测3.3V是否正常同时确认没有短路。再焊STM32最小系统SWD接口接上ST-Link尝试下载一个GPIO翻转的测试程序确认芯片能跑。最后再焊OLED排针、语音模块插座和按键。焊接过程中最容易出问题的是AMS1117的引脚方向。丝印上一般有标记但如果板子是嘉立创EDA自动生成的封装还是建议对照数据手册确认输入输出引脚。有一个我踩过的低级错误第一次画板时电解电容极性画反上电后电容直接鼓包排查了半天才发现是封装引脚定义和实际元件不一致。热风枪和电烙铁配合使用焊接QFN封装芯片之前先在焊盘上涂好助焊剂用热风枪吹焊能大大提高成功率。硬件调试时万用表主要量三处3.3V是否稳定、GND是否连通、SWDIO/SWCLK引脚是否确实连到MCU。示波器不是必须的但如果有可以看晶振引脚有没有起振波形。没有示波器也不慌用ST-Link读芯片ID能确认JTAG/SWD链路通不通这是最直接的判断方法。3. 软件实现报站逻辑、语音播报与站点显示3.1 按键消抖与站点状态机实现按键用了PA0和PA1外部下拉电阻按下为高电平。单片机按键最经典的问题是抖动机械按键按下瞬间会有几毫秒到十几毫秒的不稳定电平如果不处理一次按下可能触发多次事件。我的处理方法是“延时消抖加状态机事件”。在按键中断里先判断电平然后调用一个10ms延时再次确认电平状态确认后再发事件。这个做法虽然简单但在报站器这种低频操作场景下足够可靠。更工程化的方案是用定时器每10ms扫描一次按键维护按键释放、按下、长按三个状态不过对课程设计来说中断加延时消抖已经够用。状态机核心代码逻辑是这样的typedef enum { ST_IDLE, // 待机 ST_DRIVING, // 行驶中 ST_ARRIVED, // 已到站 ST_DEPARTED // 已离站 } BusState; volatile BusState bus_state ST_IDLE; volatile uint8_t cur_station 0; void OnArriveKey(void) { switch (bus_state) { case ST_DRIVING: case ST_DEPARTED: bus_state ST_ARRIVED; Break_Station_Play(cur_station); // 播放“XX站到了” OLED_ShowStation(cur_station); break; default: break; } } void OnDepartKey(void) { if (bus_state ST_ARRIVED) { bus_state ST_DEPARTED; Next_Station_Play(cur_station 1); // 播放“下一站XX” OLED_ShowNextStation(cur_station 1); } }站点数据用结构体数组统一管理后续增加站点只改数组就行typedef struct { const char *name_gbk; // 站名GBK编码字符串 } StationInfo; const StationInfo station_list[] { {\xB8\xA3\xD6\xDD\xC2\xB7}, // 福州路GBK编码示例 {\xC8\xCB\xC3\xF1\xB9\xE3\xB3\xA1}, // 人民广场 // 更多站点... }; #define STATION_COUNT (sizeof(station_list) / sizeof(station_list[0]))3.2 SYN6288语音播报的串口指令格式SYN6288通过串口接收固定格式的指令帧。合成播报的帧格式是帧头0xFD后面跟两个字节的数据长度长度值等于“命令字1字节加文本字节数”再跟命令字0x01最后是GBK编码的中文文本。举个例子播放文本“你好”GBK编码是C4 E3 BA C3四个字节长度就是145完整帧是FD 00 05 01 C4 E3 BA C3。向模块发送这段数据模块就会合成播报“你好”。实际发送函数可以这样写void Syn6288_PlayText(const char *gbk_text) { uint16_t len strlen(gbk_text) 1; // 命令字占1字节 uint8_t buf[128]; uint8_t idx 0; buf[idx] 0xFD; buf[idx] (len 8) 0xFF; buf[idx] len 0xFF; buf[idx] 0x01; // 合成播放命令 while (*gbk_text) { buf[idx] *gbk_text; } UART1_SendBuffer(buf, idx); }发送完一帧后要留一点延时让模块有时间完成合成播报。我实测在9600波特率下合成一句约20个字的提示语大约需要2到3秒所以每帧播报结束后要等播报完成再发下一帧否则模块可能丢弃指令。这一点在连续播报“欢迎乘坐XX路本车开往XX下一站XX”这种长句子时尤其重要。3.3 DFPlayer语音文件播放的备用方案如果你的项目更看真人声轨或者希望语音更自然可以换成DFPlayer Mini加TF卡。DFPlayer的串口指令帧固定10字节以0x7E开头、0xEF结尾中间包括版本号、命令、参数和校验和。指定曲目使用命令0x03音量设置使用命令0x06校验和是对指令中间若干字节累加后取反加一。我建议直接参考成熟的开源DFPlayer库函数不用自己从头写协议。语音文件用Edge浏览器的朗读功能或手机上的文字转语音APP生成MP3命名按track001.mp3、track002.mp3这样递增放入TF卡根目录。一个站点一个文件文件多了以后代码里用站点序号和MP3序号直接一一对应。DFPlayer方案的缺点是每改一个站名都要重新生成音频文件比较麻烦优点是语音自然、播放稳定、功放音质好。如果追求演示效果这个方案很值得考虑。我在实际调试时两款模块都试过最后为了站名可以灵活修改还是回归到了SYN6288但DFPlayer方案作为备选写在说明文档里方便别人按需求替换。3.4 GBK转UTF-8语音中文乱码的根治方法SYN6288这个模块最折磨人的就是中文编码。模块要求GBK编码的中文文本而现代Keil MDK默认把.c文件保存为UTF-8编码代码里直接写的中文字符串在内存中是UTF-8字节。把UTF-8字节发过去模块收到的是非法文本表现就是播报乱码、静默或者提示音异常。我一开始也在这里卡了很久后来整理了三种可靠的解决方法。第一种改源文件编码。在Keil MDK的Edit-Configuration-Editor-Encoding里把默认编码改成GB2312或者Chinese Simplified再把.c文件另存为ANSI格式。这样源码里的汉字字符串编译器保存时就是GBK字节直接发送即可。这个方案最省事但要注意整个工程里所有包含中文的.c文件都要统一编码一旦某个文件还是UTF-8单独一个站点名就会乱。第二种离线转码成字节数组。用在线工具或者本地脚本把每个站名先转成GBK十六进制例如“福州路”转成“B8 A3 D6 DD C2 B7”直接写进const uint8_t数组。这样代码不依赖文件编码无论用什么工具打开都是稳定的。缺点是写站名时要额外做一次转码不能直接写中文。第三种运行时UTF-8转GBK。写一个转换函数在代码里处理编码转换。这种方法适合动态文本但转换逻辑复杂还会消耗Flash和RAM对报站器这种固定站点列表的项目没有必要。我最推荐的是第二种。虽然多一步转码操作但一劳永逸而且烧录后行为完全可预期。调试时先用“你好”这种固定字符串把通路测通再换真实站名能大幅减少变量。3.5 OLED站点显示与串口调试日志OLED显示用SSD1306的I2C驱动地址通常为0x78或0x7A。中景园电子的OLED例程非常经典库函数简洁直接移植到STM32工程里就行。需要注意I2C地址那行宏定义不同模块有区别白屏先查地址总是没错。显示内容我做了三行布局第一行显示“NEXT:下一站名”第二行显示“NOW:当前站名”第三行显示“剩余:X站”。由于0.96寸OLED显示汉字需要额外字库取模为了省事第一版我只用英文加拼音显示站名比如“NEXT: RENMING SQ”。演示视频里语音播报中文屏幕显示拼音观众也能理解而且避免了自己做字模的额外工作量。想要显示中文时用PCtoLCD2002为需要的汉字逐字取模再做一个简单的字库数组功能上完全支持。串口调试日志是整个调试过程的好帮手。我用USART2输出日志每次状态机切换时打印一行当前状态、站点序号、即将播报的内容。调试时用USB转TTL接PA2和PA3电脑串口助手看输出。这个看似简单的操作在定位“按键没反应”“语音和显示不同步”等问题时效率比用示波器还高。4. 常见问题排查与调试实录4.1 语音模块不播报或乱码先确认模块是不是坏了单独给模块供电用USB转TTL直接发FD 00 05 01 C4 E3 BA C3如果模块播报“你好”说明模块正常。随后再检查STM32一侧用万用表测PA9到模块RX之间是否导通确保焊接没问题。乱码基本就是编码问题按上一节的方法把文本转成GBK字节再发。还有一个小细节容易被忽略SYN6288上电后默认波特率是9600如果STM32串口初始化成115200模块收不到任何指令必须先设置波特率或者保持一致。4.2 程序下载失败的三种原因下载失败是STM32项目里最常见的问题。第一种是SWD接口没接好SWDIO和SWCLK松动或者线序接反第二种是芯片的BOOT0被拉高芯片复位后进入ISP模式不响应SWD第三种是目标板供电不足ST-Link自带的3.3V输出带不动整个板子。我的排查顺序是先测目标板3.3V再测SWD引脚电压最后检查BOOT0电平。把这三项排除后90%的下载问题都能解决。4.3 OLED白屏OLED白屏直接查I2C地址、SCL/SDA接线和供电电压。我先用万用表确认OLED模块的VCC脚确实是3.3V而不是误接5V把屏幕背光烧了。然后用逻辑分析仪抓SCL和SDA波形如果能看到正常的I2C起始条件和地址帧说明硬件没问题问题在初始化时序或者地址宏定义上。如果SCL上有波形但SDA一直高八成是OLED模块地址不对把0x78改成0x7A再试。4.4 按键误触发和状态错乱按键误触发的根源是抖动我一开始用纯中断方式按一次结果播报了三遍后来改成“中断加10ms延时确认”才稳定。状态错乱往往是因为在语音播报还没有完成时又按了按键。解决方式是在状态机里加一个“busy”标志语音模块正在播报时忽略进站和出站事件等播报完成后才允许进入下一个状态。这个机制模仿的是真实公交车的“播放中不响应下一指令”在实际演示中效果很好。我把一些容易遇到的问题整理成速查表方便参考现象最可能原因排查顺序语音完全无声模块电源或喇叭线先测模块供电再测喇叭语音播报乱码中文编码不符合GBK先发固定文本确认通路语音播到一半停波特率不匹配或指令过长确认模块波特率分段发送OLED白屏I2C地址不对先查地址再查接线按键一次触发多次未消抖加延时或定时器消抖下载失败SWD线序/BOOT0先测供电再测引脚4.5 视频演示时的现场避坑项目标题里包含视频演示这一点实际上非常关键。我在录演示视频时发现语音播报和OLED显示存在一件事如果同时按下进站键和离站键或者间隔太短状态机虽然不会乱但语音会连续播报两句观感很不好。为了演示效果我专门在按键事件里加了2秒的防重入保护每次播报结束后才允许下一次事件触发。另一个演示时容易翻车的地方是环境噪音。SYN6288的合成语音音量默认值不是最大我用串口指令把音量调到较高档位同时把喇叭朝向摄像头和观众。视频里最忌讳的是屏幕反光拍摄时把OLED屏幕摆正角度亮度调到合适值画面才清晰。这些细节不影响功能但直接影响项目评价值得认真对待。5. 从“能演示”到“像产品”扩展方向5.1 自动报站用传感器替代按键按键模拟报站能完成功能演示但“智能”属性还不够突出。我建议做一个简单的自动报站扩展在公交车站牌模型两侧装一对红外对管模拟车门位置。当“车辆”驶过站牌遮挡红外对管触发一个外部中断代替人工按键实现自动触发报站。这个扩展只需要加一对红外模块一个GPIO中断状态机本身不需要改代码改动量很小但演示效果立刻不一样。如果想更接近真实系统可以加GPS模块比如ATGM336H通过串口输出NMEA数据。维护一个站点经纬度数组解析GGA语句里的经纬度计算与下一站的距离小于阈值就触发到站播报。这个方案的工程量明显上来了但也是这个项目最有技术含量的扩展方向。5.2 增加车载环境信息显示报站器如果顺带显示车厢环境信息会显得更完整。加一个DHT11温湿度传感器接任意GPIO用单总线协议读取温湿度显示在OLED的第二行或第三行。DHT11价格便宜、库函数丰富单总线时序需要仔细写但代码量不大。实测下来DHT11的湿度响应比较慢读取间隔不能太短一般2秒读一次就够了。还可以用TIM定时器输出PWM驱动无源蜂鸣器在报站前播放一小段“叮咚”提示音模拟真实公交的报站前提示音。PWM频率调在2kHz左右声音穿透力强又不刺耳播报前鸣响0.5秒用户注意力会被吸引过来整个系统更有层次感。5.3 引入FreeRTOS优化任务划分如果主控换成STM32F103ZET6或者你想把系统做得更工程化可以引入FreeRTOS。把功能拆成几个独立任务按键扫描任务、语音控制任务、OLED刷新任务、站务逻辑任务。任务之间通过消息队列传递事件比如按键任务检测到按下后向站务任务发送“进站事件”站务任务再把播报内容发给语音任务。这样做的好处是每个任务逻辑简单清晰系统扩展多个传感器时也不会互相阻塞。但我要提醒一句对于这套报站器裸机状态机已经完全够用FreeRTOS更多是学习价值。如果是毕设想展示操作系统能力加FreeRTOS是加分项如果只是为了把项目跑通没必要为了用而用。5.4 外壳、供电与整车模型硬件做稳定之后外观是决定演示效果的另一半。我见很多同学用一个裸板加几根杜邦线做演示功能都对但视觉上很吃亏。建议用3D打印一个简单的公交车外壳或者至少用亚克力板做一个小盒子把PCB、电池、喇叭都装进去OLED屏从外壳正面露出按键和USB口开孔。供电方面演示现场没有稳定电源时用18650电池加锂电池充电模块是最靠谱的方案。5V输出可以用传统的“升压模块电池保护板”但更建议直接买带5V输出的移动电源方案安全稳定。车载12V输入可以再接一个DC-DC降压模块到5V让整个装置也可以接在模型轨道电源上。我在给装置做外壳时发现一个经验喇叭不要紧贴外壳壁那样播报时会产生共振声音发闷。在喇叭和外壳之间留2到3毫米空隙或者贴一块泡棉胶声音立刻清晰很多。这种细节在演示视频里非常明显。这个项目做完之后最大的收获不是把原理图画得多漂亮而是理解了一个完整系统的软硬件配合关系。我建议大家拿到开发板后先不要急着写代码更不要急着画PCB先把方案选型、状态划分、接口定义在纸上写清楚。做到后面你就会发现前期这半小时的规划能帮你省下好几天改板子和调代码的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑