简介基于51单片机的出租车计价器课程设计资料包面向电子工程、嵌入式方向的初学者及高校学生解决课程设计与实训中从硬件搭建到计费逻辑实现的完整流程问题。资源共46个文件压缩包约1.62MB以C语言源码、Keil工程文件、Proteus仿真工程、原理图及hex烧录文件为主另有obj、lst等编译中间文件与调试记录覆盖开发、仿真、烧录各环节。程序模块包含按键输入、LCD显示、EEPROM参数存储、DS1302时钟驱动等计费规则与中断逻辑清晰可读原理图详细展示了单片机、传感器、显示模块之间的连接便于对照PCB布局或实物调试。已有1505人学习下载。通过该案例可掌握51单片机外设驱动、定时器/计数器应用及Proteus虚拟仿真排错技巧适合作为课程设计参考或嵌入式入门实践项目。1. 出租车计价器课程设计51单片机必做项目的完整资源与落地思路51单片机的课程设计选题里出租车计价器几乎和电子钟、蓝牙小车并列前三它把外中断计数、定时器时基、按键消抖、状态机切换和液晶显示全部压进一个巴掌大的系统做完这一套MCU入门阶段的核心外设基本就通了大半。这份基于51单片机的出租车计价器设计资源打包了Keil源码工程、Proteus仿真图、电路原理图和设计报告适合正在赶课程设计的学生也适合想完整复现一遍“脉冲计数→里程计算→费用显示”这条链路、顺带补一补原理细节的自学者。它要回答的问题很具体里程怎么数得准费用怎么算得清按键和显示怎么配合不打架。2. 硬件方案选型与引脚规划计价器骨架怎么搭才不返工2.1 功能需求拆解计价器到底要算哪几笔账出租车计价器看着简单真把需求拆开至少要处理六个面板起步价、超起步里程单价、等待费、单程往返加价、单价调整和复位。课程设计里最常见的计价规则是“起步价8元含3公里超出后每公里2.4元等待每5分钟折算1公里”具体参数如下表。计费项目常见设定说明起步价8.00元含前3公里超起步里程2.40元/公里超过3公里后按里程累加等待费每5分钟折算1公里车速过低或按下暂停键时累计单程/往返可选超出某里程后单价上浮课程设计可不做单价调整长按设置键进入通过按键步进修改存储到EEPROM这一步别急着写代码。把计费规则先画成“里程在哪个区间、费用按什么公式涨、等待状态是否叠加”后面写状态机才不至于东补一块西补一块。课程设计评分通常盯着三处里程累计准不准、金额计算对不对、按键操作稳不稳规则表就是这三条评分的依据。2.2 核心器件选型用STC89C52加霍尔传感器的理由主控选STC89C52或AT89S52就够了理由很直白8KB Flash能装下完整的计价程序还剩富余两个外部中断INT0/INT1一个数脉冲、一个留作备用三个定时器里拿一个做50ms时基、一个做脉冲测速资源完全不紧张。这类资源包里默认就是这个级别的芯片换更强的单片机反而要改引脚和初始化得不偿失。里程传感器常见三种方案霍尔开关、干簧管和光电对管。霍尔元件典型型号A3144是无触点输出抗震动、响应快磁钢贴车轮每转一圈输出一个脉冲这是课程设计里最稳的做法干簧管便宜但颠簸路段容易误触发光电对管在户外强光下需要遮光处理。这套资源按霍尔传感器来写程序仿真里用脉冲发生器或手动开关模拟霍尔输出实物移植时直接对号入座。显示部分的选择会影响整个程序结构。LCD1602能直接显示“3.25KM 8.60RMB”这种带单位的字符演示效果好、报告截图直观缺点是占用的I/O多数码管动态扫描更贴近真车计价器但消隐处理和位选时序会多出一堆坑。这套资源用LCD1602做主显示数码管版本留作扩展后面我会讲两者的切换思路。2.3 电路连接与引脚分配每根线都要有明确用途引脚分配直接决定程序里寄存器操作怎么写下面这张表是这套设计里最常用的接线方式和资源包里的原理图能对上。信号接MCU引脚电平逻辑备注霍尔脉冲输出P3.2 / INT0下降沿触发需要10k上拉电阻启动/停止键P1.0按下为低同一个键按状态切换暂停/等待键P1.1按下为低和启动键互斥处理功能/设置键P1.2按下为低长按进入单价调整LCD1602 RSP2.5高数据低命令接单片机普通I/OLCD1602 RWP2.6接地只写即可省一个控制位LCD1602 ENP2.7下降沿锁存使能信号LCD1602 D0-D7P0.0-P0.7数据总线必须接上拉排阻两个容易翻车的细节在这里提前说P0口是开漏输出不加上拉电阻LCD数据线电平就会飘仿真里可能看不出问题实物一定花屏霍尔传感器的输出端也要接10k上拉到VCC否则脉冲高电平不确定INT0下降沿触发会漏记或者乱记。复位电路用经典的10uF电容加10k电阻上电延时约100ms足够让单片机稳定启动。2.4 脉冲当量计算计价准不准全看这一处里程计费精度的源头是“一个脉冲对应多少米”这个参数算错后面所有显示和金额都是白搭。常见做法是设车轮半径0.31米周长约1.95米每圈一个磁钢对应一个脉冲那么一个脉冲就是约1.95米。课程设计演示时为了好算经常简化成1脉冲等于0.1米或0.125米仿真里点几下开关就能看到里程明显变化也方便核对。// 脉冲当量换算轮胎周长除以每圈脉冲数 #define TIRE_RADIUS_M 0.31f // 车轮半径单位米 #define TIRE_PERIMETER_M (6.28318f * TIRE_RADIUS_M) // 周长 #define PULSE_PER_TURN 1 // 每圈脉冲数磁钢个数 #define PULSE_TO_METER (TIRE_PERIMETER_M / PULSE_PER_TURN)参数说明TIRE_RADIUS_M按实际轮胎标注修改PULSE_PER_TURN是车轮转一圈霍尔输出的脉冲个数磁钢贴得多就填几。实物标定更准的办法是推车走一段已知距离数出总脉冲数再反推单个脉冲当量这比查轮胎规格更可靠因为胎压和负重都会改变实际周长。整个程序中只有这一处宏定义控制里程精度后期标定只改它别在多个文件里散着写硬编码。3. 软件核心实现里程计数、计价算法与显示刷新3.1 用INT0外部中断做里程计数为什么不用查询方式里程脉冲是异步事件行驶中霍尔输出的频率可能到几百赫兹甚至更高如果用主循环轮询查询电平变化程序在显示刷新或按键扫描时就会丢脉冲。外部中断方案把计数放到硬件级触发里每个下降沿自动进中断丢失概率大幅下降这是所有计价器程序的共识。// INT0外部中断初始化下降沿触发 void int0_init(void) { IT0 1; // 1下降沿触发0低电平触发 EX0 1; // 使能INT0中断 EA 1; // 开总中断 } volatile unsigned long pulse_count 0; // 里程脉冲累计 // 中断服务函数只做计数不做其他运算 void int0_isr(void) interrupt 0 { pulse_count; }逻辑说明IT01把INT0配置为下降沿触发霍尔输出从高到低的那一瞬间进入中断ISR里只做pulse_count不计算里程不刷新显示这样中断服务时间极短主循环不会被拖累。参数说明pulse_count用volatile unsigned long因为它在中断和主循环之间共享不加volatile的话编译器可能把它优化进寄存器导致主循环读到的不是最新值。32位无符号数可以累计到几十亿个脉冲对课程设计场景完全不会溢出。3.2 定时器0搭建50ms时基时间基准怎么算才不偏计价器里所有跟时间相关的功能——等待费计时、开机时间、显示刷新频率——都靠一个稳定的时基。最常见做法是让定时器0工作在模式116位计数每50ms中断一次中断里累计tick_50ms再分频出秒和分钟。void timer0_init(void) { TMOD 0x01; // 定时器0模式116位定时 TH0 0x3C; // 12MHz晶振下50ms初值的高8位 TL0 0xB0; // 低8位 ET0 1; // 使能定时器0中断 TR0 1; // 启动定时器 } void timer0_isr(void) interrupt 1 { TH0 0x3C; // 重装初值保持50ms周期 TL0 0xB0; tick_50ms; if (tick_50ms 20) { tick_50ms 0; second_count; } }参数说明12MHz晶振的机器周期是1us12分频50ms需要计数50000次定时器初值等于65536减去50000即15536换算成十六进制是0x3CB0所以TH00x3C、TL00xB0。如果换成11.0592MHz晶振机器周期约1.085us同一初值走出来的时间会偏短必须重算。这也是很多模板宁可用12MHz的原因——串口通信用不到的时候12MHz的定时器初值最好算。3.3 计价状态机待机、运行、等待、结束四态怎么切计价器不能是“一个主循环从头跑到底”的写法必须用状态机把不同工况隔开待机时只显示单价运行时里程和金额联动等待时只累计等待时间并把每5分钟折算成1公里费用结束时冻结金额等复位。每个状态下按键的含义也不同这就需要一个清晰的状态转移表。typedef enum { STATE_IDLE, // 待机显示单价等待启动 STATE_RUN, // 运行里程累计超起步价计算 STATE_WAIT, // 等待等待时间累计每5分钟按1公里计 STATE_STOP // 结束金额冻结等待复位 } MeterState; MeterState meter_state STATE_IDLE; void meter_proc(unsigned char key) { switch (meter_state) { case STATE_IDLE: if (key KEY_START) meter_state STATE_RUN; break; case STATE_RUN: if (key KEY_WAIT) meter_state STATE_WAIT; else if (key KEY_STOP) meter_state STATE_STOP; break; case STATE_WAIT: if (key KEY_WAIT) meter_state STATE_RUN; break; case STATE_STOP: if (key KEY_RESET) { pulse_count 0; // 清零里程脉冲 distance_m 0; // 清零累计里程 total_fee_c 0; // 清零金额 meter_state STATE_IDLE; } break; } }逻辑说明状态机的好处是每个按键在每个状态下只有唯一含义比如KEY_START在IDLE态是启动在RUN态就会被忽略或定义为别的功能不会出现“按一下复位结果金额还在涨”的混乱现场。参数说明复位操作在STOP态内完成清掉三个累计量再回到IDLE而不是直接跳转这样确保金额冻结期间乘客看到的数字不会被意外改动。3.4 费用计算用整数“分”运算避开浮点的坑Keil C51的默认int是16位最大值32767如果用“元”做单位8.00和2.40这种小数直接算浮点会引入库函数开销还容易在乘除过程中溢出。常见做法是全部用“分”做最小单位8.00元存成8002.4元存成240显示的时候再除以100拆出元和角分。#define BASE_DIST_M 3000 // 起步里程3公里单位米 #define BASE_FEE 800 // 起步价8.00元单位分 #define PRICE_PER_KM 240 // 每公里2.4元单位分 unsigned long distance_m 0; unsigned int total_fee_c 0; void fee_update(void) { if (distance_m BASE_DIST_M) { total_fee_c BASE_FEE; // 起步价内不额外收费 } else { // 超出部分按每公里单价累加结果仍以分为单位 total_fee_c BASE_FEE (unsigned long)(distance_m - BASE_DIST_M) * PRICE_PER_KM / 1000; } }逻辑说明先判断里程是否在起步区间内是就直接给起步价否则用超出的米数乘以单价再除以1000把米换算成公里结果加到起步价上。参数说明(distance_m - BASE_DIST_M) * PRICE_PER_KM这一句先乘后除中间结果可能超过65535所以前面加了(unsigned long)强转防止16位溢出最终结果total_fee_c用unsigned int能存到65535分即655元一趟出租车行程绝对够用。3.5 LCD1602显示刷新主循环里分配好刷新节奏显示刷新不能放在中断里否则会拖慢脉冲计数和定时精度。常规做法是在主循环里用tick_50ms做个简单的分频大约每100ms刷新一次液晶既能看到里程和金额的变化又不会因为刷新太快让液晶模块反应不过来。void lcd_display_update(void) { char buf[17]; unsigned int km_int distance_m / 1000; // 取整公里 unsigned int km_dec (distance_m % 1000) / 10; // 取小数后两位 sprintf(buf, DIS:%d.%02dKM, km_int, km_dec); lcd_write_string(0, 0, buf); sprintf(buf, FEE:%d.%02dRMB, total_fee_c / 100, total_fee_c % 100); lcd_write_string(1, 0, buf); } void main_loop(void) { unsigned char tick_flag 0; while (1) { key key_scan(); meter_proc(key); fee_update(); // 100ms刷新一次显示避免液晶忙不过来 if (tick_50ms 2) { tick_50ms 0; lcd_display_update(); } } }参数说明sprintf是偷懒但可靠的做法会把浮点和字符串拼接都处理好代价是引入printf类库导致代码体积增加几百字节8KB Flash完全扛得住。如果Flash紧张就改成自己拆数字加字符表定义一个seg[]数组用除法和取模逐位取数字再查表拼字符串代码量更小但可读性差一些。两种方式资源包里一般都有注释建议先跑通sprintf版本再考虑优化。4. Proteus仿真与Keil联调把实物风险前置到电脑里4.1 Proteus仿真电路搭建要点拿到资源包先别急着看代码第一步是在Proteus里把电路图完整搭出来。要点有四个单片机选AT89C52晶振12MHz配两个20pF到30pF的电容P0口接LCD1602数据线同时接一组上拉排阻P3.2接霍尔脉冲源仿真里用开关或者脉冲发生器模拟复位电路用10uF电容加10k电阻上电时保证RST脚有一个足够宽的高电平脉冲。搭电路时最容易漏的是LCD1602的对比度引脚V0仿真里不接电位器可能会导致显示乱码。做法是取一个10k电位器中间抽头接V0两端分别接VCC和GND调节到屏幕上出现清晰的黑块字符。霍尔脉冲模拟部分我一般习惯用Proteus信号发生器里的方波模式频率设到10Hz到50Hz就能模拟车轮低速和快速两种状态比手动点开关更能看出计数稳定性。4.2 Keil工程配置与编译代码工程用Keil打开文件结构通常是main.c放主循环和状态机lcd1602.c放显示驱动i2c.c放24C02读写头文件里定义引脚和参数宏。新建工程时芯片型号选Atmel AT89C52然后在Options for Target的Output选项卡里勾选Create HEX File编译后才会在工程目录生成要烧进Proteus的.hex文件。编译时如果报error C141: syntax error先看是不是少写了分号或者花括号不匹配报error C267: identifier expected多半是变量名打错了或者头文件没包含。这类课程设计工程最常见的问题是main.c里重复定义了pulse_count——它在int0_isr和自己写的初始化函数里各定义了一次链接时报L104: MULTIPLE PUBLIC DEFINITIONS。遇到这个错误把变量声明放进头文件用extern导出只在main.c里定义一次。4.3 加载HEX文件并验证完整功能流程双击Proteus里的单片机在弹出的编辑窗口中Program File一栏选择刚编译出的hex文件然后点运行。验证顺序建议按下面的表走每一条都对应一个功能模块哪个环节不对就回看那个模块的代码。操作步骤预期结果按下启动键液晶进入运行状态里程显示开始可变化给P3.2施加10个脉冲里程增加10×脉冲当量里程低于3公里金额始终显示8.00里程超过3公里金额按超出里程×单价累加按下暂停键进入等待状态里程不涨等待时间累计等待满5分钟金额额外增加1公里费用按下停止键金额冻结不变按下复位键里程、金额全部清零回到待机这条验证流程就是课程设计答辩时的演示脚本。如果某一步不通过优先检查两个地方一是脉冲当量宏定义是否和你的仿真脉冲频率匹配二是meter_proc里按键状态编码是否和key_scan返回值一致。资源包里的代码通常按这个流程调好换到自己的工程时先跑一遍确认再改参数。4.4 仿真和实物的差异清单仿真跑通不等于实物一定能跑这个坑几乎所有做过课设的人都踩过。仿真里没有按键抖动、没有电源毛刺、没有电磁干扰霍尔信号干净得像教科书而实物这三样全都有。差异项Proteus仿真实物电路按键输入无抖动按下即稳定电平金属触点抖动明显必须软件消抖晶振起振无需负载电容也能跑必须配12-30pF负载电容上电复位电平干净很少异常电源上冲可能引起误复位要加复位电路霍尔信号方波理想边沿有毛刺和缓慢的边沿变化需要施密特整形显示模块接上就亮对比度电位器、上拉电阻少一个都不行我一般会在仿真通过后再花十分钟挨个检查这几个差异点按键扫描有没有消抖、霍尔输入端有没有上拉和滤波电容、LCD的V0有没有可调电阻。把这四条在原理图画板之前落实实物焊接翻车的概率能少一半。5. 计价器避坑指南五个高频翻车点与排查方法5.1 按键抖动导致状态机乱跳现象按下启动键一次状态在IDLE和RUN之间来回切换按复位键时金额有时清零有时不清零。原因机械按键的金属触点按下瞬间会产生几毫秒到十几毫秒的抖动电平在高低之间反复跳变单片机在极短时间内识别到多次边沿把它当成多次按键输入。解决按键扫描函数里加10ms延时消抖并且等待松手后再返回键值避免一次长按被重复触发。unsigned char key_scan(void) { unsigned char key_val KEY_NONE; if ((P1 0x07) 0x07) return KEY_NONE; // 无按键按下 delay_ms(10); // 10ms软件消抖 if ((P1 0x01) 0) key_val KEY_START; else if ((P1 0x02) 0) key_val KEY_WAIT; else if ((P1 0x04) 0) key_val KEY_RESET; while ((P1 0x07) ! 0x07); // 等待松手 return key_val; }逻辑说明先读一次P1判断有没有键按下有就延时10ms再读一次两次都确认是低电平才算有效最后的while循环把按键按住期间的所有重复触发全部屏蔽掉只有松手后函数才返回。参数说明10ms是经验值机械按键的抖动通常不超过10ms调太短消不掉抖动调太长响应变肉实际调试时可以用示波器看按键波形再定。5.2 定时器计时和真实时间对不上现象程序里等了60秒秒表实测只有57秒或者多了5秒等待费的5分钟折算明显偏短或偏长。原因定时器0中断服务函数里除了重装初值还做了其他操作中断响应本身需要几个机器周期累加多了时间就跑快仿真时在中断里打断点停在ISR中时间失真更严重。解决先确认晶振频率和初值匹配再用示波器量一个I/O口的翻转周期做实际校准。void timer0_isr(void) interrupt 1 { TH0 0x3C; // 重装初值 TL0 0xB0; tick_50ms; if (tick_50ms 20) { tick_50ms 0; second_count; P2_0 ~P2_0; // 翻转一个测试引脚用示波器量周期 } }参数说明把P2.0在每秒处翻转一次示波器量出来的周期应该是精确的2秒如果偏了就微调TH0和TL0。比如实测周期偏长说明定时器走得慢可以适当减小初值让每次中断提前一点反之则增大初值。这个校准方法是排查时间类问题的通用手段做电子钟、交通灯课设一样能用。5.3 里程多计数脉冲信号抖动造成虚增现象手在霍尔信号线附近晃一下里程就增加了好几米仿真里手动点一次开关计数加了两次。原因霍尔输出端没有滤波信号边沿有毛刺仿真里用普通开关模拟脉冲时开关本身也有抖动INT0下降沿触发会把抖动沿也算进去。解决在软件里加最短间隔滤波记录上一次脉冲进入中断的时间片间隔不足一定值的脉冲直接丢弃。unsigned char last_pulse_tick 0; void int0_isr(void) interrupt 0 { unsigned char now (unsigned char)(second_count * 20 tick_50ms); // 相邻两个有效脉冲间隔必须大于2个时间片(约100ms) if ((unsigned char)(now - last_pulse_tick) 2) { pulse_count; last_pulse_tick now; } }逻辑说明用一个8位的时间片计数器记录上次有效脉冲的时刻下次中断进来时算差值小于2就认为是抖动直接丢弃。参数说明2个时间片约等于100ms按0.1米一个脉冲算对应的最高可测车速约36km/h课程设计演示够用真车高速场景要把这个阈值调小否则高速时会把真实脉冲误丢。5.4 LCD1602不显示或花屏现象上电后液晶背光亮但一个字都没有或者屏幕上全是黑色方块乱码。原因三个常见坑——P0口没加上拉电阻数据线高电平驱动不足对比度引脚V0悬空或电压不对字符显示不出来初始化函数里延时不够液晶上电后还没完成内部复位就开始收命令。解决P0接上拉排阻V0接10k电位器分压仿真和实物都要调初始化函数开头加至少50ms延时让液晶模块稳定后再发第一组命令。按这个顺序排查绝大多数花屏问题都能解决。5.5 掉电后单价和累计里程全丢现象设置好2.4元/公里的单价断电再上电单价回到默认值1.0或者变成一个乱数之前跑的里程也没了。原因参数和里程都只存在RAM里51单片机掉电后RAM内容全部丢失。课程设计里最常见的忽略点答辩时老师常问“司机调好的价格断电为什么没了”。解决用24C02这类I2C接口EEPROM保存参数上电时读回并做范围校验读到的值不在合理区间就回退默认值。void param_load(void) { unsigned char val; i2c_read(0xA0, 0x00, val); // 24C02器件地址0xA0读地址0x00 if (val 100 val 500) // 单价合法范围1.0元到5.0元 price_per_km val; else price_per_km 240; // 非法值回退默认2.4元 }逻辑说明先读EEPROM保存的单价值判断是否在100到500分之间是就用不是就回默认240分。参数说明0xA0是24C02写操作的器件地址读操作用0xA1这个范围校验很重要因为EEPROM第一次上电时读回的往往是0xFF不校验的话255分会被当成2.55元单价价格全乱套。6. 从仿真到实用三个进阶技巧与最终验证6.1 用脉冲间隔测车速自动切入等待计费资源包里的基础版本通常靠手动按键进入等待状态但真实出租车是车速低于某个阈值自动开始等待计费。进阶做法是记录相邻两个脉冲的时间间隔间隔越长说明车速越慢连续N秒低于阈值就自动把状态机切到WAIT。// 在50ms时基里检查车速状态 void speed_check(void) { if (pulse_interval_tick 30) // 两个脉冲间隔超过1.5秒说明车速很低 { slow_count; if (slow_count 20) // 持续低速约1秒 { meter_state STATE_WAIT; } } else { slow_count 0; } }参数说明pulse_interval_tick需要在int0中断里更新slow_count在定时器中断里累加阈值按实际车速要求调整。这个逻辑补上之后演示时只要停止给脉冲计价器就能自动进入等待计费比手动按键更像真车。6.2 长按按键进入参数校准模式单价调整如果每次按一下就进设置行驶中容易误触。常见的稳妥做法是在IDLE状态长按功能键超过2秒才进入校准模式校准模式下短按加价、长按减价再按功能键退出并把新单价写入24C02。用状态机扩展一个SETTING子状态不会影响原有四态流程。6.3 数码管版本注意动态扫描消隐如果想把LCD1602换成数码管动态扫描一个必须处理的细节是消隐切换位选时先关闭所有位选再送段码数据最后打开目标位选否则会产生拖影。扫描频率一般设在1kHz到2kHz之间低于500Hz肉眼能看到闪烁高于5kHz占用CPU太多。最终验证建议把前面4.3节的流程完整走一遍再加一条连续跑10分钟每5分钟记录一次里程和金额看等待费折算是否符合规则。从那以后我每次拿到这种51单片机课程设计资源都会强制按“脉冲当量→状态机→显示刷新”的顺序在Proteus里先走一遍再动烙铁硬件反馈和软件逻辑分开验证这是这套计价器设计教给我最实在的一课。希望帮到你。本文还有配套的精品资源点击获取