1. 项目概述与核心思路
最近在折腾射频相关的项目,想搞点跟别人不太一样的东西。市面上玩SDR(软件定义无线电)的很多,但我想试试用最基础的硬件和FPGA,从零开始搭一个完整的NFC读卡器。NFC的载波频率是13.56MHz,调制方式是调幅(ASK),这意味着硬件门槛可以很低——一块便宜的FPGA、一个3Msps的ADC,再加几个分立元件,就能玩起来。更重要的是,从载波生成、调制解调,到ISO14443A协议栈的编解码,全部在FPGA里用Verilog实现,形成一个完整、可复现的射频数字系统。这个项目就是FPGA-NFC,一个完全开源的、从分立元件到协议层的NFC PCD(读卡器)实现。
这个读卡器能干什么?简单说,它能通过串口命令,与市面上最常见的MIFARE Classic 1K(俗称M1卡,比如很多门禁卡)进行完整的ISO14443A标准交互,包括寻卡、防冲突、选卡等底层操作。对于想深入理解NFC物理层和链路层协议,或者想学习如何在FPGA中实现混合信号处理(数字逻辑+ADC接口+DSP算法)的朋友来说,这是一个非常“硬核”且有趣的实践项目。整个系统麻雀虽小,五脏俱全,涵盖了射频前端设计、数字信号处理、实时协议栈和串口通信,是一次对数字系统设计能力的综合锻炼。
2. 系统架构与模块拆解
整个系统的核心思想是“软硬结合,数字处理”。硬件负责产生13.56MHz载波并接收微弱的卡片调制信号,FPGA则负责所有的调制、解调、协议处理和用户交互。下图清晰地展示了数据流和模块划分:
Host-PC (UART) <---> FPGA <---> 模拟前端电路 <---> NFC卡片(PICC)2.1 硬件链路:从比特流到电磁波
硬件部分的核心是一个自制的NFC Breakout Board。它的作用是在FPGA的数字世界和空中的13.56MHz电磁波之间架起桥梁。
发送链路(PCD-to-PICC):FPGA的
carrier_out引脚输出一个13.56MHz的方波。这个方波驱动一个N沟道MOSFET(FDV301N),后者再去驱动一个由电感和电容组成的并联谐振电路。这个电路会在13.56MHz发生谐振,将方波中的基波分量大幅放大,从而在天线线圈中产生一个强力的、纯净的正弦波载波。调制过程很简单:当需要发送“1”时,FPGA正常输出载波;当需要发送“0”时,FPGA关闭载波输出。这就是100% ASK调制。接收链路(PICC-to-PCD):这是本项目最巧妙也最具挑战的部分。卡片(PICC)通过改变自身线圈的负载来调制反射回的信号,这种调制是微弱的2%-10% ASK。直接采样13.56MHz的载波需要极高的ADC采样率(>27Msps)。为了降低硬件成本,我们采用了一个经典的模拟技巧:包络检波。使用一个高速二极管(1N4148)对天线上的信号进行检波,再用RC电路提取出信号的包络线。这样,高频的13.56MHz载波被滤除,我们得到的是一个频率为847.5kHz(载波的1/16,即副载波频率)的基带信号。对这个847.5kHz的信号进行采样,只需要一个中等速度的ADC(本项目选用3Msps的AD7276B)即可。
2.2 FPGA内部数字处理流水线
FPGA内部的Verilog代码构成了一个高效的处理流水线,主要分为发送链、接收链和控制接口三大部分。
发送链(TX Chain):
uart_rx.v & uart_rx_parser.v: 接收来自PC的串口命令(ASCII格式的十六进制字节),将其解析为待发送的字节流,并存入FIFO。nfca_tx_frame.v:协议封装模块。这是发送链的核心协议处理单元。它从FIFO中读取字节,按照ISO14443A标准的规定,进行位编码(将每个字节拆分为位)、添加帧起始/结束标志、并计算并附加CRC校验码。用户无需手动计算CRC,模块会自动完成。nfca_tx_modulate.v:调制模块。它接收来自帧封装模块的比特流,并生成对应的100% ASK调制控制信号carrier_out。它精确控制每个比特的发送时长,确保符合标准规定的位时序。
接收链(RX Chain):
ad7276_read.v:ADC驱动模块。它通过SPI接口以40.68MHz的时钟(3Msps * 13.56 ≈ 40.68M,为降低时钟树复杂度而选取)控制AD7276B ADC,持续读取检波后的模拟信号数字化结果。nfca_rx_dsp.v:数字信号处理(DSP)核心。这是整个项目算法难度最高的部分。ADC采样得到的是包含噪声、直流偏移和微弱ASK调制的数字序列。该模块需要实时检测出幅度那微小的2%-10%的变化。我采用的算法是: a.中值滤波:用一个滑动窗口计算中值,有效滤除脉冲噪声。 b.基线跟踪与减法:用滤波后的信号减去一个缓慢跟踪的基线(近似直流分量),得到纯交流的调制信号。 c.自适应阈值判决:根据信号的能量动态计算一个阈值,当交流信号超过阈值时判为“1”,否则为“0”。这种比例阈值法能适应不同卡距导致的信号强度变化。nfca_rx_tobits.v:位同步与帧定界模块。它接收DSP模块输出的比特流,通过搜索特定的帧起始模式(SOF)来锁定位边界,并将连续的比特流切割成独立的位。nfca_rx_tobytes.v:字节重组模块。它将接收到的位按照ISO14443A标准进行解码(处理防冲突时的位导向帧),重新组装成字节,并通过FIFO缓存。
控制与交互:
nfca_controller.v:顶层状态机控制器。它协调发送和接收流程。例如,在发送完一帧命令后,自动切换到接收状态,并开启一个超时定时器。uart_tx.v: 将接收FIFO中的卡片响应字节,转换成ASCII十六进制格式,通过串口发送回PC。fpga_top.v:最顶层的系统集成模块。它例化了PLL(将外部50MHz时钟倍频到81.36MHz供核心逻辑使用),并将所有子模块、IO引脚连接起来。
设计心得:时钟域规划整个系统涉及多个时钟域:外部晶振的50MHz,PLL生成的81.36MHz系统主时钟,以及驱动ADC的40.68MHz SPI时钟。处理好跨时钟域数据传输(例如从ADC读取模块到DSP模块)是关键。本项目在关键路径上使用了异步FIFO(
fifo_sync.v虽然名为sync,但在顶层通过正确连接实现了异步功能)来安全地传递数据,避免了亚稳态问题。这是高速数字系统设计中必须重视的一点。
3. 硬件搭建与FPGA部署详解
3.1 PCB制作与焊接要点
项目的硬件核心是一块自制的NFC Breakout Board。设计文件已在立创EDA开源。打样和焊接时需要注意以下几点:
物料采购:BOM清单中的核心器件包括:
- FDV301N: N沟道MOSFET,用于驱动谐振电路。务必确认引脚顺序(GDS)。
- L1 (1.2μH电感)和C1 (115pF电容): 它们构成了13.56MHz的并联谐振电路。电感的品质因数(Q值)尽量高,电容建议使用NP0/C0G材质的贴片电容,以保证谐振频率稳定。
- D1 (1N4148): 高速开关二极管,用于包络检波。不能用普通的整流二极管代替,其反向恢复时间必须足够短。
- U1 (AD7276B): 3Msps、12位SAR型ADC。注意其供电电压为2.35V至3.6V,本项目采用3.3V供电。其SPI接口为3.3V CMOS电平。
- 线圈: 采用4匝的矩形空心线圈。线径和匝间距会影响电感量和Q值,从而影响读写距离。严格按照PCB上的布线来绕制。
焊接与调试:
- ADC(AD7276B)是MSOP-8封装,焊接需要一定的技巧,建议使用热风枪和助焊膏。焊接后务必检查有无短路或虚焊。
- 焊接完成后,先不要连接FPGA。用万用表测量电源输入端(J1)对地电阻,排除短路。然后上电(7-9V),测量板上3.3V LDO的输出是否正常。
- 最关键的一步:用示波器探头连接测试点J3(SMA接口)。在FPGA未连接、系统空闲时,这里应该是一个接近直流的电平。当FPGA工作并输出载波时,这里应能看到一个847.5kHz(副载波频率)的正弦波,其幅度会随着FPGA的发送数据而明显变化(100% ASK)。
3.2 FPGA工程配置与引脚约束
将RTL/目录下的所有.v文件添加到你的FPGA开发工具(Quartus, Vivado等)工程中。顶层文件是fpga_top.v。
时钟管理:
fpga_top.v中使用了Altera Cyclone IV的altpll原语来生成81.36MHz的系统时钟。如果你使用的是其他厂商的FPGA(如Xilinx),必须替换这部分代码。例如在Vivado中,你需要使用Clock Wizard IP核来生成一个81.36MHz的时钟。这是整个数字逻辑的“心脏”,频率必须准确。引脚分配(Constraints): 根据
fpga_top.v模块的端口定义,在你的FPGA开发板上找到对应的物理引脚并进行约束。以下是一个示例(以某个通用开发板为例):# 假设开发板时钟输入是PIN_E1,按键复位是PIN_A7 set_location_assignment PIN_E1 -to clk50m set_location_assignment PIN_A7 -to rstn_btn # NFC Breakout Board 接口 (使用PMOD接口) set_location_assignment PIN_B12 -to carrier_out set_location_assignment PIN_A12 -to ad7276_csn set_location_assignment PIN_D12 -to ad7276_sclk set_location_assignment PIN_C12 -to ad7276_sdata # UART接口 (连接板载USB转串口芯片) set_location_assignment PIN_B14 -to uart_rx set_location_assignment PIN_A14 -to uart_tx # LED指示灯 (可选) set_location_assignment PIN_A8 -to led0 set_location_assignment PIN_B8 -to led1 set_location_assignment PIN_A9 -to led2特别注意:
ad7276_sclk频率高达40.68MHz,必须分配到FPGA的全局时钟引脚或高性能IO引脚上,并且必须使用排针直接连接,避免使用杜邦线。杜邦线的分布电感和电容会严重劣化高速信号,导致ADC采样失败。编译与下载:完成约束后,编译工程并生成比特流文件,下载到FPGA中。观察板载LED:
led0常亮表示PLL锁相成功;led1在FPGA发送载波时点亮;led2在发送完毕等待卡片响应时点亮。这是一个直观的状态指示。
4. 软件交互:串口命令实战
FPGA部署成功后,就可以通过串口工具与NFC卡片对话了。串口配置为:波特率9600,8位数据位,无校验,1位停止位(9600,8,N,1)。交互模式是“一问一答”,PC发送一行命令,FPGA执行并返回一行结果。每行命令或响应以\r或\n结尾。
强烈建议使用“串口调试助手”类软件,而非Putty等终端工具。因为FPGA设计了一个节能机制:收到命令后开启载波为卡片供电,如果1.2秒内没有新命令,则自动关闭载波。对于自动化脚本,1.2秒足够;但对于手动输入,时间非常紧张。“串口调试助手”通常支持一次性发送多行命令,FPGA会逐条执行,从而保证卡片持续上电。
4.1 基础寻卡流程(以M1卡为例)
下面我们通过一个完整的例子,演示如何与一张M1卡进行初次对话。假设卡片UID为4B BE DE 79 52。
发送REQA(请求应答):这是唤醒卡片的第一个命令。发送字节
0x26。发送:26 接收:04 0004 00是卡片返回的ATQA(Answer to Request),表明卡片已就绪,并准备进入防冲突流程。防冲突与获取UID:发送防冲突命令
0x93 0x20。0x93是SELECT命令的代码,0x20表示请求一个完整的UID。发送:93 20 接收:4B BE DE 79 52卡片返回了它的5字节UID。注意:在实际多卡环境下,这一步可能会返回冲突信息(如
01:1),需要执行完整的防冲突算法,下文会详述。选择卡片:使用SELECT命令
0x93 0x70,后面跟上完整的UID,来选中这张卡。发送:93 70 4B BE DE 79 52 接收:08 B6 DD卡片返回SAK(Select Acknowledge)
0x08,后跟两个字节的CRCB6 DD。SAK=0x08是一个关键标识,它告诉我们这张卡是MIFARE Classic 1K(M1卡)。至此,ISO14443A的底层激活流程完成。M1卡特定命令:认证第一阶段:ISO14443A标准到此为止,后续是M1卡自己的协议。例如,进行密钥认证的第一步(Phase 1),命令为
0x60 0x07(其中0x07是密钥类型A,块地址0)。发送:60 07 接收:EF 9B B6 5A卡片返回一个4字节的随机数(Nt)。这将用于后续的相互认证过程。更复杂的读写操作需要在上层应用(如Python脚本)中实现三轮认证和复杂的命令流,这超出了本FPGA项目的范畴,但FPGA已经为你铺平了底层的通信道路。
实操技巧:CRC的自动处理在发送命令时,你不需要手动计算和添加CRC校验码。
nfca_tx_frame.v模块会自动在需要CRC的帧尾部追加CRC。同样,接收时,FPGA也不会剥离CRC,而是将原始数据(包括CRC)一并通过串口返回。这既简化了用户操作,也便于调试时核对数据完整性。
4.2 深入理解防冲突(AntiCollision)机制
防冲突是ISO14443A标准中非常精妙的一部分,它允许多张卡同时进入读写器场区并被逐一识别。我们的FPGA实现完整支持该机制。下面通过一个实例来剖析其工作原理。
假设场区内有两张卡:
- 卡A UID:
4B BE DE 79 52(二进制: 0100 1011 ...) - 卡B UID:
01 1D DD 79 B8(二进制: 0000 0001 ...)
首次防冲突请求:
发送:93 20 接收:01:1卡片们同时回复UID的第一个字节。FPGA进行“位与”比较,发现所有卡回复的第0位都是
1,但第1位有卡回复0(卡B),有卡回复1(卡A),发生了冲突。FPGA的nfca_rx_tobytes.v模块检测到这一冲突,并报告:01:1。意思是:我收到了一个不完整的字节0x01(高7位是冲突位,被置0),冲突发生在从最低位(LSB)数起的第1位(bit 1)。选择其中一张卡(例如UID bit1=1的卡A):我们需要发送一个位导向帧。命令
0x93 0x22中的0x22表示“我要指定UID的前2个比特”。数据部分03:2表示发送比特11(0x03的二进制是00000011,但只发送低2位)。发送:93 22 03:2 接收:48 BE DE 79 52只有UID前两位是
11的卡(卡A)会响应。它回复了UID剩余的部分48 BE DE 79 52。注意0x48也是一个不完整字节(0100 1000),它的低2位是冲突位,需要与我们发送的11进行组合。组合方式就是按位或:0x48 | 0x03 = 0x4B。这样就得到了卡A完整的第一个UID字节0x4B。选择另一张卡(UID bit1=0的卡B):发送指定前2比特为
01的命令。发送:93 22 01:2 接收:00 1D DD 79 B8组合:
0x00 | 0x01 = 0x01。得到卡B的完整UID首字节。
这个过程可以递归进行,直到完整识别出所有卡的UID。FPGA的防冲突逻辑完全按照标准实现,能够处理任意多张卡的场景。你可以通过发送一系列逐步指定更多比特的命令,来观察FPGA如何逐位“筛选”卡片,这有助于深刻理解防冲突算法的本质。
5. 调试排错与信号分析
硬件项目难免遇到问题。如果卡片放上去没反应,或者数据不对,请按照以下步骤系统性地排查:
5.1 初步诊断与串口反馈
检查基础通信:确保FPGA程序已正确下载,串口线连接正确,波特率设置为9600。发送命令
26(REQA)。- 如果没有任何回复:检查FPGA的供电、时钟、下载接口。确认
led0(PLL锁定指示灯)是否常亮。 - 如果回复字符
n:这是一个好信号!说明FPGA的串口接收、命令解析、载波发射、接收链路数字部分都在工作。n代表“No card detected or error”(未检测到卡片或接收错误)。问题大概率出在模拟前端或卡片耦合上。
- 如果没有任何回复:检查FPGA的供电、时钟、下载接口。确认
电源与连接检查:
- 测量NFC Breakout Board的供电电压(7-9V输入,3.3V输出)是否正常。
- 重中之重:检查
carrier_out、ad7276_sclk、ad7276_csn、ad7276_sdata这四根线与FPGA的连接是否牢固。强烈建议用排针焊接,避免杜邦线。高速的SPI时钟(40.68MHz)对信号完整性要求很高。
5.2 示波器深度分析
示波器是调试射频和混合信号系统的“眼睛”。将探头连接到Breakout Board的J3(SMA接口,即包络检波输出点)。
观察载波与调制:
- 在串口助手发送
26命令时,触发示波器单次捕获。 - 你应该能看到一个清晰的时序波形:首先是一个幅值稳定的847.5kHz正弦波(载波开启,为卡片供电),随后波形会出现明显的“凹陷”(100% ASK调制,发送
0x26的数据位),最后恢复稳定正弦波等待卡片回复。 - 如果看不到调制凹陷:说明发送链路有问题。检查FPGA的
carrier_out引脚是否有13.56MHz方波输出,检查MOSFET FDV301N是否损坏,检查谐振电路(L1, C1)的焊接和参数。
- 在串口助手发送
观察卡片响应:
- 在上一步的波形中,在发送完毕后的“等待期”,仔细放大观察波形。
- 如果卡片在场且被正确激活,你会看到在稳定的847.5kHz正弦波上,叠加了一个非常微弱的、规律性的幅度变化(可能只有几十毫伏)。这就是卡片通过负载调制返回的2%-10% ASK信号。
- 如果看不到任何微小变化:
- 卡片问题:确认卡片是ISO14443A标准的(M1卡或UID卡)。尝试更换卡片或调整卡片在线圈上的位置和角度。
- 谐振频率偏移:13.56MHz的谐振点非常尖锐。用电容表测量C1的容值,或用电感表测量L1的电感值,看是否与设计值(115pF, 1.2μH)偏差过大。可以尝试并联或串联小容量电容进行微调。
- 检波电路问题:检查二极管D1(1N4148)方向是否正确,检查RC滤波网络(R2, C3)的焊接。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 串口无任何响应 | 1. FPGA未正确编程 2. 串口线连接错误或驱动问题 3. 时钟未起振 | 1. 重新下载程序,确认led0亮。2. 换用其他串口工具,检查TX/RX是否接反。 3. 用示波器测FPGA主时钟引脚。 |
串口始终回复n | 1. 卡片不在场或类型不对 2. 载波未产生或强度不足 3. 接收链路ADC或DSP故障 | 1. 确认使用ISO14443A卡,紧贴线圈。 2. 用示波器测J3点,看有无847.5kHz正弦波和调制凹陷。 3. 用示波器测ADC的SPI接口,看是否有数据波形。检查 ad7276_read.v模块的SPI时序。 |
| 能收到ATQA但后续命令失败 | 1. 防冲突逻辑问题 2. 卡片响应信号太弱,DSP解调出错 3. CRC校验失败(虽然本项目不校验接收CRC,但卡片可能因通信错误不响应) | 1. 使用单卡测试,排除防冲突复杂度。 2. 用示波器仔细观察卡片响应信号的幅度,尝试微调线圈与卡的距离和角度。 3. 检查 nfca_rx_dsp.v中的阈值参数,可能需要根据你的硬件微调。 |
| 通信不稳定,时好时坏 | 1. 电源噪声 2. 机械连接松动(特别是杜邦线) 3. 外部电磁干扰 | 1. 在电源输入端并联一个大电容(如100μF电解电容)和一个小电容(0.1μF陶瓷电容)。 2.将所有连接改为排针直插。 3. 远离手机、电脑开关电源等强干扰源。 |
5.4 高级调试:使用仿真验证逻辑
如果硬件检查无误,但协议逻辑仍有疑问,可以利用项目自带的仿真环境进行验证。项目SIM/目录下提供了基于Icarus Verilog (iverilog) 的测试平台。
- 安装仿真工具:按照iverilog的官方指南或项目文档中的链接进行安装。
- 运行仿真:在
SIM/目录下,运行批处理文件tb_nfca_controller_run_iverilog.bat(Windows)或相应的shell脚本(Linux)。 - 查看波形:仿真会生成一个
dump.vcd文件。使用GTKWave或其他VCD波形查看器打开它。你可以清晰地看到:uart_rx数据如何被解析。nfca_tx_frame如何生成带CRC的帧。nfca_tx_modulate如何产生carrier_out调制信号。- 接收链路各个阶段的信号处理情况(虽然无法模拟真实的卡片响应,但可以验证DSP模块对仿真输入的处理是否正确)。
仿真对于理解代码的数据流、调试协议状态机非常有用,是硬件部署前的重要验证手段。
6. 项目总结与扩展思考
通过这个FPGA-NFC项目,我们完成了一次从模拟射频前端到数字协议处理的完整穿越。它不仅仅是一个“能读卡”的工具,更是一个绝佳的教学平台和研究起点。你可以通过修改DSP算法来尝试不同的解调方案(比如尝试使用数字锁相环或更复杂的自适应滤波器),也可以尝试支持ISO14443B或ISO15693标准(硬件已部分支持),甚至可以实现一个简单的门禁系统原型。
回顾整个实现,有几点关键设计值得再次强调:
- 包络检波的运用,是降低ADC采样率、从而降低系统成本的关键。
- 中值滤波+自适应阈值的DSP方案,在保证实时性的同时,提供了良好的抗噪声和自适应能力。
- 完整的ISO14443A状态机实现,包括位级防冲突处理,展示了用硬件描述语言实现复杂通信协议的可行性。
- 串口交互设计使得上层应用开发变得非常简单,任何能操作串口的语言都能控制这个读卡器。
最后,关于性能优化,如果你希望获得更远的读卡距离或更稳定的通信,可以从以下几个方面入手:一是优化谐振电路和天线线圈的Q值;二是为ADC的模拟电源设计更干净的LDO和滤波电路;三是在FPGA的DSP模块中,可以尝试引入数字自动增益控制(AGC)来扩大动态范围。这个项目就像一棵树的主干,已经生长出来,至于它能开出什么花、结出什么果,就取决于每一位动手实践者的想象力了。