RFID實踐——.NET IoT程序讀取高頻RFID卡/標簽
這篇文章是一份RFID實踐的保姆級教程,將詳細介紹如何用 Raspberry Pi 連接 PN5180 模塊,并開發 .NET IoT 程序讀寫ISO14443 和 ISO15693協議的卡/標簽。
設備清單
- Raspberry Pi必需套件(主板、電源、TF卡)
- PN5180
- ISO15693標簽
- 杜邦線
- 面包板 ( 可選)
- GPIO擴展板 (可選 )
本文中用到的樹莓派型號是 Raspberry Pi Zero 2 W,電源直接使用充電寶替代(官方電源是5.1V / 2.5A DC)。
RFID基礎——高頻RFID協議、讀寫模塊和標簽中介紹過 PN5180 是一款價格便宜且支持全部高頻 RFID 協議的讀寫模塊。網購 PN5180 模塊時通常會送一張ICODE SLIX卡和 Mifare S50 卡。
ISO15693標簽選用的是國產的復旦微電子芯片的標簽。額外購買是為了測試多張標簽同時在射頻場中防碰撞功能。
杜邦線用于連接 Raspberry Pi Zero 2 W 和 PN5180 模塊。GPIO擴展板會標注邏輯引腳,配合面包板使用,方便連接多種傳感器。
樹莓派連接 PN5180
在 PN5180 上,您會注意到它上有13個引腳,這些引腳中只有9個是需要連接到樹莓派的GPIO引腳。對應關系如下表所示。表中特意標注出邏輯引腳和物理引腳,是因為后邊程序中需要設置引腳編號。詳情在后續代碼部分會進行解釋。
| NXP5180 | 邏輯引腳 | 物理引腳 |
|---|---|---|
| +5V | 5V | 2 |
| +3.3V | 3V3 | 1 |
| RST | GPIO4(GPCLK0) | 7 |
| NSS | GPIO3(SCL) | 5 |
| MOSI | GPIO10(SPI0-MOSI) | 19 |
| MISO | GPIO9(SPI0-MISO) | 21 |
| SCK | GPIO11(SPI0-SCLK) | 23 |
| BUSY | GPIO2(SDA) | 3 |
| GND | GND | 9 |
| GPIO | - | |
| IRQ | - | |
| AUX | - | |
| REQ | - |
下圖灰色區域中 1~40 是物理引腳編號,兩側標注的是邏輯引腳,例如物理引腳編號3的標注是 GPIO2 ,也就是對應的邏輯引腳編號為2。


到了這里,準備工作已經完成,接下來就是編碼了。
編寫.NET IoT程序
.NET IoT庫中已經實現了 PN5180 的部分功能。比如輪詢 ISO14443-A 和 ISO14443-B 類型的卡,以及對它們的讀寫操作,但是沒有實現對 ISO15693 協議卡的支持。
PN5180通過SPI和GPIO進行工作,它以特定的方式通過GPIO使用SPI進行通信。這就需要手動管理SPI的引腳選擇,Busy 引腳用于了解 PN5180 什么時候可以接收和發送信息。
首先,引用- System.Device.Gpio和 Iot.Device.Bindings兩個包,然后用下面的代碼創建SPI驅動程序、重置 PN5180 和創建 PN5180 實例。
var spi = SpiDevice.Create(new SpiConnectionSettings(0, 1) { ClockFrequency = Pn5180.MaximumSpiClockFrequency, Mode = Pn5180.DefaultSpiMode, DataFlow = DataFlow.MsbFirst });
// Reset the device
var gpioController = new GpioController();
gpioController.OpenPin(4, PinMode.Output);
gpioController.Write(4, PinValue.Low);
Thread.Sleep(10);
gpioController.Write(4, PinValue.High);
Thread.Sleep(10);
var pn5180 = new Pn5180(spi, 2, 3);
第1行代碼創建 SpiDevice 實例,其中設置 DataFlow = DataFlow.MsbFirst ,即首先發送最高有效位。需要注意的是,這里指的是主機與 PN5180 模塊之間的 SPI 總線的傳輸順序,RFID基礎——ISO15693標簽存儲結構及訪問控制命令說明中協議規定首先傳輸最低有效位指的是 VCD 與 VICC 之間的射頻通信,兩者是不同的數據傳輸過程。
第4行代碼創建 GpioController 實例,GpioController 類 的無參構造函數使用邏輯引腳編號方案作為默認方案。
第5行代碼開啟編號4的引腳,這個編號也就是指的邏輯引腳編號 GPIO4。
第11行創建 PN5180 讀寫器實例。構造函數定義如下:
public Pn5180 (System.Device.Spi.SpiDevice spiDevice, int pinBusy, int pinNss, System.Device.Gpio.GpioController? gpioController = default, bool shouldDispose = true);
第一個參數是 spi 設備實例,第二個參數是 Busy 引腳編號,第三個參數是 Nss 引腳編號,這里都是指的邏輯編號。代碼中的參數需和前面引腳對應表中指定的一致。
訪問ISO14443協議卡
訪問ISO14443協議卡比較簡單,調用 ListenToCardIso14443TypeA, ListenToCardIso14443TypeB 輪詢射頻場中的 PICC,然后選中卡進行操作,下邊是監聽 ISO14443-A 和 ISO14443-B 類型卡的示例代碼:
do
{
if (pn5180.ListenToCardIso14443TypeA(TransmitterRadioFrequencyConfiguration.Iso14443A_Nfc_PI_106_106, ReceiverRadioFrequencyConfiguration.Iso14443A_Nfc_PI_106_106, out Data106kbpsTypeA? cardTypeA, 1000))
{
Console.WriteLine($"ISO 14443 Type A found:");
Console.WriteLine($" ATQA: {cardTypeA.Atqa}");
Console.WriteLine($" SAK: {cardTypeA.Sak}");
Console.WriteLine($" UID: {BitConverter.ToString(cardTypeA.NfcId)}");
}
else
{
Console.WriteLine($"{nameof(cardTypeA)} is not configured correctly.");
}
if (pn5180.ListenToCardIso14443TypeB(TransmitterRadioFrequencyConfiguration.Iso14443B_106, ReceiverRadioFrequencyConfiguration.Iso14443B_106, out Data106kbpsTypeB? card, 1000))
{
Console.WriteLine($"ISO 14443 Type B found:");
Console.WriteLine($" Target number: {card.TargetNumber}");
Console.WriteLine($" App data: {BitConverter.ToString(card.ApplicationData)}");
Console.WriteLine($" App type: {card.ApplicationType}");
Console.WriteLine($" UID: {BitConverter.ToString(card.NfcId)}");
Console.WriteLine($" Bit rates: {card.BitRates}");
Console.WriteLine($" Cid support: {card.CidSupported}");
Console.WriteLine($" Command: {card.Command}");
Console.WriteLine($" Frame timing: {card.FrameWaitingTime}");
Console.WriteLine($" Iso 14443-4 compliance: {card.ISO14443_4Compliance}");
Console.WriteLine($" Max frame size: {card.MaxFrameSize}");
Console.WriteLine($" Nad support: {card.NadSupported}");
}
else
{
Console.WriteLine($"{nameof(card)} is not configured correctly.");
}
}
while (!Console.KeyAvailable);
有關 ISO14443協議的更多操作可以查看Iot.Device.Bindings中 PN5180 的文檔iot/src/devices/Pn5180 at main · dotnet/iot。
訪問ISO15693協議卡
由于Iot.Device.Bindings中的 PN5180 并沒有實現對 ISO15693協議的支持,因此需要自行實現這部分功能。
PN5180 模塊的工作原理可以簡單的理解為主機向 PN5180 模塊發送開啟、配置射頻場、操作卡/標簽(VICC)的命令,PN5180 模塊接收到操作卡/標簽(VICC)的命令時,通過射頻信號與卡/標簽(VICC)進行數據交互。尋卡過程的步驟如下:
- 加載ISO 15693協議到RF寄存器
- 開啟射頻場
- 清除中斷寄存器IRQ_STATUS
- 把PN5180設置為IDLE狀態
- 激活收發程序
- 向卡/標簽(VICC)發送16時隙防沖突的尋卡指令
- 循環16次以下操作
- 讀取RX_STATUS寄存器,判斷是否有卡/標簽響應
- 如果有響應,發送讀卡指令然后讀取卡的響應
- 在下一次射頻通信中只發送EOF(幀結束)而不發送數據。
- 把PN5180設置為IDLE狀態
- 激活收發程序
- 清除中斷寄存器IRQ_STATUS
- 向卡/標簽(VICC)發送EOF(幀結束)
- 關閉射頻場
上述步驟中只有步驟6向卡/標簽(VICC)發送16時隙防沖突的尋卡指令和步驟7.7向卡/標簽(VICC)發送EOF(幀結束)是 PN5180 和卡/標簽(VICC)之間的數據交互,其余的步驟都是PN5180 與主機之間通過SPI通信。
PN5180與主機通信
PN5180設計了24個主機接口命令,涉及讀寫寄存器、讀寫EEPROM、寫數據到發送緩沖區,從接收緩沖區讀數據,加載RF配置到寄存器,開啟關閉射頻場。包含44個寄存器,它們控制著PN5180處理器的行為。每個寄存器占4個字節。主機處理器可以通過4個不同的命令改變寄存器的值:write_register、 write_register_and_mask、 write_register_or_mask、write_register_multiple。
以下是本文中用到的主機接口命令說明:
write_register
這個命令將一個32位的值寫入配置寄存器。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x00 |
| 參數 | 1 | 寄存器地址 |
| 參數 | 4 | 寄存器內容 |
WRITE_REGISTER_OR_MASK
該命令使用邏輯或操作修改寄存器的內容。先讀取寄存器的內容,并使用提供的掩碼執行邏輯或操作,然后把修改后的內容寫回寄存器。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x01 |
| 參數 | 1 | 寄存器地址 |
| 參數 | 4 | 邏輯或操作的掩碼 |
WRITE_REGISTER_AND_MASK
該命令使用邏輯與操作修改寄存器的內容。先讀取寄存器的內容,并使用提供的掩碼執行邏輯與操作,然后把修改后的內容寫回寄存器。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x02 |
| 參數 | 1 | 寄存器地址 |
| 參數 | 4 | 邏輯與操作的掩碼 |
LOAD_RF_CONFIG
該命令用于將射頻配置從EEPROM加載到配置寄存器中。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x11 |
| 參數 | 1 | 發送器配置的值 |
| 寫入的數據 | 1 | 接收機配置的值 |
RF_ON
該命令打開內部射頻場。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x16 |
| 參數 | 1 | 1,根據 ISO/IEC 18092 禁用沖突避免 |
RF_OFF
該命令關閉內部射頻場
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x17 |
| 參數 | 1 | 虛字節 |
PN5180和卡/標簽(VICC)數據交互
PN5180和卡/標簽(VICC)數據交互本質上也是主機發送命令給 PN5180 模塊,然后 PN5180 把數據寫入緩沖區,接著射頻傳輸給卡/標簽(VICC),卡/標簽(VICC)響應后通過射頻傳出給 PN5180 模塊的接收緩沖區,主機發送命令讀取緩沖區數據。
SEND_DATA
該命令將數據寫入射頻傳輸緩沖區,開始射頻傳輸。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x09 |
| 參數 | 1 | 最后一個字節的有效位數 |
| 寫入的數據 | 1~260 | 最大長度為260的數組 |
最后一個字節的有效位數為0表示最后一字節所有的bit都被傳輸,1~7表示要傳輸的最后一個字節內的位數。
READ_DATA
從VICC成功接收數據后,該命令從射頻接收緩沖區讀取數據。
| 負載 | 長度 | 值/描述 |
|---|---|---|
| 命令編碼 | 1 | 0x0A |
| 參數 | 1 | 0x00 |
| 讀取的數據 | 1~508 | 最大長度為508的數組 |
代碼實現輪詢ISO15693卡
PN5180 和卡/標簽(VICC)之間的數據交互都是遵循RFID基礎——ISO15693標簽存儲結構及訪問控制命令說明中的命令。只需用代碼實現 PN5180 的主機接口指令以及ISO15693的訪問控制命令即可。首先Fork dotnet/iot版本庫,然后在 Pn5180.cs中加入以下監聽 ISO15693 協議卡的代碼:
/// <summary>
/// Listen to 15693 cards with 16 slots
/// </summary>
/// <param name="transmitter">The transmitter configuration, should be compatible with 15693 card</param>
/// <param name="receiver">The receiver configuration, should be compatible with 15693 card</param>
/// <param name="cards">The 15693 cards once detected</param>
/// <param name="timeoutPollingMilliseconds">The time to poll the card in milliseconds. Card detection will stop once the detection time will be over</param>
/// <returns>True if a 15693 card has been detected</returns>
public bool ListenToCardIso15693(TransmitterRadioFrequencyConfiguration transmitter, ReceiverRadioFrequencyConfiguration receiver,
#if NET5_0_OR_GREATER
[NotNullWhen(true)]
#endif
out IList<Data26_53kbps>? cards, int timeoutPollingMilliseconds)
{
cards = new List<Data26_53kbps>();
var ret = LoadRadioFrequencyConfiguration(transmitter, receiver);
// Switch on the radio frequence field and check it
ret &= SetRadioFrequency(true);
Span<byte> inventoryResponse = stackalloc byte[10];
Span<byte> dsfid = stackalloc byte[1];
Span<byte> uid = stackalloc byte[8];
int numBytes = 0;
DateTime dtTimeout = DateTime.Now.AddMilliseconds(timeoutPollingMilliseconds);
try
{
// Clears all interrupt
SpiWriteRegister(Command.WRITE_REGISTER, Register.IRQ_CLEAR, new byte[] { 0xFF, 0xFF, 0x0F, 0x00 });
// Sets the PN5180 into IDLE state
SpiWriteRegister(Command.WRITE_REGISTER_AND_MASK, Register.SYSTEM_CONFIG, new byte[] { 0xF8, 0xFF, 0xFF, 0xFF });
// Activates TRANSCEIVE routine
SpiWriteRegister(Command.WRITE_REGISTER_OR_MASK, Register.SYSTEM_CONFIG, new byte[] { 0x03, 0x00, 0x00, 0x00 });
// Sends an inventory command with 16 slots
ret = SendDataToCard(new byte[] { 0x06, 0x01, 0x00 });
if (dtTimeout < DateTime.Now)
{
return false;
}
for (byte slotCounter = 0; slotCounter < 16; slotCounter++)
{
(numBytes, _) = GetNumberOfBytesReceivedAndValidBits();
if (numBytes > 0)
{
ret &= ReadDataFromCard(inventoryResponse, inventoryResponse.Length);
if (ret)
{
cards.Add(new Data26_53kbps(slotCounter, 0, 0, inventoryResponse[1], inventoryResponse.Slice(2, 8).ToArray()));
}
}
// Send only EOF (End of Frame) without data at the next RF communication
SpiWriteRegister(Command.WRITE_REGISTER_AND_MASK, Register.TX_CONFIG, new byte[] { 0x3F, 0xFB, 0xFF, 0xFF });
// Sets the PN5180 into IDLE state
SpiWriteRegister(Command.WRITE_REGISTER_AND_MASK, Register.SYSTEM_CONFIG, new byte[] { 0xF8, 0xFF, 0xFF, 0xFF });
// Activates TRANSCEIVE routine
SpiWriteRegister(Command.WRITE_REGISTER_OR_MASK, Register.SYSTEM_CONFIG, new byte[] { 0x03, 0x00, 0x00, 0x00 });
// Clears the interrupt register IRQ_STATUS
SpiWriteRegister(Command.WRITE_REGISTER, Register.IRQ_CLEAR, new byte[] { 0xFF, 0xFF, 0x0F, 0x00 });
// Send EOF
SendDataToCard(new Span<byte> { });
}
if (cards.Count > 0)
{
return true;
}
else
{
return false;
}
}
catch (TimeoutException)
{
return false;
}
}
需要注意的是,尋卡指令SendDataToCard(new byte[] { 0x06, 0x01, 0x00 })發送的數據只有請求標志、命令、掩碼長度,并沒有CRC校驗碼,我推測是 PN5180 m模塊內部進行了CRC校驗,目前并沒有找到相關資料證實這個猜測。同樣,用 PN5180 讀寫標簽數據塊以及其他訪問控制指令也不需要CRC校驗碼。
讀寫ISO15693協議卡
由于支持 ISO15693 協議的讀寫器不只是 PN5180 ,因此把對 ISO15693 協議卡的具體讀寫操作放在 PN5180 的實現類中不太合適。這里定義了一個 IcodeCard 的類型,該類實現了 ISO15693 協議中常用的命令,并在構造函數中注入 RFID 讀寫器。執行指定操作時,調用 RFID 讀寫器的 Transceive 方法傳輸請求指令并接收響應進行處理。以下是主要代碼:
public class IcodeCard
{
public IcodeCard(CardTransceiver rfid, byte target)
{
_rfid = rfid;
Target = target;
_logger = this.GetCurrentClassLogger();
}
/// <summary>
/// Run the last setup command. In case of reading bytes, they are automatically pushed into the Data property
/// </summary>
/// <returns>-1 if the process fails otherwise the number of bytes read</returns>
private int RunIcodeCardCommand()
{
byte[] requestData = Serialize();
byte[] dataOut = new byte[_responseSize];
var ret = _rfid.Transceive(Target, requestData, dataOut.AsSpan(), NfcProtocol.Iso15693);
_logger.LogDebug($"{nameof(RunIcodeCardCommand)}: {_command}, Target: {Target}, Data: {BitConverter.ToString(requestData)}, Success: {ret}, Dataout: {BitConverter.ToString(dataOut)}");
if (ret > 0)
{
Data = dataOut;
}
return ret;
}
/// <summary>
/// Serialize request data according to the protocol
/// Request format: SOF, Flags, Command code, Parameters (opt.), Data (opt.), CRC16, EOF
/// </summary>
/// <returns>The serialized bits</returns>
private byte[] Serialize()
{
byte[]? ser = null;
switch (_command)
{
case IcodeCardCommand.ReadSingleBlock:
// Flags(1 byte), Command code(1 byte), UID(8 byte), BlockNumber(1 byte)
ser = new byte[2 + 8 + 1];
ser[0] = 0x22;
ser[1] = (byte)_command;
ser[10] = BlockNumber;
Uid?.CopyTo(ser, 2);
_responseSize = 5;
return ser;
// 略去代碼....
default:
return new byte[0];
}
}
/// <summary>
/// Perform a read and place the result into the 4 bytes Data property on a specific block
/// </summary>
/// <param name="block">The block number to read</param>
/// <returns>True if success. This only means whether the communication between VCD and VICC is successful or not </returns>
public bool ReadSingleBlock(byte block)
{
BlockNumber = block;
_command = IcodeCardCommand.ReadSingleBlock;
var ret = RunIcodeCardCommand();
return ret >= 0;
}
}
只需以下代碼就可以監聽射頻場中的 ISO15693 類型的卡并進行讀寫操作:
if (pn5180.ListenToCardIso15693(TransmitterRadioFrequencyConfiguration.Iso15693_ASK100_26, ReceiverRadioFrequencyConfiguration.Iso15693_26, out IList<Data26_53kbps>? cards, 20000))
{
pn5180.ResetPN5180Configuration(TransmitterRadioFrequencyConfiguration.Iso15693_ASK100_26, ReceiverRadioFrequencyConfiguration.Iso15693_26);
foreach (Data26_53kbps card in cards)
{
Console.WriteLine($"Target number: {card.TargetNumber}");
Console.WriteLine($"UID: {BitConverter.ToString(card.NfcId)}");
Console.WriteLine($"DSFID: {card.Dsfid}");
if (card.NfcId[6] == 0x04)
{
IcodeCard icodeCard = new IcodeCard(pn5180, card.TargetNumber)
{
Afi = 1,
Dsfid= 1,
Uid = card.NfcId,
Capacity = IcodeCardCapacity.IcodeSlix,
};
for (byte i = 0; i < 28; i++)
{
if (icodeCard.ReadSingleBlock(i))
{
Console.WriteLine($"Block {i} data is :{BitConverter.ToString(icodeCard.Data)}");
}
else
{
icodeCard.Data = new byte[] { };
}
}
}
else
{
Console.WriteLine("Only Icode cards are supported");
}
}
}
最后,就是把程序部署到 Raspberry pi 上,具體操作可以參照 Raspberry pi 上部署調試.Net的IoT程序。

浙公網安備 33010602011771號