# PN532 — Byte-Level Communication Protocol The PN532 is a "dumb" NFC front-end: the host drives it by writing *command frames* and reading back *response frames*. This document describes those bytes — the framing, checksums, acknowledgements, error codes and command set — independent of the physical transport (HSU/UART, I²C or SPI). The transport only changes *how the bytes are clocked in/out*; the frame content is identical everywhere. > Sources: NXP *PN532 User Manual* UM0701-02 (the host protocol) and NXP > `PN532_C1.pdf` (the datasheet, for transport framing and the low-level CIU > command set). --- ## 1. The Information Frame Every command and every response is one *information frame* of bytes: ``` | Preamble | Start code | LEN | LCS | TFI | DATA ... | DCS | Postamble | | 00 | 00 FF | | | | | | 00 | ``` | Field | Size | Value / meaning | |-------|------|-----------------| | Preamble | 1 | `0x00` (see §1.4 for length rules) | | Start code | 2 | `0x00 0xFF` | | LEN | 1 | Length of the *data field* = 1 (TFI) + N (payload bytes). `0x01..0xFF`. | | LCS | 1 | Length Checksum — `(0x100 − LEN) & 0xFF`, i.e. `LEN + LCS == 0` (mod 256). | | TFI | 1 | Frame identifier: `0xD4` host→PN532, `0xD5` PN532→host. | | DATA | N | Command code (PD0) followed by parameters (PD1…PDn). | | DCS | 1 | Data Checksum — `(0x100 − (TFI + Σ DATA)) & 0xFF`, i.e. `TFI + Σ DATA + DCS == 0` (mod 256). | | Postamble | 1 | `0x00` (see §1.4) | **Checksums in one line (two's complement):** ``` LCS = (!LEN) + 1 DCS = (!(TFI + DATA0 + DATA1 + ...)) + 1 ``` A frame is rejected (no ACK is returned) if `LEN + LCS != 0` or the data checksum does not sum to zero. ### 1.1 Extended information frame (payload > 255 bytes) The firmware supports up to **264** data bytes (265 including TFI). Beyond the normal frame's 255-byte limit it uses an *extended* frame: ``` 00 00 FF FF FF LENM LENL LCS TFI PD0 ... PDn DCS 00 └─┬─┘ └──┬──┘ │ LEN = FF LCS = FF │ ← both fixed to 0xFF (normally an "error" LEN/LCS pair) LCS ← lower byte of [LENM + LENL + LCS] = 0x00 ``` - `LEN` and `LCS` are fixed to `0xFF`. - Real length: `LENGTH = LENM × 256 + LENL` = number of bytes in TFI + payload. - `LCS` satisfies `LENM + LENL + LCS == 0` (mod 256). - The host *may* use the extended frame for short frames too; the PN532 always picks the right form (normal ≤ 255, extended > 255). ### 1.2 Frame Identifier (TFI) | TFI | Direction | |-----|-----------| | `0xD4` | Host → PN532 | | `0xD5` | PN532 → Host | | `0x7F` | Error frame (PN532 → host, §2) | ### 1.3 Response command byte The response echoes the command code **+ 1**: | Command (host→PN532) | Response (PN532→host) | |----------------------|----------------------| | `GetFirmwareVersion` `0x02` | `0x03` | | `SAMConfiguration` `0x14` | `0x15` | | `InListPassiveTarget` `0x4A` | `0x4B` | | `InDataExchange` `0x40` | `0x41` | | … | `command + 1` | ### 1.4 Preamble / Postamble length rules They are **not** always a single `0x00` byte: - **Host → PN532** (HSU and I²C): preamble and postamble may be `0..n` bytes; the value has no impact on frame processing. The PN532 only synchronises on the `0x00 0xFF` start code. - **Host → PN532** (SPI): preamble and postamble **must** be exactly one `0x00` byte. - **PN532 → Host**: always a single `0x00` byte. This can be disabled entirely with `SetParameters` flag `fRemovePrePostAmble` (bit 6) to save 2 bytes per frame. --- ## 2. Acknowledge / NACK / Error Frames ``` ACK : 00 00 FF 00 FF 00 ← PN532 ↔ host: "frame received OK" NACK : 00 00 FF FF 00 00 ← host → PN532 only: "resend your last response" Error: 00 00 FF 01 FF 7F 81 00 ← PN532 → host: "syntax error at application level" ``` - The **ACK** has two roles: acknowledging a received frame, and (when sent by the host during command processing) **aborting** the current process. - The **NACK** is used *only* by the host, to ask the PN532 to retransmit its last response (after a corrupt/absent response). The PN532 never sends NACK — it just stays silent on a data-link error. - The **Error frame** is returned when the PN532 sees an *unknown command code* or *incorrect parameters* in an otherwise valid frame. ### 2.1 Dialog structure The host is always the master: ``` host ── command frame ──▶ PN532 host ◀── ACK ──────────── PN532 (must arrive within 15 ms; else host resends) ... PN532 executes ... host ◀── response frame ─ PN532 host ── (optional ACK) ─▶ PN532 ``` - **15 ms rule (HSU)**: the ACK must follow the command within 15 ms. If the host sees no ACK, it resends the command. - **Abort**: a new command, or a bare ACK, aborts the current process; the PN532 then answers only the last command received. - **Data-link errors** that silence the PN532: LCS error, DCS error, framing error (HSU stop bit = 0), HSU timeout (frame not fully received within ~4× a 256-byte frame; e.g. **89 ms** at 115200 baud, 44 ms at 230400, 8 ms at 1.288 M). --- ## 3. Status Byte and Error Codes RF commands (`InDataExchange`, `TgGetData`, `InListPassiveTarget`, …) return a status byte as the first payload byte: ``` 7 6 5 .. 0 NADPresent MI Error code ``` - bit 7 `NADPresent` — payload contains a NAD byte (DEP / ISO14443-4 PCD). - bit 6 `MI` — More Information (chaining) in progress. - bits 0–5 — error code (`0x00` = success). Error code list: | Code | Cause | |------|-------| | `0x00` | OK | | `0x01` | Time out — target did not answer | | `0x02` | CRC error detected by the CIU | | `0x03` | Parity error detected by the CIU | | `0x04` | Erroneous bit count during anticollision (14443-3 Type A / 18092 106 k) | | `0x05` | Framing error during MIFARE operation | | `0x06` | Abnormal bit-collision during bitwise anticollision at 106 k | | `0x07` | Communication buffer size insufficient | | `0x09` | RF buffer overflow (CIU_Error BufferOvfl) | | `0x0A` | RF field not switched on in time by counterpart (active mode) | | `0x0B` | RF protocol error | | `0x0D` | Temperature error — antenna drivers switched off | | `0x0E` | Internal buffer overflow | | `0x10` | Invalid parameter (range / format) | | `0x12` | DEP: unsupported command received from initiator | | `0x13` | DEP / MIFARE / 14443-4: data format does not match spec | | `0x14` | MIFARE: authentication error | | `0x23` | ISO14443-3: UID check byte wrong | | `0x25` | DEP: invalid device state | | `0x26` | Operation not allowed in this configuration | | `0x27` | Command not acceptable in current context (unknown target number, …) | | `0x29` | Target released by its initiator | | `0x2A` | 14443-3B: card ID mismatch (wrong card) | | `0x2B` | 14443-3B: previously activated card disappeared | | `0x2C` | NFCID3 mismatch (initiator vs target) in DEP 212/424 passive | | `0x2D` | Over-current detected | | `0x2E` | NAD missing in DEP frame | --- ## 4. Transport-Specific Bytes ### 4.1 HSU / UART - Full-duplex, up to **1.288 Mbaud** (default 115200), 8 data bits, LSB first, 1 stop bit. - Frames are sent as-is (see §1), preamble/postamble may be 0..n bytes. - Hardware preamble filter strips `00 00 FF` from incoming frames. ### 4.2 I²C - **Address**: 7-bit `0x24`; 8-bit write `0x48`, read `0x49` (`SLV+W = 0x48`, `SLV+R = 0x49`). - Fast mode up to 400 kHz. - The frame is "slightly modified": on **reads**, a **status byte** is prepended. On **writes** (commands) the frame is written verbatim (no status byte). ``` RDY status byte: bit 0 = RDY (bits 7..1 reserved) RDY = 0 → no frame available RDY = 1 → frame follows ``` Read sequence: START → read 1 status byte → if `RDY == 0`, STOP and retry; if `RDY == 1`, keep reading the whole frame before STOP. A STOP before the full frame discards the remaining bytes. ``` RDY ... RDY RDY frame ``` ### 4.3 SPI - Slave, SCK up to **5 MHz**. - Every transfer starts with a **direction byte** (2 LSBs): | First byte | Operation | |-----------|-----------| | `xxxx xx01` (`0x01`) | Data write (host → PN532) | | `xxxx xx10` (`0x02`) | Status read (PN532 → host) | | `xxxx xx11` (`0x03`) | Data read (PN532 → host) | - Status register (1 byte): **bit 0 = RDY**. Poll `0x02` until `RDY == 1`, then read with `0x03`. - SPI preamble/postamble **must** be exactly one `0x00` byte. - Optionally use the `P70_IRQ` pin (handshake) to skip status polling. --- ## 5. Host Command Set Each command is the first payload byte (PD0) after the `0xD4` TFI. `In` = initiator, `Tg` = target. | Command | Code | Parameters | |---------|------|-----------| | `Diagnose` | `0x00` | NumTst (1), [InParam…] | | `GetFirmwareVersion` | `0x02` | — | | `GetGeneralStatus` | `0x04` | — | | `ReadRegister` | `0x06` | Address (2, big-endian) | | `WriteRegister` | `0x08` | Address (2) + Value (1…n) | | `ReadGPIO` | `0x0C` | — | | `WriteGPIO` | `0x0E` | P3, P7 | | `SetSerialBaudRate` | `0x10` | Baud rate (1) | | `SetParameters` | `0x12` | Flags (1) | | `SAMConfiguration` | `0x14` | Mode (1), Timeout (1), [IRQ (1)] | | `PowerDown` | `0x16` | WakeUpEnable (1), GenerateIRQ (1) | | `RFConfiguration` | `0x32` | CfgItem (1), … | | `RFRegulationTest` | `0x58` | TxMode (1) | | `InJumpForPSL` | `0x46` | ActPSL (1), … | | `InJumpForDEP` | `0x56` | ActPass (1), … | | `InListPassiveTarget` | `0x4A` | MaxTg (1), BrTy (1), [InitiatorData] | | `InATR` | `0x50` | — | | `InPSL` | `0x4E` | Tg (1) | | `InDataExchange` | `0x40` | Tg (1), [DataOut…] | | `InCommunicateThru` | `0x42` | DataOut… | | `InDeselect` | `0x44` | Tg (1) | | `InRelease` | `0x52` | Tg (1) | | `InSelect` | `0x54` | Tg (1) | | `InAutoPoll` | `0x60` | PollNr (1), … | | `TgInitAsTarget` | `0x8C` | Mode (1), … | | `TgGetData` | `0x86` | — | | `TgSetData` | `0x8E` | DataIn… | | `TgGetInitiatorCommand` | `0x88` | — | | `TgResponseToInitiator` | `0x90` | Data… | | `TgGetTargetStatus` | `0x8A` | — | | `TgSetGeneralBytes` | `0x92` | Data… | | `TgSetMetaData` | `0x94` | Data… | --- ## 6. Worked Examples ### 6.1 GetFirmwareVersion (`0x02`) Request: ``` 00 00 FF 02 FE D4 02 2A 00 └─preamble+start─┘ │ │ │ │ └ postamble LEN=02 ───────┘ │ │ └ DCS=0x2A LCS=FE ──────────┘ └ command 0x02 TFI=0xD4 ───────────┘ ``` Response (`IC=0x32` PN532, firmware 1.6, rev 0x06, support 0x07): ``` 00 00 FF 06 FA D5 03 32 01 06 07 E8 00 │ │ │ │ │ │ └ DCS │ │ │ │ │ └ support (0x07) │ │ │ │ └ rev │ │ │ └ firmware version (0x01 = 1.6) │ │ └ IC = 0x32 (PN532) │ └ cmd+1 = 0x03 └ TFI = 0xD5 ``` `support` bitmask: `0x01` ISO/IEC 14443A, `0x02` ISO/IEC 14443B, `0x04` ISO/IEC 18092 (NFCIP-1). ### 6.2 SAMConfiguration (`0x14`) Params: Mode=`0x01` (normal), Timeout=`0x14` (20 × 50 ms = 1 s), IRQ=`0x01` (drive P70_IRQ). ``` 00 00 FF 05 FB D4 14 01 14 01 02 00 │ │ │ │ │ └ DCS │ │ │ └ IRQ │ │ └ Timeout (LSB 50 ms; 0x00 = no timeout) │ └ Mode (0x01 normal, 0x02 virtual card, 0x03 wired card, 0x04 dual card) └ command 0x14 ``` Response: ``` 00 00 FF 02 FE D5 15 16 00 ``` ### 6.3 InListPassiveTarget (`0x4A`) Params: MaxTg=`0x01`, BrTy=`0x00` (106 kbps ISO/IEC 14443A): ``` 00 00 FF 04 FC D4 4A 01 00 E1 00 │ │ │ └ BrTy (0x00 A, 0x01/0x02 FeliCa 212/424, 0x03 B, 0x04 Jewel) │ │ └ MaxTg (max 2) │ └ command 0x4A ``` Response (`0x4B`) for a MIFARE card with 4-byte UID `DE AD BE EF`: ``` 00 00 FF 0C F4 D5 4B 01 01 04 00 08 04 DE AD BE EF 96 00 │ │ │ │ │ │ │ │ └── NFCID1 ──┘ └ DCS │ │ │ │ │ │ │ └ NFCID length (4) │ │ │ │ │ │ └ SEL_RES │ │ │ │ └── SENS_RES (04 00) │ │ └ target number (Tg = 0x01) │ └ NbTg = 0x01 └ TFI 0xD5 ``` ### 6.4 InDataExchange (`0x40`) Params: Tg=`0x01`, then the raw card command — MIFARE Classic READ block 4 (`0x30 0x04`): ``` 00 00 FF 05 FB D4 40 01 30 04 B7 00 │ │ │ └─────┘ └ DCS │ │ └ MIFARE READ (0x30) block 04 │ └ target Tg = 0x01 (bit 6 = MI for DEP chaining) └ command 0x40 ``` Success response (`0x41`): Status=`0x00`, then 16 data bytes (`00..0F`): ``` 00 00 FF 13 ED D5 41 00 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 72 00 │ │ └ status 0x00 = success │ └ cmd+1 = 0x41 └ TFI 0xD5 ``` ### 6.5 ReadRegister (`0x06`) Params: register address (2 bytes, big-endian). Reading `CIU_RxSel` (`0x6307`): ``` 00 00 FF 04 FC D4 06 63 07 BC 00 ``` Response (`0x07`) with value `0x84`: ``` 00 00 FF 03 FD D5 07 84 A0 00 ``` --- ## 7. MIFARE / ISO/IEC 14443A Card Bytes These are the payload bytes passed inside `InDataExchange` / `InCommunicateThru`. | Operation | Card command | |-----------|--------------| | Authenticate key A / B | `0x60` / `0x61` + block + UID + key (6) | | Read block | `0x30` + block | | Write block | `0xA0` + block + 16 data bytes | | Write (Ultralight) | `0xA2` + page + 4 data bytes | | Transfer (write commit) | `0xB0` + block | | Decrement | `0xC0` + block + 4-byte value | | Increment | `0xC1` + block + 4-byte value | | Restore | `0xC2` + block | FeliCa payload codes: Polling `0x00`, Request Service `0x02`, Request Response `0x04`, Read Without Encryption `0x06`, Write Without Encryption `0x08`, Request System Code `0x0C`. --- ## 8. SetParameters (`0x12`) flags ```text D4 12 Flags ``` | Bit | Flag | Meaning | |-----|------|---------| | 0 | `fNADUsed` | Use NAD in DEP / 14443-4 PCD | | 1 | `fDIDUsed` | Use DID (DEP) / CID (14443-4 PCD) | | 2 | `fAutomaticATR_RES` | Auto-generate ATR_RES in target mode | | 4 | `fAutomaticRATS` | Auto-send RATS after selecting 14443-4 card | | 5 | `fISO14443-4_PICC` | Emulate ISO14443-4 PICC | | 6 | `fRemovePrePostAmble` | Omit preamble + postamble in frames sent to host | --- ## 9. CIU Command Set (low-level, from datasheet §8.6.20) The firmware drives a *Contactless Interface Unit* (CIU) whose commands live in the `CIU_Command` register. These are **not** host commands — they surface only via `WriteRegister`/`ReadRegister`. Listed for completeness: | Command | Code | Action | |---------|------|--------| | Idle | `0000` | No action; cancel current command | | Config | `0001` | Configure CIU for FeliCa / MIFARE / NFCIP-1 | | GenerateRandomID | `0010` | Generate 10-byte random ID | | CalcCRC | `0011` | Run CRC coprocessor (or self-test) | | Transmit | `0100` | Transmit data from FIFO | | NoCmdChange | `0111` | Modify `CIU_Command` bits without changing command | | Receive | `1000` | Activate receiver | | SelfTest | `1001` | Activate self-test | | Transceive | `1100` | Transmit then auto-receive (initiator) or vice-versa | | AutoColl | `1101` | FeliCa polling / MIFARE anticollision (card mode) | | MFAuthent | `1110` | MIFARE Classic authentication | | SoftReset | `1111` | Reset the CIU | --- ## 10. Minimum Startup Sequence 1. **Reset** — pulse `RSTPD_N` (high → low → wait ~400 ms → high), let the PN532 boot. 2. **GetFirmwareVersion** (`0x02`) — sanity-check the link and chip. 3. **SAMConfiguration** (`0x14`, mode `0x01`) — enable the SAM in normal (reader) mode; required before any RF command. 4. Poll with **InListPassiveTarget** (`0x4A`), then transact with **InDataExchange** (`0x40`).