
简介一套基于51单片机的出租车计价器完整开发资料包面向单片机课程设计、电子设计竞赛及毕业设计人群。方案覆盖硬件原理、软件编程、仿真验证与答辩汇报全流程包含原理图、源程序、Proteus仿真、实物演示视频、设计论文和答辩技巧等内容可直接作为项目模板或课设蓝本。资源共181个文件压缩包约90.26MB主要文件类型包括sch/pcb/dsn原理图、c/h/hex程序文件、doc/docx/pdf论文文档、png/jpg截图与txt说明等其中原理图与仿真文件用于硬件电路设计和验证源程序便于修改调试论文与答辩文档可辅助撰写报告。项目功能完善1602液晶实时显示时间、日期、行驶距离、当前金额与总价按键支持设置时间、日期、起步金额及历史数据查看通过霍尔传感器测量电机转速并换算里程计费逻辑清晰贴近真实出租车计价场景。已有2819人学习下载适合具备51单片机基础、需要系统完成课设或毕设的同学使用。1. 出租车计价器为什么难在“算得准”乘客从上车到下车的几公里里计价器干的事远不止“显示一个钱数”它要实时接收车轮转速信号把脉冲数换算成里程再按“起步价超程单价等待费”这套规则不断刷新金额。很多人在 51 单片机课程设计里把这个题简化成“按键加数码管”结果实训现场才发现在真实场景下最难的不是算钱而是轮速忽快忽慢时还能计得稳、掉电后不丢数据。这篇文章按一个工程落地顺序来讲先把计价器的功能拆成可实现的模块再给出硬件接线和软件状态机的核心代码最后补充 Proteus 仿真和实物调试里最容易踩的坑。新手可以跟着把模块接起来跑通有几年经验的工程师也能从脉冲滤波、EEPROM 掉电存储和晶振误差校准这几处看到可复用的细节。2. 基于 51 单片机的计价器方案从脉冲到里程换算2.1 计价器的功能拆解与核心器件选型一台完整的出租车计价器前端是传感器中间是单片机后面是显示和存储。51 单片机在这个场景里任务并不重但信号链路和计费逻辑必须分清优先级脉冲采集来自霍尔传感器或干簧管的轮速信号必须用外部中断不能靠主循环扫描计时基准计价器要算等待费、夜晚加价需要一个稳定的秒基准用定时器中断产生系统节拍人机交互至少要有“空车/载客”“白天/夜间”“单价设置”这几个键LCD 显示里程和金额掉电保存营运中突然断电当前金额、累计里程不能丢失需要一片 EEPROM 或者单片机内置的 Flash 模拟存储。器件选型没有唯一答案下面这组是我在多轮课程设计和小型工程里常用、Proteus 里也能直接找到模型的组合。模块推荐器件选型理由备注主控STC89C52RC 或 AT89S5251 内核学校实验箱/开发板覆盖面广资料多12T 模式的指令周期是 1μs12MHz轮速传感器霍尔开关 A3144有源输出直接出数字电平抗抖动比干簧管好车轮上贴小磁钢或装带齿圆盘显示LCD1602能同时显示里程、单价、金额仿真和实物都稳定存储AT24C02掉电不丢数据I2C 时序简单2KB 够存金额、总里程、单价晶振11.0592MHz串口调试波特率能整除定时器计数也能用纯做计价器 12MHz 也 OK要注意51 单片机的外部中断是在每个机器周期采样一次引脚电平的轮速太低不会漏但必须在硬件上做好整形和滤波否则一个抖动就能让里程跳一大截。2.2 轮速脉冲换算成里程的公式与定点数处理计价器的里程计算本质是一元函数行驶里程 累计脉冲数 × 单位脉冲对应的里程。单位脉冲里程由车轮周长和每圈脉冲数决定。假设车轮滚动一圈周长 $C$米每圈产生 $N$ 个脉冲每个脉冲对应$$meter_per_pulse \frac{C}{N}$$例如周长 2.0 米、每圈 1 个脉冲那么每个脉冲就是 2.0 米。实际工程里更推荐每圈多装几个磁钢这样低速时也能在几百毫秒内感知到运动避免“车走了一秒没跳数”的错觉。软件里不直接用浮点数原因很简单51 的浮点库又大又慢运算时间还会影响中断响应。常见做法是先把金额需要的精度放大到“厘”或“分”里程放大到“米”或“0.1 米”全程用 unsigned long 做整数运算显示时再缩小回去。在实际计价器里我会这样定义参数#define PULSES_PER_REV (1) // 每圈脉冲数 #define WHEEL_CIRC_MM (2000) // 车轮周长单位 mm #define PER_PULSE_MM (WHEEL_CIRC_MM / PULSES_PER_REV) // 每脉冲 2000mm volatile unsigned long pulse_cnt; // 外部中断累计的脉冲数 unsigned long total_dist_mm; // 总行驶距离单位 mm代码逻辑解释外部中断每来一次pulse_cnt主循环定时把pulse_cnt与计数上次采样值的差值乘上PER_PULSE_MM累加到total_dist_mm。用“mm”而不是“m”做单位是为了在 16 位定时器溢出不频繁的前提下尽量保留精度。显示层再去掉小数点。起步价内行驶里程可以直接从total_dist_mm判断不需要单独另存跳表用的也是这个变量但要小心“脉冲经过 2 米才累计一次”带来的误差下文状态机里会专门处理若车轮打滑、急刹车导致脉冲异常增多需要在算法层做“短时间内位移超过上限则不累加”的保护。2.3 计费规则用状态机而不是用 if 堆加价规则一多用 if 层层嵌套会把自己绕晕。计价器常见的规则有三条起步价内比如 2 公里只收起步价超过起步里程后按每跳多少米加收一档停车等待或低速行驶时按等待时长计费。这三条规则会同时生效而且互相之间要叠加我最常用的结构是“状态机 变量快照”。状态机上只需要三个主状态IDLE空车待客不计费显示空车牌RUNNING载客行驶中随时累计里程、等待、金额ARRIVED到达结清金额显示应收款等待司机清零。状态迁移条件按下“载客”键从IDLE进入RUNNING按下“到达”键从RUNNING进入ARRIVED按下“清零”键回到IDLE。在这个基础上白天/夜间切换只是修改单价变量不影响状态迁移这样代码就不容易写乱。3. 硬件电路搭建脉冲输入、LCD1602、EEPROM 怎么接3.1 最小系统与电源设计要点51 单片机最小系统三件套电源、晶振、复位。计价器项目里还有个长期被忽略的点电源纹波会直接影响中断引脚的电平判断所以去耦电容比优化代码优先级更高。电源入口车载电瓶 12V经 7805 降到 5V7805 后面并联一个 470μF 电解电容和一个 0.1μF 瓷片电容单片机 VCC 引脚旁边再加一个 0.1μF离引脚越近越好晶振用 11.0592MHz两个 30pF 负载电容分别接到晶振两端复位电路用 10μF 电容串联 10kΩ 电阻到地高电平复位。有串口调戏需求时11.0592MHz 非常香因为 9600 波特率误差为 0纯做计价器也可以换成 12MHz定时器初值更好算但后续串口打印调试信息会麻烦一点。如果条件允许直接在硬件设计阶段留一个串口下载和调试口后面校准里程会省很多力气。3.2 霍尔脉冲输入与整形电路A3144 霍尔开关是集电极开路输出必须加上拉电阻否则输出高电平的时候引脚是悬空的单片机会读到随机电平。规范接法如下A3144 的 VCC 接 5VGND 接 GND输出引脚 OUT 接一个 10kΩ 上拉电阻到 VCC再串联一个 1kΩ 电阻接到单片机外部中断 0 输入引脚 P3.2P3.2 和 GND 之间并联一个 0.1μF 电容构成简单的 RC 低通滤波吸收高频毛刺。如果现场环境电磁干扰强烈可以在中间加一级 74HC14 施密特触发器整形。施密特触发器会把缓慢变化的边沿修整成陡峭的数字电平同时还有一定滞回可以抑制输出电压在阈值附近反复抖动。实物测试时用手转动车轮如果在 LCD 上的里程跳字一次跳好几个数说明硬件滤波不够不要急着改软件。Proteus 仿真时则不用操心这些直接用信号发生器或者“DCLOCK”原件模拟霍尔输出注意把脉冲周期调到 10ms 到 100ms 之间去接近真实轮速。仿真环境下最容易犯的错误是忘记给 A3144 输出端加上拉电阻逻辑分析仪看波形发现一直是低电平。3.3 LCD1602 的 4 线接法与 Proteus 仿真的差异LCD1602 是这次项目里显示的不二选择数据总线可以做成 8 位也可以做成 4 位。我通常用 4 位模式省下来的引脚可以给矩阵键盘或者 EEPROM 用。原理是每次传一个字节时先传高 4 位再传低 4位速度足够显示更新并不频繁。LCD1602 引脚接到单片机说明RSP2.6寄存器选择0 命令1 数据RWP2.5接地只写模式简化读写时序ENP2.7使能信号高脉冲触发D4P0.4数据位高 4 位D5P0.5由于 P0 口内部无上拉必须外接 10kΩ 排阻D6P0.6同上D7P0.7同上V0电位器中间抽头调节对比度不接屏会很暗看不到内容在 Proteus 里要特别注意仿真库的 LCD1602 引脚标号可能与实物不同部分版本会直接使用DB0~DB7实际是用 8 位模式。用 4 位模式前先在器件属性里确认是否有D4~D7引脚或者干脆在仿真里统一用 8 位模式实物里再切 4 位这样能少一个折腾点。另外Proteus 仿真里无论怎么调对比度V0 都不能直接接 GND否则很多版本的屏幕根本不亮正确的做法是放一个 1kΩ 电位器中间抽头接 V0两边分别接 5V 和 GND。3.4 AT24C02 掉电存储与 I2C 接口连接计价器要存的数据不多总金额、总里程、当前单价、上次运营状态。AT24C02 容量 2Kbit即 256 字节存这些绰绰有余。连线固定为SCL 接 P2.1SDA 接 P2.0两个引脚各通过 4.7kΩ 上拉电阻到 VCCA0、A1、A2 全部接地器件地址固定为 0xA0写/ 0xA1读。写 AT24C02 有两点容易因为看起来很合理而犯错第一I2C 时序必须严格按照起始、停止、应答信号来写软件模拟时每一小段之间加上微秒级延时第二EEPROM 写一个字节后需要最多 5ms 的内部写入时间期间不能发下一个字节否则数据会丢失通常做法是在每页写入后直接延时 10ms。实物调试时可以用逻辑分析仪抓 SCL/SDA 波形看有没有应答位没有逻辑分析仪就串行打印回读校验一次写 A 地址、一次读回来对比这是几十行代码能解决的可靠性问题。4. 软件实现定时器中断、脉冲计数与计价状态机4.1 用定时器 1 产生 50ms 系统节拍计价器需要一个稳定的时间基准用来计算等待费、定时刷新显示以及对脉冲做滑动检测。51 单片机定时器 1 工作方式 1 是 16 位定时器在 11.0592MHz、12T 模式下机器周期为 1.085μs定时 50ms 需要计数值 46080初值即65536-4608019456十六进制为0x4C00。volatile unsigned int sys_tick_ms; // 累计的毫秒数用于等待计费 void Timer1_Init(void) { TMOD 0x0F; // 清空定时器1的相关位 TMOD | 0x10; // 定时器1工作方式116位计数 TH1 0x4C; // 高8位初值11.0592MHz下50ms中断 TL1 0x00; // 低8位初值 ET1 1; // 开定时器1中断 TR1 1; // 启动定时器1 EA 1; // 开总中断 } void Timer1_ISR(void) interrupt 3 { TH1 0x4C; // 重装初值消除累计误差 TL1 0x00; sys_tick_ms 50; // 每进一次中断增加50ms }初值重装是在进中断后立即进行的这个顺序很重要如果先处理业务逻辑再装初值会导致下一次定时短于 50ms长时间运行后秒基准越来越快。在此基础上计算等待费的逻辑立刻变得简单在状态机里用一个变量记录“最近一次位移发生的时刻”当sys_tick_ms与它的差连续超过 60 秒就按每分钟等待费累加一次金额。这个判断放在主循环里做不需要额外开一个定时器。4.2 外部中断 0 的脉冲计数与防抖策略脉冲信号接在 P3.2/INT0 上设置为下降沿触发。每次脉冲到来触发一次interrupt 0服务函数累加计数。代码如下volatile unsigned long pulse_total; // 总脉冲数用于里程累计 volatile unsigned long pulse_last_ms; // 上次脉冲发生时的系统时间 void Ext0_ISR(void) interrupt 0 { unsigned long now sys_tick_ms; if (now - pulse_last_ms 3) // 间隔小于3ms认为是抖动或干扰 return; // 丢弃本次脉冲 pulse_total; pulse_last_ms now; }这里的防抖思路是时间窗口过滤轮速对应的脉冲间隔远大于 3ms而接点抖动、电磁毛刺的时间通常远小于 3ms。用一个 50ms 定时器的累计值来当作毫秒时间戳不需要另外加延时程序也不阻塞中断响应。要注意pulse_last_ms也放在中断里更新这保证了计数点与时间基准同步。如果把这个判断放到主循环里查高速旋转时主循环可能还没来得及处理计数就已经丢失了。真实轮速下脉冲频率上限可以这样估算车轮周长 2m最高车速 120km/h 即每秒 33.3m对应每秒约 17 个脉冲即便多装几个磁钢外部中断也远远扛得住。51 单片机的瓶颈不在这里反而在低速时所以大力度优化中断函数意义不大把精力放在防抖和换算才更值。4.3 计价状态机起步价、超程单价与等待时间状态机的实现用一个enum加一个switch就足够了enum { IDLE, RUNNING, ARRIVED } state; unsigned long total_money_cents; // 金额单位分 unsigned long total_dist_mm; // 本次行程累计里程单位mm unsigned int wait_seconds; // 本次行程累计等待秒数 // 参数 #define START_PRICE_CENTS 800 // 起步价 8.00 元 #define START_DIST_MM 2000 // 起步里程 2km #define PER_STEP_DIST_MM 500 // 每跳 500m #define PER_STEP_PRICE 100 // 每跳加 1.00 元 #define WAIT_PER_MIN 100 // 等待 1 分钟加 1.00 元 void state_machine(void) { static unsigned long last_total; static unsigned long last_wait_point; switch (state) { case IDLE: total_money_cents 0; total_dist_mm 0; wait_seconds 0; last_total 0; last_wait_point 0; break; case RUNNING: // 里程累加由主循环周期计算这里只做跳表判断 if (total_dist_mm - last_total PER_STEP_DIST_MM) { if (total_dist_mm START_DIST_MM) { total_money_cents PER_STEP_PRICE; } last_total total_dist_mm; } // 等待计费整个1分钟窗口内没有新脉冲 if (pulse_total last_wait_point) { wait_seconds; if (wait_seconds 60) { wait_seconds - 60; total_money_cents WAIT_PER_MIN; } } else { wait_seconds 0; last_wait_point pulse_total; } break; case ARRIVED: // 显示最终金额等待司机按下“清零”回到 IDLE break; } }两个细节在真实场景里很值得保留。跳表判断用的是total_dist_mm - last_total而不是直接比较total_dist_mm这防止了起步价边界附近反复加跳等待计费则用一个“整分钟没有脉冲”的窗口来判断比用低俗速度阈值更简单也更贴近实际计价逻辑。如果现场要求“车速低于 12km/h 才算等待”只需要把“没有脉冲”换成“每秒脉冲数低于 2”就能扩展不会推翻现有状态机架构。另一个常见错误是自动跳表时没有处理上电瞬间的last_total 0。解决办法是把last_total设为静态变量在IDLE状态下随时置零这样每次载客从同一个起点开始不会把上一次的里程带到这一次的跳表判断里。4.4 在 LCD1602 上显示里程、单价与金额LCD 显示在每个计价周期结束时刷新一次即可不需要在中断里操作。4 位模式初始化需要按固定顺序发三次0x03、再发0x02进入 4 位模式然后设置显示方向和光标。关键的初始化代码是这样void Lcd1620_Init(void) { Lcd_Write_Cmd(0x03); delay_us(5000); Lcd_Write_Cmd(0x03); delay_us(1000); Lcd_Write_Cmd(0x03); delay_us(1000); Lcd_Write_Cmd(0x02); delay_us(1000); // 切换为4位总线 Lcd_Write_Cmd(0x28); // 4线、两行、5x7 Lcd_Write_Cmd(0x0C); // 开显示、关光标 Lcd_Write_Cmd(0x06); // 写入后地址加一 Lcd_Write_Cmd(0x01); // 清屏 }显示内容按两行排布第一行显示“里程 00.0km”第二行显示“金额 8.00元”。由于数据是整数运算格式化时把整数除法和取模分别拿到整数部分和小数部分不要调用sprintf去用浮点转换那样代码体积会暴涨。void Lcd_Show_Run_Info(void) { unsigned int km_int, km_dec; km_int total_dist_mm / 1000; km_dec (total_dist_mm % 1000) / 100; // 显示一位小数 Lcd_Set_Cursor(0, 0); Lcd_Print_Str(Dist ); Lcd_Print_Char(0 km_int/10); Lcd_Print_Char(0 km_int%10); Lcd_Print_Char(.); Lcd_Print_Char(0 km_dec); Lcd_Print_Str(km); Lcd_Set_Cursor(0, 1); Lcd_Print_Str(Fee ); Lcd_Print_Num(total_money_cents / 100); // 元 Lcd_Print_Char(.); Lcd_Print_Num((total_money_cents % 100) / 10); // 角 Lcd_Print_Num(total_money_cents % 10); // 分 Lcd_Print_Str(yuan); }这套显示函数配合状态机时要注意在ARRIVED状态不要再实时刷新第二行的金额否则司机还没看清单价就被下一次跳表覆盖了。ARRIVED下的显示应该固定“金额 xx.x 元”不变只等下一次载客时清零。5. 校准实测与仿真排错让里程表和时钟都可信5.1 Proteus 仿真里最容易忽略的三个点用 Proteus 调试时观察目标不要只盯着 LCD 上有没有数而是拆成数据源、处理、输出三层分别验证。第一信号源用DPULSE或外加一个方波发生器频率从 1Hz 慢慢调高到 20Hz看外部中断的计数是否跟随第二定时器中断是否准确直接在中断里翻转一个引脚接示波器既可以看到周期也能确认进中断的频率第三LCD1602 的 V0 电位器电平是否在 1V 到 3V 之间仿真中对比度不对时屏幕的方块格会非常明显。跳表逻辑在仿真里可以用加速数据验证把等待计时和里程跳表参数临时缩小十倍避免在实际运行完时间和长度上都等待太久。验证完成后把参数恢复原值这种临时调参的方法比设计一大堆复杂的测试点要实用许多。5.2 里程系数现场校准的办法烙铁焊完实物后不要以为公式算出来的每脉冲 2.000 米就能直接用。轮胎气压不同、磁钢位置磨损、轮毂打滑都会影响实际行驶距离。常见做法是找一段已知 500 米的路面推着车走一个完全来回读取实际累计脉冲数然后反推real_per_pulse_mm (500000) / (pulse_count/2); // 注意来回除2把这个值写回 EEPROM作为修正系数。后续运行时不用改程序修改存储区的参数即可。软件里单位精度不高、放大倍数不足时修正以后误差仍然存在。所以强烈建议里程内部单位统一为 mm外部系数能精确到小数点后一位时总里程误差在几百米级别的长运营里就会被积累出来。校准前先检查晶振是否准否则后期的采样时间基准就是错的改轮径系数也救不回来。5.3 时钟漂移的影响与补偿方向51 单片机内部定时器的计时误差来源主要是晶振负载电容偏差和环境温度变化一天下来漂移几秒到几十秒都有可能。计价器的等待费按分钟计这种漂移在长时间运营里会让金额出现可感知的偏差。工程上常用的处理办法是加 DS1302 或 PCF8563 外部 RTC但很多课程设计不允许额外芯片。折中方案是在外部中断 1 上接一个 32768Hz 晶振和 CD4060 分频让秒基准不依赖内部定时器或者用 50Hz 市电过零信号做秒基准。这两种方案都会增加硬件复杂度算上 PCB 面积和成本我更推荐直接换带内置 RTC 的单片机比用软件补偿可靠得多。如果受限于现状只能用内部定时器那就把补偿值做成一个可配置项例如每 24 小时偏差 N 毫秒由司机或维修人员在调试菜单里修改单片机每秒自动调整中断重装值。这个思路虽然不是真正的无源校准但在工程现场足够实用也方便做验收测试。5.4 一组可复用的测试用例验收计价器时不要只看“数字会跳”要有明确判定标准。下面这套案例覆盖主干路径和边界条件实测时可直接套用。序号测试场景输入设置预期结果1空车状态不跳表按下载客键前给 100 个脉冲金额保持 0.00 元2起步价内不计超程载客后累计 1.5km显示起步价 8.00 元3超程跳表载客后累计 2.5km金额变为 9.00 元4等待计费载客后 1 分钟无脉冲金额增加 1.00 元5掉电保存载客 1km 后断电重新上电金额和里程恢复为断电前值6脉冲抖动连续输入 1kHz 毛刺 1 秒里程增加不超过 1 个脉冲对应值最后一行的抖动测试正是硬件 RC 滤波和软件时间窗口共同作用的结果。把这一组用例全部跑通计价器的核心逻辑才算真正可信剩下要做的就是把单价、夜间加价这类参数改成实际应用的版本代码结构和状态机都不需要再动。本文还有配套的精品资源点击获取