0

I'm writing PortExtender, a library to convert an Arduino Pro Mini in a port expander and I ran into what seems to be a compatibility problem between Arduino and nodeMCU: I got different answers depending on who is the master (Arduino or nodeMCU).

This is my test configuration:

Test configuration

At the center, the Slave Arduino, with buttons, leds, and ldr for testing pin access.

I have two master, but only one is used at a time. The inactive one is powered off and disconected from the two others. The slave get its power from the master.

I compiled and ran the same Master program in the two master, while the slave remains the same. This is the Master test program:

#include <Wire.h>
#include <PortExtender.h>

const int SLAVE_ADDRESS = 42;

PortExtender port = PortExtender(SLAVE_ADDRESS);

void setup ()
{
  Serial.begin(9600);
  delay(1000);
  Serial.println("Master");
  Serial.println("-----");

  Wire.begin();
  int r;
  do {
    r = port.reset();    
    Serial.print("Resetting Port Expander. Status=");Serial.println(r);
    delay(100);
  } while(r != SLAVE_ADDRESS);
}

void loop() {
}

The test just sent a soft-reset to the slave. The slave answers with its own address (42), just to check that the command works.

Master 1 execution

This is the correct result from Master 1 (Arduino Pro Mini):

Master 1 response

In fact, I can run a complete test suit with no problems, testing the button, controlling the LED and reading the LDR.

Master 2 execution

This is the result from Master 2 (nodeMCU):

enter image description here

Response is some random value that change from run to run. Slave, however, receive the command and return the right response, as checked thru Serial Monitor connected to the slave.

Everything seems to work fine, but answer from slave looks like it changed "in the wire".

PortExtender library code

The relevant methods involved are:

int PortExtender::reset() {
    return send(CMD_RESET);
}

int PortExtender::send(byte cmd) {
    Wire.beginTransmission(slave);
    Wire.write(cmd);
    Wire.endTransmission(false); // Also tested with true.        
    return readAnswer();
}

int PortExtender::readAnswer() {
    short answer;

    Wire.requestFrom(slave, sizeof(answer));    
    PortExtender::read((byte*) &answer, sizeof(answer));
    return (int) answer;    
}

void PortExtender::read(byte* p, int len) {
    while (len > 0 && Wire.available() > 0) {
        *p++ = Wire.read();
        len--;
    }
}

Arduino ints are 2 bytes, nodeMCU ints are 4 bytes. Data from slave is send as short (Arduino, 2 bytes), received as short (nodeMCU, 2 bytes) and then promoted to int (nodeMCU, 4 bytes).

PortExtender.h

#ifndef PortExtender_h
#define PortExtender_h
/*
 *  
 */
#include <Arduino.h>
#include <inttypes.h>

#define NO_CMD             0
#define CMD_DIGITAL_WRITE  1
#define CMD_DIGITAL_READ   2
#define CMD_ANALOG_WRITE   3
#define CMD_ANALOG_READ    4
#define CMD_PIN_MODE       5
#define CMD_SET_WATCHDOG   6
#define CMD_START_WATCHDOG 7
#define CMD_STOP_WATCHDOG  8
#define CMD_RESET          9
#define CMD_PLAY          10
#define CMD_PLAY_STEP     11
#define CMD_DEBOUNCE      12
#define CMD_TONE          13
#define CMD_TONE_LEN      14
#define CMD_NO_TONE       15

#define ERROR_NO_RESET    -1
#define ERROR_PARAMETER   -2

struct PAR_TONE {
    byte pin;
    unsigned int freq;
};

struct PAR_TONE_LEN {
    byte pin;
    unsigned int freq;
    unsigned long len;
};

class PortExtender
{
  private:
      short slave;
      int send(byte* b, int len);
      int send(byte cmd);
      int send(byte cmd, byte arg1);
      int send(byte cmd, byte arg1, byte arg2);
      int readAnswer();

  public:
      PortExtender(int slaveAddress);

      /*
       *     All same as Arduino, except as noted.
       */
      int pinMode(int pin, int mode); 
      //    Set debouncer. 
      int debounce(int pin, short debounce); 

      int digitalWrite(int pin, int value);
      int digitalRead(int pin);
      int analogWrite(int pin, int value);
      int analogRead(int pin);
      int tone(int pin, unsigned int freq);
      int tone(int pin, unsigned int freq, unsigned long len);
      int noTone(int pin);

      /*
       * Watchdog functions
       */
      //    Designate the output pin and watchod parameters.
      int setWatchdog(int pin, unsigned long time, unsigned long pulse);
      int startWatchdog();
      int stopWatchdog();

      /*
       *    Timed switching of a pin, with a giving set of intervals.
       */
      int play(int pin, unsigned int score[]);

      /*
       *    Reset Port Extender. 
       */
      int reset();
      static void read(byte*, int len);
};

#endif

Slave PortExtender (Arduino Pro Mini)

This is work in progress. Digital/Analog read/write completed, other methods (play, watchdog and tone still to be tested).

/*
   Programable Port Extender by Look Alterno.

   Convert your Arduino in a I2C slave that extends
   the number of pin for your master Arduino.

   Your master Arduino will be able to:
   - Read/Write digital/analog pins.
   - Program one-shot pulses of any length.
   - Program PWM with arbitrary period and duty cycle.

   Applications:
   - Make timers for external appliances.
   - Implement watchdogs for resetting things.
*/

//  ---- Change this address to a suitable value if needed ----
#define MY_ADDRESS  42

#include <Bounce2.h>
#include <Wire.h>
#include <PortExtender.h>

//  A CMD_RESET has been received from master.
boolean bReset = false;

//  These port are programmables.
struct {
  byte pin;
  byte exp;
} pins[] = {{2, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}, {7, 0},
  {8, 0}, {9, 0}, {10, 0}, {11, 0}, {12, 0}, {13, 0},
  {A0, 0}, {A1, 0}, {A2, 0}, {A3, 0}
};

const short nPins = sizeof(pins) / sizeof(pins[0]);

struct {
  Bounce debouncer;
  short pin;
  short interval;
} debouncer = {Bounce(), 0, 0};

#define WD_NORMAL 0
#define WD_PULSE  1

struct {
  int pin;
  int state;
  boolean active;
  unsigned long lastBeat;
  unsigned long time;
  unsigned long pulse;
  unsigned long wait;
  unsigned long startPulse;
} watchdog;

#define SCORE_MAX_STEP 50
struct {
  boolean active;
  int pin;
  unsigned long time; // start time of current step.
  int freeStep;       // for filling the array.
  int currentStep;    // for playing the array.
  unsigned int step[SCORE_MAX_STEP];
} score;

typedef void (*fcnptr)();

void cmdBeat();
void cmdDigitalWrite();
void cmdDigitalRead();
void cmdAnalogWrite();
void cmdAnalogRead();
void cmdPinMode();
void cmdWatchdog();
void cmdStartWatchdog();
void cmdStopWatchdog();
void cmdReset();
void cmdPlay();
void cmdPlayStep();
void cmdDebounce();
void cmdTone();
void cmdToneLen();
void cmdNoTone();

struct {
  byte  cmd;
  fcnptr fcn;
} commands[] = {
  { NO_CMD,             cmdBeat},
  { CMD_DIGITAL_WRITE,  cmdDigitalWrite},
  { CMD_DIGITAL_READ,   cmdDigitalRead},
  { CMD_ANALOG_WRITE,   cmdAnalogWrite},
  { CMD_ANALOG_READ,    cmdAnalogRead},
  { CMD_PIN_MODE,       cmdPinMode},
  { CMD_SET_WATCHDOG,   cmdWatchdog},
  { CMD_START_WATCHDOG, cmdStartWatchdog},
  { CMD_STOP_WATCHDOG,  cmdStopWatchdog},
  { CMD_RESET,          cmdReset},
  { CMD_PLAY,           cmdPlay},
  { CMD_PLAY_STEP,      cmdPlayStep},
  { CMD_DEBOUNCE,       cmdDebounce},
  { CMD_TONE,           cmdTone},
  { CMD_TONE_LEN,       cmdToneLen},
  { CMD_NO_TONE,        cmdNoTone}
};

unsigned long now = millis();
byte  lastCommand;
short answer;

void setup() {
  Serial.begin(9600);
  Serial.println("--- Port Extender ---");

  initAll();
  Wire.begin(MY_ADDRESS);
  Wire.onReceive(receiveCommand);
  Wire.onRequest(sendAnswer);
}

void loop() {
  now = millis();

  checkDebouncer();
  checkWatchdog();
  checkPlay();
}

void checkDebouncer() {
  debouncer.debouncer.update();
}

/*
   Start playing a score on pin.
   The pin start at the last value set by master (HIGH or LOW)
*/
void startPlay() {
  noInterrupts();
  score.active = true;
  score.time   = millis();
  score.currentStep = 0;
  digitalWrite(score.pin, !digitalRead(score.pin));
  interrupts();
}

void checkPlay() {
  noInterrupts();

  if (score.active) {
    unsigned long stepEnd = score.time + score.step[score.currentStep];

    if (now >= stepEnd) {
      score.currentStep++;

      if (score.currentStep >= score.freeStep) {
        score.active = false;
      } else {
        digitalWrite(score.pin, !digitalRead(score.pin));
        score.time = now;
      }
    }
  }

  interrupts();
}

/*
   Each if is time to reset the master.
   After resetting the master, soft-reset the slave.
*/
void checkWatchdog() {
  if (watchdog.active) {

    switch (watchdog.state) {
      case WD_NORMAL:
        if (now - watchdog.lastBeat > watchdog.time) {
          pinMode(watchdog.pin, OUTPUT);
          digitalWrite(watchdog.pin, HIGH);
          watchdog.startPulse = now;
          watchdog.state = WD_PULSE;
        }

        break;

      case WD_PULSE :
        if (now - watchdog.startPulse > watchdog.pulse) {
          digitalWrite(watchdog.pin, LOW);
          cmdReset();
        }

        break;
    }
  }
}


/*
    Receive a command from Master
*/
void receiveCommand(int howMany) {
  lastCommand = Wire.read();
  //  Serial.print("command="); Serial.println(lastCommand);
  answer = 0;

  if (!bReset && lastCommand != CMD_RESET) {
    //  The first command received must be a CMD_RESET.
    answer = ERROR_NO_RESET;
  } else {
    fcnptr f = commands[lastCommand].fcn;
    f();

    watchdog.lastBeat = now;
  }
}

  // called by interrupt service routine when outgoing data is requested
  void sendAnswer() {
    Wire.write((byte *) &answer, sizeof(answer));
    lastCommand = NO_CMD;
  }
  void cmdBeat() {
  }

  void cmdReset() {
    //  Make a soft reset.
    initAll();
    bReset = true;
  }

  /*
     Initialize all variables and pins.
  */
  void initAll() {
    score.active = false;
    watchdog.active = false;
    debouncer.pin = -1;

    int size = sizeof(pins) / sizeof(pins[0]);

    for (int i = 0; i < size; i++) {
      int pin = pins[i].pin;
      pinMode(pin, OUTPUT);
      digitalWrite(pin, LOW);
    }

    answer = MY_ADDRESS;
  }

  void cmdPinMode() {
    if (Wire.available() == 2) {
      byte pin = Wire.read();
      byte val = Wire.read();
      pinMode(pin, val);
    }
  }

  void cmdDebounce() {
    if (Wire.available() == 2) {
      byte pin = Wire.read();
      byte val = Wire.read();
      debouncer.pin = pin;
      debouncer.interval = val;
      debouncer.debouncer.attach(pin);
      debouncer.debouncer.interval(val);
    }
  }

  void cmdDigitalWrite() {
    if (Wire.available() == 2) {
      byte pin = Wire.read();
      byte val = Wire.read();

      digitalWrite(pin, val);
    }
  }

  void cmdDigitalRead() {
    if (Wire.available() == 1) {
      byte pin = Wire.read();

      if (pin == debouncer.pin) {
        answer = debouncer.debouncer.read();
      } else {
        answer = digitalRead(pin);
      }
    }
  }

  void cmdAnalogWrite() {
    if (Wire.available() == 2) {
      byte pin = Wire.read();
      byte val = Wire.read();

      analogWrite(pin, val);
    }
  }

  void cmdAnalogRead() {
    if (Wire.available() == 1) {
      byte pin = Wire.read();

      answer = analogRead(pin);
    }
  }

  void cmdTone() {


  }

  void cmdToneLen() {

  }

  void cmdNoTone() {
    if (Wire.available() == 1) {
      byte pin = Wire.read();
      noTone(pin);

    }
  }

  void cmdPlay() {
    if (Wire.available() == 1) {
      score.active = false;
      score.pin = Wire.read();
      score.freeStep = 0;
    }
  }

  void cmdPlayStep() {
    unsigned int val;

    if (Wire.available() == 2) {
      val = Wire.read() << 8 | Wire.read();

      if (val > 0) {
        Serial.print("val=");
        Serial.println(val);
        score.step[score.freeStep++] = val;
      } else {
        startPlay();
      }
    } else {
      answer = ERROR_PARAMETER;
    }
  }

  void cmdWatchdog() {
    PortExtender::read((byte *) &watchdog.pin, sizeof(watchdog.pin));
    PortExtender::read((byte *) &watchdog.time, sizeof(watchdog.time));
    PortExtender::read((byte *) &watchdog.pulse, sizeof(watchdog.pulse));
    watchdog.active = false;
  }

  void cmdStartWatchdog() {
    watchdog.active = true;
  }

  void cmdStopWatchdog() {
    watchdog.active = false;
  }

  int searchPin(int pin) {
    int i = 0;

    while (i < nPins && pins[i].pin != pin) {
      i++;
    }

    return (i < nPins) ? i : -1;
  }

The question

What is going wrong here?

  • Please show a working sketch with a complete PortExtender.h and the code of the slave. I can not recognize a pattern: 22272 = 0x5700 = 0b0101.0111.0000.0000, 47 = 0x2A = 0b0010.1010. You don't even know if you are reading something at all. Could you test if Wire.available is not zero ? – Jot Jul 10 '17 at 21:18
  • @Jot. Remember: an Arduino Master can control LED, read buttons, read analog input, but nodeMCU fails. – user31481 Jul 10 '17 at 21:29
  • Thanks. You should add that test to check if Wire.requestFrom actually received some bytes. Does the nodeMCU support I2C clock pulse stretching ? The Slave code working with the Arduino as Master does not matter. The code could have some details that makes the nodeMCU fail. The slave code does not set 'lastCommand' and 'answer' as volatile and the interrupt routine 'receiveCommand' can be long. If the nodeMCU does support I2C clock pulse stretching does it allow a very long stretch by the Slave 'receiveCommand' code ? Do you have a logic analyzer ? – Jot Jul 10 '17 at 21:46
  • @Jot. I added "volatile". Same result. And reset() is short and fixed-time. I printed requestFrom() and available() and both returns "2". And clearly the slave is receiving the requestFrom() because it's executing the callback. I don't have a logic analyzer. – user31481 Jul 10 '17 at 22:08
  • I can not pinpoint the problem. The 0x5700 is very suspicious, there must be something in it. What is the Endian Byte Order for Arduino and nodeMCU ? Is everything running at 3.3V ? A 5V Arduino board needs 3.5V on the I2C bus to see it as a valid high. My best advice is to add more tests. For example send in the slave 0x33 instead of 'answer'. Check in the master the error return of Wire.endTransmission. And so on. – Jot Jul 10 '17 at 22:34
0

Hardware problem!

The nodeMCU I'm using fails (mostly resets and garbage in the Serial Monitor) when inserted in the breadboard. That is. Them run OK when I let them just hanging from the USB cable.

I tested two of them in different breadboards. I also tested the breadboard for shorts and failed connections with an ohmmeter.

These are culprits:

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy