ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于51单片机的出租车计价器设计:霍尔测速、状态机与Proteus仿真

基于51单片机的出租车计价器设计:霍尔测速、状态机与Proteus仿真 简介基于51单片机的出租车计价器全套开发资料面向电子、自动化等专业学生及单片机初学者适合课程设计、毕业设计或综合实训使用。资源覆盖从原理图设计、程序编写到仿真验证的完整流程功能实现1602液晶显示当前时间、日期、行驶距离、金额与总价支持按键设置时间、起步金额并能查看历史数据同时通过霍尔传感器测量电机转速来模拟车轮脉冲计价逻辑贴近真实出租车。压缩包共181个文件约90.26MB以Keil工程/C源码、原理图与PCB设计、Proteus仿真、论文及答辩文档为主体另含数十张截图和图片方便撰写报告或制作PPT。已有2819人学习下载对于希望快速搭建计价器原型、理解单片机定时/计数与液晶显示应用的人而言是一套上手门槛低、可直接参考的完整方案。1. 计价器难的不是计价是里程和状态的纠缠一台出租车计价器表面看就是把里程乘上单价实际难点在两处一是里程从哪来二是“空车、载客、停车等待”三种状态切换时如何让计费不跳变。霍尔传感器每圈输出一个脉冲车轮周长约两米十几公里路程累计数万个脉冲如果在中断里用浮点累加51单片机的定时器基准会被拖到漂移单价越高累计误差越离谱。下面把C51源码、Proteus仿真和原理图拆开讲从硬件测速到计价状态机最后落到EEPROM掉电保存和多费率扩展适合正在做51单片机课程设计或想搞清楚计价逻辑的人。2. 霍尔传感器测速与1602显示硬件选型与接线2.1 霍尔转速传感器的开漏输出与上拉常见做法是使用A3144或AH3144这类开关型霍尔传感器检测到磁场时输出拉低没有磁场时输出高阻。由于是开漏输出必须外接一个4.7kΩ到10kΩ的上拉电阻否则MCU读不到稳定的高电平。上拉电阻取值不宜太小太大会让脉冲边沿变缓影响外部中断的下降沿触发我一般选4.7kΩ。输出端建议先经过一级RC低通再进单片机I/O典型值为10kΩ电阻串联加0.1μF电容到地可以滤掉轮子振动带来的毛刺。直接接P3.2引脚配置为外部中断0下降沿触发。注意霍尔传感器的VCC按模块要求接5VGND与单片机共地否则信号电平无法对齐。2.1.1 每转一个脉冲转速和里程怎么换算设车轮直径为D厘米每转一圈霍尔输出1个脉冲那么每个脉冲对应的行驶距离为πD。比如直径66cm的轮胎每脉冲对应约2.07米。在代码里为了避开浮点把πD放大100倍用整数保存最后换算成0.1米或0.01千米单位。参数取值说明车轮直径D66 cm实际车胎外径每转脉冲数N1霍尔磁钢固定于轮毂脉冲对应弧长π×D/N约2.07m启动按键P1.0空车→载客等待切换按键P1.1载客途中临时停车停止按键P1.2结束计费设置/菜单按键P1.3进入时间、单价设置如果霍尔装在电机轴上还要乘传动比这里简化处理。转速rpm的计算是每秒脉冲数×60例如每秒产生5个脉冲转速为300rpm。对应车速等于每秒脉冲数×每个脉冲距离×3.6单位km/h。2.2 1602显示模块的8位并口接线1602液晶在本项目里要同时显示时间、日期、行驶距离、当前金额和总价信息量大8位并行模式写起来更直白。RS接P2.0RW接P2.1E接P2.2数据口D0到D7接P0.0到P0.7。P0口内部没有上拉必须外接10k排阻否则高电平驱动能力不足显示会花屏或缺笔画。LCD的V0对比度脚用2k电位器分压到0.5V左右具体以能看到清晰字符为准。背光正极串100Ω限流电阻接5V别直接挂在电源上。Proteus仿真里LM016L模型不带背光不需要处理这个。2.3 为什么时间日期先用软件时钟1602要显示时间和日期很多方案直接上DS1302但课程设计里用定时器做软件时钟更省成本也更容易讲清中断调度。51单片机没有独立RTC只能靠定时器0产生毫秒中断累计秒、分、时、日。缺点是一旦断电时间归零所以我在第5章给出扩展DS1302或EEPROM保存时间断点的思路。如果你准备答辩软时钟可以现场演示按键改时间不用等RTC生效。3. 定时器中断、脉冲计数与计价状态机C51程序实现3.1 定时器0做时基外部中断0做脉冲计数单片机默认用11.0592MHz晶振一方面是为了串口波特率能整除另一方面定时器0做1ms中断时初值接近整数。代码里用宏计算初值#define FOSC 11059200UL // 晶振频率 #define T0_1MS (65536 - FOSC/12/1000) // 12T模式初值取整 volatile unsigned long pulse_count; // 外部中断累计脉冲 volatile unsigned long last_pulse_count; // 上一秒快照 volatile unsigned long pulse_delta; // 这一秒的脉冲增加量 volatile unsigned long total_pulse; // 总脉冲数 volatile unsigned char ms_count 0; volatile unsigned char sec_flag 0; void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0方式116位 TH0 T0_1MS 8; TL0 T0_1MS 0xFF; ET0 1; EA 1; TR0 1; } void Int0_Init(void) { IT0 1; // 下降沿触发 EX0 1; PX0 1; // 高优先级避免丢失脉冲 } void Int0_ISR(void) interrupt 0 { pulse_count; // 只做自增不做其他运算 } void Timer0_ISR(void) interrupt 1 { TH0 T0_1MS 8; TL0 T0_1MS 0xFF; if (ms_count 1000) { ms_count 0; EA 0; // 关闭中断保护32位读取 unsigned long snapshot pulse_count; EA 1; pulse_delta snapshot - last_pulse_count; last_pulse_count snapshot; total_pulse pulse_delta; sec_flag 1; } }这里没有在外部中断里把pulse_count清零而是用“差值法”计算每秒脉冲增量。原因在于外部中断0的优先级高于定时器0如果定时器中断里执行total_pulse pulse_count; pulse_count 0;此时又来一个霍尔脉冲外部中断会打断定时器中断脉冲自增后再回到定时器中断刚才的total_pulse已经把旧值取走随后的pulse_count 0就会把新到的脉冲一并清掉造成丢脉冲。差值法只依赖两次快照的差即便外部中断打断定时器pulse_count在快照前后只会增加不会减少差值天然不丢。32位长整型读取前关EA保护防止多字节读取被外部中断打散。初值计算中FOSC/12/1000在整型运算里等于921实际上不是严格的1ms而是1.0008ms左右累计误差约0.08%一天差7秒课程演示完全够用。如果换成12MHz晶振初值要重新算这里不展开。3.2 里程、金额的整数定点运算里程累计不能按秒去乘浮点数否则每秒钟5个脉冲算出的2.07米反复浮点累加单片机会被拖慢。把每个脉冲的距离放大100倍#define PI_100 314 // 3.14159放大100倍 #define WHEEL_D_CM 66UL // 车轮直径66cm #define DIST_PULSE_MM (PI_100 * WHEEL_D_CM) // 每脉冲对应距离*10 #define DIST_PULSE_M_X100 (DIST_PULSE_MM / 100) // 每脉冲0.01米即厘米单位解释一下PI_100 * WHEEL_D_CM得到的是πD×100单位cm乘以100还是cm除以100后变成mm这段注释容易混淆。这里重新定义直接让每个脉冲的距离以0.01米为单位每脉冲距离0.01米 πD(cm) / 100 若D66cmπD207cm除以100得到2.07即每脉冲2.07个0.01米对应207。 整数表示为 20700 / 1000但不如直接用毫米单位。实际更好的做法是#define DIST_PULSE_0_01M ((PI_100 * WHEEL_D_CM 50) / 100) // 0.01m为单位的每脉冲距离66cm直径下约等于207即每个脉冲0.2米多一点。总里程0.01mtotal_pulse * DIST_PULSE_0_01M当total_pulse超过4万时容易超出16位所以要声明为unsigned long。金额计算同样用“分”做单位unsigned long StartPrice_Fen 300; // 起步价3元 unsigned int StartKm 3; // 起步里程3公里 unsigned long UnitPrice_FenPerKm 150; // 每公里1.5元 unsigned long WaitPrice_FenPerMin 50; // 每分钟等待0.5元 unsigned long calc_fare(unsigned long distance_0_01m, unsigned char wait_minutes, unsigned char hour) { unsigned long fare StartPrice_Fen; unsigned long distance_km_x100 distance_0_01m / 1000; if (distance_km_x100 StartKm * 100) { fare (distance_km_x100 - StartKm * 100) * UnitPrice_FenPerKm / 100; } fare (unsigned long)wait_minutes * WaitPrice_FenPerMin; if (hour 23 || hour 5) { fare fare * 120 / 100; // 夜间附加20% } return fare; }distance_0_01m除以1000变成0.1公里再乘100变成0.01公里单独的运算很容易丢精度。建议在源码里统一保存“0.01米”和“0.01元”两种单位只在显示时换算。上面计算里distance_km_x100实际单位是0.01公里数值上是公里×100所以起步里程3公里要写成StartKm * 100差值再乘单价除以100这样能把超过起步里程的金额一次性算出来。这个细节是答辩老师最喜欢追问的点。3.3 计价状态机与按键复用计价器逻辑需要三个运行状态空车IDLE、载客RUNNING、停车等待WAIT。停车等待和正常行驶的费率不同所以不能只在总里程上做文章。typedef enum { STATE_IDLE 0, STATE_RUNNING, STATE_WAIT_M } VS_State; volatile VS_State cur_state STATE_IDLE; volatile unsigned char wait_minutes 0; volatile unsigned char run_minutes 0; void state_machine_run(void) { unsigned char key scan_key(); switch (cur_state) { case STATE_IDLE: if (key KEY_START) { cur_state STATE_RUNNING; clear_run_data(); } break; case STATE_RUNNING: if (key KEY_WAIT) { cur_state STATE_WAIT_M; wait_minutes 0; } else if (key KEY_STOP) { save_history(); cur_state STATE_IDLE; } break; case STATE_WAIT_M: if (sec_flag) { if (wait_sec 60) { wait_sec 0; wait_minutes; } } if (key KEY_RUN) cur_state STATE_RUNNING; break; } }按键扫描函数scan_key()里要做消抖消抖时不建议用长延时阻塞因为定时器中断照样在跑但LCD刷新会被卡住。常见做法是每10ms采样一次连续两次结果相同才确认有效返回值在松开后更新。长按菜单复用需要区分短按和长按可以在扫描到按下时启动一个累加器超过1秒仍按住就进入设置菜单松开时如果不满足长按就当作普通键处理这样四个按键就能完成日期、时间、起步金额设置。4. Proteus仿真跑通与常见信号完整性坑4.1 用脉冲源代替霍尔传感器做仿真Proteus里没有真实的轮速传感器直接用信号发生器的数字脉冲接到单片机P3.2。放置一个DSOURCE或IPULSE设置脉冲频率为5Hz对应每秒5个脉冲车速大约36km/h。注意脉冲源的幅度要设为0到5V不是TTL默认否则电压太大可能仿真报错。LM016L液晶的RW引脚如果直接接GND读写时序只用写命令能省一个I/O。程序里如果判断忙标志必须把RW接回P2.1并按常规时序操作。初始化时4线模式和8线模式都有对应延时建议用8线模式仿真逻辑分析仪观察起来更直观。4.2 定时器初值精度和中断优先级较量仿真时容易遇到两个现象一是1602显示正常但里程偏快二是按键设时间时LCD字符乱跳。前者多半是初值计算时用了12MHz又跑到11.0592的开发板上导致秒基准偏大或偏小后者是按键扫描在主循环里用长延时消抖把LCD刷新的节拍完全打乱。把按键扫描挪进定时器中断里用状态位通知主循环执行能明显改善花屏。外部中断0的优先级设置很重要。如果霍尔脉冲频率高于定时器中断频率例如电阻小车实验里电机转速很高每秒几十个脉冲外部中断处理短还能接受。但注意C51里interrupt 0对应外部中断0interrupt 1对应定时器0不要在中断号上写错。4.3 仿真和实物的信号完整性差异Proteus对引脚电平是理想化的实际电路霍尔传感器连线过长时会引入几十毫伏的噪声如果直接进P3.2一次轮子振动可能被识别成一串脉冲。硬件上除了加RC滤波软件还可以在外部中断里记录脉冲时刻两次脉冲间隔小于5ms就忽略。下面给出一段防抖计数unsigned long last_int_time 0; unsigned char int_debounce_flag 0; void Int0_ISR(void) interrupt 0 { unsigned long now ms_counter; // 系统毫秒计数器 if (now - last_int_time 5) { pulse_count; last_int_time now; } }ms_counter是定时器0中断里自增的32位毫秒值外部中断通过它做时间窗判断。这个技巧在霍尔转速传感器、光电编码器上都适用。仿真时如果脉冲源频率太高人的肉眼跟不上可以在P3.2加虚拟示波器观察波形用逻辑分析仪数一数1秒内脉冲个数再和1602显示的里程换算值对照。故障现象可能原因排查方法1602显示全黑或乱字符对比度电压不对调节2k电位器使V0为0.3-0.6V1602只显示不刷新忙标志检测时序错误直接改用延时方案或检查RW接线里程明显偏大霍尔毛刺被多次计数用示波器观察P3.2波形加迟滞滤波公里金额跳变严重长整型溢出检查total_pulse是否超过65535按键设置时间无效主循环被阻塞把延时消抖改为定时器轮询扫描5. 掉电保存历史数据与多费率扩展EEPROM和按键复用技巧仿真里每次关闭电源里程和总价都会归零实际计价器要求断电后至少保留最近几次运营记录。最简单可靠的方案是使用AT24C02作为外部EEPROMI2C时序用普通I/O模拟不需要占用串口。下面是写一个字节到AT24C02的常见函数注意START、STOP条件和应答位检查sbit I2C_SCL P3^6; sbit I2C_SDA P3^7; void I2C_Start(void) { SDA 1; SCL 1; _nop_(); SDA 0; _nop_(); SCL 0; } void I2C_Stop(void) { SDA 0; SCL 1; _nop_(); SDA 1; } bit I2C_WriteByte(unsigned char dat) { unsigned char i; for(i 0; i 8; i) { SCL 0; SDA (dat 0x80) ? 1 : 0; dat 1; _nop_(); SCL 1; } SCL 0; SDA 1; // 释放总线 _nop_(); return SDA; // 读从机应答 } unsigned char EEPROM_Write(unsigned char addr, unsigned char dat) { I2C_Start(); if(I2C_WriteByte(0xA0)) return 0; if(I2C_WriteByte(addr)) return 0; if(I2C_WriteByte(dat)) return 0; I2C_Stop(); delay_ms(10); // 写周期 return 1; }按键复用技巧可以这样实现设置菜单多用长按“设置”键进入短按只能切换空车/载客/停止。进入历史数据查看时短按“加”键循环显示最近十次总价和日期保存时把EEPEOM地址按当前记录号取模写到文件末尾读取时逆序拿出来。这样一台上位机打印出来的记录不需要额外芯片就能在答辩现场演示。夜间费率扩展则直接复用第3章的hour参数在晚上23点到次日5点让结算金额上浮20%这个逻辑还能继续挂到DS1302实时时钟上把软件时钟替换成RTC后掉电也能保持日期时间整个项目就从课设变成能落地的工程原型。本文还有配套的精品资源点击获取
返回列表