ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F407驱动SL2823:从SPI到M1卡认证的NFC读卡器实战

2026/9/16 18:16:31 拓冰建站 浏览量
STM32F407驱动SL2823:从SPI到M1卡认证的NFC读卡器实战 简介一套用于快速搭建NFC读写系统的完整工程源码基于STM32F407与捷联微芯SL2823 NFC模块适合嵌入式开发者、物联网学习者及毕业设计人员参考。工程以Keil项目形式交付代码覆盖SL2823的初始化配置、I2C/SPI接口通信、寄存器读写、命令交互、中断处理、CRC校验与超时等错误恢复以及NFC应用层数据解析等关键环节并给出可直接编译运行的底层驱动骨架便于二次开发。压缩包共142个文件以68个.h头文件和58个.c源文件为主搭配启动文件、链接脚本及工程配置文件整体大小约752KB结构清晰能帮助阅读者快速定位驱动代码。目前已有240人学习使用对于需要在STM32平台接入NFC功能、理解SL2823驱动流程或进行项目预研的开发者这份代码可以提供较完整的参考实现涵盖从硬件初始化到应用交互的完整链路。1. 在 STM32F407 上把 SL2823 当主力 NFC 读卡器而不是继续用 RC522一个门禁读头或产线工位要读 ISO14443A 的 M1 卡主控是 STM32F407NFC 前端选什么老方案是 RC522便宜但底层状态机烦人天线参数稍微偏一点就寻卡不稳换 PN532 又要多一整套固件和串口协议。捷联微芯 SL2823 正好卡在这个中间位同样是 13.56MHz 读卡前端支持 ISO14443A/B 和 Mifare 系列成本可控SPI/UART/I2C 可选很多板厂直接把它做成邮票孔模组烧录好固件后对 MCU 就是一个发命令、收状态的外设。这篇文章按我最常走的路径讲从接口选型、CubeMX 初始化、驱动分层到寻卡、M1 认证读写和天线调试全程用硬件 SPI 而不是模拟时序代码基于 HAL 库标准库用户也能对照改。适合正在做门禁、充电桩、固定资产盘点或者实验室工具的人。2. 先定接口和硬件连接SPI 为什么比模拟 I2C 省心2.1 SL2823 的三种接口怎么选SL2823 这类读卡芯片一般同时引出 SPI、UART、I2C。看到网上很多人搜stm32f407 模拟 i2c甚至stm32f407 硬件 iic多半是被 I2C 地址和 ACK 时序折腾过。我一般直接选 SPI原因有三个F407 的 SPI1 挂在 APB2 上分频后能稳定跑到 10MHz 以上而读卡芯片的 SPI 从机上限通常就是 10MHz刚好吃满。SPI 是主从片选制不存在 I2C 那种总线竞争和地址配置问题驱动代码里少一个 7 位地址的处理。SL2823 的天线收发寄存器动辄几十字节SPI 全双工一次搬完配合 DMA 几乎不占 CPU。如果你一定要用 UART那走的是 AT 指令固件通道不是寄存器通道本文不展开。I2C 模式留给引脚紧张的板子但注意上拉电阻要按芯片手册配我遇到过 4.7k 上拉太强导致 SCL 高电平被拉低、NFC 命令频繁超时的情况。2.2 CubeMX 里 SPI 与 GPIO 的最小配置以 SPI1 为例F407 的 PA5、PA6、PA7 分别是 SCK、MISO、MOSI片选和复位用普通 GPIO 更稳。我习惯把 NSS 设为软件管理避免硬件 NSS 在通信中途被自动拉低造成帧错位。SL2823 引脚STM32F407 引脚模式VCC3V3电源注意天线驱动电流会瞬态拉高GNDGND共地靠近芯片引脚SCKPA5 / SPI1_SCK复用推挽MOSI (DIO)PA7 / SPI1_MOSI复用推挽MISOPA6 / SPI1_MISO复用输入CSPA4GPIO 推挽输出平时拉高RSTPE2GPIO 推挽输出上电先拉低IRQPE4GPIO 输入可配 EXTI 下降沿中断CubeMX 里 SPI1 的参数这样填Full-Duplex Master8 BitsMotorola MSB FirstPrescaler 选 8即 84MHz / 8 约 10.5MHz超过 SL2823 标称上限的话降到 16 分频。CPOL/CPHA 取决于芯片手册很多读卡芯片要求 CPOL0、CPHA0也就是空闲时钟低电平、第一个边沿采样如果你照抄别人的代码发现读回全是 0xFF先查这一项。2.3 上电时序和复位代码读卡芯片的复位不是简单的拉高拉低。SL2823 内部有晶振起振和固件装载过程掉电再上电时必须等 VCC 稳定后再释放复位。我见过有人把 RST 和 MCU 的复位引脚接在一起导致 MCU 和 NFC 同时起来NFC 固件还没就绪第一条命令就超时。void sl2823_hard_reset(void) { HAL_GPIO_WritePin(GPIOE, RST_Pin, GPIO_PIN_RESET); // 拉低复位 HAL_Delay(10); // 保证内部放电完成 HAL_GPIO_WritePin(GPIOE, RST_Pin, GPIO_PIN_SET); // 释放复位 HAL_Delay(50); // 等固件启动至少 30ms }这段代码里的 10ms 不是随便写的F407 的 GPIO 翻转很快但芯片内部电源去耦电容需要时间放电。50ms 的等待时长我一般会在调试期放大到 200ms 验证一次确认系统能跑后再缩回来。如果复位后立刻发命令返回值往往是 0x01 或 0xFF这是新手最容易踩的坑。3. 驱动分三层寄存器读写、命令帧、应用原语3.1 底层 SPI 收发函数驱动不要所有功能堆在一个大函数里。我的做法是先写一个sl2823_io()它负责单字节全双工交换所有读写都基于它。片选在每次交换前后拉低拉高保证芯片不会把相邻两次访问当成同一帧。static uint8_t sl2823_io(uint8_t out) { uint8_t in 0; HAL_GPIO_WritePin(GPIOA, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, out, in, 1, 10); HAL_GPIO_WritePin(GPIOA, CS_Pin, GPIO_PIN_SET); return in; }逻辑说明HAL 的TransmitReceive是全双工一个时钟周期内同时发出out并采样收进inNFC 芯片的命令返回就是这样以交换方式完成的。超时时间 10ms 已经很大因为单字节 SPI 在 10MHz 下不到 1us真正耗时的是芯片内部把射频命令送到卡片并等卡片应答但那部分由芯片处理不会堵在 SPI 总线里。写寄存器和读寄存器基于这个原语void sl2823_write_reg(uint8_t reg, uint8_t val) { CS_LOW(); sl2823_io(0x00); // 方向位写 sl2823_io(reg 0x7F); // 寄存器地址清掉最高位 sl2823_io(val); // 数据 CS_HIGH(); } uint8_t sl2823_read_reg(uint8_t reg) { uint8_t rx 0; CS_LOW(); sl2823_io(0x01); // 方向位读 sl2823_io(reg 0x7F); rx sl2823_io(0x00); // 空字节时钟移出数据 CS_HIGH(); return rx; }参数说明reg 0x7F把地址限制在 7 位最高位留给方向标记这是多数 SPI 读卡芯片的通用做法。如果你手上的 SL2823 手册用的是先发地址再用一条独立命令区分读写那只需把 0x00/0x01 替换成对应命令字即可。3.2 命令帧格式与校验SL2823 固件层通常要求主机把命令字 长度 数据 CRC打包成帧芯片处理完帧后回一个带状态字的响应帧。帧格式各家略有不同但思路一致域长度说明命令字1 字节如软复位 0x01、发送射频命令 0x10长度1 字节后续数据字节数不含 CRC数据N 字节具体命令参数CRC2 字节通常从命令字算到数据末尾因为帧长度很短CRC 用查表法最省事F407 主频高逐字节计算也毫秒级完成。实际发送射频命令时我推荐直接封装一个 transceive 原语内部完成组帧、校验、发送、收状态uint8_t sl2823_transceive(uint8_t *tx, uint16_t tx_len, uint8_t *rx, uint16_t *rx_len) { uint8_t frame[64]; uint16_t crc; frame[0] CMD_TRANSCEIVE; // 发送并接收 frame[1] (uint8_t)tx_len 2; // 数据长度 frame[2] (uint8_t)tx_len; // 子长度 memcpy(frame[3], tx, tx_len); crc sl2823_crc16(frame, 3 tx_len); frame[3 tx_len] (uint8_t)(crc 8); frame[3 tx_len 1] (uint8_t)(crc 0xFF); // 这里调用片选和全帧 SPI 发送省略重复代码 sl2823_frame_send(frame, 5 tx_len); // 读取响应帧检查状态字 if (sl2823_frame_recv(rx, rx_len) ! STATUS_OK) { return ERR_RF_TIMEOUT; } return STATUS_OK; }逻辑说明frame[2]是子长度这是因为部分固件把发送字节数和有效数据区分开方便芯片内部的 FIFO 管理。你移植到别的读卡芯片时这层可以按手册裁掉。返回值ERR_RF_TIMEOUT代表芯片已经把射频命令发出去了但没等到卡片应答这种超时和 SPI 通信错误要分开处理上层才能区分没卡和坏了。3.3 初始化序列软复位到天线开启上电后光拉高复位引脚还不够通常要再走一遍软复位和配置读取。配置里最重要的是射频增益和应答超时窗口增益太高会把相邻卡片一起吵醒太低则读卡距离缩水到一两厘米。uint8_t sl2823_init(void) { sl2823_hard_reset(); if (sl2823_write_reg(REG_SOFTRESET, 0x01) ! STATUS_OK) { return ERR_RESET; } HAL_Delay(20); uint8_t ver sl2823_read_reg(REG_VERSION); if (ver 0x00 || ver 0xFF) { return ERR_VERSION; // 芯片没进工作状态 } sl2823_write_reg(REG_RF_GAIN, 0x18); // 中等增益距离约 4cm sl2823_write_reg(REG_RF_TIMEOUT, 0x30); // 应答超时约 10ms sl2823_write_reg(REG_TX_MODE, 0x03); // 开天线使能收发 return STATUS_OK; }参数说明REG_VERSION读到 0x00 或 0xFF 是常见的两个失败形态。0x00 说明芯片刚复位完还在忙需要再等等0xFF 说明 SPI 时序不对——先查 CPOL/CPHA再查片选极性。REG_RF_GAIN的 0x18 不是通用值不同的天线板阻抗不同这个值要在样机上现场调。4. 寻卡、防碰撞与 M1 块读写实战4.1 ISO14443A 的 REQA 与防碰撞RFID 读卡的第一步是让卡片应答。ISO14443A 规定发送 0x26 唤醒处于空闲状态的卡片卡片回 2 字节 ATQA。SL2823 的 transceive 原语会直接把这个过程透传给 MCU。uint8_t nfc_request_a(nfc_card_t *card) { uint8_t cmd 0x26; // REQA uint8_t rx[8]; uint16_t len 0; if (sl2823_transceive(cmd, 1, rx, len) ! STATUS_OK) { return ERR_NO_CARD; } if (len 2) { return ERR_FRAME; // ATQA 必须是 2 字节 } card-atqa[0] rx[0]; card-atqa[1] rx[1]; return STATUS_OK; }卡返回 ATQA 后如果其长度为 7 字节的 UID就要走三层防碰撞级联 1、2、3。最常用的 M1 S50 是 4 字节 UID只走级联 1。防碰撞命令是 0x93第二字节是 NVB0x20 表示请求完整 UID0x70 表示带部分 UID 解析冲突位。uint8_t nfc_anticoll_uid4(nfc_card_t *card) { uint8_t tx[2] {0x93, 0x20}; uint8_t rx[8]; uint16_t len 0; if (sl2823_transceive(tx, 2, rx, len) ! STATUS_OK) { return ERR_NO_CARD; } if (len ! 5) { // 4 字节 UID 1 字节 BCC return ERR_FRAME; } for (int i 0; i 4; i) { card-uid[i] rx[i]; } card-uid_len 4; // 校验 BCC前 4 字节异或等于第 5 字节 if ((rx[0] ^ rx[1] ^ rx[2] ^ rx[3]) ! rx[4]) { return ERR_BCC; } return STATUS_OK; }逻辑说明BCC 校验失败最常见的原因是场内有另一张卡同时应答导致 4 字节 UID 被混合。遇到这种情况做法是重新发 REQA 再防碰撞最多重试 3 次。另外注意len ! 5的判断必须在读数据之前否则越界访问会把栈里的脏数据当 UID 用。4.2 M1 认证装载密钥与验证M1 S50 每个扇区有 A 密钥和 B 密钥。SL2823 读取数据块之前必须先对目标块做认证。很多教程只写调用认证命令却不说密钥字节序——密钥从高位到低位逐字节放进命令不能按数组顺序想当然。我一般这样封装uint8_t m1_auth_block(nfc_card_t *card, uint8_t block, uint8_t key_type, uint8_t *key6) { uint8_t tx[8]; uint8_t rx[8]; uint16_t len 0; tx[0] (key_type KEY_TYPE_A) ? 0x60 : 0x61; tx[1] block; memcpy(tx[2], key6, 6); // 6 字节密钥高位在前 if (sl2823_transceive(tx, 8, rx, len) ! STATUS_OK) { return ERR_AUTH; // 认证失败或超时 } // 芯片返回 0x00 表示认证通过 return (len 0 rx[0] 0x00) ? STATUS_OK : ERR_AUTH; }参数说明block是绝对块号0~63。M1 的前 32 个扇区每扇 4 块后 8 个扇区每扇 16 块直接传绝对块号可以让上层少做扇区换算。密钥长度必须是 6 字节多一个少一个都可能被芯片当作非法帧丢弃返回 0x01。默认密钥全 0xFF 在生产环境必须换掉否则等于给卡片开后门。4.3 数据块读写与值块操作认证通过后就能读写了。读块命令 0x30写块 0xA0数据总是 16 字节。写块前必须保证卡片已经在选卡状态SL2823 在认证成功后会自动保持选卡态但如果中间做过寻卡操作选卡态会丢失。uint8_t m1_read_block(uint8_t block, uint8_t data[16]) { uint8_t tx[2] {0x30, block}; uint8_t rx[18]; uint16_t len 0; if (sl2823_transceive(tx, 2, rx, len) ! STATUS_OK) { return ERR_TIMEOUT; } if (len ! 16) { return ERR_FRAME; // 数据区必须是完整 16 字节 } memcpy(data, rx, 16); return STATUS_OK; } uint8_t m1_write_block(uint8_t block, uint8_t data[16]) { uint8_t tx[18]; uint8_t rx[4]; uint16_t len 0; tx[0] 0xA0; tx[1] block; memcpy(tx[2], data, 16); if (sl2823_transceive(tx, 18, rx, len) ! STATUS_OK) { return ERR_WRITE; // 卡片可能处于只读状态 } return STATUS_OK; }M1 的块 3 是扇区尾块里面存着密钥 A、存取控制位、密钥 B。写这个块要格外小心一旦存取控制位写错这一整个扇区就永久锁死只能报废。所以我给块 3 单独加一个上层保护函数在业务代码里不允许直接调m1_write_block去写块 3。4.4 返回值状态码与排查方向SL2823 的响应帧里状态字可以用一张表快速定位问题状态字含义排查方向0x00正常无需处理0x01帧格式错误查命令字、长度字节、CRC0x02无卡片应答查天线匹配、卡是否在感应区0x0A认证失败查密钥字节序、块号、是否选卡0x0B写块失败查是否越权、块尾是否锁定这张表是通用读卡芯片的常见状态语义你的 SL2823 固件版本如果不同以数据手册附录的状态码清单为准。排查时我在工程里保留一个调试宏把每次 transceive 的原始帧打印到串口状态字异常时能直接看到芯片回的是什么而不是靠猜。5. 天线调谐快速验证与 FreeRTOS 任务接入5.1 没有网分仪怎么确认天线匹配天线匹配是 SL2823 最玄学的一环。理想情况是用网络分析仪看 13.56MHz 处的 S11低于 -15dB 才算合格。手头没设备时我一般用示波器探头10x 档轻触天线线圈两端看谐振波形峰值再测一个最实用的指标卡片从贴近到完全离开连续读卡 100 次记录最大稳定读卡距离。SL2823 的射频增益寄存器可以从默认值往上调几格如果距离不升反降说明天线已经过耦合反过来降增益往往立竿见影。5.2 把 NFC 轮询挂进 FreeRTOS在正点原子 F407 板子上跑 FreeRTOS 时NFC 任务放 5ms 周期、优先级中低即可因为 SL2823 的 SPI 收发一次不到 1ms主要是卡片应答等射频时间。任务主体不要做重消费逻辑读到 UID 后只投递到队列由业务任务处理。void nfc_task(void *arg) { nfc_card_t card; uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; for (;;) { if (nfc_request_a(card) STATUS_OK) { if (nfc_anticoll_uid4(card) STATUS_OK) { // 认证块 1然后读 16 字节数据 if (m1_auth_block(card, 1, KEY_TYPE_A, key) STATUS_OK) { uint8_t block[16]; if (m1_read_block(1, block) STATUS_OK) { xQueueSend(nfc_queue, card, 0); } } } } vTaskDelay(pdMS_TO_TICKS(NFC_POLL_INTERVAL_MS)); } }轮询周期我单独定义成NFC_POLL_INTERVAL_MS宏默认 80ms。这个值不是拍脑袋读卡芯片寻卡一次最快 20ms如果事务里还带认证和读块50ms 起步。调小到 30ms 能提升刷卡响应但天线会持续发射射频场功耗和电磁辐射都会上去。只在门禁这类非低功耗场景才用 30ms产线和实验室建议保持 100ms 以上配合卡片只要在感应区停留足够时间就能被读到。本文还有配套的精品资源点击获取