简介:这套基于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 单片机引脚 | 仿真参数注意事项 |
|---|---|---|---|
| LCD1602 | RS、RW、E | P2.2、P2.3、P2.4 | 4 线模式,数据接 P0.4–P0.7 |
| LCD1602 | DB4–DB7 | P0.4–P0.7 | P0 口接 10k 上拉排阻到 VCC |
| DS1302 | RST、SCLK、DAT | P3.4、P3.5、P3.6 | 三线制,读时序需要额外延时 |
| AT24C02 | SCL、SDA | P2.0、P2.1 | I2C 总线接 4.7k 上拉电阻 |
| 功能按键 | 入场、出场、查询 | P1.0、P1.1、P1.2 | 按键另一端接地,按下为低电平 |
| 蜂鸣器 | BZ | P1.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,直接整除。第二,如果选 12MHz,9600 波特率会有约 2% 的累计误差,在仿真里偶尔能撑住,但大量数据传输时会偶发乱码。DS1302 的 32.768kHz 晶振要单独挂在 RTC 芯片的 X1/X2 引脚上,这两个引脚在 Proteus 里经常被漏连,表现是时间不走。
下面是 Keil 中串口和定时器初始化的参考写法,这段代码放在 main 函数之前:
void Uart_Init(void) { SCON = 0x50; // 串口模式1,8位UART,允许接收 TMOD &= 0x0F; // 保留T0设置 TMOD |= 0x20; // T1模式2,自动重装 TH1 = 0xFD; // 9600波特率 @ 11.0592MHz TL1 = 0xFD; TR1 = 1; // 启动T1 ES = 1; // 开串口中断 EA = 1; // 开总中断 }代码逻辑说明:SCON=0x50 将串口配置为模式 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 Rate | 9600 | 与 Keil 串口初始化一致 |
| Data Bits | 8 | 51 串口模式 1 固定 8 位 |
| Parity | NONE | 不启用校验 |
| Stop Bits | 1 | 标准停止位 |
| ASCII Mode | Text | 显示字符串而非 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 分钟 | parkMin<=15 | 0 元 |
| 15 分钟 | parkMin<=15 | 0 元 |
| 16 分钟 | 进入 <=60 分支 | 5 元 |
| 60 分钟 | 进入 <=60 分支 | 5 元 |
| 75 分钟 | 5 + 1*2 | 7 元 |
| 2 小时 | 5 + 4*2 | 13 元 |
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 文件交付使用,才算完整跑通一遍全过程。
本文还有配套的精品资源,点击获取