Examples

This section provides practical examples for the two main course tasks: reading and writing NFC tags. These examples demonstrate the fundamental NFC operations and serve as the foundation for building more complex applications.

Course Tasks Overview

This project was developed as part of an NFC course with the following progression:

  1. Task 1: Tag Reading - Learn to detect and read NFC card information

  2. Task 2: Tag Writing - Learn to write data to NFC cards

  3. Extended Project: Access Control System - Full implementation combining both tasks

Task 1: Reading NFC Tags

The first course task focuses on reading NFC card UIDs and information. This is the foundation of any NFC application.

Learning Objectives

  • Initialize and configure the PN532 NFC reader

  • Detect when a card enters the reader field

  • Read card UID (Unique Identifier)

  • Identify different card types (Mifare Classic, NTAG, etc.)

  • Display and process card information

Example Code: read_example.cpp

Location: examples/read_example.cpp

Complete Source Code

The full example code is shown below. It demonstrates configurable communication modes (I2C/SPI) and reading modes (Polling/IRQ), along with comprehensive card type detection and UID display.

examples/read_example.cpp - Complete NFC Tag Reading Example
  1/*
  2 * NFC Tag Read Example - Course Task 1
  3 * 
  4 * This example demonstrates basic NFC tag reading functionality using the NFCReader class.
  5 * This is the first task in the NFC course: reading tag UIDs and card information.
  6 * 
  7 * What you'll learn:
  8 * - How to initialize the PN532 NFC reader
  9 * - How to detect when a card is present
 10 * - How to read card UID (Unique Identifier)
 11 * - How to identify different card types
 12 * - How to display card information
 13 * 
 14 * Supported Cards:
 15 * - Mifare Classic 1K/4K (4-byte UID)
 16 * - Mifare Ultralight (7-byte UID)
 17 * - NTAG213/215/216 (7-byte UID)
 18 */
 19
 20#include <Arduino.h>
 21#include "NFCReader.h"
 22
 23// ========== CONFIGURATION ==========
 24// Choose communication mode: I2C or SPI
 25#define USE_SPI  // Comment this out and uncomment USE_I2C to use I2C mode
 26// #define USE_I2C
 27
 28// Choose reading mode: POLLING or IRQ
 29// POLLING: Actively checks for cards at regular intervals
 30// IRQ: Uses interrupt for faster, more efficient detection
 31#define USE_IRQ_MODE  // Comment out for polling mode
 32
 33// ========== CREATE NFC READER INSTANCE ==========
 34#ifdef USE_I2C
 35  #ifdef USE_IRQ_MODE
 36    NFCReader nfcReader(NFCCommMode::I2C, NFCReadMode::IRQ);
 37  #else
 38    NFCReader nfcReader(NFCCommMode::I2C, NFCReadMode::POLLING);
 39  #endif
 40#elif defined(USE_SPI)
 41  #ifdef USE_IRQ_MODE
 42    NFCReader nfcReader(NFCCommMode::SPI, NFCReadMode::IRQ);
 43  #else
 44    NFCReader nfcReader(NFCCommMode::SPI, NFCReadMode::POLLING);
 45  #endif
 46#else
 47  #error "Please define either USE_I2C or USE_SPI"
 48#endif
 49
 50// ========== HELPER FUNCTIONS ==========
 51
 52/**
 53 * Print card type as human-readable string
 54 */
 55void printCardType(NFCCardType cardType) {
 56  Serial.print("Card Type: ");
 57  switch (cardType) {
 58    case NFCCardType::MIFARE_CLASSIC_1K:
 59      Serial.println("Mifare Classic 1K");
 60      break;
 61    case NFCCardType::MIFARE_CLASSIC_4K:
 62      Serial.println("Mifare Classic 4K");
 63      break;
 64    case NFCCardType::MIFARE_ULTRALIGHT:
 65      Serial.println("Mifare Ultralight");
 66      break;
 67    case NFCCardType::NTAG:
 68      Serial.println("NTAG (213/215/216)");
 69      break;
 70    default:
 71      Serial.println("Unknown");
 72      break;
 73  }
 74}
 75
 76/**
 77 * Print UID in hexadecimal format
 78 */
 79void printUID(const uint8_t* uid, uint8_t length) {
 80  Serial.print("UID: ");
 81  for (uint8_t i = 0; i < length; i++) {
 82    if (uid[i] < 0x10) Serial.print("0");
 83    Serial.print(uid[i], HEX);
 84    if (i < length - 1) Serial.print(" ");
 85  }
 86  Serial.println();
 87}
 88
 89/**
 90 * Print complete card information
 91 */
 92void printCardInfo(const NFCCardInfo& card) {
 93  Serial.println("┌─────────────────────────────────────┐");
 94  Serial.println("│       CARD DETECTED                 │");
 95  Serial.println("├─────────────────────────────────────┤");
 96  
 97  // Card type
 98  Serial.print("│ ");
 99  printCardType(card.cardType);
100  
101  // Physical UID
102  Serial.print("│ Physical ");
103  printUID(card.uid, card.uidLength);
104  Serial.print("│ UID Length: ");
105  Serial.print(card.uidLength);
106  Serial.println(" bytes");
107  
108  // Card ID (for 4-byte UIDs)
109  if (card.uidLength == 4) {
110    Serial.print("│ Card ID (Decimal): ");
111    Serial.println(card.cardID);
112  }
113  
114  // Cloned UID info (if present)
115  if (card.hasClonedUID) {
116    Serial.println("│ ");
117    Serial.println("│ ⚠️  CLONED UID DETECTED");
118    Serial.print("│ Cloned ");
119    printUID(card.clonedUID, card.clonedUIDLength);
120    Serial.println("│ ");
121    Serial.print("│ Effective ");
122    printUID(card.getEffectiveUID(), card.getEffectiveUIDLength());
123  }
124  
125  Serial.println("└─────────────────────────────────────┘");
126  Serial.println();
127}
128
129// ========== MAIN PROGRAM ==========
130
131void setup() {
132  // Initialize serial communication
133  Serial.begin(115200);
134  while (!Serial) delay(10);  // Wait for serial port to connect (needed for some boards)
135  
136  // Print header
137  Serial.println("═══════════════════════════════════════════");
138  Serial.println("     NFC TAG READ EXAMPLE - TASK 1         ");
139  Serial.println("═══════════════════════════════════════════");
140  Serial.println();
141  
142  // Display configuration
143  Serial.println("Configuration:");
144  #ifdef USE_I2C
145    Serial.println("  Communication: I2C");
146  #else
147    Serial.println("  Communication: SPI");
148  #endif
149  
150  #ifdef USE_IRQ_MODE
151    Serial.println("  Read Mode: IRQ (Interrupt-based)");
152  #else
153    Serial.println("  Read Mode: POLLING");
154  #endif
155  Serial.println();
156  
157  // Initialize NFC reader
158  Serial.println("Initializing NFC reader...");
159  if (!nfcReader.begin()) {
160    Serial.println("❌ Failed to initialize NFC reader!");
161    Serial.println("Please check:");
162    Serial.println("  - PN532 module connections");
163    Serial.println("  - Power supply");
164    Serial.println("  - Communication mode setting");
165    while (1) {
166      delay(1000);  // Halt execution
167    }
168  }
169  
170  Serial.println("✓ NFC reader initialized successfully!");
171  Serial.println();
172  Serial.println("═══════════════════════════════════════════");
173  Serial.println("Ready! Place a card near the reader...");
174  Serial.println("═══════════════════════════════════════════");
175  Serial.println();
176}
177
178void loop() {
179  // Read card information
180  NFCCardInfo cardInfo = nfcReader.readCard();
181  
182  // Check if a card was detected
183  if (cardInfo.detected) {
184    // Card found! Print all information
185    printCardInfo(cardInfo);
186    
187    // Example: You can use the UID for access control or logging
188    // For example, check if this is a specific card:
189    // if (cardInfo.uidLength == 4 && 
190    //     cardInfo.uid[0] == 0xAB && 
191    //     cardInfo.uid[1] == 0xCD && 
192    //     cardInfo.uid[2] == 0xEF && 
193    //     cardInfo.uid[3] == 0x12) {
194    //   Serial.println("This is my special card!");
195    // }
196    
197    // Wait a bit before reading again to avoid repeated detections
198    delay(2000);
199    
200    // Reset card state to allow detection of the same card again
201    nfcReader.resetCardState();
202    
203    Serial.println("Ready for next card...");
204    Serial.println();
205  }
206  
207  // In polling mode, add a small delay to avoid excessive checking
208  #ifndef USE_IRQ_MODE
209    delay(100);
210  #endif
211}

Key Features

  • Configurable communication mode (I2C or SPI via USE_I2C define)

  • Configurable reading mode (Polling or IRQ via USE_IRQ_MODE define)

  • Comprehensive card type detection (Mifare Classic 1K/4K, NTAG, Ultralight)

  • Formatted UID display with byte count

  • Auto-detection and card state reset for continuous reading

  • Detailed serial output for debugging and learning

Key Concepts

NFCCardInfo Structure

The NFCCardInfo structure contains all information about a detected card:

struct NFCCardInfo {
  bool detected;              // True if card was found
  uint8_t uid[7];            // Physical UID (manufacturer)
  uint8_t uidLength;         // UID length (4 or 7 bytes)
  NFCCardType cardType;      // Card type identifier
  uint32_t cardID;           // Numeric ID (for 4-byte UIDs)

  // Advanced features (card cloning)
  bool hasClonedUID;         // True if card has cloned UID
  uint8_t clonedUID[7];      // Cloned UID data
  uint8_t clonedUIDLength;
};

Communication Modes

  • I2C Mode: Simpler wiring, shared bus, slower

  • SPI Mode: More wires, dedicated bus, faster (recommended)

Reading Modes

  • POLLING: Actively checks for cards at intervals (~100ms)

  • IRQ: Interrupt-based detection, faster and more efficient (recommended)

Running the Example

Step 1: Hardware Setup

  • Connect PN532 module to Arduino Nano:

    • SPI mode (recommended):

      • SCK → Pin 13

      • MISO → Pin 12

      • MOSI → Pin 11

      • SS → Pin 10

      • IRQ → Pin 2 (optional, for IRQ mode)

      • RST → Pin 3

      • VCC → 3.3V or 5V (check your module)

      • GND → GND

  • Connect Arduino to computer via USB

Step 2: Build and Upload

The examples can be built directly from the examples/ directory using PlatformIO environments. This keeps your main system code intact.

Method 1: VS Code PlatformIO Extension (Recommended)

  1. Open project folder in VS Code

  2. Option A: Use PlatformIO Sidebar

    • Click PlatformIO icon in left sidebar

    • Expand PROJECT TASKStask1_read

    • Click Upload (builds and uploads automatically)

    • Click Monitor to view serial output

  3. Option B: Use VS Code Tasks

    • Press Ctrl+Shift+P (Cmd+Shift+P on macOS)

    • Type “Tasks: Run Task”

    • Select PlatformIO: Upload Task 1 (Read)

    • Then select PlatformIO: Monitor Task 1

Method 2: Command Line with Python Module (Always Works)

# Build and upload read example
python -m platformio run -e task1_read --target upload

# Open serial monitor
python -m platformio device monitor --baud 115200

Method 3: Command Line (if pio is in PATH)

# Build and upload read example
pio run -e task1_read --target upload

# Open serial monitor
pio device monitor --baud 115200

Method 4: With specific COM port

# Add --upload-port to any of the above commands
python -m platformio run -e task1_read --target upload --upload-port COM13
pio run -e task1_read --target upload --upload-port COM13

Note

Changing Default Environment for Upload Button:

The PlatformIO extension’s upload button builds the default environment specified in platformio.ini. To change which example is uploaded by the button:

  1. Open platformio.ini

  2. Modify the default_envs line:

    [platformio]
    default_envs = task1_read  # Change to: task1_read, task2_write, or nanoatmega328
    
  3. Save and use the upload button

Alternatively, use VS Code tasks or terminal commands to specify the environment explicitly.

Step 3: Finding Your COM Port

Windows:

# List available COM ports
pio device list

# Or check Device Manager → Ports (COM & LPT)

Linux/macOS:

# List USB serial devices
pio device list

# Or
ls /dev/tty.*        # macOS
ls /dev/ttyUSB*      # Linux
ls /dev/ttyACM*      # Linux (alternative)

Step 4: Open Serial Monitor

After uploading, open the serial monitor to view output:

# Monitor with correct baud rate (115200)
pio device monitor --baud 115200

# Or if using Python module
python -m platformio device monitor --baud 115200

Important: The examples use 115200 baud rate. If you see garbled text, verify the baud rate is correct.

Step 5: Testing

  • Place an NFC card near the reader

  • Observe the UID and card type in serial output

  • Try different card types

  • Press Ctrl+C to exit the monitor

Expected Output

═══════════════════════════════════════════
     NFC TAG READ EXAMPLE - TASK 1
═══════════════════════════════════════════

Configuration:
  Communication: SPI
  Read Mode: IRQ (Interrupt-based)

✓ NFC reader initialized successfully!
Ready! Place a card near the reader...

┌─────────────────────────────────────┐
│       CARD DETECTED                 │
├─────────────────────────────────────┤
│ Card Type: NTAG (213/215/216)
│ Physical UID: 04 A2 3C 12 5E 4F 80
│ UID Length: 7 bytes
└─────────────────────────────────────┘

Task 2: Writing to NFC Tags

The second course task focuses on writing data to NFC cards. Different card types require different write approaches.

Note

After uploading the write example, always open the serial monitor to see the results:

pio device monitor --baud 115200

The baud rate must be 115200, otherwise you’ll see garbled output.

Warning

Memory Usage Note: The write example uses more RAM than the read example due to write verification and multiple String objects. If you encounter “data size greater than maximum allowed” errors:

  1. The code will still work but without buffer overflow protection

  2. Reduce serial output strings if needed

  3. Use the optimized build flags (already included in platformio.ini)

  4. Consider simplifying the example by removing some write operations

Arduino Nano has only 2KB RAM. The examples are designed to be educational with verbose output, which uses more memory than a production system would.

Learning Objectives

  • Understand the difference between block-based and page-based storage

  • Write text strings to NFC cards

  • Write binary data to specific addresses

  • Verify written data

  • Handle authentication (for Mifare Classic)

  • Avoid common mistakes (writing to protected areas)

Example Code: write_example.cpp

Location: examples/write_example.cpp

Complete Source Code

The full example code demonstrates writing to both NTAG and Mifare Classic cards with verification and multiple write operation examples.

examples/write_example.cpp - Complete NFC Tag Writing Example
  1/*
  2 * NFC Tag Write Example - Course Task 2
  3 * 
  4 * This example demonstrates how to write data to NFC cards using the NFCReader class.
  5 * This is the second task in the NFC course: writing data to NFC tags.
  6 * 
  7 * What you'll learn:
  8 * - How to write data to different types of NFC cards
  9 * - How to write text strings to tags
 10 * - How to write binary data
 11 * - How to verify written data
 12 * - Differences between Mifare Classic and NTAG/Ultralight writing
 13 * 
 14 * Supported Cards:
 15 * - Mifare Classic 1K/4K (block-based, requires authentication)
 16 * - Mifare Ultralight (page-based, 4 bytes per page)
 17 * - NTAG213/215/216 (page-based, 4 bytes per page)
 18 * 
 19 * IMPORTANT NOTES:
 20 * - Block 0 (manufacturer block) is READ-ONLY and CANNOT be written to
 21 * - For Mifare Classic, NEVER write to sector trailers (blocks 3, 7, 11, etc.) - will lock sector
 22 * - For NTAG/Ultralight, avoid lock bytes and OTP areas
 23 * - Always use safe user data areas: Block 4+ for Mifare, Page 4+ for NTAG
 24 * - Default authentication key for Mifare Classic: FF FF FF FF FF FF
 25 */
 26
 27#include <Arduino.h>
 28#include "NFCReader.h"
 29
 30// ========== CONFIGURATION ==========
 31// Choose communication mode: I2C or SPI
 32#define USE_SPI  // Comment this out and uncomment USE_I2C to use I2C mode
 33// #define USE_I2C
 34
 35// Choose reading mode: POLLING or IRQ
 36#define USE_IRQ_MODE  // Comment out for polling mode
 37
 38// ========== CREATE NFC READER INSTANCE ==========
 39#ifdef USE_I2C
 40  #ifdef USE_IRQ_MODE
 41    NFCReader nfcReader(NFCCommMode::I2C, NFCReadMode::IRQ);
 42  #else
 43    NFCReader nfcReader(NFCCommMode::I2C, NFCReadMode::POLLING);
 44  #endif
 45#elif defined(USE_SPI)
 46  #ifdef USE_IRQ_MODE
 47    NFCReader nfcReader(NFCCommMode::SPI, NFCReadMode::IRQ);
 48  #else
 49    NFCReader nfcReader(NFCCommMode::SPI, NFCReadMode::POLLING);
 50  #endif
 51#else
 52  #error "Please define either USE_I2C or USE_SPI"
 53#endif
 54
 55// ========== HELPER FUNCTIONS ==========
 56
 57/**
 58 * Print card type as human-readable string
 59 */
 60void printCardType(NFCCardType cardType) {
 61  Serial.print(F("Card Type: "));
 62  switch (cardType) {
 63    case NFCCardType::MIFARE_CLASSIC_1K:
 64      Serial.println(F("Mifare Classic 1K"));
 65      break;
 66    case NFCCardType::MIFARE_CLASSIC_4K:
 67      Serial.println(F("Mifare Classic 4K"));
 68      break;
 69    case NFCCardType::MIFARE_ULTRALIGHT:
 70      Serial.println(F("Mifare Ultralight"));
 71      break;
 72    case NFCCardType::NTAG:
 73      Serial.println(F("NTAG (213/215/216)"));
 74      break;
 75    default:
 76      Serial.println(F("Unknown"));
 77      break;
 78  }
 79}
 80
 81/**
 82 * Print write result with appropriate formatting
 83 */
 84void printWriteResult(const NFCWriteResult& result, const __FlashStringHelper* operation) {
 85  Serial.print(F("  "));
 86  Serial.print(operation);
 87  Serial.print(F(": "));
 88  
 89  if (result.success) {
 90    Serial.print(F("✓ SUCCESS"));
 91    if (result.verified) {
 92      Serial.println(F(" (Verified)"));
 93    } else {
 94      Serial.println();
 95    }
 96  } else {
 97    Serial.print(F("✗ FAILED - "));
 98    Serial.println(result.errorMessage);
 99  }
100}
101
102// ========== MAIN PROGRAM ==========
103
104void setup() {
105  // Initialize serial communication
106  Serial.begin(115200);
107  while (!Serial) delay(10);  // Wait for serial port to connect
108  
109  // Print header
110  Serial.println(F("═══════════════════════════════════════════"));
111  Serial.println(F("    NFC TAG WRITE EXAMPLE - TASK 2         "));
112  Serial.println(F("═══════════════════════════════════════════"));
113  Serial.println();
114  
115  // Display configuration
116  Serial.println(F("Configuration:"));
117  #ifdef USE_I2C
118    Serial.println(F("  Communication: I2C"));
119  #else
120    Serial.println(F("  Communication: SPI"));
121  #endif
122  
123  #ifdef USE_IRQ_MODE
124    Serial.println(F("  Read Mode: IRQ (Interrupt-based)"));
125  #else
126    Serial.println(F("  Read Mode: POLLING"));
127  #endif
128  Serial.println();
129  
130  // Initialize NFC reader
131  Serial.println(F("Initializing NFC reader..."));
132  if (!nfcReader.begin()) {
133    Serial.println(F("❌ Failed to initialize NFC reader!"));
134    Serial.println(F("Please check:"));
135    Serial.println(F("  - PN532 module connections"));
136    Serial.println(F("  - Power supply"));
137    Serial.println(F("  - Communication mode setting"));
138    while (1) {
139      delay(1000);  // Halt execution
140    }
141  }
142  
143  Serial.println(F("✓ NFC reader initialized successfully!"));
144  Serial.println();
145  Serial.println(F("═══════════════════════════════════════════"));
146  Serial.println(F("Ready! Place a card near the reader..."));
147  Serial.println(F("═══════════════════════════════════════════"));
148  Serial.println();
149}
150
151void loop() {
152  // Read card first to detect it
153  NFCCardInfo cardInfo = nfcReader.readCard();
154  
155  if (cardInfo.detected) {
156    Serial.println(F("┌──────────────────────────────────────────┐"));
157    Serial.println(F("│      CARD DETECTED - WRITING DATA       │"));
158    Serial.println(F("├──────────────────────────────────────────┤"));
159    Serial.print(F("│ "));
160    printCardType(cardInfo.cardType);
161    Serial.println(F("└──────────────────────────────────────────┘"));
162    Serial.println();
163    
164    NFCWriteResult result;
165    
166    // ========== EXAMPLE 1: Simple String Write (Auto-detect card type) ==========
167    Serial.println(F("Example 1: Writing string using auto-detect"));
168    String message = "Hello NFC!";
169    result = nfcReader.writeString(message, 4, true);  // Start at safe address, verify
170    printWriteResult(result, F("String write"));
171    Serial.println();
172    
173    // ========== EXAMPLE 2: Card Type-Specific Writing ==========
174    if (cardInfo.cardType == NFCCardType::MIFARE_ULTRALIGHT || 
175        cardInfo.cardType == NFCCardType::NTAG) {
176      
177      Serial.println(F("Example 2: NTAG/Ultralight specific operations"));
178      Serial.println(F("  Writing to page 4 (safe user area)..."));
179      
180      // Write 4 bytes of data to page 4
181      uint8_t pageData[] = {0x01, 0x02, 0x03, 0x04};
182      result = nfcReader.writeNTAG(4, pageData, 4, true);
183      printWriteResult(result, F("Page 4 write"));
184      
185      // Write a longer string across multiple pages
186      String longText = "NFC Course Task 2: Write";
187      result = nfcReader.writeNTAGString(5, longText, true);
188      printWriteResult(result, F("Multi-page string"));
189      
190    } else if (cardInfo.cardType == NFCCardType::MIFARE_CLASSIC_1K ||
191               cardInfo.cardType == NFCCardType::MIFARE_CLASSIC_4K) {
192      
193      Serial.println(F("Example 2: Mifare Classic specific operations"));
194      Serial.println(F("  Using default key: FF FF FF FF FF FF"));
195      Serial.println(F("  Writing to block 4 (Sector 1, safe area)..."));
196      
197      // Write 16 bytes to block 4 (first user block in Sector 1)
198      uint8_t blockData[16];
199      for (int i = 0; i < 16; i++) {
200        blockData[i] = i + 1;  // Data: 01 02 03 ... 10
201      }
202      result = nfcReader.writeMifareClassic(4, blockData, 16, DEFAULT_KEY, false, true);
203      printWriteResult(result, F("Block 4 write"));
204      
205      // Write a string to block 5
206      String text = "Mifare Test Data";
207      result = nfcReader.writeMifareClassicString(5, text, DEFAULT_KEY, false, true);
208      printWriteResult(result, F("Block 5 string"));
209      
210    } else {
211      Serial.println(F("⚠️  Unknown card type - skipping type-specific examples"));
212    }
213    
214    Serial.println();
215    
216    // ========== EXAMPLE 3: Writing Binary Data ==========
217    Serial.println(F("Example 3: Writing custom binary data"));
218    uint8_t binaryData[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE};
219    
220    if (cardInfo.cardType == NFCCardType::NTAG || 
221        cardInfo.cardType == NFCCardType::MIFARE_ULTRALIGHT) {
222      // For NTAG: Write to page 8
223      result = nfcReader.writeNTAG(8, binaryData, 4, true);  // Only 4 bytes per page
224      printWriteResult(result, F("Binary data (page 8)"));
225    } else if (cardInfo.cardType == NFCCardType::MIFARE_CLASSIC_1K) {
226      // For Mifare Classic: Write to block 6
227      result = nfcReader.writeMifareClassic(6, binaryData, 8, DEFAULT_KEY, false, true);
228      printWriteResult(result, F("Binary data (block 6)"));
229    }
230    
231    Serial.println();
232    Serial.println(F("═══════════════════════════════════════════"));
233    Serial.println(F("Write operations complete!"));
234    Serial.println(F("═══════════════════════════════════════════"));
235    Serial.println();
236    
237    // Wait before allowing next write (prevents accidental repeated writes)
238    Serial.println(F("Waiting 3 seconds..."));
239    delay(3000);
240    
241    // Reset card state to allow re-detection
242    nfcReader.resetCardState();
243    
244    Serial.println(F("Ready for next card..."));
245    Serial.println();
246  }
247  
248  // In polling mode, add a small delay to avoid excessive checking
249  #ifndef USE_IRQ_MODE
250    delay(100);
251  #endif
252}

Key Features

This example demonstrates writing to both NTAG and Mifare Classic cards:


examples/write_example.cpp - Complete NFC Tag Writing Example
  1/*
  2 * NFC Tag Write Example - Course Task 2
  3 * 
  4 * This example demonstrates how to write data to NFC cards using the NFCReader class.
  5 * This is the second task in the NFC course: writing data to NFC tags.
  6 * 
  7 * What you'll learn:
  8 * - How to write data to different types of NFC cards
  9 * - How to write text strings to tags
 10 * - How to write binary data
 11 * - How to verify written data
 12 * - Differences between Mifare Classic and NTAG/Ultralight writing
 13 * 
 14 * Supported Cards:
 15 * - Mifare Classic 1K/4K (block-based, requires authentication)
 16 * - Mifare Ultralight (page-based, 4 bytes per page)
 17 * - NTAG213/215/216 (page-based, 4 bytes per page)
 18 * 
 19 * IMPORTANT NOTES:
 20 * - Block 0 (manufacturer block) is READ-ONLY and CANNOT be written to
 21 * - For Mifare Classic, NEVER write to sector trailers (blocks 3, 7, 11, etc.) - will lock sector
 22 * - For NTAG/Ultralight, avoid lock bytes and OTP areas
 23 * - Always use safe user data areas: Block 4+ for Mifare, Page 4+ for NTAG
 24 * - Default authentication key for Mifare Classic: FF FF FF FF FF FF
 25 */
 26
 27#include <Arduino.h>
 28#include "NFCReader.h"
 29
 30// ========== CONFIGURATION ==========
 31// Choose communication mode: I2C or SPI
 32#define USE_SPI  // Comment this out and uncomment USE_I2C to use I2C mode
 33// #define USE_I2C
 34
 35// Choose reading mode: POLLING or IRQ
 36#define USE_IRQ_MODE  // Comment out for polling mode
 37
 38// ========== CREATE NFC READER INSTANCE ==========
 39#ifdef USE_I2C
 40  #ifdef USE_IRQ_MODE
 41    NFCReader nfcReader(NFCCommMode::I2C, NFCReadMode::IRQ);
 42  #else
 43    NFCReader nfcReader(NFCCommMode::I2C, NFCReadMode::POLLING);
 44  #endif
 45#elif defined(USE_SPI)
 46  #ifdef USE_IRQ_MODE
 47    NFCReader nfcReader(NFCCommMode::SPI, NFCReadMode::IRQ);
 48  #else
 49    NFCReader nfcReader(NFCCommMode::SPI, NFCReadMode::POLLING);
 50  #endif
 51#else
 52  #error "Please define either USE_I2C or USE_SPI"
 53#endif
 54
 55// ========== HELPER FUNCTIONS ==========
 56
 57/**
 58 * Print card type as human-readable string
 59 */
 60void printCardType(NFCCardType cardType) {
 61  Serial.print(F("Card Type: "));
 62  switch (cardType) {
 63    case NFCCardType::MIFARE_CLASSIC_1K:
 64      Serial.println(F("Mifare Classic 1K"));
 65      break;
 66    case NFCCardType::MIFARE_CLASSIC_4K:
 67      Serial.println(F("Mifare Classic 4K"));
 68      break;
 69    case NFCCardType::MIFARE_ULTRALIGHT:
 70      Serial.println(F("Mifare Ultralight"));
 71      break;
 72    case NFCCardType::NTAG:
 73      Serial.println(F("NTAG (213/215/216)"));
 74      break;
 75    default:
 76      Serial.println(F("Unknown"));
 77      break;
 78  }
 79}
 80
 81/**
 82 * Print write result with appropriate formatting
 83 */
 84void printWriteResult(const NFCWriteResult& result, const __FlashStringHelper* operation) {
 85  Serial.print(F("  "));
 86  Serial.print(operation);
 87  Serial.print(F(": "));
 88  
 89  if (result.success) {
 90    Serial.print(F("✓ SUCCESS"));
 91    if (result.verified) {
 92      Serial.println(F(" (Verified)"));
 93    } else {
 94      Serial.println();
 95    }
 96  } else {
 97    Serial.print(F("✗ FAILED - "));
 98    Serial.println(result.errorMessage);
 99  }
100}
101
102// ========== MAIN PROGRAM ==========
103
104void setup() {
105  // Initialize serial communication
106  Serial.begin(115200);
107  while (!Serial) delay(10);  // Wait for serial port to connect
108  
109  // Print header
110  Serial.println(F("═══════════════════════════════════════════"));
111  Serial.println(F("    NFC TAG WRITE EXAMPLE - TASK 2         "));
112  Serial.println(F("═══════════════════════════════════════════"));
113  Serial.println();
114  
115  // Display configuration
116  Serial.println(F("Configuration:"));
117  #ifdef USE_I2C
118    Serial.println(F("  Communication: I2C"));
119  #else
120    Serial.println(F("  Communication: SPI"));
121  #endif
122  
123  #ifdef USE_IRQ_MODE
124    Serial.println(F("  Read Mode: IRQ (Interrupt-based)"));
125  #else
126    Serial.println(F("  Read Mode: POLLING"));
127  #endif
128  Serial.println();
129  
130  // Initialize NFC reader
131  Serial.println(F("Initializing NFC reader..."));
132  if (!nfcReader.begin()) {
133    Serial.println(F("❌ Failed to initialize NFC reader!"));
134    Serial.println(F("Please check:"));
135    Serial.println(F("  - PN532 module connections"));
136    Serial.println(F("  - Power supply"));
137    Serial.println(F("  - Communication mode setting"));
138    while (1) {
139      delay(1000);  // Halt execution
140    }
141  }
142  
143  Serial.println(F("✓ NFC reader initialized successfully!"));
144  Serial.println();
145  Serial.println(F("═══════════════════════════════════════════"));
146  Serial.println(F("Ready! Place a card near the reader..."));
147  Serial.println(F("═══════════════════════════════════════════"));
148  Serial.println();
149}
150
151void loop() {
152  // Read card first to detect it
153  NFCCardInfo cardInfo = nfcReader.readCard();
154  
155  if (cardInfo.detected) {
156    Serial.println(F("┌──────────────────────────────────────────┐"));
157    Serial.println(F("│      CARD DETECTED - WRITING DATA       │"));
158    Serial.println(F("├──────────────────────────────────────────┤"));
159    Serial.print(F("│ "));
160    printCardType(cardInfo.cardType);
161    Serial.println(F("└──────────────────────────────────────────┘"));
162    Serial.println();
163    
164    NFCWriteResult result;
165    
166    // ========== EXAMPLE 1: Simple String Write (Auto-detect card type) ==========
167    Serial.println(F("Example 1: Writing string using auto-detect"));
168    String message = "Hello NFC!";
169    result = nfcReader.writeString(message, 4, true);  // Start at safe address, verify
170    printWriteResult(result, F("String write"));
171    Serial.println();
172    
173    // ========== EXAMPLE 2: Card Type-Specific Writing ==========
174    if (cardInfo.cardType == NFCCardType::MIFARE_ULTRALIGHT || 
175        cardInfo.cardType == NFCCardType::NTAG) {
176      
177      Serial.println(F("Example 2: NTAG/Ultralight specific operations"));
178      Serial.println(F("  Writing to page 4 (safe user area)..."));
179      
180      // Write 4 bytes of data to page 4
181      uint8_t pageData[] = {0x01, 0x02, 0x03, 0x04};
182      result = nfcReader.writeNTAG(4, pageData, 4, true);
183      printWriteResult(result, F("Page 4 write"));
184      
185      // Write a longer string across multiple pages
186      String longText = "NFC Course Task 2: Write";
187      result = nfcReader.writeNTAGString(5, longText, true);
188      printWriteResult(result, F("Multi-page string"));
189      
190    } else if (cardInfo.cardType == NFCCardType::MIFARE_CLASSIC_1K ||
191               cardInfo.cardType == NFCCardType::MIFARE_CLASSIC_4K) {
192      
193      Serial.println(F("Example 2: Mifare Classic specific operations"));
194      Serial.println(F("  Using default key: FF FF FF FF FF FF"));
195      Serial.println(F("  Writing to block 4 (Sector 1, safe area)..."));
196      
197      // Write 16 bytes to block 4 (first user block in Sector 1)
198      uint8_t blockData[16];
199      for (int i = 0; i < 16; i++) {
200        blockData[i] = i + 1;  // Data: 01 02 03 ... 10
201      }
202      result = nfcReader.writeMifareClassic(4, blockData, 16, DEFAULT_KEY, false, true);
203      printWriteResult(result, F("Block 4 write"));
204      
205      // Write a string to block 5
206      String text = "Mifare Test Data";
207      result = nfcReader.writeMifareClassicString(5, text, DEFAULT_KEY, false, true);
208      printWriteResult(result, F("Block 5 string"));
209      
210    } else {
211      Serial.println(F("⚠️  Unknown card type - skipping type-specific examples"));
212    }
213    
214    Serial.println();
215    
216    // ========== EXAMPLE 3: Writing Binary Data ==========
217    Serial.println(F("Example 3: Writing custom binary data"));
218    uint8_t binaryData[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE};
219    
220    if (cardInfo.cardType == NFCCardType::NTAG || 
221        cardInfo.cardType == NFCCardType::MIFARE_ULTRALIGHT) {
222      // For NTAG: Write to page 8
223      result = nfcReader.writeNTAG(8, binaryData, 4, true);  // Only 4 bytes per page
224      printWriteResult(result, F("Binary data (page 8)"));
225    } else if (cardInfo.cardType == NFCCardType::MIFARE_CLASSIC_1K) {
226      // For Mifare Classic: Write to block 6
227      result = nfcReader.writeMifareClassic(6, binaryData, 8, DEFAULT_KEY, false, true);
228      printWriteResult(result, F("Binary data (block 6)"));
229    }
230    
231    Serial.println();
232    Serial.println(F("═══════════════════════════════════════════"));
233    Serial.println(F("Write operations complete!"));
234    Serial.println(F("═══════════════════════════════════════════"));
235    Serial.println();
236    
237    // Wait before allowing next write (prevents accidental repeated writes)
238    Serial.println(F("Waiting 3 seconds..."));
239    delay(3000);
240    
241    // Reset card state to allow re-detection
242    nfcReader.resetCardState();
243    
244    Serial.println(F("Ready for next card..."));
245    Serial.println();
246  }
247  
248  // In polling mode, add a small delay to avoid excessive checking
249  #ifndef USE_IRQ_MODE
250    delay(100);
251  #endif
252}

Key Features

  • Auto-detect card type and write appropriately

  • Multiple write examples (string, binary data, type-specific)

  • Write verification to ensure data integrity

  • Mifare Classic authentication with default keys

  • Safe memory area usage (avoids manufacturer blocks and sector trailers)

  • Comprehensive error reporting

  • F() macro for flash string storage (memory optimization)

Card Storage Structures

NTAG / Mifare Ultralight (Page-based)

  • Page size: 4 bytes

  • User pages: Page 4 onwards (pages 0-3 are reserved)

  • No authentication required

  • Example: NTAG213 has 45 pages (180 bytes user memory)

Page 0:  [Manufacturer Data - DO NOT WRITE]
Page 1:  [Internal Data - DO NOT WRITE]
Page 2:  [Lock Bytes - DO NOT WRITE]
Page 3:  [Capability Container]
Page 4:  [User Data] ← Safe to write
Page 5:  [User Data] ← Safe to write
...

Mifare Classic (Block-based)

  • Block size: 16 bytes

  • Sector structure: 4 blocks per sector

  • User blocks: Block 4+ (avoid sector trailers)

  • Authentication required: Default key FF FF FF FF FF FF

Sector 0:
  Block 0: [Manufacturer Block - DO NOT WRITE]
  Block 1: [Data]
  Block 2: [Data]
  Block 3: [Sector Trailer - Contains keys]

Sector 1:
  Block 4: [Data] ← Safe to write
  Block 5: [Data] ← Safe to write
  Block 6: [Data] ← Safe to write
  Block 7: [Sector Trailer - DO NOT WRITE]

Write Functions Reference

Auto-detect Functions

// Automatically detects card type and writes appropriately
NFCWriteResult writeString(const String& text,
                           uint8_t startAddress = 0,
                           bool verify = true);

NFCWriteResult writeData(const uint8_t* data,
                        uint8_t dataLength,
                        uint8_t startAddress = 0,
                        bool verify = true);

NTAG/Ultralight Specific

// Write to specific page (4 bytes)
NFCWriteResult writeNTAG(uint8_t page,
                        const uint8_t* data,
                        uint8_t dataLength,
                        bool verify = true);

// Write string across multiple pages
NFCWriteResult writeNTAGString(uint8_t startPage,
                              const String& text,
                              bool verify = true);

Mifare Classic Specific

// Write to specific block (16 bytes)
NFCWriteResult writeMifareClassic(uint8_t block,
                                  const uint8_t* data,
                                  uint8_t dataLength,
                                  const uint8_t* key = DEFAULT_KEY,
                                  bool useKeyB = false,
                                  bool verify = true);

// Write string across multiple blocks
NFCWriteResult writeMifareClassicString(uint8_t startBlock,
                                        const String& text,
                                        const uint8_t* key = DEFAULT_KEY,
                                        bool useKeyB = false,
                                        bool verify = true);

Running the Example

  1. Prepare a card: Use a spare NFC card (not your important cards!)

  2. Build and upload:

    Using PlatformIO environments (does not overwrite main system):

    # Build and upload write example
    python -m platformio run -e task2_write --target upload
    
    # Open serial monitor (REQUIRED to see output)
    python -m platformio device monitor --baud 115200
    

    With specific port:

    # Windows
    python -m platformio run -e task2_write --target upload --upload-port COM13
    python -m platformio device monitor --baud 115200
    
    # Linux/macOS
    python -m platformio run -e task2_write --target upload --upload-port /dev/ttyUSB0
    python -m platformio device monitor --baud 115200
    

    Using VS Code tasks:

    • Press Ctrl+Shift+P → Tasks: Run TaskPlatformIO: Upload Task 2 (Write)

    • Then: Ctrl+Shift+P → Tasks: Run TaskPlatformIO: Monitor Task 2

    Using PlatformIO sidebar:

    • Change default_envs in platformio.ini to task2_write

    • Click Upload button in VS Code status bar

    • Click Monitor button to view output

  3. Test writing:

    • Place card on reader

    • Observe write operations in serial output

    • Verify data was written correctly

    • Press Ctrl+C to exit monitor

Note

Baud Rate is Critical: The examples use 115200 baud. If you see garbled characters in the serial monitor, verify you’re using the correct baud rate. Default Arduino serial monitor often uses 9600 baud which will not work.

Safety Tips

Warning

DO NOT attempt to write to these areas:

  • Block 0 (Manufacturer block) - READ-ONLY, cannot be written (contains UID and manufacturer data)

  • Sector trailers (blocks 3, 7, 11, 15, etc.) - Contains authentication keys, writing incorrect data will lock the sector

  • Lock bytes (NTAG/Ultralight) - May permanently lock pages, making them read-only

  • OTP (One-Time Programmable) areas - Can only be written once

Always use safe areas:

  • Mifare Classic: Blocks 4, 5, 6, 8, 9, 10, etc. (user data blocks, avoid sector trailers)

  • NTAG/Ultralight: Pages 4 and above (check datasheet for lock byte locations)

Common Issues and Solutions

Authentication Failed (Mifare Classic)

  • Cause: Wrong authentication key

  • Solution: Try default key FF FF FF FF FF FF or obtain the correct key

Write Failed - Card Not Found

  • Cause: Card moved during write operation

  • Solution: Hold card steady on reader during entire operation

Verification Failed

  • Cause: Data written doesn’t match expected

  • Solution: Check card is not write-protected, verify correct address

RAM Usage Warning (“data size greater than maximum”)

  • Cause: Write example uses verbose String output and multiple features

  • Impact: Code will still work, but stack overflow protection is reduced

  • Solutions:

    • The optimized build flags in platformio.ini already help

    • Reduce serial debugging output if needed

    • Remove some example write operations

    • This is expected for educational examples with verbose output

    • Production code would use less RAM by reducing debug strings

Card Appears Bricked / Sector Locked

  • Cause: Attempted to write to Block 0 (will fail, but card is fine) OR wrote incorrect data to sector trailer

  • Note: Block 0 is read-only and cannot be damaged by write attempts

  • Solution for locked sector: Sector may be permanently locked if trailer was corrupted - use a new card or different sector

Advanced Topics

Card Cloning

The access control system includes advanced card cloning features (covered in Card Cloning Technology):

  • Clone UIDs to standard cards (no magic cards needed)

  • Store cloned UID in custom sector

  • Automatic UID selection for access control

NDEF Messages

For advanced users, NDEF (NFC Data Exchange Format) can be used to create standard NFC tags readable by smartphones:

  • URL records

  • Text records

  • Smart poster

  • vCard

See Adafruit_PN532 library documentation for NDEF support.

Next Steps

Tips for Development

  1. Always test with spare cards - Don’t use important cards during development

  2. Enable serial debugging - Monitor operations in real-time

  3. Use verification - Always verify writes to catch errors early

  4. Start simple - Begin with reading before attempting writes

  5. Document your addresses - Keep track of what data is stored where

  6. Handle errors gracefully - Check return values and display meaningful messages

See Also