
简介这套基于51单片机的停车场刷卡计费器毕业设计资料面向电子信息类专业学生与单片机初学者对应停车场出入管理场景解决刷卡入场提示、离场按时长计费、时间校准以及单价/车位数配置等常见设计问题。压缩包共81个文件、约30.14MB包含Keil工程源码c/h/a51/lst/hex、Proteus仿真dsn/dbk/pwi、原理图SchDoc/PDF、流程图片、元件清单以及多份器件手册和Word/PDF说明文档可对照硬件连接与软件逻辑进行学习和二次开发。目前已有122人学习浏览。内容从程序代码、电路原理图、仿真调试延伸到答辩技巧与常见问题汇总并收录DS1302、STC89C52等器件资料便于理解单片机如何驱动时钟芯片与1602液晶完成计费显示无论是用于毕业设计、课程设计还是想系统掌握51单片机应用开发都有较强的参考价值。1. 停车场刷卡计费器仿真里的硬件与逻辑约束在众多单片机课程设计题目里刷卡计费器是少有的能把“输入—处理—存储—显示”四个环节全部串起来的场景。实体停车场系统的核心在于道闸控制、RFID 读卡和后台数据库到了 51 单片机这一层任务被压缩成一张卡号、一个时间戳、一套费率和一块 EEPROM。难点因此变得非常明确读卡动作是异步事件计时必须连续LCD 刷新不能阻塞业务逻辑掉电后还要保留余额和最近几条消费记录。四件事同时压在 8 位单片机上恰好对应了定时器、外部中断、串口和 I2C 四条知识线。用 Proteus 仿真这个停车场刷卡计费器并不是偷懒而是把硬件不确定性先剥离掉先在逻辑层把计费规则跑通再考虑移植到实物。2. 停车场刷卡计费器的硬件选型与 Proteus 电路搭建2.1 选型为什么仿真普遍先用 AT89C52之后才考虑 STC搜索 51 单片机硬件设计相关教程时会发现凡是 Proteus 仿真实例几乎都默认用 AT89C52。原因很简单Proteus 的元件库里 AT89C52 的模型最稳定引脚和指令集与 STC89C52 完全兼容Keil 里选择 Atmel AT89C52 即可直接编译出 HEX 文件不需要额外配置下载电路。做仿真时选 AT89C52画板时换成 STC89C52是成本最低的迁移路径。AT89C52 的内部资源是 8KB Flash、256B RAM、3 个定时器、1 个全双工串口。对停车计费器来说8KB Flash 存代码绰绰有余256B RAM 则需要精打细算。LCD 缓冲区、卡号缓冲区和停车记录结构体加起来很容易超过 100B所以定义全局变量时要有意识地复用。仿真中还要注意 P0 口的结构它是开漏输出接 LCD 数据线时必须在外部接上拉电阻否则高电平拉不上去LCD 会白屏或显示乱码。这是所有基于 51 单片机的项目在 Proteus 里最常见的“看起来像代码错、其实是电路错”的问题。2.2 引脚分配方案与需要预留的扩展口我一般会把引脚按功能区块划分把 LCD 和 DS1302 放在独立端口把按键和 EEPROM 放在可扩展的 P1/P2 口。下面的分配方案可以直接照抄后续代码都是按这套引脚写的。功能组信号线51 单片机引脚仿真参数注意事项LCD1602RS、RW、EP2.2、P2.3、P2.44 线模式数据接 P0.4–P0.7LCD1602DB4–DB7P0.4–P0.7P0 口接 10k 上拉排阻到 VCCDS1302RST、SCLK、DATP3.4、P3.5、P3.6三线制读时序需要额外延时AT24C02SCL、SDAP2.0、P2.1I2C 总线接 4.7k 上拉电阻功能按键入场、出场、查询P1.0、P1.1、P1.2按键另一端接地按下为低电平蜂鸣器BZP1.7低电平驱动串 1k 电阻这个方案的要点是把 LCD 的 RS/RW/E 全部放到 P2 口使得 P0 口可以专心做数据总线。DS1302 放在 P3.4–P3.6 是因为 P3.0/P3.1 要留给串口接虚拟终端用来模拟刷卡。P1.0–P1.2 接三个独立按键后面用查询方式扫描就够了不需要专门开外部中断。如果之后想在实物上扩展 RC522 射频模块P1.3 到 P1.6 还空着恰好可以分配给 SPI 接口的 RST、SDA、SCK、CS。2.3 晶振频率选 11.0592MHz 的两个直接原因仿真电路里晶振频率必须和 Keil 工程里设置的一致否则串口波特率会偏。我通常选 11.0592MHz而不是 12MHz原因有两点。第一11.0592MHz 分频出的 9600 波特率误差为零定时器 T1 工作在模式 2初值用 256 - 11059200 / 12 / 32 / 9600 253也就是 0xFD直接整除。第二如果选 12MHz9600 波特率会有约 2% 的累计误差在仿真里偶尔能撑住但大量数据传输时会偶发乱码。DS1302 的 32.768kHz 晶振要单独挂在 RTC 芯片的 X1/X2 引脚上这两个引脚在 Proteus 里经常被漏连表现是时间不走。下面是 Keil 中串口和定时器初始化的参考写法这段代码放在 main 函数之前void Uart_Init(void) { SCON 0x50; // 串口模式18位UART允许接收 TMOD 0x0F; // 保留T0设置 TMOD | 0x20; // T1模式2自动重装 TH1 0xFD; // 9600波特率 11.0592MHz TL1 0xFD; TR1 1; // 启动T1 ES 1; // 开串口中断 EA 1; // 开总中断 }代码逻辑说明SCON0x50 将串口配置为模式 1即 8 位可变波特率 UART。TMOD 先保留低四位避免影响 T0 的配置再把高四位设为 0010让 T1 工作在 8 位自动重装模式。TH1/TL1 都写 0xFD计数溢出一次正好得到一个比特时间。串口中断使能后每收到一字节都会进入 interrupt 4这块留给刷卡数据接收处理。3. Proteus 仿真 51 单片机停车场刷卡计费器的刷卡模拟与状态调度3.1 没有实体卡时用虚拟终端模拟刷卡卡号Proteus 仿真 51 单片机停车场刷卡计费器时最先遇到的障碍是“卡从哪来”。RC522 模块在 Proteus 中有专用模型但使用它需要额外配置 SPI 时序而且不同版本 Proteus 的模型行为差异很大。为了保证计费逻辑可复用更稳妥的做法是用虚拟终端配合单片机串口模拟读卡虚拟终端通过 TXD 引脚向单片机发送一串卡号单片机在串口中断里接收识别到回车符后认为完成一次刷卡操作。这样做有几个实际好处第一卡号可以任意输入测试边界情况时直接用虚拟终端窗口敲字符串比点 RFID 模型上的按钮快得多第二串口接收逻辑在实物上同样成立将来换成 RC522 时只需要把 SPI 读到的卡 ID 转成同样格式的字符串主状态机完全不用改。串口中断接收卡号的代码可以这样写void Uart_Isr(void) interrupt 4 { unsigned char rx; if (RI) { // 接收中断标志 RI 0; rx SBUF; // 读取接收缓冲 if (rx \r || rx \n) { cardBuf[cardLen] \0; cardReady 1; // 置位“刷卡完成”标志 cardLen 0; } else if (cardLen 8) { cardBuf[cardLen] rx; } } }这段代码的逻辑是每次进入串口中断先清除 RI 标志防止重复响应。读取 SBUF 后判断是回车还是普通字符回车表示一帧卡号结束普通字符则存入缓冲区。cardReady 被主循环轮询一旦置位就进入计费状态机处理。这个方式在虚拟终端下运行稳定发送“000A01\r”这样的卡号主程序即可识别。3.2 主状态机入场、停车、出场三态流转刷卡计费器的主程序不应该用连续 if 堆业务逻辑而应维护一个简单的状态机。入场状态等待刷卡刷到卡后把当前时间存入停车记录结构体状态切换到“停车中”。出场状态再刷一次卡计算停车时长和费用从余额中扣减状态切回入场等待。下面是状态枚举定义typedef enum { ST_IDLE, // 待机等待读卡 ST_PARKING, // 已入场计时进行中 ST_CHARGING // 出场结算中 } SysState;状态机设计里最容易错的地方是停车记录结构体的生命周期。我一般把它定义成一个全局结构体入口刷到卡就填充卡号和入场时间出口结算是读取同一个结构体结算完成后再清空。这样当用户连续入场、连续出场时记录不会互相覆盖。结构体定义如下typedef struct { unsigned char cardNo[9]; // 卡号字符串8位\0 unsigned char startH; // 入场时 unsigned char startM; // 入场分 unsigned char startS; // 入场秒 } ParkingRecord;这里要注意 startS 也要保存因为如果入场在 10:59:59出场在 11:00:01只按小时和分钟计算时长会覆盖一分钟的差别存了秒之后时长差统一换算为秒再折算分钟边界语义才正确。3.3 虚拟终端的参数设置与常见偏差虚拟终端是 Proteus 里用来模拟串口调试助手的组件直接搜索 Virtual Terminal 放置不需要额外的串口芯片模型。它的数据线接单片机 TXD 和 RXD 时要注意交叉单片机的 TXD 只能接虚拟终端的 RXD 引脚如果接反则收不到任何数据。打开终端的属性面板确认下面几项设置参数配置值说明Baud Rate9600与 Keil 串口初始化一致Data Bits851 串口模式 1 固定 8 位ParityNONE不启用校验Stop Bits1标准停止位ASCII ModeText显示字符串而非 HEX 码仿真中如果虚拟终端显示乱码先检查波特率是否一致再检查晶振频率是否真的填了 11.0592MHz。另一个隐蔽问题虚拟终端默认只发送字符不发送回车需要在输入完卡号后手动加一个回车或者在发送缓冲区末尾附带 ‘\r’不然 cardReady 永远不会被置位。调试时可以先把虚拟终端接到串口中断里让它回显收到的字节确认链路通了再做计费逻辑。4. 停车场计费核心算法与 C51 代码实现4.1 用分钟数做计费单位避免浮点运算51 单片机没有硬件浮点单元用浮点数做金额计算会白白消耗 CPU 周期。停车场计费在单片机上的常规做法是全部使用整数时间差换算成分钟费率用“基础费 阶梯费”的整数组合余额也按整数“元”存储。下面是分钟差的计算函数输入为入场和出场的时、分、秒三个字段unsigned int CalcParkMinutes(unsigned char sh, unsigned char sm, unsigned char ss, unsigned char eh, unsigned char em, unsigned char es) { unsigned int startSec sh * 3600 sm * 60 ss; unsigned int endSec eh * 3600 em * 60 es; unsigned int diffSec; if (endSec startSec) { diffSec endSec - startSec; } else { diffSec 86400 - startSec endSec; // 跨天处理 } return (diffSec 59) / 60; // 向上取整到分钟 }这个函数先统一换算成秒避免时分秒直接相减产生的借位问题。跨天判断用 endSec 是否小于 startSec 实现。最后(diffSec 59) / 60是向上取整例如停车 61 秒按 2 分钟计费。注意这里处理的是“向上取整到分钟”不是四舍五入因为停车行业惯例是超时按整分钟收费。4.2 阶梯费率表与不足 15 分钟的处理费率规则是业务核心。常见停车场做法是最初 15 分钟免费1 小时内 5 元超过 1 小时每 15 分钟加 2 元。把这个规则翻译成 C 语言时一定要先考虑边界15 分钟整算免费还是收费超过 1 小时不足 15 分钟的余数怎么计算我一般把费率做成一张常量表并通过一个独立函数完成金额计算unsigned int CalcFee(unsigned int parkMin) { unsigned int fee 0; if (parkMin 15) { // 15分钟内免费 fee 0; } else if (parkMin 60) { // 1小时内5元 fee 5; } else { // 超出部分每15分钟加2元 unsigned int extra parkMin - 60; unsigned int units (extra 14) / 15; fee 5 units * 2; } return fee; // 单位为元 }计算逻辑里(extra 14) / 15是向上取整例如额外多出 1 分钟也按一个 15 分钟单位计费。fee 返回的是整数元直接用于后续余额扣除余额也是 unsigned int 类型。注意如果送进来的 parkMin 为 0函数返回 0说明“即停即走”不扣费。下面这张表总结了不同时长对应的预期结果可以在仿真里逐项验证停车时长计算结果应收金额10 分钟parkMin150 元15 分钟parkMin150 元16 分钟进入 60 分支5 元60 分钟进入 60 分支5 元75 分钟5 1*27 元2 小时5 4*213 元4.3 DS1302 时间读取与 BCD 码换算时间是计费系统的参照系DS1302 的读时序比写时序更敏感。三线制协议里RST 拉高表示开始通信每个 SCLK 上升沿从总线上采样一位。读一个字节时要先发送地址字节再从 DAT 脚上逐个读取 8 位数据。读回来的时间值是 BCD 码不能直接参与运算必须转换成十进制。unsigned char RTC_Read(unsigned char addr) { unsigned char val 0, i; DS1302_RST 1; // 开始通信 DS1302_WriteByte(addr | 0x01); // 读命令 for (i 0; i 8; i) { val 1; if (DS1302_DAT) { val | 0x80; } DS1302_SCLK 0; _nop_(); DS1302_SCLK 1; _nop_(); } DS1302_RST 0; // 结束通信 return val; // 返回BCD码 } #define BCD2DEC(x) (((x) 4) * 10 ((x) 0x0F))读时序的要点是数据在 SCLK 下降沿之后稳定所以代码先把 SCLK 拉低再等一个机器周期然后拉高。val 1 放在读取之前保证低位在前、高位在后时数据位不会错位。读出 BCD 码后用 BCD2DEC 宏转换成十进制例如 0x30 小时变成 30。4.4 EEPROM 掉电存储消费记录停车场计费器必须保存两类数据卡内余额和最近一次计费流水。AT24C02 只有 2Kbit 即 256 字节每张卡号 8 字节因此我把它规划成 8 字节卡号 1 字节余额 1 字节状态剩余空间留给流水记录。I2C 写入一个字节需要完整地给起始信号、写地址、写数据、停止信号void EEPROM_WriteByte(unsigned char addr, unsigned char dat) { I2C_Start(); I2C_SendByte(0xA0); // 器件地址写模式 I2C_WaitAck(); I2C_SendByte(addr); // 片内地址 I2C_WaitAck(); I2C_SendByte(dat); // 写入数据 I2C_WaitAck(); I2C_Stop(); DelayMs(5); // 等待内部写周期 }写入完成后不能马上读AT24C02 内部写周期大约 5ms仿真里这个延时更明显。连续写入多条记录时每写一条就调用一次 DelayMs。地址如果超过 255 会回卷所以在分配记录空间时要对已存储条数取模防止覆盖正在查询的记录。用仿真器验证时可以在写入前后通过 I2C Debugger 或虚拟示波器观察 SDA 线上是否有 ACK 低电平如果没有 ACK多半是上拉电阻缺失导致总线拉不高。5. 仿真调试技巧与边界场景验证方法5.1 Keil 软件仿真替代硬件的两个关键设置当计费逻辑编译通过但在 Proteus 里表现不符合预期时我通常先回到 Keil 做纯软件仿真。在 Debug 选项卡中选择“Use Simulator”再打开 Peripheral 菜单里的 Serial 窗口就可以用虚拟输入窗口向单片机串口发送卡号。设置波特率 9600、8 位数据、无校验、1 停止位和 Proteus 虚拟终端保持一致。这样可以将“单片机代码问题”和“Proteus 电路问题”分离开避免两处同时排错。5.2 用时钟加速法测试长时间停车仿真里停车两小时意味着要等真实的两小时。常见做法是直接修改 DS1302 的内部寄存器把出场时间临时拨快 3 个小时。在状态切换到“停车中”后暂停仿真通过代码里预留的调试命令把秒和分钟加大再让仿真继续运行就可以在几十秒内验证阶梯费率是否按预期计算。注意修改 RTC 后要重新读取一次完整时间不要只读分钟秒位也能影响分钟进位。下面是一个逐项验证的检查清单可以直接在仿真里对照执行测试场景输入操作预期现象免费时段入场后 15 分钟内出场余额不变LCD 显示 0 元基础时段停车 30 分钟扣 5 元蜂鸣器响跨天停车23:50 入场00:10 出场按 20 分钟计费扣 5 元余额不足余额为 3 元停车 60 分钟拒绝出场报警提示重复刷卡同一卡号连续两次入场第二次被拒绝提示已在场内5.3 利用 Keil 变量监视窗口验证时间边界如果担心分钟向上取整的边界写错可以在 Keil 的 Watch 窗口添加 parkMin 和 fee 两个变量然后在仿真中连续改变 DS1302 的秒值观察函数返回值是否与理论值一致。比如停车 15 分钟整理论应收 0 元停车 16 分钟应收 5 元。这类边界是停车计费器最容易漏掉的地方仔细测试比事后在实物上发现问题成本低得多。用 Pro 口上拉电阻确认无误、虚拟终端回显正常、状态机在各状态间切换流畅之后再把这个 Keil 工程导出的 HEX 文件交付使用才算完整跑通一遍全过程。本文还有配套的精品资源点击获取