简介:一套Keil工程文件整合了STM32F103C8T6与PN5180 NFC控制器的完整驱动与示例代码,面向嵌入式开发者,用于实现Mifare Classic及ICODE SLIX2非接触式卡片的读写,重点演示了在SPI2通信下不依赖BUSY信号、通过软件延时完成收发等待的时序处理思路。压缩包共1186个文件,约19.61MB,以.c/.h源码为主,同时包含工程配置、链接脚本、库文件及调试辅助文件,可直接在Keil环境中打开参考。目前已有1482人学习下载,代码中封装了NFC论坛库调用、命令发送与响应解析、卡操作API等关键模块,并提供了初始化、SPI配置、读写流程及调试建议,适合正在开发物联网设备、支付终端或门禁读卡器的工程师快速移植验证。 做 NFC 读卡器项目时,我翻遍了搜索引擎里关于“STM32F103、PN5180、keil 工程”的组合关键词,发现一个尴尬的事实:资料不少,但大多是零散的寄存器截图、Arduino 例程搬运,或者只讲 PN5180 本身、不讲怎么和 STM32F103 组织成一个完整可维护的 Keil 工程。这个组合几乎是做门禁、读卡器、NFC 标签读写最经典的嵌入式搭配,我自己也在踩完一堆坑后才算把流程理顺。这篇记录适合正准备把 PN5180 从 Arduino 或者 Linux 环境迁到 STM32F103 上、并且用 Keil 建工程的人,也适合已经开始动手但卡在 SPI 通信、启动文件和驱动移植上的朋友。
1. PN5180 和 STM32F103 是怎么看对眼的
1.1 为什么不是 RC522 或 PN532
很多人第一次做读卡器,脑子里第一个冒出来的是 RC522,毕竟十几块钱一片、资料满天飞、Arduino 一接就能读卡。但 RC522 只支持 ISO14443A 和 MIFARE 系列,协议覆盖面窄,射频前端的可配置性也一般,真做产品级的门禁、支付场景很容易碰壁。PN532 是另一个常见选择,接口丰富,做 NFC 交互很方便,但功率、灵敏度和协议覆盖和 PN5180 不是一个量级。
PN5180 是 NXP 的高性能 NFC 前端,支持 ISO/IEC 14443 Type A/B、ISO/IEC 15693、FeliCa 等常见卡片协议,内置收发器和协议处理固件,支持动态功率控制(DPC),还能通过 EEPROM 配置射频参数。换句话说,MCU 不需要去处理帧级的 CRC、奇偶校验这些脏活,只需要通过 SPI 给 PN5180 下发高层命令,再从 FIFO 里读结果。这正好让 STM32F103 这种“老而弥坚”的 MCU 发光发热:72MHz 主频跑起来绰绰有余,SPI 接口又是标配,成本低、资料多、开发环境成熟,市场上一抓一大把的核心板配合标准外设库,工程结构可以做到非常干净。
| 项目 | RC522 | PN532 | PN5180 |
|---|---|---|---|
| 支持协议 | ISO14443A / MIFARE | NFC 常见协议(14443A / FeliCa 等) | ISO14443A/B、ISO15693、FeliCa |
| 数据接口 | SPI/I2C/UART | UART/I2C/SPI | SPI 从机 |
| 射频能力 | 一般 | 一般 | 强,支持 DPC、功率可调 |
| 上手成本 | 极低 | 低 | 中等,需要 SPI 时序和驱动移植 |
| 典型场景 | 简单门禁 | NFC 交互、简单读卡 | 支付、门禁、工业读卡设备 |
1.2 这组合适合做什么
基于 STM32F103 加 PN5180 的 Keil 工程,最常见的落地场景是门禁读卡器、访客终端、充电桩身份识别模块、工业设备的 RFID 身份绑定,以及各种需要读取非接触式 IC 卡 UID 或数据的设备。
这些项目的共同特点是:主控要跑的业务逻辑并不复杂,但需要稳定、可配置的射频前端,而且团队往往对 STM32 生态很熟,不想为了一个 NFC 前端引入 Linux 或者更复杂的主控。STM32F103 负责按键、屏幕、继电器、串口通信这些周边,PN5180 专心做射频,各干各的活。只要 SPI 底层驱动稳定,后续换天线、调功率、加协议都是一层配置的事,不污染主控代码。
2. 硬件连接:PN5180 的 SPI 从机逻辑和上电时序
2.1 引脚分配
PN5180 模块一般引出 SPI 接口、BUSY、RST 和电源引脚。它作为 SPI 从机,工作电压 3.3V,引脚不多,接线也不复杂。下面是我常用的一组 SPI1 引脚分配,如果你板子上的复用引脚和别处冲突,换到 SPI2 也可以,但注意 SPI2 挂在 APB1 总线上,时钟上限比 SPI1 低,对 PN5180 这种通信频率不高的从机来说影响不大,就是初始化时时钟源要写对,别照抄 SPI1 的 RCC 使能。
| PN5180 引脚 | 作用 | 建议接 STM32F103 |
|---|---|---|
| SCK | SPI 时钟 | PA5(SPI1_SCK) |
| MOSI | 主机出,从机入 | PA7(SPI1_MOSI) |
| MISO | 主机入,从机出 | PA6(SPI1_MISO) |
| NSS | 片选 | PA4 或任意 GPIO |
| BUSY | 忙状态输出 | PA3(输入浮空/上拉) |
| RST | 复位输入 | PA2(推挽输出) |
| VCC / GND | 电源 | 3.3V / GND |
NSS 我从不使用 STM32 的硬件 NSS,而是用普通 GPIO 软件拉低拉高。原因很简单:PN5180 的 SPI 帧不是单纯发几字节,而是先发两字节 header 再发不定长数据,中间还可能要等 BUSY,软件片选在手,时序完全可控,排查问题也直观。BSUY 引脚一定要接,且要用输入模式轮询,不要省。很多移植跑不通的案例,根源就是没接 BUSY 或者接了没等。
2.2 复位时序和 BUSY 信号
PN5180 不是上电就能直接读寄存器,必须走一遍复位时序。我实测下来的稳定做法是:先保证 3.3V 电源稳定,RST 拉低至少 100 微秒以上,再拉高,之后等待 BUSY 引脚从高电平变成低电平,表示内部初始化完成。有些模块上电后要等 10 毫秒左右才就绪,这个时间不算长,但工程上要写成延时函数而不是靠人品。
BUSY 在 PN5180 处理内部命令时会被拉高,主机如果这时发 SPI 帧,数据会被忽略或者导致状态错乱。所以每次发起 SPI 事务前,先确认 BUSY 为低,或者事务完成后等它恢复。移植官方库时一般会有一个等待 BUSY 低电平的函数,但那个函数在异常状态下容易死循环,我的习惯是加超时,超时后打印错误并返回失败,否则项目烧进现场设备里,卡死一次就要断电重启,很难跟客户解释。
2.3 供电与天线
PN5180 的射频发射电流不小,虽然模块大多有稳压,但我不建议跟舵机、继电器、电磁锁共用同一个 3.3V 电源。如果项目里这些大电流外设必然存在,至少给 PN5180 单独加一颗 LDO,或者用双电源方案,否则读卡距离会忽远忽近,严重时直接复位重启。
天线是另一个容易被忽视的坑。如果你买的是成品模块,天线匹配电容电阻厂家已经调好,直接用没问题。如果自己画板子,天线走线宽度、线圈尺寸、谐振电容都要按 PN5180 数据手册和应用笔记(比如 NXP 关于天线设计的说明)来算,不是随便画个线圈就能有很好的读卡距离。这个主题展开又是另一篇长文,这里只提醒一句:读卡距离近,先怀疑天线,不要急着改 MCU 代码。
3. Keil 下从零建 STM32F103 标准库工程
3.1 标准库、Pack 和目录结构
虽然现在 ST 主推 HAL 库和 CubeMX,但 STM32F103 的标准外设库 V3.5.0 依然有大量存量项目在用,优势是轻、直观,适合像 PN5180 这种外设不多、逻辑清楚的工程。搜索“STM32F10x_StdPeriph_Lib”就能下载到那套经典库,解压后重点看两个目录:Libraries/CMSIS 和 Libraries/STM32F10x_StdPeriph_Driver。
MDK 版本建议用 5.x,安装完后用 Pack Installer 安装 STM32F1xx_DFP 器件支持包,否则 Device 列表里找不到 STM32F103 的具体型号。新建工程时,我习惯先把目录结构定好,不要让 Keil 自动把所有文件丢在一起:
project/ ├── User/ main.c、stm32f10x_it.c 等 ├── Libraries/ CMSIS 与标准库驱动 ├── Hardware/ PN5180 相关驱动文件 └── Objects/ 编译输出然后往工程里分 Group:Startup 放启动文件,CMSIS 放 core_cm3.c 和 system_stm32f10x.c,StdPeriph 按需添加 rcc.c、gpio.c、spi.c、usart.c、misc.c,User 放 main.c,Hardware 放 PN5180 的驱动。标准库驱动不需要全集添加,用多少加多少,否则编译时间和目标文件体积都往上走,还容易出现外设库之间的一些隐藏依赖问题。
3.2 启动文件与宏定义要匹配
这是 Keil 建 STM32F103 工程最常见的翻车点。选错启动文件或者宏定义不匹配,症状千奇百怪,最典型的是程序一上电就跑进 HardFault。
| 芯片类型 | Flash 容量 | 启动文件 | C/C++ 宏定义 |
|---|---|---|---|
| STM32F103C8T6 等 | 64KB~128KB | startup_stm32f10x_md.s | STM32F10X_MD, USE_STDPERIPH_DRIVER |
| STM32F103ZET6 等 | 256KB~512KB | startup_stm32f10x_hd.s | STM32F10X_HD, USE_STDPERIPH_DRIVER |
在 Options for Target 的 C/C++ 选项卡里填 Define 内容,同时在 Include Paths 里把 Libraries 相关的头文件目录加全。很多“找不到头文件”的编译错误,不是文件不存在,而是路径没加,或者中括号写多写少。另外,工程绝对路径里不要有中文和空格,这是 Keil 的老毛病,能避免就避免。
3.3 编译配置、授权和生成 bin
MDK 5 的授权问题一直没有消失过。我的态度比较直接:能拉正版授权或评估版就用,别花时间研究注册机。评估版有代码体积限制,PN5180 工程稍微加些协议栈就容易撞墙,破解环境里出现的调试器抽风、Pack 更新失败这类问题,排查起来特别浪费时间。这点投入真不该省。
生成 bin 文件在量产阶段很有用,配置方法是在 Options for Target 的 User 页 After Build/Rebuild 里加一行:
fromelf --bin --output=Objects/f103_pn5180.bin Objects/f103_pn5180.axf注意路径是相对 uvprojx 所在目录的,如果你把输出目录改成 Output 或者 Build,路径要对应改。烧录用 ST-Link 时,在 Debug 页选择 ST-Link Debugger,然后进入 Settings 确认能识别到芯片,否则下载时会报找不到目标。
3.4 验证工程:先点灯后串口
工程建好先别急着烧 PN5180 驱动,先在 main.c 里点亮一颗 LED,或者用串口打印一个“hello”,确认最小系统、时钟、调试器和下载链路全通。STM32F103 的外部晶振通常用 8MHz,标准库默认 HSE_VALUE 就是 8MHz,系统时钟最终通过 PLL 倍频到 72MHz。如果你的板子晶振不是 8MHz,要在 stm32f10x.h 里改 HSE_VALUE,并在 system_stm32f10x.c 里对 RCC_PLLConfig 做对应调整,否则串口波特率怎么配都是乱的。
4. PN5180 官方驱动移植:真正要改的只有三个地方
4.1 PN5180 的 SPI 帧协议
PN5180 虽然是 SPI 从机,但它的 SPI 帧和普通外设不太一样。每次事务开始,主机要把片选拉低,先发送两字节 header,再根据命令类型发送或接收 payload。第一字节是命令类型,常见的包括写寄存器、读寄存器、写 EEPROM、读 EEPROM、发送数据、读取数据等。第二字节的含义取决于命令:读写寄存器时它是寄存器地址,读写 FIFO 数据时它是数据长度。
这个设计初看有点绕,想清楚后就一句话:PN5180 通过这两字节告诉内部固件“你接下来要干什么”,然后主机按约定把数据补齐。如果第二字节的含义搞错,比如把长度当成了地址,寄存器读写会全部错位,读回来的数据就是一堆 0xFF 或者 0x00。
4.2 在 Keil 里实现 SPI 底层适配
从 NXP 的开源仓库获取 PN5180 驱动后,你会发现核心代码已经写好了,真正需要自己实现的底层函数并不多,主要是三个:SPI 数据收发、GPIO 控制片选与复位、延时。下面是一个基于标准外设库的最小适配伪代码,可以放到 Hardware 目录里:
void PN5180_SPI_Init(void) { SPI_InitTypeDef spi; GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCK: PA5, MOSI: PA7, MISO: PA6 */ gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); /* CS: PA4, RST: PA2, BUSY: PA3 */ gpio.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); GPIO_WriteBit(GPIOA, GPIO_Pin_4, Bit_SET); /* CS 默认高 */ GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET); /* RST 默认高 */ SPI_InitTypeDef spiInit; spiInit.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spiInit.SPI_Mode = SPI_Mode_Master; spiInit.SPI_DataSize = SPI_DataSize_8b; spiInit.SPI_CPOL = SPI_CPOL_Low; spiInit.SPI_CPHA = SPI_CPHA_1Edge; spiInit.SPI_NSS = SPI_NSS_Soft; spiInit.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; spiInit.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spiInit); SPI_Cmd(SPI1, ENABLE); }SPI 模式这里值得多说一句。PN5180 的手册和不同家的例程给出的模式不完全一致,我在实际项目里用 CPOL=0、CPHA=0 能正常工作,也见过有人用 CPOL=1、CPHA=1 跑起来的。遇到 MISO 读回全是 0xFF 或者 0x00 时,第一时间把 SPI 模式换一组极性和相位试,往往比反复检查寄存器快得多。速度上,建议一开始分频系数设大一点,比如 8 分频,稳定后再往 2 分频、4 分频调。
片选和字节收发实现:
void PN5180_CS_Low(void) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, Bit_RESET); } void PN5180_CS_High(void) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, Bit_SET); } uint8_t PN5180_SPI_TransferByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendByte(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveByte(SPI1); }有了这两个基础函数,就能实现 NXP 库要求的底层收发入口。关键点是发送 header 时和普通数据一样,不要额外插入延时,但每次事务结束要等 BUSY 释放后再开始下一次。
4.3 卡片轮询流程:REQA 到 UID
PN5180 的固件帮主机挡掉了大部分 ISO14443A 的底层细节。主机要做的事其实就是:开启射频场,通过 FIFO 发送 REQA,读回 ATQA,然后做防碰撞、选卡,最后拿到 UID。整体流程用伪代码看非常直观:
void PN5180_PollCard(void) { uint8_t cmd_reqa = 0x26; uint8_t atqa[2] = {0}; PN5180_RF_FieldOn(); PN5180_SendData(&cmd_reqa, 1); PN5180_ReadData(atqa, 2); if (atqa[0] != 0x00) { /* 有卡片响应,继续防碰撞和选卡 */ } }如果只是想快速读取卡片 UID,可以直接调用官方库提供的高层轮询函数,把 UID 缓冲区传进去就行。真正需要自己写状态机的场景一般是特殊协议、低功耗轮询或者多卡防碰撞策略,那时候再去啃数据手册也不迟。先让射频链路转起来,比什么都重要。
5. 实测链路:从编译不过到稳定读到 UID
5.1 排错顺序很重要
移植完成后的调试顺序是有讲究的,我见过太多人一上来就插卡测射频,结果代码跑飞了还以为是天线问题。我的固定流程是:
- 确认 Keil 工程编译通过,烧录后程序不跑飞,LED 或串口有响应。
- 用 PA8 的 MCO 功能把系统时钟引出来,或者直接看串口波特率是否准确。MCO 配置代码很简单,开启 GPIOA 和 AFIO 时钟后,把 PA8 配成复用推挽输出,再调用 RCC_MCOConfig 选择输出 PLL 二分频后的 36MHz。这个信号用示波器或者逻辑分析仪一看,时钟对不对立刻见分晓。
- 确认 SPI 通信:直接读 PN5180 的芯片版本寄存器并打印。这一步如果没通过,后面所有射频操作都没有意义。
- 确认复位流程没被跳过,BUSY 等待超时机制正常。
- 最后再把卡片放到天线上测 REQA 和 UID。
5.2 常见编译错误速查
| 错误或现象 | 原因 | 处理方式 |
|---|---|---|
| L6218E: Undefined symbol SystemInit | 启动文件或 system_stm32f10x.c 没加进工程 | 添加正确的 startup 文件,并加入 CMSIS 源文件 |
| cannot open source input file "stm32f10x.h" | Include Paths 配置不全 | 把 CMSIS 各层目录加进 Include Paths |
| 上电进入 HardFault | 宏定义与芯片容量不匹配,或启动文件加错 | 核对 STM32F10X_MD / STM32F10X_HD 与启动文件 |
| 串口打印乱码 | 系统时钟频率和 HSE_VALUE 不一致 | 检查晶振值,修改 PLL 配置 |
| SPI 读回全是 0xFF | SPI 模式不匹配、速度太快、CS 极性错误 | 换 SPI 模式,降低分频,用逻辑分析仪看波形 |
5.3 调不出来时,先确认 SPI 通没通
看似“射频没反应”的问题,最终常常落在 SPI 底层。我的血泪经验是:刚开始读 PN5180 芯片 ID 时,读回来一直是 0xFF,排查了很久寄存器配置,最后发现只是 SPI 模式没对。还有一次是 CS 引脚被复用成了别的外设,导致片选永远拉不低,整个 SPI 事务根本没被 PN5180 收到。
判断 SPI 通没通最直接的方法,是先用逻辑分析仪抓一次读寄存器事务。CS 拉低,MOSI 上能看到两字节 header,MISO 上能看到芯片回的数据。如果波形里 header 都不完整,问题一定在 GPIO 复用或 SPI 初始化;如果 header 完整但 MISO 没数据,再怀疑模式、速度和复位状态。这个 debug 思路比盲改代码高效得多。
5.4 一个小技巧:打开 SPI 日志
最后分享一个第一次移植时特别有用的技巧:在 SPI 底层收发函数里留一个日志开关。定义类似 PN5180_SPI_DEBUG 的宏,打开后每次事务都把 header 和收发数据通过串口打印出来。射频轮询时数据量不大,串口完全跟得上,你就能实时看到 REQA 发出去、ATQA 有没有回来、卡片的 UID 字节是什么顺序。等整个流程跑通了,再把宏关掉,不影响性能,也不影响代码结构。
这个技巧帮我抓到的最大一个坑是防碰撞循环里 UID 长度判断错误。PN5180 返回的 UID 可能是 4 字节、7 字节或者 10 字节,如果代码里只按 4 字节处理,卡片处理顺序一错,选卡阶段就失败。日志里把每次读到的数据打出来,配合数据手册一对照,问题当场就清楚了。
本文还有配套的精品资源,点击获取