MCP23017 I²C Speed and Wiegand 26-bit RFID Reader Performance

Hi,
I’m working on a project where I need to handle multiple Wiegand 26-bit RFID readers and expand my GPIOs. I am thinking of using the MCP23017 GPIO expander with I²C, but I’m concerned about the I²C speed when reading fast Wiegand signals.

Will the I²C speed cause issues with reading Wiegand data accurately, especially with multiple readers? Or is there a way to improve performance with the MCP23017 for this kind of application?

what host microcontroller are you planning to use?
using a Microcontroller with multiple I2C interfaces may help
consider alternative approach, e.g. multiple microcontrollers linked in a wireless mesh

I can use raspberry pi 5 or any Arduino or esp32. Trying to build a PCB with up to 8 mcp23017 or I may need to go to SPI route by using MCP23S17.
Thanks

What interface do the weigand readers use? How does using MCP23017 interfere with that?

Hi,

Wiegand readers typically use a Wiegand interface for communication, which consists of two data lines: D0 (Data 0) and D1 (Data 1). These lines carry the data in a pulse format, with each pulse representing a bit (0 or 1). The data is transmitted in a timed sequence, making the Wiegand interface time-sensitive.

The MCP23017 is an I/O expander that communicates over I2C, and it can be used to expand the number of GPIO pins on a microcontroller. However, when using it with Wiegand readers, there could be potential issues:

  1. I2C Latency: The MCP23017 uses the I2C protocol, which introduces some latency in communication. Wiegand readers, being time-sensitive, can face issues if the microcontroller reads the data from the MCP23017 too slowly. This can cause missed bits or incorrect data.

  2. Speed Limitations: The I2C bus is typically slower than other communication methods like SPI or direct GPIO, which could result in missing Wiegand pulses, especially with faster readers.

  3. Interfacing Wiegand: The MCP23017 may not be the ideal choice for reading Wiegand signals directly, as it could introduce delays. It's better suited for general-purpose GPIO tasks, such as controlling relays or LEDs, but it can struggle to handle high-speed signals like those from Wiegand readers.

Chat gpt said following

To avoid issues, it's important to use direct GPIO pins for reading Wiegand data, or consider using a higher-speed interface like SPI for communication with external expanders, rather than I2C.

how far are the Wiegand modules from the host microcontroller?
does the specification give any restrictions on data cable length?

Then do what Chat told you to do
Let us know how it works

1 Like

According to my research, Wiegand protocol may have pulse widths of 20 us. Reading these with MCP23017 may be a challenge, and more so with multiple readers. The MCP23017 I think has interrupt on change support, so that may help, but I think it's still going to be CPU intensive. If you have 32 RFID devices, worst case is that you need to handle 32 interrupts in 20 us.

Note that there is a bug in the MCP23017, which causes 2 of the inputs to be unreliable. the MCP23S17 does not have the same issue, so already the MCP23S17 may be a better option.

Bitbanging is really a "get you out of a hole" once type situation, for when you have no other option. If you need to bitbang 32 devices, it would really be better to use external logic to decode the data. You might consider using a small CPU dedicated to say decoding 4 devices.

There are probably some other creative ways to tackle it, e.g DMA a bit stream into RAM and then process it in non-realtime. I think the RFID message is only 26 bits.

As a data point, (I'm not suggesting you buy the modules) there is a 4-port Wiegand to serial converter https://www.priority1design.com.au/wieg4prt.pdf. This design appears to use a Meag48 running at 8Mhz.

So we know that a Mega48 can convert 4 Wiegand inputs and output to serial. We don't know what the CPU load is, so whether that Mega48 is capable of doing other things we don't know. Being a commercial design I would guess they used the smallest chip for the purpose.

Mega48 is around $2.50 ea, so you could consider using 8 devices to handle 32 readers.

Thanks for the information. Are you referring to a bug in the MCP23017 where the GPA7 and GPB7 pins can only function as outputs?

Yes, that one. At least if used as inputs they can be unreliable, so officially Microchip say they are output only.

Annoyingly I just built some boards with MCP23017. Not sure whether to redesign, but there doesn't seem to be any other chip with similar features (and I2C).

After all the zig-zagging and research, I am planning to use the MCP23S17 (SPI). I know more wires will need to be connected, and each chip requires one GPIO pin, but at least it will be reliable, and I can use the GPA7 and GPB7 pins for the relay (door unlock).

There is a library for reading Wiegand 26-bit readers, developed by someone using my original code for using the pin change interrupt on an Arduino UNO R3. Here is my original code. It was used in a project called Crazy People, which read three Wiegand signals to decide what picture to display. This code just concentrates on the Wiegand reading.

/* Crazy People
 * By Mike Cook (aka Grumpy Mike) April 2009
 * Three RFID readers outputing 26 bit Wiegand code to pins:-
 * Reader A (Head) Pins 4 & 5
 * Reader B (Body) Pins 6 & 7
 * Reader C (Legs) Pins 8 & 9
 * Interrupt service routine gathers Wiegand pulses (zero or one) until 26 have been received
 * Then a string is sent to processing
 */
#include "pins_arduino.h"
/*
 * an extension to the interrupt support for Arduino.
 * add pin change interrupts to the external interrupts, giving a way
 * for users to have interrupts drive off of any pin.
 * Refer to avr-gcc header files, Arduino source and atmega datasheet.
 */

/*
 * Theory: all IO pins on Atmega168 / 3289(UNO) are covered by Pin Change Interrupts.
 * The PCINT corresponding to the pin must be enabled and masked, and
 * an ISR routine provided.  Since PCINTs are per port, not per pin, the ISR
 * must use some logic to actually implement a per-pin interrupt service.
 */

/* Pin to interrupt map:
 * D0-D7 = PCINT 16-23 = PCIR2 = PD = PCIE2 = pcmsk2
 * D8-D13 = PCINT 0-5 = PCIR0 = PB = PCIE0 = pcmsk0
 * A0-A5 (D14-D19) = PCINT 8-13 = PCIR1 = PC = PCIE1 = pcmsk1
 */

volatile uint8_t *port_to_pcmask[] = {
  &PCMSK0,
  &PCMSK1,
  &PCMSK2
};

typedef void (*voidFuncPtr)(void);

volatile static voidFuncPtr PCintFunc[24] = { 
  NULL };

volatile static uint8_t PCintLast[3];

/*
 * attach an interrupt to a specific pin using pin change interrupts.
 * First version only supports CHANGE mode.
 */
 void PCattachInterrupt(uint8_t pin, void (*userFunc)(void), int mode) {
  uint8_t bit = digitalPinToBitMask(pin);
  uint8_t port = digitalPinToPort(pin);
  uint8_t slot;
  volatile uint8_t *pcmask;

  if (mode != CHANGE) {
    return;
  }
  // map pin to PCIR register
  if (port == NOT_A_PORT) {
    return;
  } 
  else {
    port -= 2;
    pcmask = port_to_pcmask[port];
  }
  slot = port * 8 + (pin % 8);
  PCintFunc[slot] = userFunc;
  // set the mask
  *pcmask |= bit;
  // enable the interrupt
  PCICR |= 0x01 << port;
}

void PCdetachInterrupt(uint8_t pin) {
  uint8_t bit = digitalPinToBitMask(pin);
  uint8_t port = digitalPinToPort(pin);
  volatile uint8_t *pcmask;

  // map pin to PCIR register
  if (port == NOT_A_PORT) {
    return;
  } 
  else {
    port -= 2;
    pcmask = port_to_pcmask[port];
  }

  // disable the mask.
  *pcmask &= ~bit;
  // if that's the last one, disable the interrupt.
  if (*pcmask == 0) {
    PCICR &= ~(0x01 << port);
  }
}

// common code for isr handler. "port" is the PCINT number.
// there isn't really a good way to back-map ports and masks to pins.
static void PCint(uint8_t port) {
  uint8_t bit;
  uint8_t curr;
  uint8_t mask;
  uint8_t pin;

  // get the pin states for the indicated port.
  curr = *portInputRegister(port+2);
  mask = curr ^ PCintLast[port];
  PCintLast[port] = curr;
  // mask is pins that have changed. screen out non pcint pins.
  if ((mask &= *port_to_pcmask[port]) == 0) {
    return;
  }
  // mask is pcint pins that have changed.
  for (uint8_t i=0; i < 8; i++) {
    bit = 0x01 << i;
    if (bit & mask) {
      pin = port * 8 + i;
      if (PCintFunc[pin] != NULL) {
        PCintFunc[pin]();
      }
    }
  }
}

SIGNAL(PCINT0_vect) {
  PCint(0);
}
SIGNAL(PCINT1_vect) {
  PCint(1);
}
SIGNAL(PCINT2_vect) {
  PCint(2);
}

// End of interrupts code and start of the reader code

volatile long reader1 = 0,reader2 = 0, reader3 = 0;
volatile int reader1Count = 0, reader2Count = 0,  reader3Count = 0;

void reader1One(void) {
  if(digitalRead(4) == LOW){
  reader1Count++;
  reader1 = reader1 << 1;
  reader1 |= 1;
  }
}

void reader1Zero(void) {
  if(digitalRead(5) == LOW){
  reader1Count++;
  reader1 = reader1 << 1;  
  }
}

void reader2One(void) {
  if(digitalRead(6) == LOW){
  reader2Count++;
  reader2 = reader2 << 1;
  reader2 |= 1;
  }
}

void reader2Zero(void) {
  if(digitalRead(7) == LOW){
  reader2Count++;
  reader2 = reader2 << 1;  
  }
}

void reader3One(void) {
  if(digitalRead(8) == LOW){
  reader3Count++;
  reader3 = reader3 << 1;
  reader3 |= 1;
  }
}

void reader3Zero(void) {
  if(digitalRead(9) == LOW){
  reader3Count++;
  reader3 = reader3 << 1;  
  }
}

void setup()
{
  Serial.begin(57000);
  // Attach pin change interrupt service routines from the Wiegand RFID readers
  PCattachInterrupt(4, reader1One, CHANGE);
  PCattachInterrupt(5, reader1Zero, CHANGE);
  PCattachInterrupt(6, reader2One, CHANGE);
  PCattachInterrupt(7, reader2Zero, CHANGE);
  PCattachInterrupt(8, reader3One, CHANGE);
  PCattachInterrupt(9, reader3Zero, CHANGE);
  delay(10);
  // the interrupt in the Atmel processor mises out the first negitave pulse as the inputs are already high,
  // so this gives a pulse to each reader input line to get the interrupts working properly.
  // Then clear out the reader variables.
  // The readers are open collector sitting normally at a one so this is OK
  for(int i = 4; i<10; i++){
  pinMode(i, OUTPUT);
   digitalWrite(i, HIGH); // enable internal pull up causing a one
  digitalWrite(i, LOW); // disable internal pull up causing zero and thus an interrupt
  pinMode(i, INPUT);
  digitalWrite(i, HIGH); // enable internal pull up
  }
  delay(10);
  // put the reader input variables to zero
  reader1 = reader2 = reader3 = 0;
  reader1Count = reader2Count =  reader3Count = 0;
  digitalWrite(13, HIGH);  // show Arduino has finished initilisation
}

void loop() {
  if(reader1Count >= 26){
//  Serial.print(" Reader 1 ");Serial.println(reader1,HEX);
  Serial.println("A");Serial.println(reader1 & 0xfffffff);
  reader1 = 0;
  reader1Count = 0;
     }
     
  if(reader2Count >= 26){
  Serial.println("B");Serial.println(reader2 & 0xfffffff);
  reader2 = 0;
  reader2Count = 0;
     }
     
 if(reader3Count >= 26){
  Serial.println("C");Serial.println(reader3 & 0xfffffff);
  reader3 = 0;
  reader3Count = 0;
     }
    
}

I would forget all about the MCP23x17 chip. This is not the way to go. This is a complex chip and is not suited to your problem.

Also forget about Chat GPT and anyone who recommends it.

For context here are some pictures:-

Video

So how will you know when to read the MCP?

Yes Jim but that is the total wrong way to solve this problem.

It wouldn't be the first time when the OP wants to solve a problem in a way they think is the way, and it turns out totally wrong, would it?

Plus Karma from the previous forum of +2500 = Karma 5500+

I have some doubts about MCP23017 in this application, and the ability of the CPU to poll multiple devices. Even with a fast CPU, it might be a challenge. Usually there is a significant overhead added per SPI transaction, even if the data rate is high.

The MCP23S17 has interrupt on change function, so you could set it to interrupt when a data pin goes low, at least then you only need to poll when a pulse has occurred.

I guess there are other issues to do with rejecting pulses that are too short.

How about ATmega1284P? By using pin interrupt I could use up to 32 pins?

You can use 32 pins but there are actually only 4 interrupts, one for each port.

Why do you need 32 pins?

For reading wiegand reader 2 pins for (D0 and D1) and 1 for lock (relay) . Since I need multiple locks like up to 20 or 30

I don't follow.
For 20 you would 20x3 = 60 pins
Fo 30 you would need 30x3= 90 pin

How do you figure only 32?