SPI with multiple devices attached – Arduino, Heltec WiFi Lora 32 and RFID-RC522

SPI with multiple devices attached – Arduino, Heltec WiFi Lora 32 and RFID-RC522

It is possible to attach several sensors (slaves) etc. to one SPI connection that exists on an Arduino or other microcontrollers like the Heltec Wifi Lora 32 (master). As an example have I tested the RFID-RC522 as an common RFID reader/writer on the Heltec development board.

To understand how, do you have to know a little about the basics of the SPI connection. A link to Wikipedia: https://en.wikipedia.org/wiki/Serial_Peripheral_Interface.

SPI is in general called a four-wire serial bus, where the wires are designated  CLK, MOSI, MISO and SS. Here is CLK (CLocK) often also called SCK, and SS (Slave Select) also CS which stands for “Chip Select”. On some units like the RFID-RC522, is this also called SDA. I don’t know if this is because the chip of the card reader/writer also can handle I2C, and hence this naming.

Regards i2C on the RFID-RC522; Don’t let you be fooled by this naming, nor the information about I2C on board. Before you can use I2C, you would have to remove the chip and drill a hole below it. Then again solder it on. More on that here: https://forum.arduino.cc/index.php?topic=442750.0. If you just short take a look at the size of the pins, you will soon forget all about that solution. Unless you have the tools, of course. – Hence the reason for me to continue with SPI.

How to do it with SPI

The wire SS/CS/SPA is interesting as this can be used to distinguish between the sensors that should respond, while CLK, MOSI, MISO are shared in use amongst the sensors. Look at the following figure “Multiple slave select” to see the principle of the wiring.

If you use several pins on the Arduino/Heltec, for each sensor/slave one, and always only activate one (by putting it low), will you be able to let only that selected device communicate over the SPI connection. SPI is constructed to act this way. Further details can be read at the source for the figure: https://www.allaboutcircuits.com/technical-articles/spi-serial-peripheral-interface/.

Arduino

Take a look at this link for details: http://www.learningaboutelectronics.com/Articles/Multiple-SPI-devices-to-an-arduino-microcontroller.php.

Heltec WiFi Lora 32

The RC522 has a few more wires than the SPI connection contains by nature. You start the wiring by share the CLK, MOSI and MISO with the Lora sender of the Heltec. The ESP32 is born with three SPi connections, and the Lora sender uses the one called VSPI.

CLK/SCK => GPIO5, pin 5 of the ESP32

MOSI => GPIO27, pin 27 of the ESP32

MISO => GPIO19, pin 19 of the ESP32

Note that MISO on the Heltec still is MISO on the RC522, And MOSI equally is the same on both devices. Note the meaning of those two expressions:

MOSI => Master Output Slave Input

MISO => Master Input Slave Output

Of course, you also have to connect ground and 3.3V power wire to the RFID-RC522.

Then, you select two pins on the Heltec that aren’t occupated by some of your sensors. I chose pin 35 and 17.

SS/CS/SDA => 17

RST => 35 (I once said pin 2, but pin 35 works without dismantling the wire during flashing)

Pictures of the setup.

Overview. Note how I ignored using two yellow and two black. What a random grab. Well, you will be able to figure it out.
The RFID-RC522
Pins 19 (MISO-yellow), 5 (CLK/SCK-orange) and 17 (SS/CS/SDA-yellow). !!! Don’t use pin 2 for RST as shown on the picture, use pin 35 on the other side of the development board (RST-black) !!!!
Pin 27 (MOSI-wine red). Use pin 35 on this side of the development board for RST (black).
Ground and 3.3V where ever you find it.

Software

You can find the code on Github https://github.com/itofficeeu/ito-rfid-rc522-lora, or copy the below.

/* UPDATED 2019.08.26 - 12:00 */

#include <CayenneLPP.h>
#include <lmic.h>
#include <hal/hal.h>
#include <Wire.h>
#include <SPI.h>
#include <SSD1306.h>
#include <MFRC522.h>

/* Heltec WiFi Lora 32 V2 */
//const int RST_PIN = 33;           /* Reset pin */

/* Heltec WiFi Lora 32 V1 */
const int RST_PIN = 35;           /* Reset pin */

const int SS_PIN = 17;            /* Slave select pin */

MFRC522 mfrc522(SS_PIN, RST_PIN); /* Create MFRC522 instance */
int loop_count = 0;
static osjob_t sendjob;

/* LoRaWAN NwkSKey, network session key*/
static const PROGMEM u1_t NWKSKEY[16] = 
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

/* LoRaWAN AppSKey, application session key */
static const u1_t PROGMEM APPSKEY[16] = 
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

/* LoRaWAN end-device address (DevAddr) */
static const u4_t DEVADDR = { 0x00000000 };

/* These folowing callbacks are only used in over-the-air activation, 
 * so they are left empty here (we cannot leave them out completely unless
 * DISABLE_JOIN is set in config.h, otherwise the linker will complain). */
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }

/* Schedule data trasmission in every this many seconds 
 * (might become longer due to duty cycle limitations).
 * Fair Use policy of TTN requires update interval of at least several min. 
 * We set update interval here of 1 min for testing. */
const unsigned TX_INTERVAL = 60;

/* Pin mapping according to Cytron LoRa Shield RFM */
const lmic_pinmap lmic_pins = {
  .nss = 18,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 14,
  .dio = {26, 32, 33},
};
/*   .dio = {26, 32, 33}, for Heltec WiFi Lora 32 V1 */
/*   .dio = {26, 34, 35}, for Heltec WiFi Lora 32 V2 */

char DateAndTimeString[20]; /* 19 digits plus the null char */
uint32_t timer = millis();
uint8_t  ColumnPlus = 0;
CayenneLPP lpp(51);

/* I2C sensors can per default not use pins 21 and 22 on the Heltec when the display
 * is used with SSD1306.h. One solution to that can be to let the physical wiring of 
 * the I2C sensors use pin 4 and 15. Redefine the I2C to use these pins then. */
#define PIN_SDA 4
#define PIN_SCL 15

#include <AsyncDelay.h>
#include <SoftWire.h>
SoftWire sw(PIN_SDA, PIN_SCL);

/* Rewiring the OLED to use pin 4 and 15 instead of 21 and 22. */
#define DISPLAY_USE 1          // Use DISPLAY_USE to disable the display entirely.
#define DISPLAY_ON 1           // Turn the display off, even if it is initialized.
SSD1306 display(0x3C, PIN_SDA, PIN_SCL);

void onEvent (ev_t ev) 
{
  switch(ev) 
  {
    case EV_TXCOMPLETE:
      /* Schedule next transmission */
      os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
      LoopContent();
      break;  
    case EV_RXCOMPLETE:
      if (LMIC.dataLen)
      {
        Serial.printf("Received %d bytes\n", LMIC.dataLen);
      }
      break;
    case EV_SCAN_TIMEOUT:
        Serial.println(F("EV_SCAN_TIMEOUT"));
        break;
    case EV_BEACON_FOUND:
        Serial.println(F("EV_BEACON_FOUND"));
        break;
    case EV_BEACON_MISSED:
        Serial.println(F("EV_BEACON_MISSED"));
        break;
    case EV_BEACON_TRACKED:
        Serial.println(F("EV_BEACON_TRACKED"));
        break;
    case EV_JOINING:
        Serial.println(F("EV_JOINING"));
        break;
    case EV_JOINED:
        Serial.println(F("EV_JOINED"));
        break;
    case EV_RFU1:
        Serial.println(F("EV_RFU1"));
        break;
    case EV_JOIN_FAILED:
        Serial.println(F("EV_JOIN_FAILED"));
        break;
    case EV_REJOIN_FAILED:
        Serial.println(F("EV_REJOIN_FAILED"));
        break;
    case EV_LOST_TSYNC:
        Serial.println(F("EV_LOST_TSYNC"));
        break;
    case EV_RESET:
        Serial.println(F("EV_RESET"));
        break;
    case EV_LINK_DEAD:
        Serial.println(F("EV_LINK_DEAD"));
        break;
    case EV_LINK_ALIVE:
        Serial.println(F("EV_LINK_ALIVE"));
        break;
    default:
      Serial.printf("Unknown event\r\n");
      break;
  }
}

void do_send(osjob_t* j)
{
  /* Check if there is not a current TX/RX job running */
  if (LMIC.opmode & OP_TXRXPEND) 
  {
    Serial.printf("OP_TXRXPEND going on. => Will not send anything.\r\n");
  }
  else if (!(LMIC.opmode & OP_TXRXPEND)) 
  {
    lpp.reset();

    //uint8_t wakeup_reason = 9;
    //lpp.addAnalogInput(20, wakeup_reason);

    /* Prepare upstream data transmission at the next possible time. */
    LMIC_setTxData2(1, lpp.getBuffer(), lpp.getSize(), 0);
    //Serial.printf("Packet queued\r\n");
  }
  /* Next TX is scheduled after TX_COMPLETE event. */
}

/**
 * @author Andreas C. Dyhrberg
 */
void setup()
{
  Serial.begin(115200);
  while (!Serial);

  SPI.begin();                        // Init SPI bus
  delay(100);
  mfrc522.PCD_Init();                 // Init MFRC522
  delay(100);
  Serial.println("");
  mfrc522.PCD_DumpVersionToSerial();  // Show details of PCD - MFRC522 Card Reader details
  delay(100);

#if DISPLAY_USE
  pinMode(16,OUTPUT);
  digitalWrite(16, LOW); // set GPIO16 low to reset OLED
  delay(50);
  digitalWrite(16, HIGH);
  delay(50);
  display.init();
  display.setFont(ArialMT_Plain_10);
  delay(50);
  display.drawString( 0, 0, "Starting up ...");
  display.drawString( 0,20, "- and initializing...");
  display.display();
#endif
  
  /* LMIC init */
  os_init();

  /* Reset the MAC state. Session and pending data transfers will be discarded. */
  LMIC_reset();

  /* Set static session parameters. Instead of dynamically establishing a session
   * by joining the network, precomputed session parameters are be provided. */
  uint8_t appskey[sizeof(APPSKEY)];
  uint8_t nwkskey[sizeof(NWKSKEY)];
  memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
  memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
  LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
  
  /* Select frequencies range */
  //LMIC_selectSubBand(0);
  
  /* Disable link check validation */
  LMIC_setLinkCheckMode(0);
  
  /* TTN uses SF9 for its RX2 window. */
  LMIC.dn2Dr = DR_SF9;
  
  /* Set data rate and transmit power for uplink
   * (note: txpow seems to be ignored by the library) */
  LMIC_setDrTxpow(DR_SF7,14);
  Serial.printf("LMIC setup done!\r\n");

  /* Start job */
  do_send(&sendjob);

  delay(500);
}
 
void loop()
{
  LoopContent();
  loop_count++;
  if (loop_count>15)
  {
    loop_count = 0;
    Serial.println("");
  }
  delay(1000); /* Delay between loops. */
}

#if DISPLAY_USE
#if DISPLAY_ON
/**
 * @author Andreas C. Dyhrberg
 */
int DisplayPrintUid(int x, int y, MFRC522 mfrc522){
  int indent = 0;
  int indent_uid = 48;
  display.drawString( x,y, "Card UID:              ");
  for (byte i = 0; i < mfrc522.uid.size; i++) {
    if(mfrc522.uid.uidByte[i] < 0x10)
    {
      display.drawString((x+indent_uid+indent),y, "0");
      indent = indent+5;
      display.drawString((x+indent_uid+indent),y, String(mfrc522.uid.uidByte[i],HEX));
      indent = indent+7;
    } else
    {
      display.drawString((x+indent_uid+indent),y, String(mfrc522.uid.uidByte[i],HEX));
      indent = indent+14;
    }
  }
}
#endif
#endif

/**
 * @author Andreas C. Dyhrberg
 */
void LoopContent()
{
  /* Look for new cards */
  if ( ! mfrc522.PICC_IsNewCardPresent()) {
    //Serial.print("iNCP-");
    Serial.print(".");
    return; /* Skip the rest of the code in this function called LoopContent() */
  }
  /* Select one of the cards */
  if ( ! mfrc522.PICC_ReadCardSerial()) {
    Serial.print("RCS-");
    return; /* Skip the rest of the code in this function called LoopContent() */
  }

  /* Dump debug info about the card; PICC_HaltA() is automatically called */
  Serial.println("");
  mfrc522.PICC_DumpToSerial(&(mfrc522.uid));
  delay(100);

  // UID only
  Serial.print(F("Card UID:"));
  for (byte i = 0; i < mfrc522.uid.size; i++) {
    if(mfrc522.uid.uidByte[i] < 0x10)
      Serial.print(F(" 0"));
    else
      Serial.print(F(" "));
    Serial.print(mfrc522.uid.uidByte[i], HEX);
  } 
  Serial.println();

#if DISPLAY_USE
  digitalWrite(16, LOW); // set GPIO16 low to reset OLED
#if DISPLAY_ON
  delay(50);
  digitalWrite(16, HIGH);
  display.init();
  display.setFont(ArialMT_Plain_10);
  int prefill = DisplayPrefill(loop_count);
  display.drawString( 0,0, "Count:            : itoffice.eu");
  display.drawString((47+prefill),0, String(loop_count));

  DisplayPrintUid(0,10, mfrc522);

  display.display();
#endif
#endif

  /* Make sure LMIC is ran too - only done if return is not called before now */
  os_runloop_once();
}

/**
 * @author Andreas C. Dyhrberg
 */
int DisplayPrefill(int value) {
  int prefill = 0;
  if(value < 100){
     prefill = 6;
  }
  if(value < 10){
      prefill = prefill+6;
  }
  return prefill;
}

Extract from verbose output during compilation

In the verbose log can you see the librarias and their versions used. Note that ArduinoJson is an older version (5.10.1). How far newer versions can be used, will I leave to you to find out. I know for sure that the newest can’t be used.

Using board 'heltec_wifi_lora_32' from platform in folder: /home/xox/.arduino15/packages/esp32/hardware/esp32/1.0.4
Using core 'esp32' from platform in folder: /home/xox/.arduino15/packages/esp32/hardware/esp32/1.0.4

Multiple libraries were found for "SSD1306.h"
 Used: /home/xox/Arduino/libraries/ESP8266_and_ESP32_OLED_driver_for_SSD1306_displays

Multiple libraries were found for "MFRC522.h"
 Used: /home/xox/Arduino/libraries/MFRC522

Multiple libraries were found for "ArduinoJson.h"
 Used: /home/xox/Arduino/libraries/ArduinoJson

Multiple libraries were found for "CayenneLPP.h"
 Used: /home/xox/Arduino/libraries/CayenneLPPDecode

Multiple libraries were found for "lmic.h"
 Used: /home/xox/Arduino/libraries/IBM_LMIC_framework

Multiple libraries were found for "Wire.h"
 Used: /home/xox/.arduino15/packages/esp32/hardware/esp32/1.0.4/libraries/Wire

Multiple libraries were found for "SPI.h"
 Used: /home/xox/.arduino15/packages/esp32/hardware/esp32/1.0.4/libraries/SPI

Multiple libraries were found for "AsyncDelay.h"
 Used: /home/xox/Arduino/libraries/AsyncDelay

Multiple libraries were found for "SoftWire.h"
 Used: /home/xox/Arduino/libraries/SoftWire

Using library CayenneLPPDecode at version 1.0 in folder: /home/xox/Arduino/libraries/CayenneLPPDecode 
Using library IBM_LMIC_framework at version 1.5.0 arduino-2 in folder: /home/xox/Arduino/libraries/IBM_LMIC_framework 
Using library Wire at version 1.0.1 in folder: /home/xox/.arduino15/packages/esp32/hardware/esp32/1.0.4/libraries/Wire 
Using library SPI at version 1.0 in folder: /home/xox/.arduino15/packages/esp32/hardware/esp32/1.0.4/libraries/SPI 
Using library ESP8266_and_ESP32_OLED_driver_for_SSD1306_displays at version 4.1.0 in folder: /home/xox/Arduino/libraries/ESP8266_and_ESP32_OLED_driver_for_SSD1306_displays 
Using library MFRC522 at version 1.4.5 in folder: /home/xox/Arduino/libraries/MFRC522 
Using library AsyncDelay at version 1.1.0 in folder: /home/xox/Arduino/libraries/AsyncDelay 
Using library SoftWire at version 2.0.0 in folder: /home/xox/Arduino/libraries/SoftWire 
Using library ArduinoJson at version 5.10.1 in folder: /home/xox/Arduino/libraries/ArduinoJson 

Sketch uses 257097 bytes (19%) of program storage space. Maximum is 1310720 bytes.
Global variables use 16916 bytes (5%) of dynamic memory, leaving 310764 bytes for local variables. Maximum is 327680 bytes.

In the Arduino IDE can you turn on the verbose output in the preferences:

Results and Conclusion

A solution for connection several sensors/slaves via SPI to Arduino and other microcontrollers is shown. I must admit that the code doesn’t leave any IoT data on the TTN server yet for the Lora part. So regards the Heltec is this a success in the sense that:

  • The microcontroller runs the without (visible) conflicts between the the Lora sender and the RFID-RC522.
  • The principle of several sensors on SPI is demonstrated for the Heltec too.
  • A possible pin configuration is demonstrated for the Heltec.
  • The Heltec reads the RFID cards and show those data in the terminal and the ID on the display also.

The Lora code does for now only send when the Heltec is reset – by the reset button. I know for sure that my Lora library is outdated. This could be one source for errors.

For now, will I leave further investigations regards Lora to the reader or at least for my part; to the future. Maybe will I make an update one day.

The basic principle of using SPI for several sensors on Arduino and other microcontrollers like the Heltec is demonstrated.