SPI总线协议与RFID-RC522读卡器驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
UART 外设告一段落。今天换 SPI,先读卡。版本寄存器能读出来,芯片才算活着,然后再贴卡。


open("/dev/nfc") 返回正数,chmod 0666 也做了。读版本寄存器得到 0x00。于是有人开始刷卡、换天线、把 SPI 升到 20 MHz、怀疑卡坏了。
先别刷卡。0x00 和 0xFF 的意思是 SPI 上没读到芯片。卡还没上场。VersionReg 地址是 0x37,不是 0x00。你看到的 0x00 往往是:没软复位、把 read() 的返回值当字节数、或者把「先 write 地址再 read 数据」这套 I2C 习惯套到了这个驱动上。
产品 rk3568_evb。OpenHarmony 4.1,用户态 32 位 armv7 musl。节点 /dev/nfc,硬件 SPI,不是 GPIO 位拍。终极判据是 4 字节 UID,不是版本号好看不好看。

1. 现象
三类现场,看起来都像「读卡失败」,根因差很远。
A. 节点在,版本是 0x00 / 0xFF。 ls -l /dev/nfc 是 crw,open 成功。读 0x37 得到 0。有人立刻去调 ISO14443 状态机,FIFO 空、Request 超时,于是再换卡。芯片根本没在总线上应答,刷一百张也是 0。
B. 版本是 0xB2,页面报异常。 旧逻辑只认 0x92。兼容片能读卡,UID 正常。把判定放宽之前,SPI 速率、CS 极性、3.3 V 全换了一遍,全是空操作。
C. 版本正常,UID 永远 no-card。 刷的是 125 kHz ID 卡。RC522 是 13.56 MHz、ISO14443A。低频卡物理层就不对,不是天线没开。
根因按层拆:
| 层 | 看什么 |
|---|---|
| 节点 | `/dev/nfc` 是字符设备,0666 |
| 协议 | `read`/`write` 成功返回 **0**;读是「1 字节地址放进缓冲再 read 回填」 |
| 复位 | 先 `CommandReg=0x0F`,等 50 ms,再读 `VersionReg=0x37` |
| 版本 | `0x91`/`0x92` 正品;`0x88`/`0x90`/`0xB2`/`0xC2` 兼容片可用;`0x00`/`0xFF` 未检出 |
| 读卡 | Request `0x26`,Anticoll `0x93 0x20`,4 字节 UID + BCC |
| 卡 | ISO14443A 白卡;125 kHz ID 卡读不了 |
2. 硬件 SPI,不是 GPIO 位拍
应用 open/read/write
│ /dev/nfc
▼
内核 rc522:把寄存器地址编成 SPI 第一拍,spi_sync
│
▼
SPI3 CS0 / CLK / MOSI / MISO ≤ 8 MHz
│
▼
RC522 VersionReg / FIFO / TxControl
│
▼
13.56 MHz 天线 ──► ISO14443A 卡 ──► UID
MAX7219 点阵常用两根 GPIO 自己翻 CLK/MOSI,那是位拍,没有 /dev/nfc,也没有寄存器地址这一说。RC522 走控制器 SPI3,片选 0。两套协议不能混:把位拍时序抄过来,CS 乱跳,VersionReg 永远是 0。
SPI3 的脚和部分扩展模组共。共脚就一次一个。MAX7219 还插着的时候,CLK/MOSI 被两边抢,RC522 读出来的也是 0x00。验收 RFID 时把点阵拔掉。
电气:3.3 V。SCK、MOSI、MISO、CS 对上 SPI3 的 m1 组。TX1/TX2 天线线圈在模块上,用户态只开 TxControlReg 的 TX1/TX2 输出,不要去 GPIO 里找「天线脚」。
3. 设备树
控制器 SPI3,片选 0,频率 8 MHz。板级:
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
&spi3 {
pinctrl-names = "default";
pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;
status = "okay";
rc522: rc522@0 {
compatible = "board,rc522";
reg = <0>;
spi-max-frequency = <8000000>;
};
};
compatible 必须和驱动 of_device_id 一致。写成随便的字符串,probe 不发生,/dev/nfc 不出现。reg = <0> 是 CS0。频率 8 MHz 够,飞线到 20 MHz 波形塌,读出来又是 00。
改 DTS 之后 dtb 进 boot_linux 和 resource。生效经常是 resource 里的 rk-kernel.dtb。dd 打 /dev/block/ 下的分区,先 cat /proc/partitions 认介质。
现场核对:
hdc shell "ls -l /dev/nfc"
hdc shell "dmesg | grep -i nfc"
hdc shell "ls /sys/bus/spi/devices"
节点必须是 crw。第一位是 -,是有人 echo > /dev/nfc 建的假文件,open 也许成功,后面 read 不是 SPI。删掉假文件,让驱动重新创 c 节点。
dmesg 期望看到 probe 成功。没有 probe:compatible 对不上,或 &spi3 仍是 disabled。
HAP 不是 root。临时 chmod 0666 /dev/nfc,重启会丢。开机脚本放 device/board/rk/rk3568_evb/board_perm/board-perm.sh,由 *.rk30board.cfg 里 once = 1 的服务拉起来。hdc 能读、应用不能,就是 0666 没进镜像。
4. 用户态协议:一次 read 就是一次读寄存器
内核把 SPI 地址位移、bit7 读写位藏掉。应用看到的约定是:
- 读: 1 字节缓冲区先放寄存器地址,调用一次
read。驱动copy_from_user取出地址,SPI 读,把值copy_to_user回同一块缓冲。 - 写: 2 字节
{addr, val}调用一次write。
驱动 .read / .write 成功返回 0,不是 POSIX 教科书上的字节数。if (read(fd, &b, 1) != 1) 会把好结果当失败,接着用未更新的 b。若 b 当初就是地址 0x37,你可能误以为读到了版本;若 b 当初清零,VersionReg 看起来永远是 0。
不要 write(addr); read(&val) 两次系统调用。第二次 read 会被当成「再读一个寄存器」,地址是上一次留下的值。I2C 的「先写指针再读」在这里是错的。完整 RcRead / RcWrite 在下面那份 C 里。
read/write 不走 ioctl。不要给 RFID 无故加 ioctl。
内核侧 SPI 第一拍:读 ((reg << 1) & 0x7e) | 0x80,写 (reg << 1) & 0x7e。应用不要自己移位。把 0x37 直接放进缓冲即可。
CS 由控制器按 reg = <0> 拉。驱动里若再 gpio 翻 CS,和控制器打架。这份驱动的片选使能是空的,靠控制器。
5. 先软复位,再读 VersionReg
不复位就读,上电后寄存器可能是垃圾,偶发 0x00,被判未检出。50 ms 不是玄学,复位命令本身要时间。
enum { CommandReg = 0x01, VersionReg = 0x37 };
RcWrite(CommandReg, 0x0F); /* PCD_RESETPHASE */
usleep(50000);
v = RcRead(VersionReg);
判定:
| 值 | 含义 |
|---|---|
| `0x91` `0x92` | 正品 |
| `0x88` `0x90` `0xB2` `0xC2` | 兼容片,可用 |
| `0x00` `0xFF` 或读失败 | 未检出:SPI、供电、接线、权限、没复位、把返回值当字节数 |
| 其它非零 | SPI 通了,直接试读卡 |
本板见过 0xB2。兼容片不是故障。版本号只说明 SPI 和芯片还活着。UID 才是终极判据。0x00 时不要刷卡,先查节点、probe、read 返回值、复位,再查线。
6. ISO14443A:Request 0x26,Anticoll 0x93 0x20
Type A 卡。Request 用 PICC_REQIDL=0x26(空闲卡),短帧 7 位。防冲突发 0x93 0x20,返回 4 字节 UID + 1 字节 BCC(四字节异或)。uid[0]^uid[1]^uid[2]^uid[3] == bcc,否则 BCC 错。
无卡返回 no-card,这是常态,不是 SPI 坏了。先确认 VersionReg 已经不是 00,再谈无卡。
125 kHz ID 卡(门禁里那种又厚又圆、印刷 10 位十进制号的)物理层是 125 kHz ASK,不是 13.56 MHz Type A。Request 一定失败。验收用普通 13.56 MHz 白卡,贴天线 0~4 cm。
两张卡叠在一起走完整防冲突,这份用户态只做单卡。叠卡报 anticoll-err,先拿开一张。
芯片不提供「这次传输结束」的用户态信号,靠读 ComIrqReg:清 FIFO,把命令推进 FIFO,写 PCD_TRANSCEIVE,置 BitFramingReg 的 StartSend,轮询 Idle/Rx。超时还没有标志:没卡或天线没开。ErrorReg 的 0x1B 有位:协议/CRC/碰撞,当失败。
这段必须在 C 里跑。ArkTS 去轮询 SPI 寄存器会把 UI 卡成幻灯片,也更容易把时序拉长到芯片超时。寄存器常量和命令字写在下面那份 C 里,不要漏 CommandReg/FIFODataReg/TxControlReg/VersionReg。
7. 完整 C:打开、复位、版本分类、Request、Anticoll、打印 UID
rc522_uid.c:
/* rc522_uid.c — OpenHarmony 4.1 用户态,32 位 musl
* rc522_uid
* 打开 /dev/nfc,软复位,读 VersionReg,再 Request+Anticoll 打 UID。
* 驱动 read/write 成功返回 0,不要当字节数。
*/
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define PCD_IDLE 0x00
#define PCD_AUTHENT 0x0E
#define PCD_RECEIVE 0x08
#define PCD_TRANSMIT 0x04
#define PCD_TRANSCEIVE 0x0C
#define PCD_RESETPHASE 0x0F
#define PCD_CALCCRC 0x03
#define PICC_REQIDL 0x26
#define PICC_ANTICOLL1 0x93
#define CommandReg 0x01
#define ComIEnReg 0x02
#define DivIEnReg 0x03
#define ComIrqReg 0x04
#define DivIrqReg 0x05
#define ErrorReg 0x06
#define Status1Reg 0x07
#define Status2Reg 0x08
#define FIFODataReg 0x09
#define FIFOLevelReg 0x0A
#define WaterLevelReg 0x0B
#define ControlReg 0x0C
#define BitFramingReg 0x0D
#define CollReg 0x0E
#define ModeReg 0x11
#define TxModeReg 0x12
#define RxModeReg 0x13
#define TxControlReg 0x14
#define TxAutoReg 0x15
#define TxSelReg 0x16
#define RxSelReg 0x17
#define RxThresholdReg 0x18
#define DemodReg 0x19
#define CRCResultRegM 0x21
#define CRCResultRegL 0x22
#define ModWidthReg 0x24
#define RFCfgReg 0x26
#define GsNReg 0x27
#define CWGsPReg 0x28
#define ModGsPReg 0x29
#define TModeReg 0x2A
#define TPrescalerReg 0x2B
#define TReloadRegH 0x2C
#define TReloadRegL 0x2D
#define TCounterValRegH 0x2E
#define TCounterValRegL 0x2F
#define VersionReg 0x37
#define MI_OK 0
#define MI_NOTAGERR (-1)
#define MI_ERR (-2)
#define MAXRLEN 18
static int g_rc = -1;
static unsigned char RcRead(unsigned char addr)
{
unsigned char b = addr;
int ret = (int)read(g_rc, &b, 1);
if (ret < 0)
fprintf(stderr, "read addr 0x%02x: %s\n", addr, strerror(errno));
return b;
}
static void RcWrite(unsigned char addr, unsigned char val)
{
unsigned char b[2];
b[0] = addr;
b[1] = val;
int ret = (int)write(g_rc, b, 2);
if (ret < 0)
fprintf(stderr, "write 0x%02x: %s\n", addr, strerror(errno));
}
static void SetBit(unsigned char reg, unsigned char mask)
{
RcWrite(reg, (unsigned char)(RcRead(reg) | mask));
}
static void ClrBit(unsigned char reg, unsigned char mask)
{
RcWrite(reg, (unsigned char)(RcRead(reg) & (unsigned char)~mask));
}
static const char *verdict(int v)
{
if (v == 0x91 || v == 0x92)
return "正品";
if (v == 0x88 || v == 0x90 || v == 0xB2 || v == 0xC2)
return "兼容片,可用";
if (v == 0x00 || v == 0xFF)
return "未检出";
return "未知,SPI 通了,去读 UID";
}
static void antenna_on(void)
{
if (!(RcRead(TxControlReg) & 0x03))
SetBit(TxControlReg, 0x03);
}
static int pcd_com(unsigned char cmd, unsigned char *in, unsigned char inlen,
unsigned char *out, unsigned int *outbits)
{
unsigned char irq_en = 0, wait_for = 0, n, last;
unsigned int t;
int status = MI_ERR;
if (cmd == PCD_AUTHENT) {
irq_en = 0x12;
wait_for = 0x10;
} else if (cmd == PCD_TRANSCEIVE) {
irq_en = 0x77;
wait_for = 0x30;
}
RcWrite(ComIEnReg, (unsigned char)(irq_en | 0x80));
ClrBit(ComIrqReg, 0x80);
RcWrite(CommandReg, PCD_IDLE);
SetBit(FIFOLevelReg, 0x80);
for (t = 0; t < inlen; t++)
RcWrite(FIFODataReg, in[t]);
RcWrite(CommandReg, cmd);
if (cmd == PCD_TRANSCEIVE)
SetBit(BitFramingReg, 0x80);
t = 2000;
do {
n = RcRead(ComIrqReg);
t--;
} while (t && !(n & 0x01) && !(n & wait_for));
ClrBit(BitFramingReg, 0x80);
if (t) {
if (!(RcRead(ErrorReg) & 0x1B)) {
status = MI_OK;
if (n & irq_en & 0x01)
status = MI_NOTAGERR;
if (cmd == PCD_TRANSCEIVE) {
n = RcRead(FIFOLevelReg);
last = RcRead(ControlReg) & 0x07;
if (last)
*outbits = (unsigned int)(n - 1) * 8 + last;
else
*outbits = (unsigned int)n * 8;
if (n == 0)
n = 1;
if (n > MAXRLEN)
n = MAXRLEN;
for (t = 0; t < n; t++)
out[t] = RcRead(FIFODataReg);
}
} else {
status = MI_ERR;
}
}
SetBit(ControlReg, 0x80);
RcWrite(CommandReg, PCD_IDLE);
return status;
}
static int pcd_request(unsigned char *tag)
{
unsigned char buf[MAXRLEN];
unsigned int bits = 0;
int st;
ClrBit(Status2Reg, 0x08);
RcWrite(BitFramingReg, 0x07);
SetBit(TxControlReg, 0x03);
buf[0] = PICC_REQIDL;
st = pcd_com(PCD_TRANSCEIVE, buf, 1, buf, &bits);
if (st == MI_OK && bits == 0x10) {
tag[0] = buf[0];
tag[1] = buf[1];
return MI_OK;
}
return MI_ERR;
}
static int pcd_anticoll(unsigned char *uid)
{
unsigned char buf[MAXRLEN];
unsigned char bcc = 0, i;
unsigned int bits = 0;
int st;
ClrBit(Status2Reg, 0x08);
RcWrite(BitFramingReg, 0x00);
ClrBit(CollReg, 0x80);
buf[0] = PICC_ANTICOLL1;
buf[1] = 0x20;
st = pcd_com(PCD_TRANSCEIVE, buf, 2, buf, &bits);
if (st == MI_OK) {
for (i = 0; i < 4; i++) {
uid[i] = buf[i];
bcc ^= buf[i];
}
if (bcc != buf[4])
return MI_ERR;
}
SetBit(CollReg, 0x80);
return st;
}
static void iso14443a_init(void)
{
RcWrite(CommandReg, PCD_RESETPHASE);
usleep(50000);
RcWrite(ModeReg, 0x3D);
RcWrite(TReloadRegL, 30);
RcWrite(TReloadRegH, 0);
RcWrite(TModeReg, 0x8D);
RcWrite(TPrescalerReg, 0x3E);
RcWrite(TxAutoReg, 0x40);
ClrBit(Status2Reg, 0x08);
RcWrite(RxSelReg, 0x86);
RcWrite(RFCfgReg, 0x7F);
usleep(10000);
antenna_on();
}
int main(void)
{
unsigned char tag[2], uid[4];
int v, i, st;
g_rc = open("/dev/nfc", O_RDWR);
if (g_rc < 0) {
fprintf(stderr, "open /dev/nfc: %s\n", strerror(errno));
return 1;
}
RcWrite(CommandReg, PCD_RESETPHASE);
usleep(50000);
v = RcRead(VersionReg);
printf("VersionReg=0x%02X %s\n", v, verdict(v));
if (v == 0x00 || v == 0xFF) {
fprintf(stderr, "未检出,先别刷卡。查 SPI3、0666、复位、read 返回值。\n");
close(g_rc);
return 2;
}
iso14443a_init();
st = pcd_request(tag);
if (st != MI_OK) {
printf("no-card(贴 13.56 MHz ISO14443A 白卡,0~4 cm;125 kHz ID 卡读不了)\n");
close(g_rc);
return 3;
}
printf("ATQA=%02X %02X\n", tag[0], tag[1]);
st = pcd_anticoll(uid);
if (st != MI_OK) {
printf("anticoll-err 或 card-bcc-err(先拿开叠着的第二张)\n");
close(g_rc);
return 4;
}
printf("UID=");
for (i = 0; i < 4; i++)
printf("%02X", uid[i]);
printf("\n");
close(g_rc);
return 0;
}
32 位 musl:
OHOS=$HOME/code/opcode/OpenHarmony
CLANG=$OHOS/prebuilts/clang/ohos/linux-x86_64/llvm/bin/clang
SYSROOT=$OHOS/prebuilts/ohos-sdk/linux/11/native/sysroot
$CLANG --target=arm-linux-ohos --sysroot=$SYSROOT \
-O2 -Wall \
-Wl,--dynamic-linker=/system/lib/ld-musl-arm.so.1 \
-o rc522_uid rc522_uid.c
file rc522_uid
# ELF 32-bit LSB executable, ARM, interpreter /system/lib/ld-musl-arm.so.1
hdc file send rc522_uid /data/local/tmp/rc522_uid
hdc shell "chmod 0755 /data/local/tmp/rc522_uid"
hdc shell "chmod 0666 /dev/nfc"
hdc shell "/data/local/tmp/rc522_uid"
file 若是 aarch64,exec 失败。不要用主机 gcc。用户态必须 armv7 musl。
8. 验收
先跑一遍,卡离天线。期望类似:
VersionReg=0xB2 兼容片,可用
no-card(贴 13.56 MHz ISO14443A 白卡,0~4 cm;125 kHz ID 卡读不了)
0x91 / 0x92 / 0x88 / 0x90 / 0xC2 同样过。到这里 SPI 通了。再贴白卡,0~4 cm,再跑:
VersionReg=0xB2 兼容片,可用
ATQA=04 00
UID=A1B2C3D4
同一张卡 UID 稳定。换一张,UID 变。这是终极判据。
0x00 未检出:不要在空寄存器上跳 ISO 状态机。No such file 是 DTS/probe;Permission denied 是 0666。版本正常仍 no-card:卡距、卡类型、125 kHz。版本永远 0 且代码写了 read(...) != 1:驱动成功返回 0。
9. 排障顺序
1. ls -l /dev/nfc 字符设备?0666?
2. dmesg probe,spi3.0 在不在
3. compatible 是 board,rc522,和驱动 of_device_id 一致
4. 软复位 CommandReg=0x0F,等 50 ms,读 0x37
5. 00/FF:供电 3.3V、MISO、CS0、read 返回值、不要 write 再 read
6. 91/92/88/90/B2/C2:去读 UID,兼容片不是故障
7. no-card:13.56 MHz 白卡,0~4 cm;125 kHz ID 卡读不了
8. anticoll-err:叠卡、BCC
9. 工具行通、HAP 不行:权限、32 位 so
10. 和 MAX7219 共脚:一次一个;位拍协议不要抄到 SPI3
路径:
| 用途 | 路径 |
|---|---|
| DTS | `device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts` `&spi3` `rc522@0` |
| 驱动 | `device/board/rk/rk3568_evb/kernel/rc522.c` |
| 节点 | `/dev/nfc` |
| 工具 | `/data/local/tmp/rc522_uid` |
| 放权 | `device/board/rk/rk3568_evb/board_perm/board-perm.sh` |
系列第 51 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐




所有评论(0)