Interfacing Arduino UNO with a PN512 based NFC module


Source code:  arduino_pn512_i2c_test.zip

I recently purchased a new Ender 3 V2 3D printer, which left me with my older Davinci Mini WiFi printer taking up space in my closet. This printer has an NFC module (Fig. 1) that reads information from the tags on filament spools, so I thought it could be fun to interface it with an Arduino UNO and build an NFC card reader. 


Fig. 1 - NFC module of the Davinci Mini WiFi
and its antenna.

Fig. 2 - PN512 NFC frontend chip.

1. Reverse engineering


To start the project, I need first to identify the chip in the module and determine how it is wired to the connector. The components of the module are protected by a metal shield that, however, is not soldered to the board and can be easily removed, revealing a PN512 chip (Fig. 2). This is a broadly adopted NFC frontend [1] and its datasheet is publicly available [2]. This chip is available in different package types and this NFC module mounts the 32-pin HVQFN32 version (SOT617-1). From Sec. 9 of the datasheet we find that this chip supports direct interfacing of hosts using SPI, I2C-bus, or serial UART interfaces and that the specific interface can be selected by configuring the connections of a fiex combination of pins. If you look at Table 142 (Fig. 3) you can note that if pin A1 is set to HIGH state then the I2C communication protocol is selected, otherwise the state of pin A0 switches between the SPI and UART mode. 

Fig. 3 - Table 142 from PN512 datasheet [2].
FIg. 4 - Pinout from PN512 datasheet [2].

So, let's identify first the ground (GND) and the power supply (VDD) pins on the connector. Pin 2 is clearly connected to the ground (Fig. 5) while the capacitor near the main connector (the one on the top of the board in Fig. 1) can be used to identify the pin connected to the VDD. A multimeter in continuity testing mode shows that one of the pins of the capacitor is connected to the ground and the other one is connected to pin 1 of the connector, therefore pin 1 = VDD and pin 2 = GND.

Fig. 5 - PN512 wiring diagram.
Now, the pinout diagram in the datasheet (Fig. 4) helps us to identify pins A0 and A1, which are the ones at the top-left corner of the chip. A close inspection reveals that A0 is connected to the ground through a 0 Ohm resistor (see Fig. 5) and A1 is connected to the test point labeled as TP44 and to another 0 Ohm resistor that does not seem to be connected to the ground. Using the multimeter, I can confirm that TP44 is not connected to the ground, ergo A0 = 0 and A1 = 1. This pin configuration indicates the PN512 communicates using the I2C interface.

Looking again at Table 142 (Fig. 3), we see that the I2C interface uses ALE and D7, which act as SDA and SCL and are connected to the two test points labeled TP113 and TP55, respectively (see Fig. 5). A continuity test with the multimeter tells us that pin 5 = SDA and pin 6 = SCL. Finally, poking around with the multimeter I find that pin 3 and pin 4 are connected to an unlabeled test point and to test point TP 114, respectively, which in turn are connected to pint NRSTPD and IQR on the chip. 

To sum it up, Table 1 shows how the pins of the main connector are wired to the pins of the PN512


On connector 1 2 3 4 5 6
On PN512 VDD GND NRSTPD IRQ SDA SCL
Table. 1 - Main connector pinout.

So far so good! Now we know how to connect the NFC module to the Arduino UNO: GND and VDD of the connector go to GND and 3.3V on the UNO, respectively; SDA and SCL of the connector go to SDA and SCL on the UNO, respectively; NRSTPD can be connected to any of the general purpose pins of the UNO,  let's say pin 4; IRQ  should be attached to a pin that support interrupts, for example the pin 3 on the UNO.

The last step to enable a working I2C communication between the Arduino UNO and the NFC module is to determine the I2C address of the latter. We know from the PN512 datasheet Sec. 9.4.5 that when pin A0 is set to LOW, as in our case, the upper 4 bits of the device bus address are set to 0101, thus we expect it to be in the range 0x28 - 0x2F. To determine the actual address we can use an I2C scanner, like the MultiSpeedI2CScanner [3], that reveals the address used by this module is 0x28.

2. Writing the firmware

Since the PN512 is widely used, there is already a library designed to interface with the PN512 and it can be imported directly into the Arduino IDE, the MFRC522_PN512 [4][5]. The only issue is that this library works only with the SPI interface, so we have to implement the I2C communication handling ourselves.

The base class to handle PN512 communication in MFRC522_PN512 is the class PN512, which is a subclass of the MFRC522. The class MFRC522 is defined in the files MFRC522.h and MFRC522.cpp. In particular, all the communication with the chip is handled by the functions PCD_WriteRegister and PCD_ReadRegister. So, we can create a new file, let's call it PN512_I2C.h in which we define our new class PN512_I2C as a sub-class of PN512: 

#ifndef PN512_I2C_H
#define PN512_I2C_H

#include <Arduino.h>
#include "PN512.h"
#include <Wire.h>

class PN512_I2C: public PN512
{
  using PN512::PN512;

  public: 
    void setI2CAddress(byte addr) { i2c_addr = addr; }
    byte getI2CAddress(byte addr) { return addr; }

    void PCD_WriteRegister(PCD_Register reg, byte value) override;
    void PCD_WriteRegister(PCD_Register reg, byte count, byte *values) override;
    byte PCD_ReadRegister(PCD_Register reg) override;
    void PCD_ReadRegister(PCD_Register reg, byte count, byte *values, byte rxAlign) override;

  private:
    byte i2c_addr = 0x28;
};

#endifPN512_I2C.h



#include <Arduino.h>
#include <MFRC522.h>
#include "PN512_I2C.h"


void PN512_I2C::PCD_WriteRegister(PCD_Register reg,	byte value) {
  reg = reg >> 1;
  Wire.beginTransmission(this->i2c_addr);
  Wire.write(reg);
  Wire.write(value);
  Wire.endTransmission();
}

/**
 * Writes a number of bytes to the specified register in the MFRC522 chip.
 * The interface is described in the datasheet section 8.1.2.
 */
void PN512_I2C::PCD_WriteRegister(PCD_Register reg, byte count, byte *values) {
  reg = reg >> 1;
  Wire.beginTransmission(this->i2c_addr);
  Wire.write(reg);
  for (byte index = 0; index < count; index++) {
    Wire.write(values[index]);
  }
  Wire.endTransmission();
}

/**
 * Reads a byte from the specified register in the MFRC522 chip.
 * The interface is described in the datasheet section 8.1.2.
 */
byte PN512_I2C::PCD_ReadRegister(PCD_Register reg) {
  reg = reg >> 1;
  Wire.beginTransmission(this->i2c_addr);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(this->i2c_addr, 1, true);
  return Wire.read(); // Read the value back. Send 0 to stop reading.
}

/**
 * Reads a number of bytes from the specified register in the MFRC522 chip.
 * The interface is described in the datasheet section 8.1.2.
 */
void PN512_I2C::PCD_ReadRegister(PCD_Register reg, byte count, byte *values, byte rxAlign) {
	if (count == 0) {
		return;
	}
  reg = reg >> 1;
	byte index = 0;
 
  Wire.beginTransmission(this->i2c_addr);
  // MSB == 1 is for reading. LSB is not used in address. Datasheet section 8.1.2.3.
  Wire.write(0x80 | reg);
  Wire.endTransmission();

  count--;
  // Only update bit positions rxAlign..7 in values[0]
  if (rxAlign) {
    // Create bit mask for bit positions rxAlign..7
    byte mask = (0xFF << rxAlign) & 0xFF;
    // Read value
    Wire.requestFrom(this->i2c_addr, 1, true);
    byte value = value = Wire.read();
    // Apply mask to both current value of values[0] and the new data in value.
    values[0] = (values[0] & ~mask) | (value & mask);
    index++;
  }

  while (index < count) {
    Wire.requestFrom(this->i2c_addr, 1, true);
    values[index] = Wire.read();
    index++;
  }
  Wire.requestFrom(this->i2c_addr, 1);
  values[index] = Wire.read();
}PN512_I2C.cpp

Finally, we can write the main sketch pn512_i2c_test.ino:

#include "PN512_I2C.h"
#include <Wire.h>

#define RST     4 // set pin 4 as reset
#define IRQ     3 // set pin 3 as IRQ

//create reader instace setting also the reset pin
PN512_I2C reader(RST);

//init counter for first boot
byte serialCounter;


void setup() {
  Serial.begin(115200);
  Serial.println("Initializing...");

  Wire.begin();

  reader.setI2CAddress(0x28);
  reader.PCD_Init();
  reader.PCD_AntennaOn();

  pinMode(LED_BUILTIN, OUTPUT);

  pinMode(IRQ, INPUT);
  attachInterrupt(digitalPinToInterrupt(IRQ), PN512_iqr, RISING);

  if (reader.PCD_PerformSelfTest()) {
    Serial.println("Self test OK!");
  } else {
    Serial.println("ERROR: self test FAILED!");
  }
}

volatile bool new_interrupt = false;

void loop() {
  if(Serial && serialCounter == 0) {
    serialCounter = 1;
    reader.PCD_DumpVersionToSerial();
    Serial.println("Reader is ready, scan card or tag");
  }

  if (new_interrupt) {    
    new_interrupt = false;    
  }

  if(!reader.PICC_IsNewCardPresent()) return;   //wait for new card to be present
  if(!reader.PICC_ReadCardSerial())   return;   //read that new card 
  reader.ledBlink(50, LED_BUILTIN);             //indicator when the card is read

  byte cardUID[reader.uid.size];                

  for(byte i = 0; i < reader.uid.size; i++){
    cardUID[i] = reader.uid.uidByte[i];
  }

  Serial.println("===========");
  reader.PICC_DumpToSerial(&(reader.uid));
  Serial.println("-----------");
  reader.PICC_DumpDetailsToSerial(&(reader.uid));
  Serial.println("-----------\n\n");
  delay(50);                                    //optional delay
}

void PN512_iqr() {
  // Here we can handle the interrupts...
  new_interrupt = true;
}pn512_i2c_test.ino
However, if we try to compile the sketch as it is we will encounter an error because the functions PCD_WriteRegister and PCD_WriteRegister were not declared as virtual methods in the library. We need to manually edit the file MFRC522.h that is located in $arduino_lib_dir/MFRC522_PN512/src, where $arduino_lib_dir is the path where all the libraries are downloaded. We need to find where the aforementioned functions are defined and we need to prepend the attribute virtual to each definition. In the end, they should look like
virtual void PCD_WriteRegister(PCD_Register reg, byte value);
virtual void PCD_WriteRegister(PCD_Register reg, byte count, byte *values);
virtual byte PCD_ReadRegister(PCD_Register reg);
virtual void PCD_ReadRegister(PCD_Register reg, byte count, byte *values, byte rxAlign = 0);

This modification will not affect the functionality of the library but allows us to easly override the functions used to communicate with the PN512 chip. Unfortunately, this is not persistent and you need to remake this edit every time you update the library. If you want to try it yourself, you can dowload the source code from this link: arduino_pn512_i2c_test.zip

3. Testing and conclusions

Now we can compile the sketch and upload it to the UNO. In the video below, I test the NFC reader using an old expired VISA and a NFC tag of a vendor machine. It detects the card and is able to read their UID and some other information but, obviously, it cannot read protected memory of the cards. I expect that a normal NFC tag should work as expected.





Disclaimer

All the information in thispost is published in good faith and for general information/educational purposes only. All trademarks and copyrighted materials belong to the respective holders. For further information, please read t he full disclaimer notice.

Comments