Saturday, 10 March 2018

SPI Master & Slave Programs with the ESP8266 NodeMCU ESP-12E Lua V3 and Arduino IDE

ESP-12E as SPI Master (top) and SPI Slave (bottom)
After learning how to program the ESP8266 using the Arduino IDE, I thought it might be a good idea to interface the ESP8266 with Arduino-family devices. There are plenty of articles in the Web on interfacing the ESP8266 with Arduino CPUs like the Uno but comparatively few on interfacing to Arduino non-CPU PCBs.

PCBs like this relay boards are easy to interface to; you use the same method as the blinking LED sketch in my last post.
This 5V Arduino relay module costs as little as RM5

But other PCBs like this MAX7219 LED 7-segment Display costs a mere RM9.60 and uses the SPI or Serial Peripheral Interface:

MAX7219 LED 7-segment display module
 Quite a few PCBs use the I2C interface, including this delicious RM5.90 realtime clock:

PCF8563T Realtime Clock

SPI involves a master device and a slave device. I used an ESP-12E for both. The master code from the Arduino IDE "Examples->SPISlave->SPISlave_SafeMaster" menu works and is reproduced here as the code tends to change from version to version:

/*
    SPI Safe Master Demo Sketch
    Connect the SPI Master device to the following pins on the esp8266:

    GPIO    NodeMCU   Name  |   Uno
   ===================================
     15       D8       SS   |   D10
     13       D7      MOSI  |   D11
     12       D6      MISO  |   D12
     14       D5      SCK   |   D13

    Note: If the ESP is booting at a moment when the SPI Master has the Select line HIGH (deselected)
    the ESP8266 WILL FAIL to boot!
    This sketch tries to go around this issue by only pulsing the Slave Select line to reset the command
    and keeping the line LOW all other time.

*/
#include <SPI.h>

class ESPSafeMaster
{
private:
    uint8_t _ss_pin;
    void _pulseSS()
    {
        digitalWrite(_ss_pin, HIGH);
        delayMicroseconds(5);
        digitalWrite(_ss_pin, LOW);
    }
public:
    ESPSafeMaster(uint8_t pin):_ss_pin(pin) {}
    void begin()
    {
        pinMode(_ss_pin, OUTPUT);
        _pulseSS();
    }

    uint32_t readStatus()
    {
        _pulseSS();
        SPI.transfer(0x04);
        uint32_t status = (SPI.transfer(0) | ((uint32_t)(SPI.transfer(0)) << 8) | ((uint32_t)(SPI.transfer(0)) << 16) | ((uint32_t)(SPI.transfer(0)) << 24));
        _pulseSS();
        return status;
    }

    void writeStatus(uint32_t status)
    {
        _pulseSS();
        SPI.transfer(0x01);
        SPI.transfer(status & 0xFF);
        SPI.transfer((status >> 8) & 0xFF);
        SPI.transfer((status >> 16) & 0xFF);
        SPI.transfer((status >> 24) & 0xFF);
        _pulseSS();
    }

    void readData(uint8_t * data)
    {
        _pulseSS();
        SPI.transfer(0x03);
        SPI.transfer(0x00);
        for(uint8_t i=0; i<32; i++) {
            data[i] = SPI.transfer(0);
        }
        _pulseSS();
    }

    void writeData(uint8_t * data, size_t len)
    {
        uint8_t i=0;
        _pulseSS();
        SPI.transfer(0x02);
        SPI.transfer(0x00);
        while(len-- && i < 32) {
            SPI.transfer(data[i++]);
        }
        while(i++ < 32) {
            SPI.transfer(0);
        }
        _pulseSS();
    }

    String readData()
    {
        char data[33];
        data[32] = 0;
        readData((uint8_t *)data);
        return String(data);
    }

    void writeData(const char * data)
    {
        writeData((uint8_t *)data, strlen(data));
    }
};

ESPSafeMaster esp(SS);

void send(const char * message)
{
    Serial.print("Master: ");
    Serial.println(message);
    esp.writeData(message);
    delay(10);
    Serial.print("Slave: ");
    Serial.println(esp.readData());
    Serial.println();
}

void setup()
{
    Serial.begin(115200);
    SPI.begin();
    esp.begin();
    delay(1000);
    send("Hello Slave!");
}

void loop()
{
    delay(1000);
    send("Are you alive?");
}

Unfortunately the "Examples->SPISlave->SPISlave_Test" did not work for me. The Slave could read the master but its reply seems corrupted. Luckily I found a working version in the ESP8266 Community Forum:

/*
    SPI Slave Demo Sketch
    Connect the SPI Master device to the following pins on the esp8266:

    GPIO    NodeMCU   Name  |   Uno
  ===================================
     15       D8       SS   |   D10
     13       D7      MOSI  |   D11
     12       D6      MISO  |   D12
     14       D5      SCK   |   D13

    Note: If the ESP is booting at a moment when the SPI Master has the Select li
ne HIGH (deselected)
    the ESP8266 WILL FAIL to boot!
    See SPISlave_SafeMaster example for possible workaround

*/

#include "SPISlave.h"

void setup()
{
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    Serial.println();

    // data has been received from the master. Beware that len is always 32
    // and the buffer is autofilled with zeroes if data is less than 32 bytes long
    // It's up to the user to implement protocol for handling data length
    SPISlave.onData([](uint8_t * data, size_t len) {
        String message = String((char *)data);
        char answer[33];
        if(message.equals("Hello Slave!")) {
            strcpy(answer, "Hello Master!");
        } else if(message.equals("Are you alive?")) {
            sprintf(answer, "Alive for %u seconds!", millis() / 1000);
        } else {
            strcpy(answer, "Say what?");
        }
        SPISlave.setData(answer);
        Serial.printf("Question: %s\Answer: %s\n", (char *)data, answer);
    });

    // The master has read out outgoing data buffer
    // that buffer can be set with SPISlave.setData
    SPISlave.onDataSent([]() {
        Serial.println("Answer Sent");
    });

    // status has been received from the master.
    // The status register is a special register that bot the slave and the master can write to and read from.
    // Can be used to exchange small data or status information
    SPISlave.onStatus([](uint32_t data) {
        Serial.printf("Status: %u\n", data);
        SPISlave.setStatus(millis()); //set next status
    });

    // The master has read the status register
    SPISlave.onStatusSent([]() {
        Serial.println("Status Sent");
    });

    // Setup SPI Slave registers and pins
    SPISlave.begin();

    // Additional setting to have MISO change on falling edge
    SPI1C2 |= 1 << SPIC2MISODM_S;

    // Set the status register (if the master reads it, it will read this value)
    SPISlave.setStatus(millis());

    // Sets the data registers. Limited to 32 bytes at a time.
    // SPISlave.setData(uint8_t * data, size_t len); is also available with the same limitation
    SPISlave.setData("Ask me a question!");
}

void loop() {}

The two ESP-12E, master and slave are wired thus:

As per the comments in the two sketches, the ESP-12E pin assignments are:

                  Master               Slave
              D8       SS      -   SS          D8
              D7      MOSI  -   MOSI    D7
              D6      MISO  -   MISO    D6
              D5      SCK    -   SCK      D5

I plugged both into the same laptop, and the slave came up as /dev/ttyUSB0 and the master /dev/ttyUSB1. By clicking on "Tools->Port" I could switch between the two serial ports and program the master and slave in turn.

When working correctly you should get the following output from the Master's Serial Monitor:

Master: Are you alive?
Slave: Alive for 44 seconds!

Master: Are you alive?
Slave: Alive for 45 seconds!

Master: Are you alive?
Slave: Alive for 46 seconds!

Master: Are you alive?
Slave: Alive for 47 seconds!

Master: Are you alive?
Slave: Alive for 48 seconds!

Now if you flipped over to /dev/ttyUSB0 and relaunched the Serial Monitor, you should get:

Question: Are you alive?Answer: Alive for 44 seconds!
Answer Sent
Question: Are you alive?Answer: Alive for 45 seconds!
Answer Sent
Question: Are you alive?Answer: Alive for 46 seconds!
Answer Sent
Question: Are you alive?Answer: Alive for 47 seconds!
Answer Sent

There you have it, SPI communication using the ESP8266 and Arduino IDE. 

Happy Trails.
 

No comments:

Post a Comment