ARTICLE DETAIL

资讯详情

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

51单片机停车场计费系统设计与Proteus仿真(DS1302+AT24C02)

51单片机停车场计费系统设计与Proteus仿真(DS1302+AT24C02) 简介一套基于51单片机与Protues仿真的停车场刷卡计时计费系统设计资源面向单片机课程设计、毕业设计及嵌入式入门学习者完整演示了车辆刷卡进场、出场自动计费结算、时间校准单价设置、车位数量配置及掉电数据保存等核心流程。资源包共47个文件、约1.48MB除C语言源码.c/.h与可烧录HEX文件外还提供Protues仿真工程.DSN/.DBK、AD原理图.SchDoc、流程图.bmp、PDF说明文档与元件清单.xls可覆盖电路设计、程序编写、仿真调试全环节。目前已有53人学习下载。该设计功能完整且上手友好适合在Proteus中直接运行观察计时计费逻辑也可参照AD原理图和流程图理解DS1302时钟、I2C总线、LCD显示及射频刷卡模块的协同工作方式为二次开发或项目答辩提供坚实支撑。1. 停车场计费最容易翻车的点不是读卡而是掉电停车场计费最容易翻车的点不是读卡而是掉电之后单价和剩余车位全部归零。这套基于51单片机的Protues仿真设计把ds1302时钟芯片、I2C存储器AT24C02和12864液晶组合在一起在PC上就能完整跑通“刷卡进场、计时、结账出场”的流程。源码包里除了main.c、ds1302.c、i2c.c、AMPIRE128X64.c这些主控文件还带着仿真DSN、AD原理图、流程图和元件清单属于拿到手就能直接改的工程包。适合正在做课程设计、毕业设计或者想快速验证一个停车场管理方案的开发者。下面按“时间基准 → 参数持久化 → 计费状态机 → 仿真调试 → 实物落地”的顺序拆开讲。2. ds1302与I2C存储计费系统的“时间基准”和“记忆体”2.1 为什么计费系统必须用独立RTC而不是单片机定时器51单片机内部定时器是RAM里的计数器主电源一断数据全没而且精度依赖于主晶振和指令周期的累加长时间运行误差会累积到分钟级。停车场计费要跨小时、跨天时间基准必须独立存在所以设计里用了ds1302。ds1302是串行实时时钟芯片三线接口CE、SCLK、IO外部挂一个32.768kHz晶振再接一颗3V纽扣电池主电源掉电后RTC仍然走时。代码里ds1302.c负责把这层时序封装成“读时间数组”和“写时间数组”两个函数主程序不直接碰寄存器位操作。// ds1302.c 连续读取7个时间寄存器 void DS1302_ReadTime(unsigned char *t) { unsigned char i; DS1302_CE 1; // 拉高CE启动三线通信 DS1302_WriteByte(0x8E); // 写保护寄存器地址 DS1302_WriteByte(0x00); // 解除写保护允许后续读写 for (i 0; i 7; i) { DS1302_WriteByte(0x81 i * 2); // 秒0x81、分0x83…年0x8D DS1302_IO_MODE_IN(); // 数据线切为输入方向 t[i] BCD2DEC(DS1302_ReadByte()); DS1302_IO_MODE_OUT(); // 恢复输出方向 } DS1302_CE 0; // 结束本次访问 }这段代码把ds1302的读写命令字节规律讲清楚了命令字节最低位是读/写方向0x81最低位为1表示读秒寄存器之后每次地址加2就依次跳到分、时、日、月、周、年。读回来的是BCD码所以外面套了一层BCD2DEC转成十进制方便后续比较大小。写保护必须提前放开否则读时间也会被拒绝。2.2 AT24C02掉电保存存储地址分配与I2C读写时序单价、最大车位、当前已停车辆数这些参数必须落到非易失介质上。设计里选的是AT24C02256字节容量走I2C总线。I2C占用两根线SDA和SCL器件地址由A0/A1/A2引脚决定在Proteus仿真里这三脚接地所以7位地址是0x50加上读写方向位就变成0xA0写和0xA1读。// i2c.c AT24C02单字节写入 void EEPROM_WriteByte(unsigned char addr, unsigned char dat) { I2C_Start(); // 起始条件SCL为高时SDA拉低 I2C_SendByte(0xA0); // 器件地址 0x501 | 0写 I2C_SendByte(addr); // 片内字节地址 I2C_SendByte(dat); // 待保存的数据 I2C_Stop(); // 停止条件 delay_ms(5); // 等待芯片内部写周期完成 } unsigned char EEPROM_ReadByte(unsigned char addr) { unsigned char dat; I2C_Start(); I2C_SendByte(0xA0); // 先发写方向的“伪写”指定地址 I2C_SendByte(addr); I2C_Start(); // 重新产生START进入读模式 I2C_SendByte(0xA1); // 器件地址 | 1读 dat I2C_ReceiveByte(); // 接收1字节 I2C_Stop(); return dat; }I2C读操作一定要做一次“伪写”先把片内地址指针定位到目标位置再重新START切换成读方向。很多新手直接发0xA1就收数据结果读回来永远是0xFF或者上一次操作的数据。写入之后必须等5毫秒左右这期间芯片在内部擦写Flash不等待就连续写下一字节会丢数据。存储地址规划直接影响到掉电恢复的正确性。这个工程里我建议按下面的布局分配存储地址内容取值范围默认值0x00停车单价元/小时1-990x020x01最大车位数1-2540x140x02已停车辆数0-2540x000x10当前在场卡号0-2550xFF把卡号放在0x10而不是紧挨着系统参数是为了避免单页连续写时误覆盖。AT24C02单字节写没有页边界问题但按照这个布局扩展多卡数组时0x10到0x1F可以连续存16个卡号后面加功能不用动前面的地址映射。2.3 上电初始化顺序先恢复参数再开显示主程序main.c里的初始化顺序很重要。常见做法是先初始化ds1302总线然后立刻从AT24C02把单价、车位数、已停车辆数读回全局变量最后才初始化LCD。反过来先点亮屏幕再读存储开机瞬间屏幕上显示的是默认值会闪一下而且如果读取被LCD初始化卡住掉电保存的参数反而迟迟没恢复。// main.c 上电恢复参数 void main(void) { DS1302_Init(); // 配置ds1302接口与写保护状态 if (EEPROM_ReadByte(0x00) 0xFF EEPROM_ReadByte(0x01) 0xFF) { // 出厂状态写入默认配置 EEPROM_WriteByte(0x00, DEFAULT_PRICE); EEPROM_WriteByte(0x01, DEFAULT_SLOTS); } g_price EEPROM_ReadByte(0x00); g_slots EEPROM_ReadByte(0x01); g_used EEPROM_ReadByte(0x02); if (g_used g_slots) g_used 0; // 防脏数据 Lcd12864_Init(); while (1) { Key_Scan(); Card_Scan(); Lcd12864_Refresh(); } }用0xFF判断“首次上电”是AT24C02这类EEPROM的通用技巧出厂整片数据就是0xFF。如果只在0x00判断一次写入后下次开机就会跳过默认值写入但卡号和已停数量坏了也不会被修复。所以我在读取g_used后加了一次越界保护已停数量大于最大车位时直接清零防止掉电瞬间写入半截数据导致满位误锁。3. 刷卡进出、计时与计费状态机怎么组织3.1 进场/出场事件与状态转移停车场的核心逻辑是一张卡在“空闲”和“在场”两个状态之间切换。进场记录卡号和时间出场用同一张卡结算费用。代码里用一个全局状态量g_state表示当前是否有车辆在场再用一个g_cur_card保存当前在场卡号。状态转移表如下当前状态刷卡事件执行动作下一状态空闲刷任意未登记卡记录卡号、读取入场时间、已停1在场在场刷同一张卡读取出场时间、计算费用、清卡号、已停-1空闲在场刷另一张卡提示“此卡已在场”拒绝进场在场这个模型假设停车场一次只处理一辆车和实际商用系统有差距但作为课程设计和仿真验证足够了。改成多卡也很容易把g_cur_card换成数组状态位保留。下面这段代码对应单卡版本// 刷卡事件入口card_id来自读卡模块或仿真按键模拟 void Card_Scan(unsigned char card_id) { if (card_id 0xFF) return; // 无卡事件 if (g_state STATE_IDLE) { // 空闲 - 进场 g_cur_card card_id; DS1302_ReadTime(g_in_time); // 记录进场时间 EEPROM_WriteByte(0x10, card_id); // 掉电后仍能识别在场卡 g_used; EEPROM_WriteByte(0x02, g_used); g_state STATE_PARKING; Lcd12864_ShowLine(0, 欢迎入场); } else if (g_cur_card card_id) { // 同卡出场结算 DS1302_ReadTime(g_out_time); fee CalcFee(g_in_time[0], g_out_time[0], g_price); g_used--; EEPROM_WriteByte(0x02, g_used); EEPROM_WriteByte(0x10, 0xFF); // 清空在场卡号 g_state STATE_IDLE; Lcd12864_ShowFee(fee); } else { Lcd12864_ShowLine(1, 此卡已在场); } }进场就把卡号写入EEPROM是为了应对“刷卡进场后正在计时突然掉电”的场景。复位后g_state初始化为空闲如果不在初始化阶段从0x10把卡号读回来这辆车就被遗忘了会白停一场。如果工程里要支持多卡只有一张卡号的方案不够得改成卡号数组并加“全部读取”逻辑。3.2 计费公式跨天、进位与最小计费单位计费先统一成分钟再计算避免“小时位借位”这种低级错误。ds1302读回的时间数组里下标0是秒、1是分、2是时所以计算时要取t[2]和t[1]。出场分钟小于进场分钟时不是直接相减而是加上24小时再减这是跨天场景的处理。// 计费核心按分钟累计金额价格单位是元/小时 unsigned int CalcFee(unsigned char *in_t, unsigned char *out_t, unsigned int price) { unsigned int in_min, out_min, diff; in_min in_t[2] * 60 in_t[1]; // 入场 时*60分 out_min out_t[2] * 60 out_t[1]; // 出场 时*60分 if (out_min in_min) { diff out_min - in_min; } else { diff 24 * 60 - in_min out_min; // 跨天累加 } if (diff 0) diff 1; // 防止除零死逻辑 if (diff 60) diff 60; // 不足1小时按1小时收 return (diff * price) / 60; // 分钟*单价/60 金额 }这里的取整策略是“不足一小时按一小时”是业务规则而不是硬件要求。实际现场如果要求按分钟计费把if (diff 60) diff 60;改成保留原分钟即可。返回值是整数单价写成整数元/小时就够用比如3元/小时停35分钟返回3元停65分钟返回6元。如果想精确到角可以把price放大10倍存成“角/小时”显示时再除10。3.3 满位判断与卡号记录满位判断放在进场分支的前置位置。如果g_used已经等于g_slots直接显示“车位已满”并返回不能进入记录卡号流程。这块逻辑在流程图上是一条独立分支很多人在Proteus里仿真发现“刷了卡没反应”其实是g_used被写成超范围值进场分支被满位条件挡住了。// 进场前的容量判断 if (g_used g_slots) { Lcd12864_ShowLine(1, 车位已满); return; }我调试时会故意把0x02地址的值改成比0x14大验证满位分支能否稳定触发再上电看恢复逻辑是否把g_used修正回合法范围。这个测试用例在后面第5章会展开。4. Proteus仿真搭建与调试从DSN到现象的对照4.1 元件选型与连线P0口上拉最容易被忽略Proteus里搭建这套系统元件选型直接决定了能不能跑起来。主控选AT89C52RTC用DS1302EEPROM用AT24C02液晶用AMPIRE128X64这几个都是Proteus自带模型不需要第三方库。连接方式按下面这张表来基本一次就能点亮模块Proteus型号关键连接主控AT89C52RST接10uF10k复位XTAL接12MHz30pF电容时钟DS1302CE/SCLK/IO接P1.0/P1.1/P1.2VBAT接3V电池存储AT24C02SDA/SCL接P2.0/P2.1各加4.7k上拉电阻显示AMPIRE128X64PSB接地进入并口模式数据线接P0口这里最容易踩的坑是P0口。51单片机P0口内部没有上拉电阻输出高电平时实际是漏极开路状态直接接12864数据线会出现乱码或白屏。Proteus仿真里用RESPACK-8排阻把P0口一并上拉到VCC整套显示才稳定。另外复位电路必须接仿真里有些案例靠内部上电复位也能跑但真实硬件没RC电路会随机复位。4.2 仿真常见坑时间不走、I2C死锁、卡刷不进去把经常遇到的故障现象按“可观察现象 → 排查顺序 → 根因”整理成表调试时直接对着查现象排查顺序常见原因时间不走或飞快ds1302晶振频率、CE启动时序仿真里晶振误设成12MHzds1302需要32.768kHzLCD白屏/花屏对比度引脚、P0上拉、初始化延时PSB电平不对或排阻没加I2C读回都是0xFF上拉电阻、器件地址、总线忙SDA/SCL悬空或地址位计算错刷卡无反应状态机是否卡在满位、卡号读取g_used掉电恢复脏数据超过g_slots金额明显不对时间数组下标、跨天分支把分和时取反或跨天没加24*60Proteus仿真和真实硬件有个明显区别仿真里ds1302晶振可以乱设程序跑起来时间走得快慢不明显。所以我会单独放一个DS1302的虚拟示波器监控SCLK和IO的跳变确认每个命令字节发了几位、命令地址对不对。I2C总线死锁时直接把SDA和SCL两个引脚的电平都拉高再重新送START就能把总线“救”回来。4.3 借助虚拟终端和printf输出关键节点Proteus的Virtual Terminal组件可以代替串口调试。单片机TXD接虚拟终端RXD波特率设置一致就能在仿真时打印进出场时间和金额。但Keil C51里printf默认输出到串口必须重定向putchar否则printf打了也没人收。// 重定向printf到串口0配合Proteus虚拟终端查看 char putchar(char c) { SBUF c; // 发送一字节到UART while (!TI); // 等待发送完成标志 TI 0; // 清标志准备下一次发送 return c; } #if DEBUG_UART // 加减调试代码进出门各打印一次 printf(in%02d:%02d out%02d:%02d fee%d\r\n, g_in_time[2], g_in_time[1], g_out_time[2], g_out_time[1], fee); #endif这种调试方式比盯着LCD判断快得多尤其是金额计算错误时printf能同时看到分钟差、跨天标记和最终金额。仿真没问题后DEBUG_UART宏直接关掉printf那几行不参与编译不影响实物程序。5. 从仿真到实物AD原理图与流程图落地检查5.1 引脚分配一致性核对AD原理图Sheet1.SchDoc和Proteus仿真的连线必须完全一致否则实物焊接完发现P2.1和P2.0交叉就有得哭了。检查方法不复杂在AD里导出网络表把DS1302、AT24C02、AMPIRE128X64涉及的引脚网络提取出来和仿真DSN的元件连接表做一次diff。# 从AD导出的netlist提取关键网络与仿真连接表比差异 grep -E DS1302|AT24C02|AMPIRE|P0[0-7] project.net ad_pins.txt diff ad_pins.txt sim_pins.txt没有现成脚本的情况下我一般按模块核对先查三线接口的CE/SCLK/IO各接哪个引脚再查I2C和LCD数据线。只要这三个模块的引脚没有交叉控制逻辑就基本能平移过去。5.2 实物化必改复位、晶振负载和ds1302后备电池仿真里很多细节被模型简化了实物上有三处必须改。第一是复位电路仿真里DSN经常直接靠上电复位实物要加标准RC复位按键复位也得预留。第二是主晶振的负载电容12MHz配22到30pF对地电容走线尽量短。第三是ds1302的后备电池隔离正极串一个1N4148二极管再接到ds1302的VBAT防止主电源给电池反向充电同时主电源掉电后电池也能正常供电。// 实物上电自检参数区非法值直接恢复默认 void PowerOnSelfTest(void) { unsigned char v EEPROM_ReadByte(0x00); if (v 0xFF || v 100) { EEPROM_WriteByte(0x00, DEFAULT_PRICE); } }类似地0x01车位数量和0x02已停数量也做同样的上下界保护。仿真里EEPROM掉电不会写到一半实物带电插拔或电源跌落时半写是真实存在的故障启动自检能把这部分风险挡住。5.3 用测试用例回归每个流程分支流程图.bmp里的分支不多但每一条都应该有对应的测试用例。建议直接照下面这张表跑用例编号操作步骤预期结果覆盖分支T01 正常进出刷A卡进场5分钟后刷同一张卡出场按1小时计费显示金额车位1空闲→在场→空闲T02 跨天停车设置时间为23:50刷A卡进场改次日00:10出场按20分钟补足1小时计费跨天分支T03 满位拒绝设置最大车位为1已有车在场刷新卡提示“车位已满”不记录新卡满位分支T04 掉电恢复修改单价为8元断电重启单价仍为8元已停数量和在场卡号保持EEPROM恢复逻辑跑完T01到T04再把流程图.bmp打开逐条分支和代码指向的地址一一对照确认图上画的每条线在代码里都有对应分支。这样做的好处是答辩或验收时被问到“掉电会怎样”“跨天怎么算”可以直接指着测试记录讲比临场读代码有说服力得多。分支数量一致是这套设计验证完的最直观指标代码和图纸对不上时先改图还是先改代码以实际运行为准。本文还有配套的精品资源点击获取
返回列表