NFC entry and exit on an Arduino Uno
Two PN532 readers share the I2C address 0x24, so each sits on its own channel of a TCA9548A: a card tapped on one signs in, on the other signs out. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $42. On Mokxi they're free.
What you'll learn
- Why two devices with the same I2C address clash
- Choosing a bus channel with a TCA9548A multiplexer
- Polling two NFC readers in turn
- Telling a new tap from a card left on the reader
- Keeping a small list of UIDs in an array
- Difficulty: Advanced
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 5
How it works
Both PN532 readers answer at I2C address 0x24, and that cannot be changed. On one bus they would clash. So each reader goes on its own channel of a TCA9548A multiplexer: entry on channel 0, exit on channel 1.
Before every conversation, the sketch sends the multiplexer one byte that joins just that channel to the main bus. Then it asks that reader whether a card is present.
A card tapped on the entry reader is added to a list of up to 4 cards inside. The same card tapped on the exit reader is removed. The Serial Monitor prints each card's UID and how many are inside.
Parts list
5 parts, plus the jumper wires. They retail for about $42; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × I2C multiplexer, TCA9548AIn Weather Station Kit.
- 2 × NFC reader, PN532
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| I2C multiplexer, TCA9548A pin SCL | Arduino Uno R3 pin SCL |
| I2C multiplexer, TCA9548A pin SDA | Arduino Uno R3 pin SDA |
| NFC reader, PN532 (2) pin IRQ | Arduino Uno R3 pin 3 |
| NFC reader, PN532 (1) pin IRQ | Arduino Uno R3 pin 2 |
| I2C multiplexer, TCA9548A pin VIN | Arduino Uno R3 pin 5V |
| NFC reader, PN532 (1) pin VCC | Arduino Uno R3 pin 5V |
| NFC reader, PN532 (2) pin VCC | Arduino Uno R3 pin 5V |
| I2C multiplexer, TCA9548A pin GND | Arduino Uno R3 pin GND |
| NFC reader, PN532 (1) pin GND | Arduino Uno R3 pin GND |
| NFC reader, PN532 (2) pin GND | Arduino Uno R3 pin GND |
| NFC reader, PN532 (1) pin SDA | I2C multiplexer, TCA9548A pin SD0 |
| NFC reader, PN532 (1) pin SCL | I2C multiplexer, TCA9548A pin SC0 |
| NFC reader, PN532 (2) pin SDA | I2C multiplexer, TCA9548A pin SD1 |
| NFC reader, PN532 (2) pin SCL | I2C multiplexer, TCA9548A pin SC1 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Two NFC readers on one I2C bus, through a TCA9548A.
//
// Every PN532 answers at 0x24 on I2C, and the address cannot be changed. Two
// of them on one bus would both answer every byte. A TCA9548A fixes that: it
// sits at 0x70, and behind it are eight more buses it joins to the main one
// when told to. Here the entry reader is on channel 0 and the exit reader on
// channel 1, and the sketch selects one before it talks to it.
//
// TCA9548A SDA, SCL -> SDA, SCL (A4, A5) VIN -> 5V, GND -> GND
// entry SDA, SCL -> SD0, SC0 IRQ -> pin 2
// exit SDA, SCL -> SD1, SC1 IRQ -> pin 3
// both readers: VCC -> 5V, GND -> GND, their switches set to I2C
//
// Press Run, then tap a card on the entry reader: it is signed in. Tap the
// same card on the exit reader and it is signed out. The serial monitor keeps
// the count. The mux's channel lights show which reader the Uno is talking to.
//
// THINGS TO TRY. Move the exit reader's SDA and SCL to SD0 and SC0 beside the
// entry reader's: channel 1 is empty, the sketch says so and stops, and the
// two readers left on channel 0 answer every byte together. Or change
// EXIT_CHANNEL to 2 and move its wires to SD2 and SC2.
#include <Wire.h>
#include <Adafruit_PN532.h>
const uint8_t MUX = 0x70;
const uint8_t ENTRY_CHANNEL = 0;
const uint8_t EXIT_CHANNEL = 1;
// I2C readers: the IRQ pin, and no reset pin (the board pulls RST up).
Adafruit_PN532 entry(2, -1);
Adafruit_PN532 exitReader(3, -1);
// The cards that are in, by UID, and the card on each reader now.
const uint8_t MAX_IN = 4;
uint8_t inside[MAX_IN][7];
uint8_t insideLength[MAX_IN];
uint8_t onEntry[7], onEntryLength = 0;
uint8_t onExit[7], onExitLength = 0;
// One byte to the TCA9548A: bit n joins channel n to the bus.
void selectChannel(uint8_t channel)
{
Wire.beginTransmission(MUX);
Wire.write(1 << channel);
Wire.endTransmission();
}
bool startReader(Adafruit_PN532 &reader, uint8_t channel, const char *name)
{
selectChannel(channel);
reader.begin();
uint32_t version = reader.getFirmwareVersion();
Serial.print(name);
if (!version) {
Serial.print(": no PN532 on channel ");
Serial.println(channel);
return false;
}
Serial.print(": PN5");
Serial.print((version >> 24) & 0xFF, HEX);
Serial.print(" firmware ");
Serial.print((version >> 16) & 0xFF);
Serial.print('.');
Serial.print((version >> 8) & 0xFF);
Serial.print(" on channel ");
Serial.println(channel);
reader.SAMConfig();
// Look for a card twice, about 30 ms, then answer "none" rather than wait
// for ever: the other reader needs its turn.
reader.setPassiveActivationRetries(1);
return true;
}
void printUid(const uint8_t *uid, uint8_t length)
{
for (uint8_t i = 0; i < length; i++) {
if (uid[i] < 0x10) Serial.print('0');
Serial.print(uid[i], HEX);
if (i + 1 < length) Serial.print(' ');
}
}
int findInside(const uint8_t *uid, uint8_t length)
{
for (uint8_t i = 0; i < MAX_IN; i++) {
if (insideLength[i] == length && memcmp(inside[i], uid, length) == 0) return i;
}
return -1;
}
uint8_t countInside()
{
uint8_t n = 0;
for (uint8_t i = 0; i < MAX_IN; i++) {
if (insideLength[i]) n++;
}
return n;
}
// Poll one reader. True, with the UID, when a card has just been laid on it;
// a card that stays on the reader is reported once.
bool newCard(Adafruit_PN532 &reader, uint8_t channel, uint8_t *last, uint8_t &lastLength, uint8_t *uid, uint8_t &length)
{
selectChannel(channel);
if (!reader.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &length, 100)) {
lastLength = 0;
return false;
}
if (length == lastLength && memcmp(uid, last, length) == 0) return false;
memcpy(last, uid, length);
lastLength = length;
return true;
}
void setup()
{
Serial.begin(9600);
Wire.begin();
Serial.println("Entry and exit readers");
bool ok = startReader(entry, ENTRY_CHANNEL, "Entry");
ok = startReader(exitReader, EXIT_CHANNEL, "Exit") && ok;
if (!ok) {
while (1) {
}
}
Serial.println("Tap a card on a reader.");
}
void loop()
{
uint8_t uid[7];
uint8_t length;
if (newCard(entry, ENTRY_CHANNEL, onEntry, onEntryLength, uid, length)) {
printUid(uid, length);
if (findInside(uid, length) >= 0) {
Serial.print(" is already in");
} else {
int slot = findInside(uid, 0);
if (slot < 0) {
Serial.print(" cannot come in, the list is full");
} else {
memcpy(inside[slot], uid, length);
insideLength[slot] = length;
Serial.print(" signed in");
}
}
Serial.print(", ");
Serial.print(countInside());
Serial.println(" inside");
}
if (newCard(exitReader, EXIT_CHANNEL, onExit, onExitLength, uid, length)) {
printUid(uid, length);
int at = findInside(uid, length);
if (at < 0) {
Serial.print(" was not signed in");
} else {
insideLength[at] = 0;
Serial.print(" signed out");
}
Serial.print(", ");
Serial.print(countInside());
Serial.println(" inside");
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
selectChannel sketch.ino
This is the multiplexer control. It sends one byte to address 0x70, and bit n of that byte joins channel n to the main bus. 1 << channel sets just one bit, so only one reader is connected at a time.
// One byte to the TCA9548A: bit n joins channel n to the bus.
void selectChannel(uint8_t channel)
{
Wire.beginTransmission(MUX);
Wire.write(1 << channel);
Wire.endTransmission();
}newCard sketch.ino
This polls one reader after selecting its channel. If no card is found, it forgets the last card and returns false. If the same UID is still there, it also returns false, so a card resting on the reader is reported once. A different UID is stored and returns true.
// Poll one reader. True, with the UID, when a card has just been laid on it;
// a card that stays on the reader is reported once.
bool newCard(Adafruit_PN532 &reader, uint8_t channel, uint8_t *last, uint8_t &lastLength, uint8_t *uid, uint8_t &length)
{
selectChannel(channel);
if (!reader.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &length, 100)) {
lastLength = 0;
return false;
}
if (length == lastLength && memcmp(uid, last, length) == 0) return false;
memcpy(last, uid, length);
lastLength = length;
return true;
}loop sketch.ino
It checks the entry reader, then the exit reader. A new card at entry is signed in unless it is already inside or the list is full. A new card at exit is signed out if it was inside. Each time, it prints the UID and the count.
void loop()
{
uint8_t uid[7];
uint8_t length;
if (newCard(entry, ENTRY_CHANNEL, onEntry, onEntryLength, uid, length)) {
printUid(uid, length);
if (findInside(uid, length) >= 0) {
Serial.print(" is already in");
} else {
int slot = findInside(uid, 0);
if (slot < 0) {
Serial.print(" cannot come in, the list is full");
} else {
memcpy(inside[slot], uid, length);
insideLength[slot] = length;
Serial.print(" signed in");
}
}
Serial.print(", ");
Serial.print(countInside());
Serial.println(" inside");
}
if (newCard(exitReader, EXIT_CHANNEL, onExit, onExitLength, uid, length)) {
printUid(uid, length);
int at = findInside(uid, length);
if (at < 0) {
// ... 10 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The Serial Monitor says no PN532 on a channel and the sketch stops This project
Each reader's SDA and SCL must go to the matching pair on the multiplexer: SD0 and SC0 for entry, SD1 and SC1 for exit. Also check each reader's switches are set to I2C. - Both readers react to every tap This project
If both readers sit on the same channel, they both answer at 0x24. Put them on different channels and matchENTRY_CHANNELandEXIT_CHANNEL. - A card cannot come in This project
The list holdsMAX_INcards, which is 4. Tap a card on the exit reader to make room. - A second sensor with the same address still does not respond, or both answer together. I2C multiplexer
Select one channel by writing a single byte to the multiplexer, with bit n set for channel n (1 << n). Talk to the sensor, then change channels. Only enable one channel at a time. - The multiplexer itself is not found. I2C multiplexer
Its address is 0x70 plus the A0 to A2 bits, so 0x70 to 0x77. Tie A0, A1, A2 to GND or VCC, tie RESET high, and run an I2C scanner. - The reader is not found or hangs. NFC reader
Set the mode switches for I2C, SPI or UART to match your wiring. The I2C address is 0x24. Wait for it to wake up. Power at 3.3 V to 5 V per the module. - Cards are not read reliably. NFC reader
Hold the card flat and close to the antenna. Use 13.56 MHz cards such as MIFARE. Keep metal away from the antenna, and use the right key to read protected blocks. - Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.