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:
Task 1: Tag Reading - Learn to detect and read NFC card information
Task 2: Tag Writing - Learn to write data to NFC cards
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.
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_I2Cdefine)Configurable reading mode (Polling or IRQ via
USE_IRQ_MODEdefine)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)
Open project folder in VS Code
Option A: Use PlatformIO Sidebar
Click PlatformIO icon in left sidebar
Expand
PROJECT TASKS→task1_readClick
Upload(builds and uploads automatically)Click
Monitorto view serial outputOption 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 1Method 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 115200Method 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 115200Method 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 COM13Note
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:
Open
platformio.iniModify the
default_envsline:[platformio] default_envs = task1_read # Change to: task1_read, task2_write, or nanoatmega328Save 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 115200Important: 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:
The code will still work but without buffer overflow protection
Reduce serial output strings if needed
Use the optimized build flags (already included in platformio.ini)
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.
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:
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
Prepare a card: Use a spare NFC card (not your important cards!)
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 Task→PlatformIO: Upload Task 2 (Write)Then: Ctrl+Shift+P →
Tasks: Run Task→PlatformIO: Monitor Task 2
Using PlatformIO sidebar:
Change
default_envsin platformio.ini totask2_writeClick Upload button in VS Code status bar
Click Monitor button to view output
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 FFor 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
Explore the full access control system in Usage Guide
Learn about card cloning in Card Cloning Technology
Review the API documentation in API Reference
Study the hardware setup in Hardware Setup
Tips for Development
Always test with spare cards - Don’t use important cards during development
Enable serial debugging - Monitor operations in real-time
Use verification - Always verify writes to catch errors early
Start simple - Begin with reading before attempting writes
Document your addresses - Keep track of what data is stored where
Handle errors gracefully - Check return values and display meaningful messages
See Also
API Reference - Complete API reference
Hardware Setup - Hardware setup and wiring
Troubleshooting - Common problems and solutions
Card Cloning Technology - Advanced card cloning features