基于RFID与STC单片机的ETC收费系统设计与扣费一致性调优 📅 发布时间:2026/9/17 15:47:05 👁 浏览次数: 简介这份资源是面向电子信息、自动化、物联网等专业学生及单片机毕设选题者的毕业设计论文全套文档围绕射频识别与单片机结合的高速ETC智能收费系统展开可用于课程设计、毕业设计参考与嵌入式入门实践。压缩包内共1个doc文档约1.31MB即完整的毕业论文正文含中英文摘要、目录、硬件设计、软件设计等章节结构接近高校毕设规范格式便于直接对照撰写与排版借鉴。论文具体展开射频通信模块RC522的非接触识别、STM32F103ZET6主控制器与系统运行时钟、按键输入与供电电路以及L298闸门电机驱动、TFTLCD液晶显示与触摸屏界面并给出卡片充值、单价设定等后台管理功能的实现思路软件部分则涉及人机交互界面设计与程序测试验证能帮助读者理清从硬件选型到流程闭环的完整设计脉络。目前已有89人学习适合需要ETC或RFID类单片机课题方案、论文框架与器件参考的读者。1. RFID 和单片机拼出来的 ETC 智能收费系统难点到底在哪高速收费站排队时栏杆抬起的那一下背后至少有四件事要在几百毫秒内完成射频场里认出一张卡、把卡号交给上位机、查到余额并扣掉通行费、驱动栏杆电机放行。任何一环慢了或者错了车里的人就只能盯着栏杆发呆。这套系统的主体并不复杂一块单片机当主控一个 RFID 读头当入口往外挂栏杆控制、车道指示灯和上位机通信。标题里的「E2021030全套」透露出它是一个完整交付的工程包这意味着它不只包含原理图还要有固件、上位机程序和联调记录。真正让人踩坑的地方从来不是「怎么读出一张卡」而是「车在动、卡在动、电还可能突然断」这三件事同时发生时系统还能不能保证只扣一次钱、只抬一次杆。做课程设计和毕业设计的同学往往止步于串口助手打印出一串卡号而一线做车道设备的工程师关心的是识别窗口够不够、写 EEPROM 到一半掉电怎么办、同一张卡被连读五次会不会扣五次费。接下来的内容按硬件选型、固件实现、高速场景参数调优、联调验证四段推进每一段都给出可复现的接线、代码和参数取值。2. 基于 RFID 与 STC 单片机的 ETC 收费系统硬件选型与最小系统2.1 先分清 5.8GHz DSRC 和 13.56MHz RFID 的差别真正跑在高速上的 ETC 车道路侧单元和车载单元之间走的是 5.8GHz 专用短程通信靠微波链路在几十米外建立会话同时配合车牌识别做二次核对。而课程设计和绝大多数套件里说的「RFID」用的是 13.56MHz 的 ISO14443A 卡或者更便宜的 125kHz 只读 ID 卡读距通常只有 3 到 10 厘米。这两者物理层完全不搭边但把它们抽象成「认卡—记账—放行」三个动作业务逻辑是相通的。想清楚这一点就不会纠结「为什么我的读头读不到五米外的车」。套件里用 13.56MHz 读头模拟 ETC本质是把车必须停在读卡器前的场景换成了卡片必须贴近读头剩下的计费流程、栏杆控制、上位机对账全部可以照搬。选型的第一条原则是先把「读距」这个数字和你的车道形态对齐。近距离刷卡口用 13.56MHz 足够如果要求车内 1 米以上不停车通过就必须上超高频或者 5.8GHz 方案成本和调试难度是另一个量级。2.2 RFID 读头模块的四个关键参数与选型表挑读头模块先看四个参数频段与协议决定能读什么卡接口决定占用单片机几个引脚供电决定电平匹配怎么做是否支持写块决定你能不能把余额直接存在卡里。模块频段/协议接口供电典型读距适用场景RC52213.56MHz / ISO14443ASPI3.3V05cm课程设计、可读写扇区、成本低PN53213.56MHz / 支持 NFCSPI / I2C / UART3.3V07cm需要和手机 NFC 交互EM4095125kHz模拟 GPIO5V010cm只读 ID 卡、抗干扰好UHF 读写模块860960MHzUART5V 或 3.3V03m多标签盘点、远距离识别做 ETC 收费逻辑验证RC522 是性价比最高的起点能读卡号也能往扇区里写余额配合 51 单片机的软件模拟 SPI 就能跑通。EM4095 只能读出厂固化的 ID余额必须存在单片机侧的 EEPROM 里适合只需要「认卡」的场景。UHF 模块读距够远但多标签同时进场时的防冲突算法要复杂得多初学阶段不建议一上来就用。还有一类模块常被忽略带串口协议的一体化读头。它把寻卡、防冲突、校验都封装好了单片机只要收帧解析即可省掉大量底层时序调试。代价是灵活性下降遇到协议不开放的模块你连天线增益都调不了。2.3 51/STC 单片机与 RC522 的引脚复用和电平匹配RC522 工作在 3.3V这是硬件设计里第一个必须处理的问题。51 系列里 STC89C52 是 5V 供电主控输出的高电平接近 5V直接打到 RC522 的输入脚上会超出其耐压范围。常见做法有三条路用 AMS1117-3.3 给读头单独供电同时给 SCK、MOSI、NSS、RST 四根下行信号加分压电阻或专用电平转换芯片或者干脆选 STC8、STC15 这类 3.3V 型号再或者用 STM32F103 这类 3.3V 主控全链路直接连。供电单独走一路 LDO 很重要。读头发射瞬间电流会跳变如果和单片机共用一个 5V 转 3.3V 的 LDO读卡时容易把单片机拉复位。天线附近也不要铺大面积覆铜会影响谐振点的稳定性。RC522 引脚STC89C52 引脚功能说明SDA / NSSP1.4片选低电平有效SCKP1.5时钟软件模拟输出MOSIP1.6主出从入MISOP1.7主入从出注意 3.3V 电平RSTP1.3复位上电需拉低再拉高3.3VAMS1117-3.3 输出独立 LDO加 10uF 0.1uF 滤波GNDGND必须共地提示MISO 是读头输出、单片机输入3.3V 高电平一般能被 5V 单片机识别为逻辑 1这一根可以不加转换反过来那四根从单片机到读头的信号一定要处理否则读卡时好时坏很难定位。2.4 用软件模拟 SPI 打通第一张卡的读写51 单片机没有硬件 SPI 外设RC522 的时序全靠 GPIO 翻转模拟。下面这段代码把最底层的字节收发和寄存器读写封好后面所有寻卡、防冲突、读写扇区的逻辑都建立在它上面。// RC522 底层驱动51 单片机软件模拟 SPI #include reg52.h sbit RC522_SCK P1^5; sbit RC522_MOSI P1^6; sbit RC522_MISO P1^7; sbit RC522_NSS P1^4; sbit RC522_RST P1^3; // 全双工交换一个字节MSB 先行 unsigned char SPI_RW(unsigned char dat) { unsigned char i, rev 0; for (i 0; i 8; i) { if (dat 0x80) RC522_MOSI 1; else RC522_MOSI 0; dat 1; RC522_SCK 1; // 上升沿从机采样 MOSI rev 1; if (RC522_MISO) rev | 0x01; // 同一时刻读回 MISO RC522_SCK 0; // 下降沿数据移位 } return rev; } // 写寄存器地址左移一位最低位 0 表示写 void RC522_WriteReg(unsigned char addr, unsigned char val) { RC522_NSS 0; SPI_RW((addr 1) 0x7E); SPI_RW(val); RC522_NSS 1; } // 读寄存器地址左移一位最低位置 1 表示读 unsigned char RC522_ReadReg(unsigned char addr) { unsigned char val; RC522_NSS 0; SPI_RW(((addr 1) 0x7E) | 0x80); val SPI_RW(0x00); // 发哑元字节换回数据 RC522_NSS 1; return val; }SPI_RW里先出 MSB 再移位是因为 RC522 规定高位先行时钟上升沿从机采样、下降沿移位这个相位关系写反了会表现为读回的寄存器全是 0xFF 或 0x00。RC522_WriteReg和RC522_ReadReg里那条地址左移一位再拼读写位的规则来自 RC522 的寄存器寻址格式很容易漏掉。上电后先拉低 RST 几个毫秒再拉高然后读VersionReg地址 0x37正常应返回 0x91 或 0x92。这一步回读是整个调试过程的分水岭读不到正确版本号说明接线、供电或 SPI 相位有问题后面寻卡不可能成功。把读头靠近卡片后发REQA命令 0x26正常会收到 ATQA 响应然后再走防冲突循环拿到 4 字节 UID第一张卡就算打通了。3. 单片机侧 ETC 收费流程的固件实现状态机、串口帧与掉电存余额3.1 把收费流程拆成八态状态机收费逻辑如果用一堆if堆在主循环里加上超时判断和异常处理很快就理不清。把它拆成状态机每个状态只关心「进入时做什么、满足什么条件跳走」。// ETC 车道主流程状态机 typedef enum { ST_IDLE 0, // 空闲周期性寻卡 ST_READ_CARD, // 卡已进场读 UID 和扇区数据 ST_AUTH, // 校验卡合法性、查余额是否足够 ST_DEDUCT, // 扣费并写入流水 ST_OPEN_GATE, // 驱动舵机抬杆 ST_WAIT_PASS, // 等红外对射确认车已通过 ST_CLOSE_GATE, // 落杆并清场 ST_FAULT // 异常锁定等人工复位 } etc_state_t; static etc_state_t g_state ST_IDLE; static unsigned long g_state_enter_ms 0; // 进入当前状态的时基 // 每次状态切换时更新进入时间用于超时判断 static void state_goto(etc_state_t next) { g_state next; g_state_enter_ms g_tick_ms; }ST_IDLE里以 50ms 为周期发寻卡命令避免天线持续满功率工作发热。ST_WAIT_PASS依赖红外对射车头挡住光路再放开才认为通行完成如果 5 秒内没有任何遮挡变化直接进ST_FAULT防止车没来却一直挂着抬杆状态。ST_FAULT不给任何自动恢复路径必须上位机下发复位帧这是为了不让异常状态被悄悄吞掉。3.2 定时器 0 做 10ms 时基控制抬杆落杆超时状态机里到处要用时间靠delay会阻塞寻卡。用定时器 0 做一个 10ms 递增的全局时基所有超时判断都拿它做差。// 定时器0 初始化10ms 12MHz 晶振经典 12T 模式 volatile unsigned long g_tick_ms 0; void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0模式116 位 TH0 0xD8; // 65536 - 10000 55536 0xD8F0 TL0 0xF0; ET0 1; TR0 1; EA 1; // 全局中断最后开 } void Timer0_ISR(void) interrupt 1 { TH0 0xD8; // 模式1 不自动重装必须手动补 TL0 0xF0; g_tick_ms 10; }重装载值由主频和机器周期决定12MHz 晶振、12T 模式下机器周期是 1us10ms 对应 10000 个周期。如果用的是 11.0592MHz 晶振这个值要重算否则时基会偏大约 8%表现为超时判断总是提前或滞后触发。判断超时的写法统一用g_tick_ms - g_state_enter_ms TIMEOUT_MS即使g_tick_ms溢出回绕无符号数相减的结果依然正确这个细节在长期运行的设备上很关键。3.3 上下位机串口帧格式与解析代码单片机和上位机之间传卡号、余额、放行指令必须有个明确的帧格式否则一次干扰就会让整条数据错位。常见做法是帧头 命令 长度 数据 校验 帧尾。字节位置含义取值示例0帧头0xAA1命令码0x01 刷卡上报 / 0x02 放行 / 0x03 心跳2数据长度0163..n数据域UID、余额、流水号n1校验命令到数据逐字节求和取低 8 位n2帧尾0x55#define FRAME_HEAD 0xAA #define FRAME_TAIL 0x55 static unsigned char rx_buf[32]; static unsigned char rx_len 0; // 串口中断里逐字节收边收边判帧头帧尾 void UART_ISR(void) interrupt 4 { unsigned char ch; if (RI) { RI 0; ch SBUF; if (rx_len 0 ch ! FRAME_HEAD) return; // 丢弃帧外噪声 if (rx_len sizeof(rx_buf)) { rx_len 0; return; } rx_buf[rx_len] ch; if (ch FRAME_TAIL) { if (CheckSumOK(rx_buf, rx_len)) { HandleFrame(rx_buf, rx_len); // 校验通过才处理 } rx_len 0; // 无论对错都清缓冲 } } }接收侧只有收到帧尾才触发解析rx_len在解析后无论成功失败都清零避免半截帧卡在缓冲里越积越多。校验覆盖命令码到数据域的全部字节帧头和帧尾不计入发送侧算法必须完全一致否则表现为上位机能收到数据但一律判为无效帧。数据域如果含多字节整数统一定好大小端并在协议文档里写死不要一半大端一半小端这类问题在联调时特别耗时间。3.4 余额与流水写进 STC 内部 EEPROM 的双备份做法余额一旦掉电丢失整个收费系统就失去意义。STC 系列自带 IAP 功能可以把一部分 Flash 当 EEPROM 用按扇区擦写。写的时候不能只存一份因为擦除到一半掉电会造成整条记录不可读。// 双备份记录主区 备份区各带 CRC16 和有效标志 #define ADDR_MAIN 0x0000 #define ADDR_BAK 0x0100 typedef struct { unsigned char valid; // 0xA5 表示有效 unsigned char uid[4]; unsigned long balance; // 单位分 unsigned long seq; // 交易序号单调递增 unsigned int crc; } card_record_t; // 写入顺序先写备份区校验通过后再覆盖主区 void SaveRecord(card_record_t *rec) { rec-crc Crc16((unsigned char *)rec, sizeof(card_record_t) - 2); IAP_EraseSector(ADDR_BAK 9); IAP_Write(ADDR_BAK, (unsigned char *)rec, sizeof(card_record_t)); IAP_EraseSector(ADDR_MAIN 9); IAP_Write(ADDR_MAIN, (unsigned char *)rec, sizeof(card_record_t)); } // 上电加载主区坏就读备份区都坏则判为异常 unsigned char LoadRecord(card_record_t *rec) { IAP_Read(ADDR_MAIN, (unsigned char *)rec, sizeof(card_record_t)); if (rec-valid 0xA5 Crc16((unsigned char *)rec, sizeof(card_record_t) - 2) rec-crc) return 1; IAP_Read(ADDR_BAK, (unsigned char *)rec, sizeof(card_record_t)); if (rec-valid 0xA5 Crc16((unsigned char *)rec, sizeof(card_record_t) - 2) rec-crc) return 1; return 0; }写入顺序是「先备份、后主区」两次操作之间掉电最坏情况是主区还是旧值、备份区是新值上电加载时主区校验通过就会读到旧余额所以还需要用seq交易序号做二次判断谁大用谁。这个序号每次扣费自增天然解决了恢复时「新旧不分」的问题。擦写寿命方面内部 Flash 一般按万次量级估算高频车道要有心跳式的写次数统计超过阈值就换存储介质或改用外置 EEPROM。4. 高速过车下 RFID 防冲突与扣费一致性的参数调法4.1 多标签同时进场ISO14443A 防冲突与 UHF 的 Q 值相邻车道两辆车同时进射频场是 ETC 场景里最典型的异常。13.56MHz 的 ISO14443A 用位冲突检测加二分搜索来挑一张卡读头发REQA场内的卡返回 ATQA如果多张卡返回的 UID 在某一位置不同读头就能感知到冲突位然后逐位置缩小范围一轮轮筛选直到唯一确定一张。整个过程读头不需要知道场里有几张卡靠的是 UID 逐位比对。超高频的玩法不同用的是帧时隙 ALOHA读头下发一个 Q 参数把时间切成 2 的 Q 次方个时隙标签随机选一个时隙应答。Q 值调大时隙多、冲突少但一轮盘点耗时长Q 值调小快是快标签一多就撞成一片。现场常用做法是动态 Q根据上一轮的冲突率和空闲率自动升降或干脆固定 Q4 起步再按实际标签密度微调。对 ETC 这条业务线来说13.56MHz 的场景下一张卡贴在读头上时基本不存在多标签冲突真正要防的是「隔壁车道的卡串读」。解决办法是压低天线功率、加金属屏蔽罩、把读头安装角度往本车道偏从物理层面把识别区压到 30 厘米以内比在软件里做复杂过滤更可靠。4.2 车速 30km/h 的识别窗口怎么算读卡周期设多少把车速换算成时间窗口是设参数的前提。30km/h 约等于 8.33m/s如果有效识别区沿车道方向长 1.2 米那么卡进入和离开这个区间的时间就是 1.2 / 8.33 ≈ 144ms。这 144ms 里要完成寻卡、防冲突、选卡、读扇区、验证、扣费、写存储、发抬杆指令任何单步超过预算就会漏车。环节典型耗时优化手段寻卡 REQA ATQA58ms缩短轮询间隔到 30ms防冲突 选卡1015ms缓存已知 UID命中直接跳过读扇区数据1020ms只读必要块别整扇区读余额校验 扣费510ms计算和存储分离先算后写写 EEPROM2040ms改为异步写先放行后落盘按这张表串行做完约 5090ms144ms 的窗口够用但不宽裕。更稳的做法是分层识别和校验必须在窗口内完成写 EEPROM 这类慢操作放到抬杆之后异步执行同时在内存里留一份待落盘队列掉电时靠双备份记录找回。轮询间隔不要设成 200ms 以上否则车都开过去了才开始寻卡。4.3 去重窗口、交易序号与重复扣费车速慢或者车在栏杆前停车再启动时同一张卡可能在几秒内被连续读到十几次。如果每次读到都计费用户就会投诉扣了多次。去重的核心是「卡号 时间窗」窗口内同一张卡只认第一次。#define DEDUP_WINDOW_MS 5000 #define DEDUP_SLOTS 8 typedef struct { unsigned char valid; unsigned char uid[4]; unsigned long last_ms; } dedup_t; static dedup_t g_dedup[DEDUP_SLOTS]; // 返回 1 表示重复刷卡应忽略返回 0 表示新交易 unsigned char IsDuplicate(unsigned char *uid) { unsigned char i, oldest 0; for (i 0; i DEDUP_SLOTS; i) { if (g_dedup[i].valid memcmp(g_dedup[i].uid, uid, 4) 0) { // 无符号相减天然处理 g_tick_ms 溢出回绕 if (g_tick_ms - g_dedup[i].last_ms DEDUP_WINDOW_MS) { return 1; } g_dedup[i].last_ms g_tick_ms; // 超窗则刷新算新交易 return 0; } } // 未命中找一个空槽或最老的槽写入 for (i 0; i DEDUP_SLOTS; i) { if (!g_dedup[i].valid) { oldest i; break; } if (g_dedup[i].last_ms g_dedup[oldest].last_ms) oldest i; } memcpy(g_dedup[oldest].uid, uid, 4); g_dedup[oldest].last_ms g_tick_ms; g_dedup[oldest].valid 1; return 0; }去重窗口设 5 秒比一次完整抬落杆周期略长。设太短车在杆前磨蹭几秒再走会被算第二次设太长同一辆车掉头回来再刷会被误判为重复而不扣费。窗口值要结合车道实际通行时间实测别直接抄。槽位数量按车道每小时车流量估8 个槽在单车道场景足够多车道共用一台控制器时要放大。4.4 参数取值表与典型故障对照参数建议取值调整依据寻卡轮询间隔3050ms车流密时取小降低发热取大去重窗口30008000ms按实测单车道通行时长取 1.2 倍抬杆保持时间35s以红外确认车辆通过为准无车落杆超时5000ms超时进故障态不等人工串口波特率115200短距机箱内可用 9600 降误码写 EEPROM 时机抬杆后异步避免阻塞识别窗口天线功率按读距实测压到本车道减少串读常见故障里「车已过杆不落」多半是红外对射被灰尘挡住光路一直判定有车「同一张卡扣两次」先看去重窗口是不是被写成 0再看 EEPROM 回读出来的seq有没有正常自增「读卡成功率忽高忽低」优先查读头供电是不是和单片机共用一路 LDO读卡瞬间的电流跳变最容易造成这种间歇性故障。5. ETC 收费系统联调与交易一致性的验证技巧5.1 串口日志打点 Python 批量刷卡压测固件里对每个状态切换都打一行日志格式统一成「时基 状态 卡号 余额」串口助手直接能看到。真正省力的是用脚本批量刷同一张卡检查去重逻辑在长时间运行下是否漏判。import serial, time ser serial.Serial(COM3, 115200, timeout1) DEDUCT_CNT 0 time.sleep(2) # 等单片机复位稳定 start time.time() while time.time() - start 60: # 连续刷 60 秒 ser.write(bytes([0xAA, 0x02, 0x00, 0x02, 0x55])) # 心跳/触发指令 line ser.readline().decode(ascii, ignore).strip() if DEDUCT in line: # 固件里扣费时打这个标记 DEDUCT_CNT 1 time.sleep(0.05) # 模拟人反复刷卡 print(60 秒内扣费次数:, DEDUCT_CNT)脚本里 0.05 秒的间隔模拟人手反复贴卡60 秒内正常应只出现 1 次DEDUCT出现多次说明去重窗口没生效或者g_tick_ms的差值判断写反了。把间隔调成 6 秒再跑一遍这时应该每次都能扣费成功用来验证窗口过期逻辑。两个极端都跑通去重这块基本可以放心。5.2 掉电中断测试与影子余额校验交易一致性最硬的验证是掉电测试在扣费写入过程中反复切断电源每次上电后检查余额是否既没丢也没重复扣。具体做法是在固件里加一个「影子余额」变量内存里维护一份理论余额写入 EEPROM 后回读并比对不一致就进故障态并上报。// 扣费事务内存影子值 - 写双备份 - 回读比对 static unsigned long g_shadow_balance; unsigned char DoDeduct(unsigned char *uid, unsigned long fee) { card_record_t rec; if (!LoadRecord(rec)) return 0; // 加载失败直接拒绝 g_shadow_balance rec.balance - fee; // 先在内存里算好 if (g_shadow_balance rec.balance) { // 无符号下溢说明余额不足 g_shadow_balance rec.balance; return 0; } rec.balance g_shadow_balance; rec.seq; memcpy(rec.uid, uid, 4); rec.valid 0xA5; SaveRecord(rec); // 双备份写入 Rec_Read LoadRecord(rec); // 回读校验 if (!Rec_Read || rec.balance ! g_shadow_balance) { state_goto(ST_FAULT); // 回读不一致锁死等人工 return 0; } return 1; // 交易成功可以抬杆 }无符号数的减法判断余额是否够扣比写成balance fee更省事下溢自然落到一个比原值大的数上。回读比对是最后一道防线如果写入后读回来的值和内存里的影子余额不符说明存储环节出了问题此时绝不能抬杆放行直接进故障态。压测时用继电器或开关串在电源线上做 200 次随机断电只要出现一次「回读不一致却仍然抬杆」就说明这道校验没接进主流程。本文还有配套的精品资源点击获取