1. 公交车报站系统到底在做什么
公交车报站系统这个东西,说白了就是公交车上的"电子嗓门"——它要知道车现在开到哪一站了,然后通过语音和显示屏告诉乘客"下一站是XX"。听起来简单,但真做起来,涉及的东西一点都不少:站点检测、语音播报、LCD显示、按键控制、状态切换,每一个环节都有坑。
我这次做的是一个基于51单片机的公交车报站系统,主控用的是STC89C52RC,配套有仿真文件、完整代码、原理图、硬件框图、软件流程图、元器件清单和参考文档。整个项目从硬件设计到软件调试走了一遍,中间踩了不少坑,也积累了一些经验,这里完整地分享出来。
这套系统适合谁看?如果你是电子信息、自动化、计算机相关专业的学生,正在做单片机课程设计或者毕业设计,这个项目可以直接拿来参考。如果你是想入门51单片机的爱好者,这个项目涉及了GPIO控制、定时器、中断、串口通信、LCD驱动、语音模块控制等核心知识点,是一个很好的练手项目。如果你已经有一定基础,想看看别人是怎么做系统级设计的,这里的设计思路和避坑经验也能给你一些启发。
整个系统能实现的功能包括:手动/自动报站切换、站点加减、语音播报、LCD显示当前站和下一站信息、按键控制等。下面我从设计思路开始,一步步拆解整个项目。
2. 系统整体设计与方案选型
2.1 为什么选51单片机作为主控
很多人可能会问,现在STM32这么便宜,ESP32功能这么强,为什么还要用51单片机?这个问题我在做方案选型的时候也想过。实际考虑下来,有几个原因:
第一,教学和入门价值。51单片机的架构简单,寄存器少,没有复杂的时钟树和中断向量表,初学者能很快理解单片机的基本工作原理。你搞懂了51的定时器、中断、串口,再去看STM32,会发现很多概念是相通的。
第二,成本极低。STC89C52RC这片芯片,零售价大概两三块钱,批量更便宜。对于公交车报站这种功能需求不复杂的场景,51完全够用,没必要上更贵的平台。
第三,资料丰富,生态成熟。51单片机的开发工具Keil C51非常成熟,网上能找到的例程和教程浩如烟海,遇到问题很容易找到解决方案。
第四,仿真支持好。Proteus对51单片机的仿真支持非常完善,可以在没有硬件的情况下先验证逻辑,这对于学生做课设来说太重要了——不用等PCB打样回来就能调代码。
当然,51也有明显的短板:RAM只有128字节(STC89C52RC是256字节),ROM 8KB,IO口驱动能力弱,没有硬件浮点运算。但公交车报站系统不需要复杂的运算,也不需要大容量存储,这些短板在这个项目里影响不大。
2.2 整体架构是怎么搭的
整个系统按功能划分为几个模块:
- 主控模块:STC89C52RC最小系统,包括晶振电路(11.0592MHz)、复位电路、电源电路
- 显示模块:LCD1602液晶屏,显示当前站和下一站信息
- 语音模块:WT588D语音芯片或者ISD1820录放音模块,负责语音播报
- 按键模块:独立按键,用于手动控制站点加减、模式切换
- 电源模块:5V供电,可以用USB供电或者7805稳压
各模块之间通过IO口连接,主控负责协调各个模块的工作。整个系统的硬件框图大致是这样的:按键输入→单片机处理→LCD显示+语音播报。
这里说一下为什么选LCD1602而不是数码管。数码管只能显示数字,没法显示站名,而报站系统需要显示中文站名(或者至少是拼音/英文),所以LCD1602是更合适的选择。虽然LCD1602只能显示两行各16个字符,但对于显示"当前站"和"下一站"这两个信息来说,刚好够用。
2.3 语音方案的取舍
语音播报是报站系统的核心功能之一,方案选择上我考虑了几种:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ISD1820录放音模块 | 电路简单,可以直接录音 | 音质一般,存储时间短,不能灵活更换语音 | 简单演示 |
| WT588D语音芯片 | 音质好,可通过串口更换语音,支持多段语音 | 需要专用下载器和上位机软件 | 正式项目 |
| SYN6288语音合成 | 任意文字转语音,不需要预先录音 | 成本较高,控制复杂 | 需要动态语音 |
| DFPlayer Mini | 支持MP3文件,音质好,价格便宜 | 需要SD卡,控制协议稍复杂 | 推荐方案 |
最终我选择了WT588D方案,因为它在仿真中比较容易模拟,而且实际硬件上也方便更换语音内容。如果你只是做仿真演示,其实用蜂鸣器模拟"嘀"一声也能凑合,但效果就差很多了。
3. 硬件设计核心细节
3.1 单片机最小系统
最小系统是整个硬件的基础,包括三个部分:
晶振电路:用11.0592MHz晶振,两个30pF瓷片电容。为什么选11.0592MHz而不是12MHz?因为11.0592MHz能让串口波特率更精确。比如要得到9600bps的波特率,用11.0592MHz晶振时,定时器初值算出来是整数,误差为0;而用12MHz的话会有误差,可能导致串口通信不稳定。虽然这个项目不一定用串口,但养成好习惯没坏处。
复位电路:经典的上电复位+按键复位电路。10uF电解电容接VCC,10K电阻接地,复位按键并联在电容两端。上电时电容充电,RST引脚维持高电平一段时间,单片机复位。按下按键时,电容放电,RST再次拉高,手动复位。
电源电路:如果用USB供电,直接取5V就行。如果用电池或者适配器,需要加一个7805稳压电路。注意7805的输入输出都要加滤波电容,输入端加100uF和0.1uF,输出端加10uF和0.1uF,这样能有效抑制电源纹波。
注意:STC89C52RC的IO口是准双向口,输出高电平时驱动能力很弱(大概只有几十微安),输出低电平时能吸收20mA左右的电流。所以驱动LED时,要用低电平驱动,也就是LED正极接VCC,负极接IO口。这个坑我在第一次做硬件的时候踩过,LED死活不亮,后来才发现是驱动方式搞反了。
3.2 LCD1602显示电路
LCD1602有16个引脚,实际用到的就那几个:
- 引脚1(VSS):接地
- 引脚2(VDD):接5V
- 引脚3(V0):对比度调节,接一个10K电位器的中间脚
- 引脚4(RS):寄存器选择,接单片机IO口
- 引脚5(RW):读写选择,一般接地(只写模式)
- 引脚6(E):使能信号,接单片机IO口
- 引脚7-14(D0-D7):数据线,接单片机P0口(需要加上拉电阻)
- 引脚15(A):背光正极,接5V(串一个限流电阻)
- 引脚16(K):背光负极,接地
这里有个细节:P0口作为普通IO使用时,需要外接上拉电阻(一般用10K排阻)。因为P0口内部没有上拉电阻,是开漏输出。如果你直接把LCD的数据线接到P0口而不加上拉电阻,LCD可能不显示或者显示乱码。这个坑很经典,很多初学者都会遇到。
对比度调节那个10K电位器也很关键。调不好的话,要么屏幕一片白,要么一片黑,什么都看不见。调节方法:上电后慢慢旋转电位器,直到字符清晰显示为止。
3.3 按键电路设计
按键电路很简单,就是独立按键加消抖电容。每个按键一端接地,另一端接单片机IO口,同时通过一个10K电阻上拉到VCC。按下按键时,IO口被拉低,单片机检测到低电平就知道按键按下了。
硬件消抖用电容并联在按键两端,一般用0.1uF。但实际调试下来,硬件消抖效果有限,软件消抖更可靠。软件消抖就是检测到按键按下后,延时10-20ms再检测一次,如果还是按下状态,就确认按键有效。
我用了4个按键:模式切换、站点加、站点减、确认/播报。按键布局要考虑操作方便性,实际安装时最好把按键放在司机容易够到的位置。
3.4 语音模块接口
WT588D语音芯片通过串口或者一线串口协议与单片机通信。一线串口模式只需要一根数据线,控制简单。单片机发送一个字节的命令,语音芯片就播放对应的语音段。
接线方式:WT588D的DATA脚接单片机的一个IO口,BUSY脚可以接另一个IO口用来检测是否在播放。供电是3.3V,如果单片机是5V系统,需要在数据线上加电平转换电路,或者串一个1K电阻限流。
语音内容需要提前用上位机软件烧录到WT588D的SPI Flash中。每段语音分配一个地址,单片机发送地址就能播放对应语音。比如地址0x00是"欢迎乘坐XX路公交车",0x01是"下一站XX站",等等。
4. 软件设计思路与代码实现
4.1 软件整体流程
软件采用前后台架构:主循环负责按键扫描和状态更新,定时器中断负责定时任务(比如语音播报的延时控制)。整体流程是:
- 系统初始化:LCD初始化、定时器初始化、串口初始化(如果用了)、变量初始化
- 进入主循环:
- 扫描按键
- 根据当前模式(手动/自动)处理按键
- 更新LCD显示
- 如果需要播报,触发语音模块
- 定时器中断:每50ms触发一次,用于按键消抖计时和自动报站计时
这个架构简单可靠,适合51单片机的资源限制。不需要上RTOS,裸机跑就够了。
4.2 LCD1602驱动代码
LCD1602的驱动是基础但容易出错的部分。关键点在于时序:写命令和写数据都要遵循一定的时序,特别是使能信号E的脉冲宽度不能太短。
#include <reg52.h> #define LCD_DATA P0 sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_EN = P2^2; void delay_ms(unsigned int ms) { unsigned int i, j; for(i = 0; i < ms; i++) for(j = 0; j < 110; j++); } void lcd_write_cmd(unsigned char cmd) { LCD_RS = 0; // 命令模式 LCD_RW = 0; // 写模式 LCD_DATA = cmd; delay_ms(1); LCD_EN = 1; delay_ms(1); LCD_EN = 0; delay_ms(2); } void lcd_write_data(unsigned char dat) { LCD_RS = 1; // 数据模式 LCD_RW = 0; LCD_DATA = dat; delay_ms(1); LCD_EN = 1; delay_ms(1); LCD_EN = 0; delay_ms(2); } void lcd_init(void) { lcd_write_cmd(0x38); // 8位数据,两行显示,5x7点阵 lcd_write_cmd(0x0C); // 显示开,光标关 lcd_write_cmd(0x06); // 写入后地址自动加1 lcd_write_cmd(0x01); // 清屏 delay_ms(5); }这段代码里,delay_ms(1)的延时时间需要根据晶振频率调整。11.0592MHz下,大概110个空循环是1ms。如果延时不够,LCD可能不响应;延时太长,刷新速度慢。实际调试时可以先用较长的延时确保能工作,再逐步缩短优化。
4.3 按键扫描与状态机
按键处理我用了一个简单的状态机。每个按键有三个状态:空闲、消抖、确认。状态转移在定时器中断里进行,每10ms检查一次。
typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_RELEASE } KeyState; KeyState key_state[4] = {KEY_IDLE, KEY_IDLE, KEY_IDLE, KEY_IDLE}; unsigned char key_pressed_flag[4] = {0, 0, 0, 0}; void timer0_isr(void) interrupt 1 { unsigned char i; TH0 = (65536 - 10000) / 256; // 10ms定时 TL0 = (65536 - 10000) % 256; for(i = 0; i < 4; i++) { switch(key_state[i]) { case KEY_IDLE: if(key_input[i] == 0) { key_state[i] = KEY_DEBOUNCE; } break; case KEY_DEBOUNCE: if(key_input[i] == 0) { key_state[i] = KEY_PRESSED; key_pressed_flag[i] = 1; } else { key_state[i] = KEY_IDLE; } break; case KEY_PRESSED: if(key_input[i] == 1) { key_state[i] = KEY_RELEASE; } break; case KEY_RELEASE: key_state[i] = KEY_IDLE; break; } } }这个状态机的好处是不会漏掉按键,也不会因为抖动误触发。key_pressed_flag在主循环里检查,如果为1就执行对应操作,然后清零。
4.4 报站逻辑实现
报站逻辑是整个系统的核心。我用一个数组存储所有站名,用一个变量记录当前站序号。报站时,显示当前站和下一站,同时触发语音播报。
#define MAX_STATIONS 20 char *station_names[MAX_STATIONS] = { "Station 1", "Station 2", // ... 更多站名 }; unsigned char current_station = 0; unsigned char total_stations = 10; unsigned char auto_mode = 0; // 0=手动,1=自动 void report_station(void) { // 显示当前站 lcd_set_cursor(0, 0); lcd_print("Current:"); lcd_print(station_names[current_station]); // 显示下一站 lcd_set_cursor(1, 0); lcd_print("Next:"); if(current_station + 1 < total_stations) { lcd_print(station_names[current_station + 1]); } else { lcd_print("Terminal"); } // 触发语音播报 voice_play(current_station); } void next_station(void) { if(current_station + 1 < total_stations) { current_station++; report_station(); } } void prev_station(void) { if(current_station > 0) { current_station--; report_station(); } }自动模式下,用一个定时器计数,每30秒自动切换到下一站。这个时间可以根据实际线路调整,城市公交一般站间行驶时间在2-5分钟,但仿真演示时30秒比较合适。
4.5 语音模块控制
WT588D的一线串口协议比较简单:拉低数据线5ms,然后发送8位数据,先发低位。每位数据用不同的脉宽表示0和1。
sbit VOICE_DATA = P3^5; void voice_play(unsigned char addr) { unsigned char i; VOICE_DATA = 0; delay_ms(5); // 起始信号 VOICE_DATA = 1; delay_ms(1); for(i = 0; i < 8; i++) { VOICE_DATA = 0; if(addr & 0x01) { delay_ms(3); // 数据1:长低电平 } else { delay_ms(1); // 数据0:短低电平 } VOICE_DATA = 1; delay_ms(1); addr >>= 1; } VOICE_DATA = 1; // 恢复空闲 }实际调试时,脉宽需要根据WT588D的数据手册微调。如果语音不播放或者播放错误,大概率是脉宽不对。用示波器看波形是最直接的调试方法。
5. Proteus仿真与调试
5.1 仿真电路搭建
Proteus仿真最大的好处是不用焊电路就能验证代码逻辑。搭建仿真电路的步骤:
- 在Proteus中新建工程,选择单片机型号STC89C52RC
- 添加LCD1602元件,连接到P0口和P2口
- 添加按键元件,连接到P3口
- 添加扬声器或者蜂鸣器模拟语音输出
- 加载编译好的HEX文件
- 运行仿真
仿真中LCD1602的对比度引脚V0需要接一个电位器,否则可能不显示。这个细节在仿真里也适用。
5.2 常见仿真问题排查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| LCD不显示 | 对比度没调好 | 调整V0引脚电位器 |
| LCD显示乱码 | 数据线接错或时序不对 | 检查P0口连接,确认上拉电阻 |
| 按键无反应 | 按键连接错误或消抖参数不对 | 检查按键接线,调整消抖时间 |
| 语音不播放 | 脉宽不对或地址错误 | 用示波器看波形,核对地址 |
| 程序跑飞 | 中断冲突或堆栈溢出 | 检查中断优先级,减少局部变量 |
提示:Proteus仿真和实际硬件有差异,仿真通过不代表硬件一定没问题。特别是时序敏感的部分(如LCD驱动、语音控制),实际硬件上可能需要微调延时参数。
5.3 仿真与实物的差异
仿真中很多问题不会暴露,比如:
- 电源纹波:仿真中电源是理想的,实际中电源纹波可能导致单片机复位
- 信号完整性:仿真中导线是理想的,实际中长导线可能引入干扰
- 驱动能力:仿真中IO口可以驱动任意负载,实际中驱动能力有限
- 晶振起振:仿真中晶振总是起振的,实际中可能因为负载电容不匹配而起振困难
所以,仿真验证逻辑,实物验证可靠性,两者缺一不可。
6. 元器件清单与选型建议
6.1 核心元器件清单
| 元件 | 型号 | 数量 | 备注 |
|---|---|---|---|
| 单片机 | STC89C52RC | 1 | 也可以用AT89S52 |
| 晶振 | 11.0592MHz | 1 | 串口通信更精确 |
| 电容 | 30pF | 2 | 晶振负载电容 |
| 电解电容 | 10uF | 1 | 复位电路 |
| 电阻 | 10K | 1 | 复位电路 |
| 电阻排 | 10K | 1 | P0口上拉 |
| 按键 | 轻触开关 | 4 | 6x6mm |
| LCD | LCD1602 | 1 | 带背光 |
| 电位器 | 10K | 1 | 对比度调节 |
| 语音芯片 | WT588D | 1 | 含SPI Flash |
| 扬声器 | 8欧姆 0.5W | 1 | 语音输出 |
| 电源 | 5V | 1 | USB供电或7805 |
6.2 选型经验
单片机选STC89C52RC而不是AT89C51:STC的芯片支持串口直接下载,不需要专用编程器,而且价格更便宜。AT89C51现在基本停产了,不容易买到。
LCD1602选带背光的:不带背光的LCD在光线暗的环境下看不清,带背光的贵不了几毛钱,体验好很多。
语音芯片选WT588D而不是ISD1820:ISD1820只能录一段语音,而且音质差。WT588D可以存储多段语音,通过串口切换,更适合报站系统。
晶振选11.0592MHz而不是12MHz:前面说过了,串口波特率更精确。虽然这个项目不一定用串口,但万一以后要加串口通信功能,不用换晶振。
7. 调试中踩过的坑与解决技巧
7.1 LCD1602不显示的三种情况
第一种:屏幕亮但无字符。这通常是对比度问题,调节V0引脚的电位器就能解决。如果调到极限还是不显示,检查V0是否接对了,有时候电位器接错线也会导致这个问题。
第二种:屏幕亮但显示乱码。这通常是数据线接触不良或者时序不对。检查P0口的连接,确认上拉电阻是否接好。如果用的是杜邦线连接,接触不良很常见,建议焊接或者用排线。
第三种:屏幕完全不亮。检查背光引脚是否接对,背光正极串的限流电阻是否合适。一般背光LED的正向电压是3V左右,电流10-20mA,5V供电的话串一个150欧姆到200欧姆的电阻比较合适。
7.2 按键误触发的解决方法
按键误触发是常见问题,原因可能有几个:
- 消抖时间不够:一般10-20ms的消抖时间就够了,但如果按键质量差,可能需要更长
- 按键电路没有上拉电阻:如果IO口内部上拉不够强,需要外接上拉
- 电源干扰:电机等大功率设备工作时会引入干扰,需要在电源端加滤波电容
我的经验是,软件消抖比硬件消抖更可靠。硬件消抖用电容,但电容值不好选,太小没效果,太大影响响应速度。软件消抖灵活,调整参数就行。
7.3 语音模块调试技巧
WT588D调试最大的问题是不知道语音有没有播放。我的做法是:
- 先用示波器看DATA线的波形,确认脉宽是否正确
- 如果波形正确但不播放,检查语音文件是否烧录成功
- 如果烧录成功但不播放,检查扬声器是否接好
- 如果都正常,可能是地址不对,尝试不同的地址
还有一个技巧:WT588D的BUSY引脚在播放时会输出低电平,可以用单片机检测这个引脚来判断是否在播放。这样可以避免在播放时发送新的播放命令导致冲突。
7.4 程序跑飞的排查思路
程序跑飞是单片机开发中最头疼的问题。排查思路:
- 检查中断函数:中断函数里不要做太多事情,尽量简短
- 检查堆栈:51单片机的堆栈空间有限,局部变量不要太多
- 检查指针:数组越界是常见原因,特别是字符串操作
- 加看门狗:如果程序跑飞,看门狗能自动复位
我在调试时遇到过一次程序跑飞,最后发现是数组越界。站名数组只有10个元素,但代码里访问了第11个,导致覆盖了其他变量。这种问题在仿真中可能不会暴露,但实际硬件上就会出问题。
8. 资料包内容与使用建议
8.1 资料包包含什么
这套资料包包含:
- 仿真文件:Proteus工程文件,可以直接打开运行
- 代码:完整的Keil C51工程,包含所有源文件
- 原理图:Altium Designer格式的原理图,也可以导出PDF
- 硬件框图:系统整体架构图
- 软件流程图:程序执行流程图
- 元器件清单:BOM表,包含型号、数量、备注
- 参考文档:相关芯片的数据手册和应用笔记
8.2 怎么用这套资料
如果你是做课设,建议按这个顺序:
- 先看硬件框图,理解系统整体架构
- 再看原理图,理解每个模块的电路连接
- 然后看软件流程图,理解程序逻辑
- 接着看代码,对照流程图理解每一段代码的作用
- 最后在Proteus中运行仿真,观察实际效果
- 如果有硬件,把代码烧录到单片机中,实际调试
如果你是做产品开发,这套资料可以作为参考,但需要注意:
- 51单片机的资源有限,如果功能复杂,建议换STM32
- 语音模块的选型要根据实际需求,WT588D适合固定语音,如果需要动态语音,考虑SYN6288
- 电源设计要考虑实际工作环境,公交车上的电源波动很大,需要加保护电路
8.3 后续扩展方向
这个系统还可以扩展很多功能:
- GPS定位:加一个GPS模块,自动检测到站,不需要手动按键
- 无线通信:加一个无线模块,与调度中心通信,实现远程监控
- 语音合成:用SYN6288替代WT588D,实现任意文字转语音
- OLED显示:用OLED替代LCD1602,显示更多信息,效果更好
- 数据存储:加一个EEPROM或者Flash,存储线路信息,方便更换线路
我个人觉得,GPS自动报站是最实用的扩展方向。手动报站需要司机操作,容易忘记或者出错。GPS自动报站可以检测车辆位置,自动触发报站,减轻司机负担。
9. 个人经验总结
做这个项目最大的体会是:硬件设计要考虑软件实现的便利性,软件设计要了解硬件的能力边界。比如按键电路,如果硬件上加了消抖电容,软件上就可以简化消抖逻辑;如果硬件上没有消抖,软件上就必须做消抖处理。
另一个体会是:仿真和实物调试是两回事。仿真通过只是第一步,实物调试才是真正的考验。我在仿真中跑得好好的代码,烧到实物上就出现了LCD显示不稳定的问题,最后发现是电源纹波太大,加了一个100uF的电解电容才解决。
最后分享一个小技巧:调试LCD1602时,如果怀疑是时序问题,可以先把所有延时加长一倍,确认能显示后再逐步缩短。这样能快速定位是时序问题还是其他问题。这个方法我在调试其他外设时也经常用,很有效。
这个项目后续还可以这样扩展:把站点信息存储在EEPROM中,通过按键或者串口修改站名,这样就不需要重新编译代码就能更换线路。对于实际应用来说,这个功能很实用。