简介:这份资料围绕基于89C2051单片机的SLE4442接触式IC卡读卡器设计展开,面向电子信息、自动化等专业的课程设计与毕业设计学习者,也可供需要了解IC卡读写底层原理的嵌入式初学者参考。内容从硬件方案论证入手,讲解89C2051核心控制、SLE4442芯片内部结构与传输协议、复位响应、命令模式、逻辑加密机制,以及RS-232串行接口和卡座接口电路,再延伸到汇编语言软件设计、数据校验与软硬件联合调试,并附电路原理图、程序流程图与程序清单,帮助读者理清读卡器从接口时序到上位机通信的完整链路。资源包内为1份pdf文档,压缩包约1.86MB,篇幅完整、结构清晰,可作为课设报告撰写与方案复现的参考模板。目前已有120人学习浏览,适合希望从硬件接口到软件协议系统掌握IC卡读写的读者入手研读。
1. 从一张门禁卡讲起:单片机读卡器到底在解决什么问题
手边放一张 13.56MHz 的 Mifare 卡、一块 RC522 模块、一片 STC89C52 和一根 USB 转串口线,卡片贴上去,串口助手里跳出一串 4 字节 UID,这就是一个最小可用的 IC 卡读卡器。门禁机、考勤机、校园卡、充电桩的刷卡模块,剥到最里层基本都是这套结构:单片机管时序和协议状态机,射频前端管跟卡对话。选 51 还是 STM32、选 RC522 还是 PN532,决定的不是能不能读,而是你在软件模拟 SPI、天线调谐、扇区密钥管理上要花多少时间。这里把硬件选型、驱动代码、扇区读写参数和调不稳时的排查路径按顺序拆开讲一遍,每条命令和每个寄存器值都能直接抄进 Keil 工程。
2. RC522 和 PN532 怎么选:IC 卡读卡器的硬件链路与 SPI 接线
2.1 射频前端和单片机各管什么
很多人第一次接线时会以为单片机在“读卡”,其实单片机一根载波都不发。13.56MHz 的交变磁场由射频前端芯片驱动天线线圈产生,卡片靠这个磁场取电,再用负载调制把数据“反射”回来。单片机做的事情只有三件:按 SPI 时序把寄存器写进去、按协议帧把命令塞进 FIFO、读回中断标志和 FIFO 里的数据。分工理清了,选型问题就变成两个独立问题——射频前端支持哪些卡型,主控能不能稳定跑出 SPI 时序。
2.2 常见方案的参数对比
| 模块/卡型 | 协议 | 接口 | 供电 | 支持卡型 | 典型场景 |
|---|---|---|---|---|---|
| MFRC522 | ISO14443A | SPI(部分小板带 I2C/UART) | 3.3V | Mifare Classic 1K/4K、Ultralight、NTAG | 门禁、考勤、毕业设计 |
| PN532 | ISO14443A/B、Felica、ISO15693 | I2C/SPI/UART 三选一 | 3.3V | 卡型更全,支持卡模拟与点对点 | 需要读 B 卡或多协议 |
| AT24C 系列 | I2C 两线 | 8 脚直连 | 1.8~5.5V | 24C01~24C512 | 接触式存储卡,非射频 |
| SLE4442/4428 | ISO7816 触点 | 三线 | 5V/3V | 逻辑加密卡 | 老式 IC 卡表具 |
RC522 的优势是便宜、资料密集、寄存器手册只有几十页;PN532 把防冲突和帧封装都做进固件里,串口发一条0x4A 01 00就能拿到 UID,代价是贵一截,而且 SPI 模式下需要先切模式。如果只要读 A 卡 UID,RC522 足够;如果手里同时有 B 卡和 15693 标签,直接上 PN532 更省事。
注意:RC522 模块标称 3.3V 供电,5V 单片机的 IO 直接怼上去短期能跑,长期会缩短模块寿命。常见做法是在 SCK、MOSI、NSS 三根线上各串一个 1k 电阻,或者换成 STC8、STC32 这类可跑 3.3V 的单片机。
2.3 与 51 单片机的接线表
| RC522 引脚 | STC89C52 引脚 | 说明 |
|---|---|---|
| SDA / NSS | P1.0 | 片选,低有效,一次通信期间拉低 |
| SCK | P1.1 | 时钟,软件模拟,空闲为低 |
| MOSI | P1.2 | 主机输出,模块输入 |
| MISO | P1.3 | 模块输出,主机输入 |
| RST | P1.4 | 复位,低有效,上电后拉高 |
| IRQ | 不接 | 查询方式够用 |
| VCC / GND | 3.3V / GND | 单独 LDO 供电更稳 |
这套接法用的是普通 IO 口,没有占用 51 的定时器和串口,后面串口打印 UID 时不会打架。
3. 在 51 单片机上跑通 RC522 的最小驱动:寻卡、防冲突、选卡
3.1 51 没有硬件 SPI,先写软件模拟
STC89C52 这类 51 内核没有 SPI 外设,只能用 IO 翻转模拟。MFRC522 的 SPI 是模式 0,时钟空闲为低、上升沿采样,手册给的最高时钟是 10MHz,实际用 1MHz 量级最稳,因为 51 在 11.0592MHz 晶振下一条指令就要一个机器周期。
// 软件模拟 SPI:模式 0,上升沿采样,一次交换 8 位 sbit RC522_NSS = P1^0; sbit RC522_SCK = P1^1; sbit RC522_MOSI = P1^2; sbit RC522_MISO = P1^3; sbit RC522_RST = P1^4; unsigned char SPI_RW(unsigned char byte) { unsigned char i, recv = 0; for (i = 0; i < 8; i++) { RC522_SCK = 0; RC522_MOSI = (byte & 0x80) ? 1 : 0; byte <<= 1; RC522_SCK = 1; // 上升沿,模块采样 MOSI recv <<= 1; if (RC522_MISO) recv |= 0x01; } RC522_SCK = 0; return recv; }循环里先拉低时钟再摆数据,拉高后读 MISO,顺序反了读回来的就是错位数据。如果读 VersionReg 拿到 0x00 或 0xFF,先查 NSS 是不是在上电时被拉低了。
3.2 寄存器读写与天线开启
RC522 的地址是 6 位,字节的最高位表示读或写,次高位表示地址自增。
unsigned char ReadRawRC(unsigned char addr) { unsigned char v; RC522_NSS = 0; SPI_RW(((addr << 1) & 0x7E) | 0x80); // 读:最高位置 1 v = SPI_RW(0x00); RC522_NSS = 1; return v; } void WriteRawRC(unsigned char addr, unsigned char value) { RC522_NSS = 0; SPI_RW((addr << 1) & 0x7E); // 写:最高位为 0 SPI_RW(value); RC522_NSS = 1; } void PcdAntennaOn(void) { unsigned char v = ReadRawRC(0x14); // TxControlReg if (!(v & 0x03)) // Tx1RFEn 和 Tx2RFEn 要同时置 1 WriteRawRC(0x14, v | 0x03); }天线这两位是很多“读不到卡”问题的根因:只置一位时载波功率不够,卡片贴到 1cm 内才偶尔有反应。复位后先读 VersionReg,正常值一般是 0x91 或 0x92,对不上就别往下调了,先把接线和供电理清。
3.3 寻卡与防冲突的调用链
RC522 的收发都走同一个底层函数:把命令和数据写 FIFO、启动命令、轮询中断标志、把结果读回来。
// PCD_TRANSCEIVE = 0x0C,PCD_AUTHENT = 0x0E char PcdComMF522(unsigned char command, unsigned char *pIn, unsigned char inLen, unsigned char *pOut, unsigned int *pOutBits) { char status = MI_ERR; unsigned char irqEn = 0, waitFor = 0, lastBits, n; unsigned int i; if (command == 0x0E) { irqEn = 0x12; waitFor = 0x10; } if (command == 0x0C) { irqEn = 0x77; waitFor = 0x30; } WriteRawRC(0x02, irqEn | 0x80); // ComIEnReg,允许中断 WriteRawRC(0x04, 0x7F); // ComIrqReg,清所有中断标志 WriteRawRC(0x0A, 0x80); // FIFOLevelReg,FlushBuffer WriteRawRC(0x01, 0x00); // CommandReg = IDLE for (i = 0; i < inLen; i++) WriteRawRC(0x09, pIn[i]); WriteRawRC(0x01, command); if (command == 0x0C) WriteRawRC(0x0D, ReadRawRC(0x0D) | 0x80); // StartSend i = 2000; // 超时计数,按主频调整 do { n = ReadRawRC(0x04); i--; } while (i && !(n & 0x01) && !(n & waitFor)); WriteRawRC(0x0D, ReadRawRC(0x0D) & 0x7F); if (i == 0) return MI_ERR; if (ReadRawRC(0x06) & 0x1B) return MI_ERR; // ErrorReg 有错 status = MI_OK; if (n & irqEn & 0x01) status = MI_NOTAGERR; // 定时器中断 = 没卡 if (command == 0x0C) { n = ReadRawRC(0x0A); lastBits = ReadRawRC(0x0C) & 0x07; *pOutBits = lastBits ? (n - 1) * 8 + lastBits : n * 8; if (n == 0) n = 1; if (n > 16) n = 16; for (i = 0; i < n; i++) pOut[i] = ReadRawRC(0x09); } WriteRawRC(0x01, 0x00); return status; }寻卡用 REQA(0x26)只叫醒空闲卡,WUPA(0x52)连挂起态的卡一起唤醒,返回的backBits应该是 0x10,即 2 字节。
char PcdRequest(unsigned char reqMode, unsigned char *tagType) { char status; unsigned int backBits; WriteRawRC(0x0D, 0x07); // BitFramingReg,最后 3 位有效 tagType[0] = reqMode; status = PcdComMF522(0x0C, tagType, 1, tagType, &backBits); if (status != MI_OK || backBits != 0x10) return MI_ERR; return MI_OK; }防冲突阶段卡片回 5 字节,前 4 字节是 UID,第 5 字节是 BCC 校验,异或不为 0 就说明通信过程被干扰了。
char PcdAnticoll(unsigned char *pSnr) { char status; unsigned char i, bcc = 0, buf[2]; unsigned int unLen; WriteRawRC(0x0D, 0x00); buf[0] = 0x93; // PICC_ANTICOLL1 buf[1] = 0x20; WriteRawRC(0x01, 0x00); status = PcdComMF522(0x0C, buf, 2, pSnr, &unLen); if (status == MI_OK) { for (i = 0; i < 4; i++) bcc ^= pSnr[i]; if (bcc != pSnr[4]) status = MI_ERR; } return status; }主循环里把两步串起来,中间加 200ms 延时,别用空转死循环,否则天线和串口都得不到足够时间。
4. Mifare 扇区读写与密钥认证:参数怎么设才不锁卡
4.1 1K 卡的扇区与块结构
Mifare Classic 1K 的 1024 字节被切成 16 个扇区,每个扇区 4 个块,每块 16 字节。块 0 是厂商块,存 UID 和厂商数据,只读;块 1、2 是数据块;块 3 是尾块,结构固定为 6 字节密钥 A + 4 字节访问位 + 6 字节密钥 B。出厂默认密钥 A 和密钥 B 通常都是 6 个 0xFF,访问位默认FF 07 80 69。
| 参数 | 取值 | 说明 |
|---|---|---|
| 扇区号 | 0~15 | 业务上按扇区划分 |
| 块地址 | 扇区号 × 4 + 块偏移 | 认证和读写都用块地址 |
| 认证模式 | 0x60 / 0x61 | 0x60 用密钥 A,0x61 用密钥 B |
| 密钥长度 | 6 字节 | 不足或超出都会认证失败 |
4.2 认证命令的参数含义
认证时送进去 12 个字节:1 字节认证模式 + 1 字节块地址 + 6 字节密钥 + 4 字节 UID。块地址填错是新手最常踩的坑——想操作扇区 3 的数据,地址要填 12 而不是 3。
char PcdAuthState(unsigned char authMode, unsigned char addr, unsigned char *pKey, unsigned char *pSnr) { unsigned char status, buf[12]; unsigned int unLen; buf[0] = authMode; // 0x60 或 0x61 buf[1] = addr; // 块地址,不是扇区号 memcpy(&buf[2], pKey, 6); memcpy(&buf[8], pSnr, 4); status = PcdComMF522(0x0E, buf, 12, buf, &unLen); if (status != MI_OK || !(ReadRawRC(0x08) & 0x08)) return MI_ERR; return MI_OK; // Status2Reg 的 MFCrypto1On 位必须为 1 }认证通过后,卡片内部会保持一个会话状态,接下来对同扇区的读写都不用再认证。换到另一个扇区必须重新认证,否则读回来全是 0。
4.3 读块与写块的返回值判断
读块命令 0x30,正常返回 16 字节,长度是 0x90 位。
char PcdRead(unsigned char addr, unsigned char *pData) { unsigned char status, buf[2]; unsigned int unLen; buf[0] = 0x30; // PICC_READ buf[1] = addr; status = PcdComMF522(0x0C, buf, 2, pData, &unLen); if (status != MI_OK || unLen != 0x90) return MI_ERR; return MI_OK; }写块要分两次交互:先发命令和地址,卡回 0x0A 表示“我准备好了”,再发 16 字节数据,卡再回一次 0x0A 才算成功。中间任何一步没回 0x0A 都要当成失败,别继续往下走。
char PcdWrite(unsigned char addr, unsigned char *pData) { unsigned char status, buf[18]; unsigned int unLen; buf[0] = 0xA0; // PICC_WRITE buf[1] = addr; status = PcdComMF522(0x0C, buf, 2, buf, &unLen); if (status != MI_OK || unLen != 4 || (buf[0] & 0x0F) != 0x0A) return MI_ERR; memcpy(buf, pData, 16); status = PcdComMF522(0x0C, buf, 16, buf, &unLen); if (status != MI_OK || unLen != 4 || (buf[0] & 0x0F) != 0x0A) return MI_ERR; return MI_OK; }4.4 改密钥的正确顺序
尾块只能整块写,不能只改其中几个字节。稳妥的顺序是:先用当前密钥认证 → 读尾块 16 字节到内存 → 在内存里改密钥 A、密钥 B 或访问位 → 写回 → 立刻用新密钥重新认证一次确认。
注意:访问位写错会让整个扇区变成只读甚至永久锁死,卡就报废了。改之前把原 16 字节完整打印出来留底,改的时候只在备份上做替换,不要凭记忆拼字节。
5. 读卡器调不稳的常见原因:天线、供电与调试技巧
读卡距离从 3cm 掉到贴着才响应,八成不是代码问题。先量供电:寻卡瞬间模块电流会冲到 100mA 以上,USB 转串口模块引出的 3.3V 往往带不动,电压一塌就表现为时好时坏。换独立 LDO 供 3.3V,并在模块电源脚旁边并一个 10μF 电解加 0.1μF 瓷片,很多“偶发读不到”会直接消失。天线本身是 PCB 走线,匹配电容一般在 27pF 附近,模块不同批次有偏差,不建议自己拆改,除非确认是天线被金属遮挡或贴在金属面板背面。
5.1 用串口日志缩小故障范围
把每一步的结果都打出来,比猜快得多。在寻卡前后各加一行输出,就能分清是根本没载波、还是载波有但防冲突失败:
unsigned char ver = ReadRawRC(0x37); // 0x91/0x92 说明 SPI 通了 printf("VER=%02X\r\n", ver); if (PcdRequest(0x52, tagType) == MI_OK) { if (PcdAnticoll(uid) == MI_OK) printf("UID=%02X%02X%02X%02X BCC=%02X\r\n", uid[0], uid[1], uid[2], uid[3], uid[4]); else printf("ANTICOLL FAIL\r\n"); // 有卡但校验不过,查天线和电源 } else { printf("NO CARD\r\n"); // 完全没响应,查供电和天线开关 }VER 读不到就先停下,其余都是空谈。串口波特率用 9600,51 在 11.0592MHz 下误差最小。
5.2 状态机卡死与掉卡重连
单片机上最容易写坏的是“卡一拿走就死在等待里”。收发的超时计数i = 2000是按主频估的,主频翻倍就要跟着调。更稳的写法是让PcdComMF522在任何分支退出前都把 CommandReg 写回 IDLE、清一次 FIFO,这样下一轮寻卡不会被上一轮的残留数据污染。业务层用一个三态循环:IDLE 只发 Request,CARD_PRESENT 做认证和读写,ERROR 先 Flush 再退回 IDLE,每个状态都只做一件事。
5.3 密钥和扇区操作的最小验证顺序
拿到新卡先别急着写。按这个顺序逐级验证:读 VersionReg → 寻卡拿 UID → 用默认密钥FF FF FF FF FF FF认证扇区 0 的块 0 → 读块 0 比对 UID 和厂商数据 → 再碰数据块。前四步全过再动尾块。密钥认证失败时先换密钥 B 试一次,很多卡只开放了其中一组;两组都失败基本可以判断这张卡被改过密钥,没有原始密钥就没有后续。
本文还有配套的精品资源,点击获取