几年前我第一次用RC522做读卡器,网上找一段例程改吧改吧,卡号就出来了。后来换了一个带独立NFC控制器的板子,要自己跑协议栈,才发现当初那些被封装掉的“读卡流程”才是真正的分水岭。ISO14443A读卡流程,说白了就是从射频场建立、请求应答、防碰撞、选卡到应用层交互的完整状态机。这篇文章我准备把这条链路从头拆到尾,把REQA、ATQA、防碰撞、SELECT、SAK、ATS这些环节背后的机制和工程细节一次说清楚。无论你是刚接触NFC的嵌入式新手,还是被奇怪的读卡问题纠缠过一阵子的老手,这篇文章应该能帮你补齐那些平时容易被忽略的底层逻辑。
1. 从RF场到UID:ISO14443A读卡流程到底在做什么
1.1 五层台阶
ISO14443A读卡流程不是一个“函数调用”,而是一组按固定顺序发生的协议交互。标准里PCD(读卡器)和PICC(卡片)之间要通过多次消息往返,才能从一张处于“未知状态”的卡变成可以交换应用数据的通信对象。这整个过程可以拆成五个阶段。
第一阶段是射频场激活。PCD产生13.56MHz的载波场,卡片进入场中后获得能量完成上电复位,等待PCD发请求。第二阶段是请求阶段,PCD发出REQA,卡片回ATQA,双方建立最基本的联系。第三阶段是防碰撞阶段,场上同时有多张卡时,PCD需要通过位级仲裁选出一张卡,拿到完整UID。第四阶段是选择阶段,PCD发送SELECT命令,卡片返回SAK,表明自己是否支持ISO14443-4、是否还有下一级防碰撞等关键信息。第五阶段才是应用层交互,根据SAK选择Mifare原生命令还是ISO14443-4的APDU传输,完成认证、读写、钱包扣款之类的具体业务。
很多人在第一步就栽跟头,是因为把这些阶段当成“发一条命令读一个数据”来写,忽略了每个阶段都有独立的超时、重试和状态迁移逻辑。比如同样一张Mifare Classic卡,在IDLE态和HALT态对同一个命令的反应完全不同。不理解状态机,就会出现“这次能读下次读不了”的玄学问题。
1.2 PICC的五个状态,决定了你得按状态机来写程序
ISO14443-3里给PICC定义了若干个状态,工程上常用的有五个:POWER-OFF、IDLE、READY、ACTIVE、HALT。每个状态下卡片对命令的响应能力不一样。
POWER-OFF是卡片没有获得足够能量时的状态,读卡器把场关掉,卡就回到这里。卡片进场后上电进入IDLE态,此时它只对REQA和WAKEUP有反应。收到REQA并且PCD通过防碰撞和SELECT流程选中它之后,进入ACTIVE态,这时才可以执行应用层命令。如果PCD发出HLTA,卡片进入HALT态,HALT态下它不再响应REQA,只能被WAKEUP重新唤醒回IDLE态。
这个状态模型解释了为什么读卡程序必须是状态机而不是顺序调用。你发REQA之前可能要先发一个WAKEUP把所有卡从HALT态拉回来,否则上一次被HALT的卡不会理你。你做完防碰撞之后如果没走SELECT,卡不会进入ACTIVE,后面发任何读写命令都会被无视。所谓“读卡流程”,本质上就是在驱动这张卡经历“IDLE→READY→ACTIVE”的状态迁移。
2. REQA与ATQA:请求阶段的短帧、应答与状态判断
2.1 短帧里的REQA/WAKEUP差异
REQA命令的值是0x26,WAKEUP是0x52,这两个命令都使用短帧格式发送,短帧一共只有7bit有效数据。别看它们参数简单,背后的分工很明确:REQA只对IDLE态的卡片有效,WAKEUP除了对IDLE态有效,还能把HALT态的卡重新拉回IDLE态。
实际工程里,轮询代码通常会在每一轮最开始发一次WAKEUP,再发REQA。原因是上一轮如果读过某张卡并发了HLTA,这张卡正处于HALT态,直接发REQA它根本不回应;先用WAKEUP唤醒它,这张卡才会回到IDLE态参与新一轮竞争。有些驱动偷懒只发REQA,结果就是单张卡正常、多张卡轮换着读的时候经常丢卡。
这里还要提一个容易被误解的点:REQA/WAKEUP虽然是短帧,但PICC回应ATQA使用的是标准帧,而且ATQA需要用CRC_A校验。也就是说,请求阶段看似简单,其实已经涉及到了编码格式切换。调试时如果逻辑分析仪上看到ATQA波形乱七八糟,先检查是不是PCD发送REQA时使用的是修正Miller编码、而解码ATQA时切换到了Manchester副载波解码,很多自定义协议的读卡器就是在这两种编码切换上出问题的。
2.2 ATQA中的UID长度和防碰撞能力
ATQA是2字节数据,不同厂商、不同型号的卡片差异很大。做底层驱动时,不能把ATQA当成一个“无关紧要的应答”,它里面包含UID长度和防碰撞能力的早期声明。
常见经验值如下表所示,注意这些只是“常见值”,不同批次、不同封装的卡片可能存在差异,最终以芯片数据手册为准。
| 卡片类型 | 常见ATQA | 常见SAK | 典型说明 |
|---|---|---|---|
| Mifare Classic 1K | 0x0400 | 0x08 | 4字节UID,走Mifare原生命令 |
| Mifare Classic 4K | 0x0400 | 0x18 | 4字节UID,块数更多 |
| Mifare Ultralight / NTAG | 0x4400 | 0x00 | 4字节UID,走UL/NTAG命令 |
| DESFire EV1 | 0x0344 | 0x20 | 7字节UID,支持ISO14443-4 |
ATQA的低位部分一般会体现UID长度和防碰撞是否完整,但厂商自定义位也很多,所以不要写“ATQA等于某个固定值就认为是什么卡”的强判断代码,而是把它作为流程分支的一个参考。实际项目中更稳妥的做法是:先用ATQA判断基本类型范围,再结合SAK一起做最终派发。
2.3 轮询时最容易犯的固定超时错误
请求阶段还有一个很容易踩的坑:REQA发出后,ATQA并不是“立刻”回来。
ISO14443A定义了PICC响应与PCD帧之间的时间关系,叫FDT(帧延迟时间),同时还规定了卡片最大响应时间FWT。对于Type A,卡片的响应存在一个最小间隔,量级通常在几十微秒到上百微秒;而最大响应时间默认大概在5毫秒量级,并且卡片后续可以通过ATS协商修改。
很多自研读卡器在轮询阶段用一个很短固定超时,比如200微秒,结果在低温、弱场或者卡片离天线较远时频繁读不到卡。因为卡片收到REQA后,内部需要完成场整流、上电稳定、协议状态机切换,最后再回ATQA,这个时间不是一个严格定值。工程上我一般把轮询阶段的ATQA等待窗口设置为5~20毫秒,同时配合多次重试;而进入ISO14443-4块传输后,再根据ATS协商的FWT动态配置块超时。这里的原则是:请求阶段宁长勿短,块传输阶段按卡片的FWT走。
3. 防碰撞的级联机制:为什么一张卡会来回“冒泡”
3.1 多卡同时进场,数据为什么会撞车
当多张卡同时处于IDLE态,PCD发出REQA后,它们可能都会回复ATQA。更麻烦的是,在防碰撞阶段,PCD要求所有IDLE/READY态的卡发送自己的UID,如果两张卡UID的前缀相同,它们会在同一时刻上拉或下拉负载,PCD解码时就会看到一位同时出现0和1的边沿,这就是“碰撞”。
ISO14443A的防碰撞机制采用“位级仲裁”:PCD先让所有卡发送完整UID片段,通过Manchester编码检测第一位碰撞的位置;然后PCD只发送碰撞之前已经正确收到的位,要求卡在剩余位继续竞争。每轮都能筛掉一批卡,最终只留下一张。这个过程很像一群人同时喊自己的编号,你只听清了前几位,就让大家从听得清的位置继续往下喊,直到只剩一个人。
3.2 NVB参数的正确计算方式
防碰撞命令的关键参数是NVB,表示“本次命令中PCD实际发送了多少有效位”。这个数字包含SEL和NVB这两个字节,而不只是后面的UID数据。
NVB的高半字节表示完整发送的字节数,低半字节表示最后一个不完整字节中发送的有效bit数。例如0x20表示只发送了2个完整字节(SEL、NVB),后面的UID数据全部不发送,这是典型的“请求完整UID”命令;0x50表示发送了5个完整字节,也就是SEL、NVB以及3字节UID前缀;0x53表示发送了5个完整字节外加第6个字节的前3bit。
计算NVB的代码非常简单,但错就错在很多人把UID数据的长度直接当成字节数去拼NVB,忽略SEL和NVB本身也要计入。一个稳妥的写法是这样:
static uint8_t build_nvb(int complete_bytes, int valid_bits) { return (uint8_t)((complete_bytes << 4) | (valid_bits & 0x0F)); } // 只发SEL+NVB,请求完整UID片段 uint8_t nvb_request = build_nvb(2, 0); // 0x20 // 选择命令:SEL+NVB+5字节数据(4字节UID+BCC) uint8_t nvb_select = build_nvb(7, 0); // 0x703.3 CT标记与多级UID拆分
ISO14443A的UID有三种长度:4字节、7字节、10字节。4字节UID只需要一级防碰撞就可以完成;7字节UID需要两级;10字节UID需要三级。
这里有个容易懵的点:级联防碰撞里每级固定携带4字节“UID片段”,但第一位可能是级联标记CT,CT的值是0x88。如果PCD在第一级防碰撞收到的前4字节中首字节是0x88,说明后面跟着的只有3个真正的UID字节,当前级并不能凑出完整UID,必须继续下一级。
以7字节UID为例,第一级拿到的数据结构是:CT=0x88、UID0、UID1、UID2、BCC1。PCD先用SEL=0x93完成这一级的选择,然后切换到SEL=0x95进行第二级防碰撞,第二级才拿到UID3、UID4、UID5、UID6、BCC2。10字节UID进一步使用SEL=0x97。每一级都要做完整的防碰撞和SELECT,不能跨级跳。
判定当前级是否结束的方法,是看收到片段的首字节是不是0x88,以及最后SAK中代表“UID是否完整”的bit。两个信号配合使用,才能确认级联是否继续。
3.4 BCC校验:一个字节保护一串卡号
每一级UID片段包含5字节数据,最后一个字节是BCC,它是前四个字节的异或结果。BCC的作用是防止防碰撞阶段因误码选出错误UID。
static uint8_t compute_bcc(const uint8_t uid_cln[4]) { return uid_cln[0] ^ uid_cln[1] ^ uid_cln[2] ^ uid_cln[3]; }举个例子,某一级收到的4字节是0x04、0x3A、0x5B、0x01,那么BCC就是0x04 ^ 0x3A ^ 0x5B ^ 0x01 = 0x64。如果收到的第5字节不是0x64,说明这一级数据已经被干扰,应当丢弃并重新发起防碰撞,而不是继续往下走。
工程实现中,防碰撞循环要有次数上限,不能无限重试。常见做法是单级最多重试3~5次,达到上限就放弃本轮轮询,回到REQA阶段重新开始,避免因为一张损坏卡卡死整个读卡流程。
4. SELECT与SAK:选卡之后的协议分叉点
4.1 SELECT命令的NVB为什么是0x70
防碰撞成功后,PCD会发送SELECT命令,明确告诉场上的卡“我要选择你”。SELECT命令由SEL、NVB、完整UID片段(4字节)加BCC、CRC_A组成。
之前我说过NVB计算要把SEL和NVB计入。完整数据时,SEL占1字节、NVB占1字节、UID片段加BCC占5字节,一共7字节,所以NVB自然就是0x70。正因为如此,很多人误以为0x70是“发7字节数据”,其实它指的是整个命令里SEL+NVB+数据字段一共7个完整字节,CRC_A不算在内。
收到SELECT后,PICC会回传SAK,卡片这时才真正进入ACTIVE态。所以判断“选卡是否成功”,不能只看SELECT发出去没、有没有回包,而要解析SAK内容。
4.2 SAK里的三个关键bit
SAK是一个字节,不同厂商的卡对这个字节的自定义位非常多,但标准和工程上最常用的三个信息点如下。
第一个是UID是否完整。SAK中bit6为1,说明当前级联还没有完成,后面还要继续防碰撞;bit6为0,说明UID已经完整,选卡成功。第二个是是否支持ISO14443-4。SAK中bit5为1,代表卡片支持ISO14443-4的块传输协议,流程要切换到RATS/ATS分支;bit5为0,代表卡片走Mifare原生命令或者其他私有命令集。第三个是厂商/型号信息。这部分没有统一标准,比如Mifare Classic 1K常见SAK=0x08,Mifare Classic 4K常见SAK=0x18,NTAG常见SAK=0x00。建议把这些值做成白名单,而不是用bit位去猜型号。
工程上最稳妥的派发逻辑是:先看bit6判断级联是否结束,再看bit5判断是否走ISO14443-4,最后用完整SAK查白名单确定具体驱动。SAK返回0x08的卡直接走Mifare Classic认证读写;SAK返回0x20的卡进入ATS流程,再通过ATS内容确认具体协议能力。
4.3 分叉后的两条路:Mifare原生命令与ISO14443-4
SAK一旦返回,读卡流程就分叉了。Mifare Classic这类卡不走ISO14443-4,而是使用NXP私有的认证和读写命令。ISO14443-4是标准块传输协议,适用于DESFire、Java Card、部分银行卡等,需要先走RATS/ATS协商参数。
这个分叉点是底层驱动设计中最容易写乱的部分。很多通用NFC库会在SAK返回后统一尝试ATS,结果遇到Mifare Classic时卡片不会响应RATS,白白耗费超时;反过来,一些只写过Mifare Classic驱动的工程师看到DESFire的SAK也直接走Crypto-1认证,结果卡在认证命令上报错。正确做法是把SAK作为主状态机的一个分支条件,两个协议栈并行实现,由SAK决定启用哪一套。
5. 应用层交互流程:认证、块传输与HALT收尾
5.1 Mifare Classic的扇区认证与读写
Mifare Classic 1K一共16个扇区,每个扇区4个块,每个块16字节。认证的单位是扇区而不是块。也就是说,你要读写某个块,必须先对所在扇区做认证。
认证命令的关键字是0x60(Key A)或0x61(Key B),后面跟块号和密钥。卡片收到后会返回一个随机数,之后所有数据交互都通过Crypto-1流密码加密。读写单个块时,读命令是0x30加块号,写命令是0xA0加块号和16字节数据。注意这里的“读写”是整块读写,驱动层需要做好块号与扇区号的换算。
这个流程有几个常见问题。一是认证失败后不能立刻重试同一个扇区,卡片内部有错误计数器,频繁重试可能让卡片在一段时间内拒绝认证。二是认证状态只对当前选中的扇区有效,切换到另一个扇区必须先重新认证。三是数据写入后建议立即读回校验,因为近距离无接触传输对场强波动很敏感,单纯“写成功”并不等于数据落对了。
5.2 ISO14443-4的RATS/ATS与块传输
支持ISO14443-4的卡片在收到SAK之后再收到RATS命令(0xE0加参数)才会进入标准块传输模式。RATS里的参数可以指定PCD的帧尺寸FSD和逻辑通道CID,卡片返回ATS作为应答。ATS里包含TL、T0、TA、TB、TC和历史字节,核心作用是告诉PCD卡片能支持多大的帧尺寸、要不要CID、要不要NAD、以及默认FWT大概是多少。
拿到ATS之后,后续数据交换使用I-block(信息块)、R-block(确认块)和S-block(控制块)。I-block承载真正的APDU数据,R-block负责ACK或NAK确认,S-block用于WTX(等待时间扩展)和DESELECT。
实际开发中,很多自定义驱动在这里栽跟头:要么把RATS参数里的FSD设置得比卡片FSC还大,导致卡片直接不回应;要么忽略ATS里的FWI,把块超时设得比卡片FWT短,导致大流量传输频繁超时。我的经验是,先把ATS原始字节完整解析出来,再根据FWI计算WTX参数,最后才进入APDU交互。
5.3 HLTA的正确时机
HLTA命令值0x50,加0x00和CRC_A发送给卡片,能让处于ACTIVE态的卡片进入HALT态。HALT态的好处是让卡片退出后续防碰撞竞争,减少场上多卡时的干扰。
这张卡读完不想要了,应该立即发HLTA;如果接下来还要读其他卡,HLTA能让当前卡暂时“退下”,下一轮轮询再通过WAKEUP把它叫回来。如果一直不发送HLTA,这张卡虽然已经读完,但它仍然处于ACTIVE态,场上其他卡片在防碰撞时可能会收到来自它的干扰信号,尤其在同时读取多张卡片的场景里,会出现“串卡”。
每次轮询开始时先发WAKEUP再发REQA的原因,也是因为上一轮可能有些卡进入了HALT态。HALT态设计是ISO14443A协议里很基础但很容易被忽略的一环,时序上一定要留给它足够的位置。
6. 实测调优:波形验证、时序边界与故障排查
6.1 逻辑分析仪观察ISO14443A需要关注的信号
调试底层读卡流程时,一把采样率足够的逻辑分析仪比什么都管用。ISO14443A中PCD到PICC方向使用的是100% ASK调制、修正Miller编码,PICC到PCD方向是847kHz副载波负载调制、Manchester编码。想同时观察两个方向的信号,最好通过天线差分信号或者芯片内部解调输出点接探头。
采样率建议不低于10MS/s,有条件直接上50MS/s。ISO14443A的副载波是847kHz,Manchester编码的位速率是106kbps,每个数据位对应约8个副载波周期,太低采样率根本看不出碰撞时那种代表性边沿错乱。
逻辑分析仪主要看三件事:一是REQA发出去之后,ATQA有没有按标准时序回来;二是防碰撞阶段,多个卡同时回应时Manchester波形是否出现碰撞特征;三是SELECT之后SAK是否完整、CRC是否通过。把这四个关键帧抓下来保存成模板,后面每次调天线、改驱动,都拿新波形和模板对比,效率会高很多。
6.2 从波形反推代码问题的一次排查记录
有一次我调试一款自研读卡器,现象是读Mifare Classic 1K成功率只有六成,失败时没有任何错误码,直接收不到SAK。一开始怀疑天线功率不够,加了功放反而更差。用逻辑分析仪抓波形后发现,REQA、ATQA、防碰撞都正常,SELECT命令也发出去了,但卡片回SAK的起始位置比驱动等待窗口晚了大概60微秒。
顺着波形继续看,发现驱动在SELECT之后的接收窗口设成了固定300微秒,而卡片因为内部Crypto-1状态初始化,实际回包超过了这个值。修正方法是把SELECT后的响应等待改成动态参数:先用一个较宽的初始窗口,后续再根据卡片类型和实测调整。最终把超时窗口调到1毫秒,成功率直接回到99%以上。
这个案例说明,读卡流程调试不能只盯着代码里的循环和返回值,卡片的物理层响应时间与驱动超时窗口的匹配,往往才是“灵异故障”的根源。
6.3 读卡失败自查表
下面这些排查点是我在实际项目中反复用到的,建议直接打印出来贴工位。
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全读不到卡 | 天线失谐、场未建立、卡片不在场 | 示波器看天线谐振波形,确认卡片是Type A |
| 偶尔能读偶尔不能 | REQA后ATQA等待窗口过短 | 延长请求阶段超时到5~20ms |
| 防碰撞循环卡死 | NVB计算错误、CRC校验失败 | 抓波形确认NVB是否为正确的完整字节数 |
| 多卡时选错卡 | 未正确执行级联或忘记HLTA | 按SEL=0x93/0x95/0x97逐步走完整状态机 |
| SAK后协议分支错误 | 只判断SAK的bit位没做白名单 | 先看bit6级联,再看bit5支持14443-4,最后查白名单 |
7. 工程落地:读卡芯片选型与低功耗轮询代码骨架
7.1 几类读卡方案的适用边界
做读卡器选型时,很多人只盯着“支持ISO14443A”这一句话,其实不同芯片对ISO14443A的覆盖深度差异巨大。
| 方案 | 特点 | 适合场景 |
|---|---|---|
| RC522/RC522 | 成本低、资料多,但对ISO14443-4支持较弱 | Mifare Classic门禁、简易读卡器 |
| PN532 | 内嵌完整NFC协议栈,支持14443A/B、Felica | 原型验证、通用NFC模块 |
| CLRC663 | 射频功率强、支持多种协议,适合专业读写器 | 工业读卡器、多协议设备 |
| ST25R3916 | 接收灵敏度高、低功耗轮询能力强 | 低功耗手持设备、高要求嵌入式 |
选型时除了看协议支持,还要看芯片是否自带完整防碰撞状态机。自带协议栈的PN532让你不用关心NVB和SAK,但代价是功耗和灵活性都受限;用RC522这类射频前端自己跑协议,则必须把ISO14443A的状态机吃透,这也是我为什么建议每位嵌入式工程师至少手动实现一遍读卡流程的原因。
7.2 低功耗轮询的设计要点
电池供电的读卡器对功耗非常敏感,低功耗轮询的核心思路是“短时间打场,快速判断有没有卡,没有立刻掉场”。
具体做法是:周期性唤醒MCU,打开射频场,发送WAKEUP和REQA,等待ATQA的时间窗口控制在几个毫秒以内。如果收到ATQA,继续执行完整读卡流程;如果没有收到,马上掉电关场,进入睡眠,等待下一个周期。打场时间越短,平均功耗越低,但太短会导致卡片上电不稳、响应概率下降,所以这个窗口需要实测权衡。
部分芯片比如ST25R3916有自动低功耗轮询模式,可以配置脉冲场参数、轮询周期和协议类型,MCU可以全程睡眠,检测到卡片再被中断唤醒。使用这类芯片时,仍然需要理解ISO14443A读卡流程,因为自动轮询只解决“发现卡”,发现之后的防碰撞、选卡和应用层流程还是要由芯片输出的中断事件驱动来完成。
7.3 一个可直接上手的轮询状态机伪代码
下面给出一个典型轮询状态机的骨架,它把流程拆成“请求、防碰撞、选卡、派发”四段,方便移植到不同芯片平台:
int iso14443a_poll(uint8_t *uid, uint8_t *uid_len, uint8_t *sak) { uint8_t uid_cl1[5]; uint8_t uid_cl2[5]; uint8_t uid_cl3[5]; // 1. 先用WAKEUP把HALT态的卡拉回IDLE,再发REQA send_short_frame(WAKEUP); send_short_frame(REQA); if (wait_atqa(20) < 0) { return -1; } // 2. 第一级防碰撞 if (anticollision(SEL_CL1, uid_cl1) < 0) { return -2; } // 3. 如果遇到级联标记,逐级向上 if (uid_cl1[0] == CT) { if (select_card(SEL_CL1, uid_cl1) < 0) { return -3; } if (anticollision(SEL_CL2, uid_cl2) < 0) { return -4; } if (uid_cl2[0] == CT) { if (select_card(SEL_CL2, uid_cl2) < 0) { return -5; } if (anticollision(SEL_CL3, uid_cl3) < 0) { return -6; } // 组装10字节UID memcpy(uid, &uid_cl1[1], 3); memcpy(uid + 3, &uid_cl2[1], 4); memcpy(uid + 7, uid_cl3, 4); *uid_len = 10; } else { // 组装7字节UID memcpy(uid, &uid_cl1[1], 3); memcpy(uid + 3, uid_cl2, 4); *uid_len = 7; } } else { // 4字节UID memcpy(uid, uid_cl1, 4); *uid_len = 4; } // 4. 发送SELECT,接收SAK,进入协议派发 if (select_card_for_sak(uid, *uid_len, sak) < 0) { return -7; } // 5. 这里根据SAK走Mifare原生分支或ISO14443-4分支 if ((*sak & 0x20) != 0) { enter_iso14443_4(); } else { enter_mifare_classic(); } return 0; }这段代码省略了超时重发、CRC校验和错误恢复,实际工程里每步都要加次数限制和状态清理。尤其是anticollision函数内部,必须根据碰撞位置动态调整NVB,不能只是一次性请求完整UID。这段骨架的价值是让你对整个读卡流程的“体积”有个直观认识:真正能用的轮询逻辑,远比一段示例代码复杂。
我在好几个项目里实践下来的体会是,ISO14443A读卡流程的难点从来不在某个单一命令,而在阶段之间的状态衔接。每次拿到新的读卡芯片或者新的卡片类型,我第一件事不是写业务逻辑,而是用逻辑分析仪把REQA到SAK的完整波形跑一遍,把标准时序存成模板。之后所有问题,包括天线调参、超时配置、协议派发,都以这份模板作为对照基准。这个习惯帮我省掉了大量“今天能读明天不能读”的排查时间,建议你也试试。