I2C and SPI bus classes and abstraction

I have just started using the Arduino, and have been repeatedly bashing my head against the apparent insanity of the I2C and SPI bus classes.

They are all provided as concrete classes, which means that you can not substitute bus drivers without modifying each and every device driver. It is so extreme, that every SPI device driver I have looked at has its own software SPI implementation built in...

In my particular case, I need two I2C busses (one hardware and one software), and need to attach instances of a device driver to each bus.

I would have thought that it would be a simple case of adding a constructor to pass down the bus driver. But the bus drivers are concrete classes - so you can't just make a new TwoWire class and pass it in.

At the moment I am experimenting by making a shared 'I2CBus' class, and making TwoWire a derived class from this. It seems to work - but I don't like having to edit the Arduino system files to make it work.

I don't really know C++, so if anybody can suggest better ways to do this, that would be great.

Have you used microcontrollers before ? You don't seem very happy :smiley_cat:
Arduino started with a ATmega8 microcontroller with 8kbyte flash for code and 512 byte of sram.

The SPI bus is not a well defined standard. Therefor it should be set according to the specific needs of the hardware.
Some hardware don't even release the MISO signal when the ChipSelect is no longer active. That makes it impossible to connect another SPI device. Many good libraries add therefor also a software SPI, just to be flexible and user friendly.

There is no official Arduino software I2C library for normal Arduino boards. Therefor the Arduino Wire library can not be used for both software and hardware I2C.

Why do you need a hardware and a software I2C bus ? Perhaps there are other ways to do that.

Since a few years, some sensors support both I2C and SPI and have the same registers for both. It makes sense to have a single interface that can use both. Perhaps that will be implemented in the future, perhaps it never will.

I have been programming microcontrollers since the mid 80s, although I have not used them in anger for the last 20 years. The Mega328 has substantially more resources than what I am used to :wink: .

Basically, I need to connect two of the same I2C sensor to the Arduino. This specific sensor does not have a selectable address, so I either need a I2C mux, or two I2C busses.

I have tested one on using the Wire library, and one using the SoftI2CMaster library - both work fine. But making the same driver work for both at the same time is turning out to be quite tricky.

Normally, I would have made TwoWire an abstract base class, and then implemented Wire as a concrete class for the hardware interface. Then SoftI2CMaster could be a concrete class for the software bus. Then just pass a class pointer to the driver.

If the compiler does its job, then it should just be an extra addition in the jump table to use a member object rather than an absolute object, and 2 bytes extra storage in the driver. But then any driver can be easily modified to use software or hardware busses.

I am sure there is a reason the Arduino devs did not abstract the bus classes - but I don't see what it is.

Not sure if I have any other option? Probably hack the drivers with big if-else chunks around the bus interface calls :frowning: .

It is perhaps better if you go along with the libraries that are available. Don't try to make it perfect.

I have a "few" questions:
Which Arduino board ?
Which Arduino IDE version ?
Which sensor ?
If that sensor is a 3.3V sensor and the Arduino is a 5V board, how do you connect them ?
Which SoftI2CMaster library ? From the Library Manager or Github or just a zip file from where ?
Is the problem that you want to use two sensors in a easy way in the code ? Is that all ?

This is a list of the I2C libraries: Arduino I2C libraries · Testato/SoftwareWire Wiki · GitHub

You could use two software I2C busses, that will make the code easier. Most software I2C libraries allow to create an object multiple times. At hardware level it is even allowed to share the SCL (only if the sensor uses standard I2C).

The Arduino is for fast prototyping and learning to write code. It is not an optimized embedded system (not at all; far from it; not even close; different universe). Arduino was developed by a student, and hijacked by teachers. It is not developed by professionals with years experience in embedded programming. You may have to lower your expectations... a lot.

Koepel:
It is perhaps better if you go along with the libraries that are available. Don't try to make it perfect.

I can see no way to make the available libraries work - which is where the problem comes in.

I have a "few" questions:
Which Arduino board ?

Nano

Which Arduino IDE version ?

1.0.5

Which sensor ?

BMP180

If that sensor is a 3.3V sensor and the Arduino is a 5V board, how do you connect them ?

3.3V on 5V. Turn off pull ups and use pull-up to 3.3V.

Which SoftI2CMaster library ? From the Library Manager or Github or just a zip file from where ?

From github - the felis-fogg one in the list you sent.

Is the problem that you want to use two sensors in a easy way in the code ? Is that all ?

Just want to use the two sensors, without having to maintain a whole tree of hacked libraries...

This is a list of the I2C libraries: Arduino I2C libraries · Testato/SoftwareWire Wiki · GitHub

You could use two software I2C busses, that will make the code easier. Most software I2C libraries allow to create an object multiple times. At hardware level it is even allowed to share the SCL (only if the sensor uses standard I2C).

That is definitely an option. Thanks.

The Arduino is for fast prototyping and learning to write code. It is not an optimized embedded system (not at all; far from it; not even close; different universe). Arduino was developed by a student, and hijacked by teachers. It is not developed by professionals with years experience in embedded programming. You may have to lower your expectations... a lot.

It is a mature project with a wealth of resources available. And it is great fun!

I am just a bit surprised at the way the bus libraries are put together - but after a lot of testing, I see why. The modern compilers are insanely good - you can really torment the objects and the compiler still inlines the code or uses static jumps. BUT use a virtual method, and it always uses a jump table. So the cost of virtualisation is quite extreme given the resource constraints. I had assumed that all indirect references would use jump tables, but this is not the case.

So virtualising the libraries would make code a lot cleaner, but the performance hit for the general case is just too much...

Okay, I think I understand it now. With the BMP180 you have to change a library one way or the other, since everything is fixed to the Arduino Wire library that uses the hardware I2C pins.

You could use an other pressure sensor. The BME280 has a SDO pin. Problem solved. Everyone happy :smiley:

You could buy a I2C multiplexer and adapt a BMP180 library. Straightforward. Will work.

Combining hardware and software I2C in a class between the BMP180 and the Arduino Wire library is very complex. Lots of work. In the end you might have to change the BMP180 and the Wire library as well. Then you will have three changed libraries. That is bad, since you will be stuck with those and new bug fixes will not get to your changed libraries.

Have you heard about the XY-problem ?
You could have asked: "How to connect two BMP180 to Arduino Nano ?" :wink:

The Arduino Nano is still a 5V board, although it runs at about 4.5V when powered via USB. The BMP180 is a 3.3V sensor. There is a conflict with the voltage levels of the I2C. The module from Adafruit has level shifters, if you use an other module I suggest to use a I2C level shifter module.
When you power the Arduino Nano with a power supply and use the onboard voltage regulator, then the Arduino Nano runs at 5.0V, and the communication with the BMP180 is beyond the specifications of datasheet.

Sorry - been head-down in the code. Everything is working now.

Switched from SoftI2CMaster to SoftwareWire - the inline assembly in the former made it impossible to include the headers in the BMP driver...

Then added an extra initialiser to the BMP driver to pass in an initialised SoftwareWire object. And then some horrible if-else wrappers to use the provided library (if provided, otherwise Wire).

Not very elegant but it works.

As for the BME 280, RMS noise is too high, BMP180 is the only easliy available one with <=2Pa RMS noise.

Level shifters are not needed, as long as you make sure the AVRs pull up resistors are turned off (remember, I2C is an open drain connection, so adding external pull-ups to 3.3V will give you a 0-3.3V signal). ARM inputs are TTL compatible, so will register high for anything over 2.7V.

I still find it rather tedious that the BUS classes can't be abstracted, but it is not the end of the world :wink: .

Thanks for all the tips and advice.

I'm sorry, but I can't follow you.

BMP180 RMS noise : 2Pa, about 17 cm.
BME280 RMS noise : 0.2 Pa, about 1.7 cm

The Arduino Nano is this one: Arduino Nano — Arduino Official Store.
It contains a ATmega328P running at about 5V with 16MHz. It is not a ARM processor.

The I2C was designed as a open collector bus. But that is not the case today. The internal ESD protection diodes of a sensor can make a path from SDA and SCL to the VCC of a sensor. Therefor pullups to 5V can be dangerous.

Pullups to 3.3V does not work either. You can read the datasheet of the ATmega328P. A normal digital input needs to be 0.6 * Vcc for a valid 'high' level, but for I2C the 'high' level needs to be 0.7 * Vcc. That is 3.5V when the ATmega328P is running at 5.0V. Since the BMP180 runs at 3.3V with pullup resistors to 3.3V, the SDA and SCL level are never high enough for a valid high at the Arduino board.

P.S.: Don't use a repeated start with the SoftwareWire. That is not fixed yet.

OK - must have been going through the devices too quickly... Was pretty sure the spec sheets I looked at - was pretty sure that the BMP180 was the only one that made spec. Good gen on the BME280 though - that will be a much better option.

ARM was a typo - meant to type AVR (but the fingers type the tech they are most familiar with).

The AVR uses standard digital pins for I2C - and it will be happy with 0.6*Vcc = 3V.

So far, everything is working perfectly (even on external power), and is within spec on the datasheet, so I am happy with the electrical connection.

Correct me if I'm wrong, but don't go on telling nonsense please.

The hardware I2C on the ATmega328P needs 0.7 * Vcc for a valid high level. It is in the datasheet. Read it.

The AVR family chips uses a number of hardware items on a single pin. The A4 and A5 are a analog input, a digital input and output and also I2C. All that hardware is inside the chip parallel operating on that pin. The digital input and output part is independent of the I2C part. You can even use all those things at the same time.

The I2C part inside the ATmega328P is according to the I2C specifications. It has a limited slew rate, a filter, a current limiting of about 3mA.

I hope this is enough to make you see that when the Arduino Nano runs at 5.0V, that the communication with a 3.3V sensor is beyond the specifications in the datasheet.

Ah - OK. Saw the electrical specs 'all pins except XTAL1 and XTAL2' - then searched for I2C. Did not see the extra electrical specs under 'Two Wire'...

If I get comms issues, I will add a 1n4001 in series with the 5V line (drop VDD to 4.4V). Should keep everybody happy :wink: .

justinschoeman:
If I get comms issues, I will add a 1n4001 in series with the 5V line (drop VDD to 4.4V). Should keep everybody happy :wink: .

I would suggest the design should plan for all parts to run within specification in the first place.

The Arduino I2C is not an interface I would want to abuse myself, it can lock up a program when there is a fault.

@justinschoeman, I didn't want to scare you, but here it is. Embrace yourself.
The Wire.endTransmission() and Wire.requestFrom() wait until the I2C bus transaction has finished. The Wire library is internally interrupt driven, but that is useless because those functions wait. That is just a minor problem.
The Wire library has loops to wait for an event. When there is something wrong with the I2C bus, it can halt the Wire library.
That means that an external event can stop the code in a microcontroller. You don't have to tell us how bad that is, we know.

Koepel:
@justinschoeman, I didn't want to scare you, but here it is. Embrace yourself.
The Wire.endTransmission() and Wire.requestFrom() wait until the I2C bus transaction has finished. The Wire library is internally interrupt driven, but that is useless because those functions wait. That is just a minor problem.
The Wire library has loops to wait for an event. When there is something wrong with the I2C bus, it can halt the Wire library.
That means that an external event can stop the code in a microcontroller. You don't have to tell us how bad that is, we know.

Thanks - as you have pointed out, Arduino is a rapid prototyping system. Occasional hangs are fine, while I am playing. If I ever decide to use it in anger, then I will add proper level shifters, or at least a diode in the VDD line to bring the input in limits.