0

I'm working on a project where I use multiple i2c IC's / sensors. Adafruit has very nice basic libraries for the individual IC's and that is very convenient (spares hours of digging through datasheets). But their libraries are inflexible. I tried to alleviate this by applying dependency injection.

So the solution would be to use the TwoWire instance and pass it as a reference to the (in my case) MCP23008 library (dependency injection) and let the library make use of the same instance of TwoWire to do all i2c calls.

To make the topic more readable - but still provide insight in what I already tested and why - I will first mention the problem and then give more background info at the bottom.

// part from the modified library for the MCP23008 
// i2c port expander by Limor Fried

MyLib_MCP23008.cpp

// I added a constructor
MyLib_MCP23008::MyLib_MCP23008(TwoWire *w)
{
    // Constructors
    this->_wire = w;
}

// convenience begin with default address
void MyLib_MCP23008::begin() {
    begin(MCP23008_ADDRESS); // defined in .h as 0x20
}

void MyLib_MCP23008::begin(uint8_t addr) {
    if (addr > 7) {
        addr = 7;
    }
    this->i2caddr = addr;
    // set defaults!
    // _wire->begin(); don't do this here, expect the dependent TwoWire to do this...
    // there is a condition for ARDUINO >= 100 else in the original...
    _wire->beginTransmission(MCP23008_ADDRESS | i2caddr);
    _wire->write((byte)MCP23008_IODIR);
    _wire->write((byte)0xFF);  // all inputs
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->write((byte)0x00);
    _wire->endTransmission();
}

I also changed all "Wire." references to "_wire->" in the other methods.

MyLib_MCP23008.h:

// I left out the defines for better reading

class MyLib_MCP23008 {
    public:
        MyLib_MCP23008(TwoWire *w = &Wire);
        void begin(uint8_t addr);
        void begin(void);

        void pinMode(uint8_t p, uint8_t d);
        void digitalWrite(uint8_t p, uint8_t d);
        void pullUp(uint8_t p, uint8_t d);
        uint8_t digitalRead(uint8_t p);
        uint8_t readGPIO(void);
        void writeGPIO(uint8_t);

    private:
        uint8_t i2caddr;
        uint8_t read8(uint8_t addr);
        void write8(uint8_t addr, uint8_t data);
        TwoWire *_wire;
};

In my test sketch (mcp23008_test.ino):

TwoWire myWire(&sercom2, 4, 3);
MyLib_MCP23008 mcp23008(&myWire);
const int mcp_address = 0x20;

void setup()
{
    myWire.begin();
    pinPeripheral(4, PIO_SERCOM_ALT);
    pinPeripheral(3, PIO_SERCOM_ALT);

    // if I can find mcp23008: 
    myWire.beginTransmission(mcp_address);
    if (myWire.endTransmission() == 0) {
        mcp23008.begin();
    }

    // irrelevant setup stuff for pinMode / Serial etc.

    mcp23008.pinMode(mcpLED, OUTPUT);
}

void loop() {

    mcpLEDState = !mcpLEDState;
    mcp23008.digitalWrite(mcpLED, !mcpLEDState); // i2c LED alas nothing...
    digitalWrite(hbLED, mcpLEDState); // heartbeat: works fine

    // after this I scan the i2c wire 
    // using begin/endTransmission calls
    // and output it to Serial
    // result: works fine...

}

The program does not crash (I use a heartbeat LED and Serial output keeps working, the i2c bus also stays responsive (begin/end) but the LED of connected to the mcp23008 does not react in the TwoWire dependency injection version of the library).

One thing I will try is adding a logic analyser to the i2c bus and see what message come by... But as the 'hacked' library solution does work without any problem I guess there is something else going on.

Note I have a work-around and that is including all the library stuff directly into the main sketch as sub ino-files, that works but is not very SOLID of course.

Background information:

To make things manageable I first focussed on the MCP23008 IC: I have connected an LED to one of the pins, writing a LOW to it will make the LED turn on (as I sink it to 3.3V) and a HIGH will turn it off. So far so good, if I hack the library to support my specific i2c port and this works without any problems:

TwoWire myWire(&sercom2, 4, 3);

Then in begin I do:

myWire.begin();
pinPeripheral(4, PIO_SERCOM_ALT);
pinPeripheral(3, PIO_SERCOM_ALT);

inside the begin method of the library.

I then replace all the Wire references in the library with myWire.

Again till this point so far so good, I can let the LED blink from my main sketch etc.

So this works for my basic case but is a very ugly solution: you might have again some different port you want to use for your MCP23008. And in my case I want to use more than one i2c device and as they all do myWire.begin(); in their begin method the i2c bus freezes etc.

And as I said earlier: one does not really want the library itself to do the myWire.begin stuff (and if needed additional pinPeripheral calls etc): one wants the main sketch to do so.

Nathan Seidle from SparkFun Electronics wrote some nice article about the inflexibility of Arduino libraries issue: https://www.sparkfun.com/news/2194

But his solution and the one he based his solution on are not working for me: I can see the MCP23008 on the bus but it does not react to my command when using the modified library.

I also looked at the https://github.com/arduino/ArduinoCore-samd/blob/master/libraries/Wire/Wire.cpp/.h as it pretty much does the same that I try to accomplish but than at the Wire level. I don't see any deviations in my code from that example but looking at one's own code is always a weakness of course.

Update

This minimalistic implementation does work: I used a standard Wire | master writer as basic sketch on one board and the standard Wire | slave receiver at the other board.

I changed the master:

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

TwoWire myWire(&sercom3, 0, 1);
GenericWireTest genericWire(&myWire);

void setup()
{
  Wire.begin(); // join i2c bus (address optional for master)
  genericWire.begin();
  Serial.begin(115200);
}

byte x = 0;

void loop()
{
  genericWire.sendMessage(x);
  Serial.print("Wrote: x = ");
  Serial.println(x);
  x++;
  delay(500);
}

CPP file:

#include "Wire.h"
#include "GenericWireTest.h"

GenericWireTest::GenericWireTest(TwoWire *w)
{
  this->_wire = w;
}

void GenericWireTest::sendMessage(int x) {
  _wire->beginTransmission(4); // transmit to device #4
  _wire->write("x is ");        // sends five bytes
  _wire->write(x);              // sends one byte
  _wire->endTransmission();    // stop transmitting
}

void GenericWireTest::begin(uint8_t addr) {
  // nothing to do in this case...
  begin();
}

void GenericWireTest::begin(void) {
  _wire->begin();
}

H-file:

#include "Wire.h"

class GenericWireTest {
    public:
        GenericWireTest(TwoWire *w = &Wire);
        void begin(uint8_t addr);
        void begin(void);

        void sendMessage(int x);

    private:
        uint8_t i2caddr;
        TwoWire *_wire;
};

This works as expected... Note: I added _wire->begin(); in order to maybe confuse the Wire channel but it does not get confused even then.

Update 2:

I was actually able to confirm that the injection works with other i2c peripherals including the FXOS8700 accelerometer/magnetometer. So it is maybe just related to the MCP or I made a little mistake somewhere. Maybe time to dig deep into the datasheet of the MCP.

Update 3: Also works fine for FXAS21002C so I guess it is some MCP related issue. I created a work-around for now by adding the MCP code to an ino file in my main project, then I could use dependency injection without any problems, not as neat as I would like, but for now workable...

JopieK
  • 68
  • 9
  • 1
    Are you able to pare this down a bit? All the discussion of electronics seems unrelated to the C++ aspect of the question and I'm struggling to follow it. – Lightness Races in Orbit Jan 11 '19 at 13:17
  • 2
    I love the enthusiasm, and the questions seems to be written well, but can you consider the _minimal_ part of creating a [minimal, complete, and verifiable](https://stackoverflow.com/help/mcve) example – JMAA Jan 11 '19 at 14:48
  • @Lightness Races in Orbit: I tried to rewrite it to become more concise by splitting it into tow parts. JMAA: trouble is one needs the hardware to 'make a round trip'. The compiler does not complain and it can be uploaded to the board. To comply as much as possible I have added the source here: https://jksoftedu.nl/files/Flexlib_MCP23008.zip – JopieK Jan 11 '19 at 15:59
  • I create a very simple library and using exactly the same principles I was able to perform communication between to SAMD21 microcontrollers without any problems. Maybe it is related to some kind of MCP23008 related issue, so next thing is to test the same principle with other sensors I need to use and again try to confirm the results. (see original post for the source). – JopieK Jan 12 '19 at 15:27
  • 1
    This is a nice exercise, but it's not something you'd ever want to do in real life, because the abstraction layer hurts performance, and these tiny microcontrollers don't have enough clock cycles to waste a bunch on abstraction and still get any useful work done. If you used templates to achieve zero-overhead abstraction, on the other hand.... – Ben Voigt Jan 12 '19 at 17:11
  • @BenVoigt I do partially agree, maybe so for an Atmega328, but I don't think it is a problem for a faster controller like a SAMD21 (and it intended to be used for use in a Cortex M0 or even M4 environments where one would have different ports available, apart from that: the SERCOM implementation itself also uses the same technique, these uC's can even run Micro Python you know). For now I haven't experienced any performance issues in relation to this board: https://www.adafruit.com/product/3463, but I must say that I delegate different tasks in my system by using a whole number of SAMD21's. – JopieK Jan 12 '19 at 17:18
  • @BenVoigt: - Setting the LED takes about 0.7ms. - Setting the MCP23008 LED takes about than 0.0035ms. (and I use a reference to wire for it) - Reading the gyro takes about 1.9ms. (I use a reference to wire for it) - Reading magneto and accelerometer takes 3.9ms. (I use a reference to wire for it) – JopieK Jan 12 '19 at 17:45
  • So a very ordinary sampling rate of 100 Hz causes the accelerometer interface code to consume 39% of CPU time? Yeah, that's high overhead. And even if your sampling rate is lower, one operation of 1.9 ms can cause you to lose incoming data on a UART. Even 0.7ms might, depending on the baud rate and incoming buffer depth. Like I said, interesting experiment, but not practical. – Ben Voigt Jan 12 '19 at 17:53
  • @BenVoigt: Ok, challenge accepted, the initial measurements were biased by Serial.print(ln) stuff which we all know is very time consuming of course so now I only measure the i2c readout now: for the gyro: 996.3us with dependency and without dependency 996.5us for the magnetometer / accelerometer: 1577us with dependency and without dependency 1576.0us (1000 samples) So it really does not make any difference, there are other factors including the slowness of i2c, Serial etc. that are much, much worse than dependency injections. I of course could give your the source for your own tests! – JopieK Jan 12 '19 at 21:52
  • 1
    But the cost of the abstraction is more than just the overhead of the virtual function call itself. It's the fact that you make a blocking function call, and perform one operation at a time without overlap. In reality embedded designs often need to use event-driven design with interrupts, or DMA buffers, to hide I2C delays. (In light of that, my suggestion of using templates to avoid the dynamic polymorphism overhead is off base, because it doesn't avoid the more severe problems). If you've got sufficient extra capacity to not care about these things, then go ahead and use DI. – Ben Voigt Jan 12 '19 at 22:02
  • @BenVoigt: Thanks for your insights, but I must say that this happens in many more places in standard Arduino libraries (e.g. the ones from Adafruit) and even core libraries. Good/solid embedded programming is definitely an art form. – JopieK Jan 12 '19 at 22:47

0 Answers0