你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

ST25R3916 实评与开发教程:从芯片启动到多协议读卡与卡模拟

[复制链接]
短笛君 发布时间:2026-8-9 19:53

ST25R3916 实战测评与开发教程:从芯片启动到多协议读卡与卡模拟

工程平台:ESP32-C3 + ST25R3916 + ESP-IDF 6.2 通信接口:I2C 支持模式:NFC-A / NFC-B / NFC-F / NFC-V 读卡,NFC-A / NFC-F 卡模拟

1. 为什么选择 ST25R3916

ST25R3916 不是一颗“发个命令就能读 UID”的简单 NFC 芯片。它更接近一套完整的 13.56 MHz 射频前端:主机需要负责模式切换、FIFO、中断、定时器以及上层协议,但换来的能力也明显强于只支持 ISO14443-A 的入门读卡芯片。

这次工程实现并验证了以下功能路径:

  • ESP32-C3 原生 I2C 主机访问;
  • 芯片身份识别、晶振启动和稳压器调整;
  • 射频场分级启动与状态回读;
  • NFC-A、NFC-B、NFC-F、NFC-V 顺序扫描;
  • NFC-A 防冲突、多 UID 级联和卡片类型识别;
  • NFC-B PUPI 枚举与 ISO-DEP 激活;
  • NFC-F/FeliCa 多时隙轮询;
  • NFC-V/ISO15693 Inventory 和 System Information;
  • MIFARE Ultralight/Type 2 Tag 模拟;
  • FeliCa Lite-S 模拟;

从使用体验看,ST25R3916 的优点和代价都很明确。

项目 实际感受
协议覆盖 很强,一颗芯片覆盖 A/B/F/V,并支持 A/F 被动目标模式
主机控制能力 很强,射频场、接收器、定时器和中断都可细调
驱动复杂度 高,寄存器空间、直接命令和协议状态机都需要认真处理
卡模拟 能力完整,但对 I2C 速度、中断处理和时序更敏感
硬件要求 天线匹配、VDD_TX、晶振和接收路径会直接影响结果
调试难度 比 MFRC522、PN532 高,但可观测寄存器也更多

如果项目只需要偶尔读取 MIFARE Classic UID,ST25R3916 会显得偏重;如果需要一套可以继续扩展到 NFC-B、FeliCa、ISO15693 和卡模拟的前端,它的价值就体现出来了。


2. 工程结构

该项目的驱动库移植自M5stack的Unit NFC驱动,STM32原生的X-Cube-NFC目前移植调通但是使用上还有点小bug,所以暂时先用ESP32进行调试。

ST25R3916/
├─ CMakeLists.txt
├─ README.md
├─ DS_st25r3916.pdf
├─ tools/
│  └─ merge_firmware.py
├─ main/
│  ├─ st25r3916_main.cpp
│  ├─ st25r3916_emulation_data.hpp
│  ├─ CMakeLists.txt
│  └─ idf_component.yml
└─ components/
   └─ M5Unit-NFC/
      └─ src/
         ├─ ST25R3916_port_config.hpp
         ├─ unit/
         │  ├─ ST25R3916_definition.hpp
         │  ├─ unit_ST25R3916.cpp
         │  ├─ unit_ST25R3916_util.cpp
         │  ├─ unit_ST25R3916_nfca.cpp
         │  ├─ unit_ST25R3916_nfcb.cpp
         │  ├─ unit_ST25R3916_nfcf.cpp
         │  └─ unit_ST25R3916_nfcv.cpp
         └─ nfc/
            ├─ layer/a、b、f、v
            ├─ isoDEP
            ├─ ndef
            ├─ apdu
            └─ crypto

代码分层如下。

image.png

这种分层的好处是,上层 NFC-A 防冲突代码不需要知道 I2C 寄存器帧怎么编码;底层驱动也不需要理解 NDEF 文本记录。出现问题时,可以按“应用逻辑 → 协议状态机 → 芯片寄存器 → 硬件射频”逐层定位。

主要类和职责可以直接对应到代码:

作用
UnitST25R3916 芯片初始化、寄存器、FIFO、中断、定时器和射频场
NFCLayerA NFC-A 轮询、防冲突、选卡、MIFARE 和 NDEF
NFCLayerB NFC-B REQB、PUPI、ATTRIB 和 ISO-DEP 入口
NFCLayerF NFC-F/FeliCa Polling、服务和块访问
NFCLayerV NFC-V Inventory、System Information 和块访问
EmulationLayerA NFC-A 被动目标状态机
EmulationLayerF NFC-F/FeliCa Lite-S 模拟状态机
IsoDEP I/R/S Block、链式传输和等待时间扩展
TLV / Record NDEF TLV、消息和记录编解码

3. 硬件连接与基本条件

当前工程的默认连接如下。

信号 ESP32-C3 说明
SDA GPIO3 I2C 数据
SCL GPIO2 I2C 时钟
IRQ GPIO4 可选;当前默认使用寄存器轮询
VDD/VDD_IO 3.3 V 当前配置为 3.3 V 供电模式
GND GND 必须共地

配置位于:

components/M5Unit-NFC/src/ST25R3916_port_config.hpp
#define ST25R3916_CFG_I2C_PORT                    0
#define ST25R3916_CFG_I2C_SCL_GPIO                2
#define ST25R3916_CFG_I2C_SDA_GPIO                3
#define ST25R3916_CFG_I2C_ADDRESS                 0x50U
#define ST25R3916_CFG_I2C_FREQUENCY_HZ            100000U
#define ST25R3916_CFG_EMULATION_I2C_FREQUENCY_HZ  400000U

普通扫描使用 100 kHz,卡模拟使用 400 kHz。这样安排不是为了追求总线带宽数字,而是因为卡模拟需要在手机外场出现后快速启动振荡器、接收器并处理目标模式中断。

400 kHz 下建议使用外部上拉:

SDA ── 4.7 kΩ ── 3.3 V
SCL ── 4.7 kΩ ── 3.3 V

除了数字接口,以下硬件条件更关键:

  1. 27.12 MHz 晶体及负载电容正确;
  2. VDD_TX 在开启射频场时不能明显跌落;
  3. 天线和匹配网络应在整机状态下调到接近 13.56 MHz;
  4. RFI1/RFI2 接收路径不能只满足读卡,还要能检测手机外场;
  5. 去耦电容应靠近 VDD、VDD_IO、VDD_TX 和稳压器相关引脚。
  6. ST25的供电电压一定要稳定 否则在开启射频瞬间出现芯片挂掉的情况

4. 编译宏:一个固件只做一种角色

扫描器和卡模拟器使用同一套底层驱动,但运行角色相反:

  • 扫描器主动产生射频场;
  • 卡模拟器关闭主动场,等待手机或读卡器的外场。

工程没有把两种角色硬塞进同一个运行循环,而是通过编译宏三选一。

#define ST25R3916_APP_MODE_SCANNER       0
#define ST25R3916_APP_MODE_EMULATE_NFCA  1
#define ST25R3916_APP_MODE_EMULATE_NFCF  2

#define ST25R3916_CFG_APP_MODE \
        ST25R3916_APP_MODE_SCANNER

4.1 通用扫描器

#define ST25R3916_CFG_APP_MODE \
        ST25R3916_APP_MODE_SCANNER

依次扫描 A、B、F、V。

4.2 NFC-A 卡模拟

#define ST25R3916_CFG_APP_MODE \
        ST25R3916_APP_MODE_EMULATE_NFCA

模拟 MIFARE Ultralight / NFC Forum Type 2 Tag。

4.3 NFC-F 卡模拟

#define ST25R3916_CFG_APP_MODE \
        ST25R3916_APP_MODE_EMULATE_NFCF

模拟 FeliCa Lite-S。

编译期互斥的好处是:扫描代码不会在卡模拟期间误开射频场,卡模拟状态机也不会被 A/B/F/V 模式切换打断。


5. ST25R3916 主机接口实现

ST25R3916 的串行接口把操作类型编码进首字节高位。驱动中的基础操作码如下:

constexpr uint8_t OP_WRITE_REGISTER{0x00};  // 00xxxxxx
constexpr uint8_t OP_READ_REGISTER{0x40};   // 01xxxxxx
constexpr uint8_t OP_LOAD_FIFO{0x80};       // 10000000
constexpr uint8_t OP_READ_FIFO{0x9F};       // 10011111
constexpr uint8_t OP_DIRECT_COMMAND{0xC0};  // 11xxxxxx

Space-A 寄存器采用 6 位地址。例如读取 IC Identity 寄存器 0x3F

constexpr uint8_t kIdentityRegister = 0x3F;
constexpr uint8_t kReadIdentityCommand =
    OP_READ_REGISTER | kIdentityRegister;  // 0x7F

完整读取代码:

uint8_t identity = 0;

i2c_master_transmit_receive(
    device,
    &kReadIdentityCommand,
    sizeof(kReadIdentityCommand),
    &identity,
    sizeof(identity),
    ST25R3916_CFG_I2C_TRANSACTION_TIMEOUT_MS
);

5.1 Space-B 访问

ST25R3916 还有一组 Space-B 扩展寄存器。访问前需要发送 0xFB 前缀:

constexpr uint8_t CMD_REGISTER_SPACEB_ACCESS{0xFB};
constexpr uint16_t PREFIX_SPACE_B =
    static_cast<uint16_t>(CMD_REGISTER_SPACEB_ACCESS) << 8;

驱动内部用 uint16_t 区分 Space-B:

inline uint16_t to_read_reg(const uint16_t regB)
{
    return (regB & OP_TRAILER_MASK)
           | PREFIX_SPACE_B
           | OP_READ_REGISTER;
}

Space-B 中包含接收相关器、过冲/下冲保护、静噪定时器和调制优化参数。自制板上如果 Space-B 某些寄存器不响应,基础功能可能仍能运行,但 NFC-B/F/V 和弱信号接收稳定性会受影响。

工程提供两种策略:

#define ST25R3916_SPACE_B_POLICY_WARN_CONTINUE  0
#define ST25R3916_SPACE_B_POLICY_REQUIRED       1

#define ST25R3916_CFG_SPACE_B_POLICY \
        ST25R3916_SPACE_B_POLICY_WARN_CONTINUE

调试阶段可以先警告后继续;量产固件更适合改成 REQUIRED,避免硬件异常被默认值掩盖。


6. 芯片身份检查

ST25R3916 的 IC Identity 寄存器位于 0x3F。工程将其拆成器件类型和修订版本:

const uint8_t type = (identity >> 3) & 0x1F;
const uint8_t revision = identity & 0x07;

return type == 0x05 && revision != 0;

其中:

constexpr uint8_t VALID_IDENTIFY_TYPE{0x05};

初始化阶段会重试 5 次,每次间隔 20 ms。这是为了处理 USB 下载后 MCU 已重启、但外部 NFC 芯片没有完全掉电的情况。

for (int attempt = 0; attempt < 5; ++attempt) {
    if (readICIdentity(type, rev) &&
        type == VALID_IDENTIFY_TYPE && rev != 0) {
        detected = true;
        break;
    }
    m5::utility::delay(20);
}

这一层检查比单纯 I2C ACK 更可靠:ACK 只能证明总线上有设备,Identity 检查才能确认地址、重复起始、读取方向和芯片型号都正确。


7. 上电初始化流程

ST25R3916 初始化不是简单的 reset()。当前驱动的流程如下。

image.png

grectgrect <foreignobject width="156" height="19.5"></foreignobject>

defsfilterfedropshadow

defsfilterfedropshadow

lineargradient

g

grectgrect <foreignobject width="101.0625" height="19.5"></foreignobject>

grectgrect <foreignobject width="156" height="19.5"></foreignobject>

defsfilterfedropshadowdefsfilterfedropshadowlineargradientgrectgrect <foreignobject width="120.328125" height="19.5"></foreignobject>

grectgrect <foreignobject width="101.0625" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml">``<span class="nodeLabel"><p>Adjust Regulators </p></div>``</foreignobject>

grectgrect <foreignobject width="156" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml">``<span class="nodeLabel"><p>进入扫描模式或卡模拟模式 </p></div>``</foreignobject>

defsfilterfedropshadowdefsfilterfedropshadowlineargradient### 7.1 防御性停止旧状态

USB 下载程序时,ST25R3916 不一定同步掉电。上一个固件可能留下 TX、RX 或定时器状态。因此初始化先执行:

writeDirectCommand(CMD_STOP_ALL_ACTIVITIES);
modify_bit_register8(
    REG_OPERATION_CONTROL,
    0x00,
    tx_en | rx_en
);

7.2 Set Default 与保护序列

if (!writeDirectCommand(CMD_SET_DEFAULT)) {
    return false;
}

随后通过 Test Access 写入数据手册要求的保护序列:

constexpr uint8_t protection_command[] = {
    0x04, 0x10
};

writeDirectCommand(
    CMD_TEST_ACCESS,
    protection_command,
    sizeof(protection_command)
);

这里不能省略。它不是协议功能配置,而是芯片上电后的保护要求。

7.3 IO Configuration

驱动会根据 I2C 时钟选择阈值位:

constexpr uint16_t get_i2c_thd_bits16(uint32_t clk)
{
    return (clk >= 1000000U)
        ? (i2c_thd016 | i2c_thd116)
        : ((clk >= 400000U) ? i2c_thd016 : 0x0000);
}

再合并接口电平和供电信息:

params = i2c_threshold
       | io_config12_i2c
       | (vdd_is_5v ? 0x0000 : sup3v);

8. 晶振启动:先看 en,再看 osc_ok

ST25R3916 的协议编码、定时器和射频接收都依赖载波时钟。工程没有只等待固定延时,而是读取状态并累计中断。

简化后的逻辑:

uint8_t operation{};
uint8_t auxiliary{};

readOperationControl(operation);
readAuxiliaryDisplay(auxiliary);

if ((operation & en) == 0) {
    clearInterrupts();
    set_bit_register8(REG_OPERATION_CONTROL, en);

    do {
        readInterrupts(irq);
        accumulated_irq |= irq;
        readAuxiliaryDisplay(auxiliary);

        if (auxiliary & osc_ok) {
            return true;
        }
        delay(poll_interval);
    } while (!timeout);
}

实际启动日志类似:

OSC: before start OP=0x03 AUX=0x00 en=0 osc_ok=0
OSC: stable AUX=0x12 IRQ=0x80000000

可关注三个值:

字段 含义
Operation Control.en 振荡器和稳压器使能
Auxiliary Display.osc_ok 晶振已经稳定
I_osc 晶振稳定中断

只有 en=1 不代表时钟已经可用。驱动必须等到 osc_ok=1,否则后续 NFC 定时可能完全失真。


9. TX Driver:调制深度和驱动电阻在同一个字节

TX Driver 寄存器高半字节是 ASK 调制参数,低半字节是输出电阻:

const uint8_t tx_driver =
    ((tx_am_modulation & 0x0F) << 4)
    | (tx_driver_resistance & 0x0F);

当前配置:

#define ST25R3916_CFG_TX_AM_MODULATION     7U
#define ST25R3916_CFG_TX_DRIVER_RESISTANCE 0U

因此启动日志为:

TX Driver: 0x70, d_res=0

d_res=0 对应较强的驱动能力。调试自制天线时,不建议一开始就追求最大场强。更稳妥的方法是:

  1. 使用较大的d_res 启动;
  2. 确认 VDD_TX 无明显跌落;
  3. 测量天线波形和芯片温升;
  4. 再逐步降低d_res

需要特别说明:卡模拟模式不主动产生载波,手机读取模拟卡时主要依赖负载调制路径,因此 d_res 不是卡模拟无法检测手机外场的首要原因。


10. 射频场分级启动

读卡器模式下,工程采用分级开启 TX 和 RX:

const uint8_t ready_operation = en;
writeOperationControl(ready_operation);

const uint8_t tx_operation = ready_operation | tx_en;
writeOperationControl(tx_operation);
delay(ST25R3916_CFG_RF_STAGE_DELAY_MS);

const uint8_t active_operation = tx_operation | rx_en;
writeOperationControl(active_operation);

对应状态:

Ready  OP=0x80
TX     OP=0x88
Active OP=0xC8

image.png

分级启动有两个实际好处:

  • 限制射频场突然开启时的电源冲击;
  • 出现黑屏、复位或 I2C 超时时,可以判断故障发生在 TX 还是 RX 阶段。

这些寄存器阶段日志目前降为 DEBUG。正常扫描只保留错误和卡片结果;需要查看时,将 M5_LOG_LEVEL 改为 4。


11. 直接命令

ST25R3916 有一组不经过普通寄存器写入路径的直接命令。常用命令包括:

CMD_SET_DEFAULT             = 0xC1;
CMD_STOP_ALL_ACTIVITIES     = 0xC2;
CMD_TRANSMIT_WITH_CRC       = 0xC4;
CMD_TRANSMIT_WITHOUT_CRC    = 0xC5;
CMD_TRANSMIT_REQA           = 0xC6;
CMD_TRANSMIT_WUPA           = 0xC7;
CMD_NFC_INITIAL_FIELD_ON    = 0xC8;
CMD_GO_TO_SENSE             = 0xCD;
CMD_GO_TO_SLEEP             = 0xCE;
CMD_RESET_RX_GAIN           = 0xD5;
CMD_ADJUST_REGULATORS       = 0xD6;
CMD_CLEAR_FIFO              = 0xDB;
CMD_REGISTER_SPACEB_ACCESS  = 0xFB;
CMD_TEST_ACCESS             = 0xFC;

驱动统一入口:

bool UnitST25R3916::writeDirectCommand(
    uint8_t cmd,
    const uint8_t* data,
    uint32_t length)
{
    TRANSACTION_GUARD();
    return writeRegister(cmd, data, length, true);
}

TRANSACTION_GUARD() 用来保证复合操作不会被另一个任务插入总线事务。


12. 中断组织:硬件四组寄存器,软件一个 32 位位图

ST25R3916 的中断分成四组:

  1. Main;
  2. Timer and NFC;
  3. Error and Wake-up;
  4. Passive Target。

驱动把它们组合成一个 uint32_t

31                 24 23                 16
+--------------------+--------------------+
| Main Interrupt     | Timer/NFC Interrupt|
+--------------------+--------------------+
| Error/Wakeup       | Passive Target     |
+--------------------+--------------------+
15                  8 7                   0
value = (static_cast<uint32_t>(main_nfc) << 16)
      | (static_cast<uint32_t>(error) << 8)
      | passive;

中断寄存器具有读清除语义,因此读取顺序和软件缓存很重要。轮询模式下:

void UnitST25R3916::update()
{
    if (!_using_irq) {
        uint32_t value{};
        readInterrupts(value);
        _stored_irq |= value & _enabled_irq;
    }
}

如果连接了物理 IRQ,引脚中断服务函数只置位标志,不在 ISR 中访问 I2C:

void IRAM_ATTR UnitST25R3916::on_irq(void* arg)
{
    auto* unit = static_cast<UnitST25R3916*>(arg);
    unit->_interrupt_occurred = true;
}

这是正确的嵌入式处理方式:I2C 事务留在任务上下文完成,ISR 只做最小工作。


13. FIFO 设计

ST25R3916 内部 FIFO 最大容量为 512 字节:

constexpr uint16_t MAX_FIFO_DEPTH{512};

发送流程通常是:

<pre class="mermaid" id="mermaid-2fed1150" data-vscode-context="{"webviewSection":"mermaid","mermaidSource":"flowchart LR\n A[Clear FIFO] --> B[写入发送数据]\n B --> C[写 Number of Transmitted Bytes]\n C --> D[Transmit with/without CRC]\n D --> E[等待 TXE 中断]","preventDefaultContextMenuItems":true}"><svg id="dmermaid-2fed1150" width="100%" xmlns="http://www.w3.org/2000/svg" class="flowchart" viewBox="0 0 1069.09375 65.5" role="graphics-document document" aria-roledescription="flowchart-v2"><g><marker id="dmermaid-2fed1150_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath"></path></marker><marker id="dmermaid-2fed1150_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath"></path></marker><marker id="dmermaid-2fed1150_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath"></path></marker><marker id="dmermaid-2fed1150_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath"></polygon></marker><marker id="dmermaid-2fed1150_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath"></circle></marker><marker id="dmermaid-2fed1150_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath"></circle></marker><marker id="dmermaid-2fed1150_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath"></circle></marker><marker id="dmermaid-2fed1150_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath"></circle></marker><marker id="dmermaid-2fed1150_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath"></path></marker><marker id="dmermaid-2fed1150_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath"></path></marker><marker id="dmermaid-2fed1150_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath"></path></marker><marker id="dmermaid-2fed1150_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath"></path></marker><g class="root"><g class="clusters"></g><g class="edgePaths"><path d="M126.656,32.75L130.823,32.75C134.99,32.75,143.323,32.75,150.99,32.75C158.656,32.75,165.656,32.75,169.156,32.75L172.656,32.75" id="dmermaid-2fed1150-L_A_B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" data-edge="true" data-et="edge" data-id="L_A_B_0" data-points="W3sieCI6MTI2LjY1NjI1LCJ5IjozMi43NX0seyJ4IjoxNTEuNjU2MjUsInkiOjMyLjc1fSx7IngiOjE3Ni42NTYyNSwieSI6MzIuNzV9XQ==" data-look="classic" marker-end="url(#dmermaid-2fed1150_flowchart-v2-pointEnd)"></path><path d="M314.656,32.75L318.823,32.75C322.99,32.75,331.323,32.75,338.99,32.75C346.656,32.75,353.656,32.75,357.156,32.75L360.656,32.75" id="dmermaid-2fed1150-L_B_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" data-edge="true" data-et="edge" data-id="L_B_C_0" data-points="W3sieCI6MzE0LjY1NjI1LCJ5IjozMi43NX0seyJ4IjozMzkuNjU2MjUsInkiOjMyLjc1fSx7IngiOjM2NC42NTYyNSwieSI6MzIuNzV9XQ==" data-look="classic" marker-end="url(#dmermaid-2fed1150_flowchart-v2-pointEnd)"></path><path d="M608.594,32.75L612.76,32.75C616.927,32.75,625.26,32.75,632.927,32.75C640.594,32.75,647.594,32.75,651.094,32.75L654.594,32.75" id="dmermaid-2fed1150-L_C_D_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" data-edge="true" data-et="edge" data-id="L_C_D_0" data-points="W3sieCI6NjA4LjU5Mzc1LCJ5IjozMi43NX0seyJ4Ijo2MzMuNTkzNzUsInkiOjMyLjc1fSx7IngiOjY1OC41OTM3NSwieSI6MzIuNzV9XQ==" data-look="classic" marker-end="url(#dmermaid-2fed1150_flowchart-v2-pointEnd)"></path><path d="M870.906,32.75L875.073,32.75C879.24,32.75,887.573,32.75,895.24,32.75C902.906,32.75,909.906,32.75,913.406,32.75L916.906,32.75" id="dmermaid-2fed1150-L_D_E_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" data-edge="true" data-et="edge" data-id="L_D_E_0" data-points="W3sieCI6ODcwLjkwNjI1LCJ5IjozMi43NX0seyJ4Ijo4OTUuOTA2MjUsInkiOjMyLjc1fSx7IngiOjkyMC45MDYyNSwieSI6MzIuNzV9XQ==" data-look="classic" marker-end="url(#dmermaid-2fed1150_flowchart-v2-pointEnd)"></path></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_A_B_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_B_C_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_C_D_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_D_E_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="dmermaid-2fed1150-flowchart-A-0" data-look="classic" transform="translate(67.328125, 32.75)"><rect class="basic label-container" x="-59.328125" y="-24.75" width="118.65625" height="49.5"></rect><g class="label" transform="translate(-29.328125, -9.75)"><rect></rect><foreignObject width="58.65625" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>Clear FIFO</p></span></div></foreignObject></g></g><g class="node default" id="dmermaid-2fed1150-flowchart-B-1" data-look="classic" transform="translate(245.65625, 32.75)"><rect class="basic label-container" x="-69" y="-24.75" width="138" height="49.5"></rect><g class="label" transform="translate(-39, -9.75)"><rect></rect><foreignObject width="78" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>写入发送数据</p></span></div></foreignObject></g></g><g class="node default" id="dmermaid-2fed1150-flowchart-C-3" data-look="classic" transform="translate(486.625, 32.75)"><rect class="basic label-container" x="-121.96875" y="-24.75" width="243.9375" height="49.5"></rect><g class="label" transform="translate(-91.96875, -9.75)"><rect></rect><foreignObject width="183.9375" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>写 Number of Transmitted Bytes</p></span></div></foreignObject></g></g><g class="node default" id="dmermaid-2fed1150-flowchart-D-5" data-look="classic" transform="translate(764.75, 32.75)"><rect class="basic label-container" x="-106.15625" y="-24.75" width="212.3125" height="49.5"></rect><g class="label" transform="translate(-76.15625, -9.75)"><rect></rect><foreignObject width="152.3125" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>Transmit with/without CRC</p></span></div></foreignObject></g></g><g class="node default" id="dmermaid-2fed1150-flowchart-E-7" data-look="classic" transform="translate(991, 32.75)"><rect class="basic label-container" x="-70.09375" y="-24.75" width="140.1875" height="49.5"></rect><g class="label" transform="translate(-40.09375, -9.75)"><rect></rect><foreignObject width="80.1875" height="19.5"><div xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"><p>等待 TXE 中断</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="dmermaid-2fed1150-drop-shadow" height="130%" width="130%"><fedropshadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"></fedropshadow></filter></defs><defs><filter id="dmermaid-2fed1150-drop-shadow-small" height="150%" width="150%"><fedropshadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"></fedropshadow></filter></defs><linearGradient id="dmermaid-2fed1150-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"></linearGradient></svg></pre>

代码示例:

clearInterrupts();
writeDirectCommand(CMD_CLEAR_FIFO);
writeFIFO(tx, tx_len);
writeNumberOfTransmittedBytes(tx_len, 0);
writeDirectCommand(CMD_TRANSMIT_WITH_CRC);

接收时先读取 FIFO Status 1/2,得到完整字节数和最后一个字节的有效位数,再按调用方缓冲区容量取数据。

对于 NFC-A 7 位 REQA、防冲突碰撞位和 MIFARE 4 位 ACK,最后一个字节的有效位数不能忽略。


14. 定时器换算

ST25R3916 的定时器基于 13.56 MHz 载波周期。工程避免浮点运算,使用整数向上取整。

14.1 No-Response Timer

uint16_t calculate_nrt(uint32_t ms, bool step4096)
{
    constexpr uint32_t FC_HZ = 13560000;
    const uint64_t step = step4096
        ? 4096ULL * 1000000ULL
        :   64ULL * 1000000ULL;

    const uint64_t us = static_cast<uint64_t>(ms) * 1000U;
    const uint64_t nrt = (us * FC_HZ + step - 1) / step;

    return clamp(nrt, 1, 0xFFFF);
}

14.2 Mask Receive Timer

uint8_t calculate_mrt(uint32_t us, bool step512)
{
    constexpr uint32_t FC_HZ = 13560000;
    const uint32_t step = step512
        ? 512U * 1000000U
        :  64U * 1000000U;

    uint32_t mrt = (us * FC_HZ + step - 1) / step;
    return clamp(mrt, 4, 0xFF);
}

使用向上取整的原因很简单:等待时间短于协议要求通常比稍长更危险。


15. 通用扫描器的整体流程

通用扫描器不是同时打开四种协议,而是不断重配 ST25R3916:

image.png

核心入口:

size_t scan_all_technologies()
{
    size_t detected = 0;

#if ST25R3916_CFG_SCAN_NFC_A
    detected += scan_nfca();
#endif
#if ST25R3916_CFG_SCAN_NFC_B
    detected += scan_nfcb();
#endif
#if ST25R3916_CFG_SCAN_NFC_F
    detected += scan_nfcf();
#endif
#if ST25R3916_CFG_SCAN_NFC_V
    detected += scan_nfcv();
#endif

    return detected;
}

每次进入模式都会执行:

g_reader.configureNFCMode(mode);
vTaskDelay(kModeSettleDelay);
g_units.update();

相关参数:

#define ST25R3916_CFG_TECH_SCAN_TIMEOUT_MS      120U
#define ST25R3916_CFG_MODE_SETTLE_DELAY_MS        5U
#define ST25R3916_CFG_MAX_CARDS_PER_TECHNOLOGY    4U
#define ST25R3916_CFG_CARD_SCAN_INTERVAL_MS      250U

如果只需要 A 和 V,可以关闭另外两种技术,缩短一轮扫描时间:

#define ST25R3916_CFG_SCAN_NFC_A 1
#define ST25R3916_CFG_SCAN_NFC_B 0
#define ST25R3916_CFG_SCAN_NFC_F 0
#define ST25R3916_CFG_SCAN_NFC_V 1

16. NFC-A:从 REQA 到具体卡片型号

NFC-A 检测分两层:

  • detect():发现卡片并获得 UID、ATQA、SAK;
  • identify():使用 ATS、GetVersion 和厂商信息进一步识别。
std::vector<m5::nfc::a::PICC> cards;

if (g_nfc_a.detect(cards, timeout_ms)) {
    for (auto& card : cards) {
        const bool identified = g_nfc_a.identify(card);
        print_nfca_card(card, identified);
    }
}

16.1 防冲突

image.png

驱动支持 4、7、10 字节 UID 的级联选择。发生碰撞时,会读取碰撞位置并逐位选择分支,直到获得完整 UID 和 BCC。

16.2 卡片识别

** SAK 无法精确区分所有卡片。例如 MIFARE Plus SL1 可能表现得像 MIFARE Classic。工程会进一步尝试:

  • RATS / ATS;
  • MIFARE GetVersion;
  • ST25TA 历史字节;
  • MIFARE Plus 安全等级探测;
  • Ultralight C 认证探测。

所以“检测到 NFC-A”与“可以读取卡内数据”是两件事。MIFARE Classic 需要 Key A/Key B,DESFire 和 Plus 还可能需要 AES 或安全消息。


17. NFC-B:PUPI 与 ISO-DEP 激活

NFC-B 先发送 REQB,卡片返回 ATQB:

std::vector<m5::nfc::b::PICC> cards;

g_nfc_b.detect(
    cards,
    0x00,                    // AFI:全部
    kMaxCardsPerTechnology,
    kTechnologyScanTimeoutMs
);

ATQB 包含:

PUPI            4 字节
Application     4 字节
Protocol Info   3 字节

需要进一步通信时,发送 ATTRIB:

const bool activated = g_nfc_b.select(card);

激活后进入 ISO-DEP,可以继续交换 APDU。但应用文件结构、AID 和访问权限没有统一答案,因此通用扫描器只做基础激活,不对未知 NFC-B 卡盲目发送业务 APDU。


18. NFC-F:多时隙 SENSF_REQ

NFC-F 使用 IDm 和 PMm 标识卡片。扫描器根据最大卡片数选择轮询时隙:

constexpr m5::nfc::f::TimeSlot configured_nfcf_time_slot()
{
#if ST25R3916_CFG_MAX_CARDS_PER_TECHNOLOGY <= 1
    return TimeSlot::Slot1;
#elif ST25R3916_CFG_MAX_CARDS_PER_TECHNOLOGY <= 2
    return TimeSlot::Slot2;
#elif ST25R3916_CFG_MAX_CARDS_PER_TECHNOLOGY <= 4
    return TimeSlot::Slot4;
#elif ST25R3916_CFG_MAX_CARDS_PER_TECHNOLOGY <= 8
    return TimeSlot::Slot8;
#else
    return TimeSlot::Slot16;
#endif
}

扫描后还会检查:

  • NFCIP-1 格式;
  • FeliCa Lite / Lite-S;
  • NDEF System Code;
  • 私有 System Code;
  • FeliCa Standard 响应模式。

最终输出 IDm、PMm、类型、格式标志和用户区容量。


19. NFC-V:Inventory、Quiet 和 System Information

NFC-V 对应 ISO15693。检测流程:

image.png

Stay Quiet 用于让已经枚举到的卡片暂时停止响应,便于继续发现其他标签。

代码会读取:

picc.dsfID;
picc.afi;
picc.icRef;
picc.blocks;
picc.block_size;

总容量:

uint16_t totalSize() const
{
    return blocks * block_size;
}

ST25DV 等大容量标签还需要扩展 System Information 和扩展块地址命令,驱动中已经做了对应分支。


20. 卡片日志去重

同一张卡一直放在天线上,如果每轮都打印,串口很快会被淹没。工程使用“协议前缀 + 卡片标识”作为去重键:

A:<UID>
B:<PUPI>
F:<IDm>
V:<UID>
bool should_report_card(const std::string& signature)
{
    const TickType_t now = xTaskGetTickCount();

    for (auto& state : g_card_reports) {
        if (state.signature == signature) {
            if ((now - state.reported_at)
                < kSameCardReportInterval) {
                return false;
            }
            state.reported_at = now;
            return true;
        }
    }

    // 新卡加入缓存
    ...
}

重复报告间隔:

#define ST25R3916_CFG_SAME_CARD_REPORT_INTERVAL_MS 2000U

补充:协议栈中的 NDEF、ISO-DEP、APDU 与加密模块

射频前端解决的是“如何可靠收发一帧”,真正的数据访问还要经过上层协议。工程中的 nfc/ 目录大致分为四块。

NDEF 与 TLV

nfc/ndef/ndef.cpp
nfc/ndef/ndef_record.cpp
nfc/ndef/ndef_tlv.cpp
nfc/layer/ndef_layer.cpp

Type 2、Type 3、Type 5 标签常见的 NDEF 数据最终都会转换成消息和记录对象。TLV 层负责处理:

Null TLV
Lock Control TLV
Memory Control TLV
NDEF Message TLV
Proprietary TLV
Terminator TLV

典型读取接口:

std::vector<m5::nfc::ndef::TLV> tlvs;

if (g_nfc_a.ndefRead(tlvs)) {
    for (const auto& tlv : tlvs) {
        if (!tlv.isMessageTLV()) {
            continue;
        }
        for (const auto& record : tlv.records()) {
            // 根据 TNF、Type 和 Payload 解释文本、URI 等记录
        }
    }
}

通用扫描器默认不自动读 NDEF,是为了避免对需要认证、写保护或专用文件系统的卡片发送不合适的命令。确定卡片类型后,再进入对应读取分支更可靠。

ISO-DEP

nfc/isoDEP/isoDEP.cpp
nfc/isoDEP/file_system.cpp
nfc/isoDEP/nfc_b_file_system.cpp
nfc/isoDEP/desfire_file_system.cpp

NFC-A Type 4、NFC-B 和 DESFire 等卡片会使用 ISO-DEP。该层处理:

  • I-Block、R-Block、S-Block;
  • PCB;
  • CID/NAD;
  • 分帧与链式传输;
  • 等待时间扩展;
  • Deselect;
  • 上层 APDU 负载。

应用层不应该直接把长 APDU 塞进 FIFO,而应交给 ISO-DEP 层根据双方最大帧长度拆分。

APDU

nfc/apdu/apdu.cpp

APDU 层负责组织 ISO/IEC 7816-4 风格命令:

CLA | INS | P1 | P2 | Lc | Data | Le

响应通常由数据和状态字组成:

Data | SW1 | SW2

只有知道目标应用的 AID、文件结构和访问规则,APDU 才有实际意义。通用 NFC-B 扫描只做到 ATTRIB/ISO-DEP 激活,避免对未知证件或 CPU 卡发送猜测命令。

Crypto1、AES 与敏感数据清理

nfc/crypto/mifare_classic_crypto1.hpp
nfc/crypto/aes.cpp
nfc/crypto/secure_zero.hpp

MIFARE Classic 使用 Crypto1,MIFARE Plus、DESFire 等安全等级会用到 AES。认证流程不仅是调用一个加密函数,还要维护随机数、密码流状态、消息计数器和 MAC。

工程中的 secure_zero 用于在认证完成或失败后清理临时密钥和会话数据。对于长期运行的嵌入式设备,这比让密钥一直留在栈内存中更稳妥。

这些上层模块与 ST25R3916 的关系可以概括为:

image.png---

21. NFC-A 卡模拟

NFC-A 模拟模式默认模拟 MIFARE Ultralight / Type 2 Tag。

constexpr uint8_t kNfcaUid[7] = {
    0x04, 0x34, 0x56, 0x78,
    0x9A, 0xBC, 0xDE
};

内存镜像位于:

main/st25r3916_emulation_data.hpp

21.1 生成模拟 PICC

m5::nfc::a::PICC picc{};

picc.emulate(
    m5::nfc::a::Type::MIFARE_Ultralight,
    kNfcaUid,
    sizeof(kNfcaUid)
);

emulate() 会根据类型配置:

  • UID 长度;
  • ATQA;
  • SAK;
  • 块数量。

21.2 写入 UID 和 BCC

Ultralight/NTAG 的厂家页中包含两级 BCC:

nfca_memory[0] = uid[0];
nfca_memory[1] = uid[1];
nfca_memory[2] = uid[2];
nfca_memory[3] = 0x88 ^ uid[0] ^ uid[1] ^ uid[2];

nfca_memory[4] = uid[3];
nfca_memory[5] = uid[4];
nfca_memory[6] = uid[5];
nfca_memory[7] = uid[6];
nfca_memory[8] = uid[3] ^ uid[4] ^ uid[5] ^ uid[6];

如果只修改 UID 数组而不更新厂家页,手机可能完成防冲突,却读取到不一致的卡片内存。

21.3 状态机

image.png

各状态含义:

状态 含义
Off 低功耗等待外部场
Idle 已检测到外场,等待或处理轮询命令
Ready 防冲突/选择过程中
Active 已被手机选中,可读写模拟内存
Halt 收到 HLTA,等待 WUPA

21.4 Go to Sense 与外场检测

进入 Off 前,命令执行瞬间必须满足:

en=1
rx_en=1

然后执行:

writeDirectCommand(CMD_GO_TO_SENSE);

最后关闭主振荡器和接收器,只保留自动外场检测:

modify_bit_register8(
    REG_OPERATION_CONTROL,
    en_fd_manual_auto,
    en | rx_en | tx_en | wu | en_fd_mask
);

外场出现后,正常路径是 I_eon 中断。考虑到 I2C 轮询可能先读清中断,状态机同时检查 Auxiliary Display 的 efd_o

const bool irq_field =
    (get_irq(I_eon32) & I_eon32) != 0;

const bool level_field = is_extra_field();

if (irq_field || level_field) {
    return goto_idle();
}

这项兜底很重要:边沿事件可能丢失,实时电平只要仍存在就能恢复状态机。

21.5 Off 诊断

卡模拟模式每秒输出一次低频诊断:

NFC-A Off诊断:OP=0x03 en_fd=3 AUX=0x00 efd_o=0 osc_ok=0

正常等待手机时:

en_fd=3
efd_o=0
osc_ok=0

手机靠近后应看到:

efd_o=1
NFC-A emulation: external field detected
NFC-A 模拟状态:Idle
NFC-A 模拟状态:Ready
NFC-A 模拟状态:Active

如果 en_fd=3 但手机靠近时 efd_o 始终为 0,应优先检查天线、匹配网络和 RFI 输入,而不是继续修改 UID 或 NDEF 数据。


22. NFC-F 卡模拟

NFC-F 模拟默认使用 FeliCa Lite-S:

constexpr uint8_t kNfcfIdm[8] = {
    0x01, 0x2E, 0x50, 0xE5,
    0x3C, 0x4B, 0x4F, 0x29
};

constexpr uint8_t kNfcfPmm[8] = {
    0x00, 0xF1, 0x00, 0x00,
    0x00, 0x01, 0x43, 0x00
};

创建模拟卡:

m5::nfc::f::PICC picc{};

picc.emulate(
    m5::nfc::f::Type::FeliCaLiteS,
    kNfcfIdm,
    kNfcfPmm
);

系统码当前为:

0x88B4

启动前需要把 IDm 和 PMm 同步到 Lite-S 内存镜像中的 ID、D_ID 块:

constexpr uint32_t id_offset  = 17U * 16U;
constexpr uint32_t did_offset = 18U * 16U;

for (uint_fast8_t i = 0; i < 8; ++i) {
    memory[id_offset + i] = idm[i];
    memory[did_offset + i] = idm[i];
    memory[did_offset + 8 + i] = pmm[i];
}

NFC-F 模拟状态比 NFC-A 少:

Off → Communicated → Selected

状态机支持 Polling、Read Without Encryption 和 Write Without Encryption 等基础命令。


23. 为什么卡模拟要使用 400 kHz I2C

扫描器主动控制通信节奏,100 kHz I2C 通常还能工作。卡模拟不同:手机产生外场后,ST25R3916 和主控必须在规定时间内完成唤醒、模式确认和响应。

工程按应用模式选择 I2C 速度:

#if ST25R3916_CFG_APP_MODE == ST25R3916_APP_MODE_SCANNER
constexpr uint32_t kI2cFrequencyHz =
    ST25R3916_CFG_I2C_FREQUENCY_HZ;
#else
constexpr uint32_t kI2cFrequencyHz =
    ST25R3916_CFG_EMULATION_I2C_FREQUENCY_HZ;
#endif

卡模拟循环不能加入常规的 10~100 ms 延时:

for (;;) {
    g_units.update();
    g_emulation_a.update();
    feed_idle_task_periodically();
}

为了让 ESP32-C3 单核 Idle 和看门狗任务运行,工程每 2 秒短暂让出 5 ms,而不是每轮延时:

#define ST25R3916_CFG_EMULATION_IDLE_FEED_INTERVAL_MS 2000U
#define ST25R3916_CFG_EMULATION_IDLE_FEED_DELAY_MS       5U

24. 日志策略

驱动和应用统一使用 M5_LIB_LOGx,避免 ESP_LOGxvprintf 两套后端输出交叉。

当前编译等级:

-DM5_LOG_LEVEL=3

等级建议:

等级 用途
2 只关注错误和警告
3 日常使用:启动结果、卡片结果、异常
4 调试模式切换、射频场和协议状态
5 跟踪寄存器、FIFO 和完整协议过程

通用扫描每轮都会重配射频场,因此以下正常阶段日志已经降为 DEBUG:

RF field: initial
RF field: Ready
RF field: TX
RF field: Active
NFC-A configuration complete

这样 INFO 级别不会被重复寄存器状态淹没。

26. 常见问题排查

26.1 I2C 扫描不到 0x50

检查顺序:

  1. VDD 和 VDD_IO;
  2. SDA/SCL 是否接反;
  3. 是否有外部上拉;
  4. 地址是否确实为 7 位0x50
  5. 示波器观察起始位和 ACK;
  6. 降低到 100 kHz 再试。

26.2 Identity 能读,但晶振不稳定

观察:

OP
AUX.osc_ok
I_osc

检查 27.12 MHz 晶体、负载电容、焊接和 PCB 走线。

26.3 开启 TX 后复位或 I2C 超时

优先检查:

  • VDD_TX 电压跌落;
  • 天线谐振过强;
  • 匹配网络短路或器件值错误;
  • d_res 是否过小;
  • 去耦和电源限流。

26.4 能读 NFC-A,读不到 B/F/V

这通常不代表芯片不支持,而是不同协议对接收器和时序更敏感。检查:

  • Space-B 访问;
  • 接收器 Configuration 1~4;
  • Correlator;
  • NRT/MRT;
  • 天线带宽;
  • 测试卡是否确实属于目标协议。

26.5 NFC-A 模拟一直 Off

先看:

en_fd
efd_o
  • en_fd != 3:Operation Control 配置失败;
  • en_fd=3,efd_o=0:手机外场没有到达 RFI 检测路径;
  • efd_o=1:软件应进入 Idle,若没有则检查状态机和 I2C;
  • 进入 Idle 但不到 Ready:检查 NFC-A 自动响应、PT Memory 和时序。

26.6 手机能发现模拟卡,但读不到 NDEF

检查:

  • UID 和厂家页是否一致;
  • 两级 BCC;
  • Capability Container;
  • NDEF TLV 长度;
  • Terminator TLV0xFE
  • 模拟内存长度是否至少达到卡片totalSize()

27. 这颗芯片适合什么项目

适合:

  • 一台设备需要覆盖 A/B/F/V;
  • 需要 ISO-DEP、APDU、NDEF 和多种标签协议;
  • 需要 NFC-A 或 NFC-F 卡模拟;
  • 需要控制场强、接收增益、定时器和低功耗外场检测;
  • 有能力调试天线和射频电源。

不太适合:

  • 只读取固定 MIFARE Classic UID;
  • 主控资源很少,不准备维护协议状态机;
  • 没有射频测量条件,却希望直接复制参考原理图得到稳定距离;
  • 需要 125 kHz 低频 ID 卡。

ST25R3916 只能处理 13.56 MHz NFC/HF RFID,不能读取 EM4100、T5577、HID Prox 等 125 kHz 卡片。


ST25R3916 的特点不是“上手简单”,而是“可控范围大”。本工程把最容易出问题的部分都拆开处理:

  • 身份寄存器确认总线和芯片;
  • osc_ok 确认晶振真正稳定;
  • TX/RX 分级开启定位电源问题;
  • 四组中断统一成 32 位软件位图;
  • A/B/F/V 使用独立协议层;
  • 扫描和卡模拟通过编译宏互斥;
  • 卡模拟同时检查I_eonefd_o
  • 读卡与模拟分别使用 100 kHz 和 400 kHz I2C;
  • 构建结束自动生成合并固件。

真正决定最终体验的,仍然是三件事:

  1. 天线与匹配网络;
  2. 中断和协议时序;
  3. 是否按具体卡片类型实现认证和数据访问。

把 ST25R3916 当成“高级寄存器版读卡器”会觉得它很繁琐;把它当成一颗可以搭建完整 NFC 前端的芯片,很多复杂设计就变得合理了。

代码后续等我调通STM32部分再放上来 如果有需要评估这颗芯片的朋友可以私信 我打包工程()

测试效果视频,检测时按照1S轮询,所以可能显得比较慢

<iframe src="https://player.bilibili.com/player.html?isOutside=true&aid=117064920467499&bvid=BV1aUum67EJm&cid=40747404313&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>


[/i][/i][/i]

image.png
Snipaste_2026-08-09_19-51-47.png
image.png
收藏 评论2 发布时间:2026-8-9 19:53

举报

2个回答
短笛君 最优答案 回答时间:昨天 09:01

图标CV到论坛就莫名乱码()

duandijun/ST25R3916_ESP32

代码仓库以及readme源文件 STM32的x-cube-nfc正在移植中

STMCU-管管 回答时间:昨天 08:46

可太全面了

所属标签

ST NFC团队

意法半导体作为RFID和NFC技术的先行者之一,提供包括NFC / RFID标签、动态NFC标签、NFC / RFID读写器 & NFC控制器,安全NFC应用的安全组件和整套解决方案等在内的HF RFID/NFC产品。


最新内容

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版