The featured picture of this blog post is by user18526052 on Freepik.

The Wire library is the one that connects your Arduino to sensors and actuators that communicate using the I2C protocol. Unfortunately, this library has a lot of shortcomings, and often you want to replace it with a different I2C library. Replacing the Wire library on a per-sketch basis turns out to be more complicated than one would expect. In this blog post, I describe an easy way to accomplish that.

What is wrong with the standard Wire library?

First of all, there is not just one Wire library, but there exist many of them. For each different architecture, you have one Wire library, simply because the I2C hardware differs for different architectures. However, they all follow, more or less, the specifications given in the Arduino reference. Here, I will focus on the AVR version of the library.

The particular implementation decisions that were made for the AVR version of the Wire library are not readily obvious—to state it politely. First, I/O is buffered using five (!) buffers of 32 bytes each, which takes away 160 bytes of precious SRAM. This is all a total waste of memory because you can do I2C communication bufferless without any obvious disadvantages. Second, since the buffer is restricted to 32 bytes, you can send and receive only 32-byte data packets, which reduces the throughput quite a lot. Third, you have to use the GPIO pins that are connected to the I2C peripheral for the I2C communication. Fourth, for this reason, you cannot use other pins for I2C buses or set up more than one I2C bus. There were several other shortcomings, such as not timing out and not supporting repeated starts. These problems have been solved, though.

Alternative I2C libraries

For the reasons mentioned above, you find several alternative I2C libraries, which try to mitigate some of the problems, often implementing only the master functionality. However, this is enough to talk to I2C peripherals.

One of these alternatives is the library SoftI2CMaster, which implements Peter Fleury’s I2C software library for AVR MCUs in the Arduino framework using inline assembly language. Compared with the standard Wire library, it does not use any buffer, and the flash footprint is smaller by a factor of four (500 instead of 2000 bytes). And on classic AVR chips running at 1 MHz, it still provides roughly 30 kHz bus speed.

However, instead of the API provided by the Wire library, SoftI2CMaster provides a procedural C-language interface. Fortunately, there exists a C++ wrapper, which provides all the functionality of the Wire library in master mode (using just one buffer of 32 bytes). The main disadvantages are that you need to fix the GPIOs used for the I2C bus at compile time and that you cannot have multiple instances of this class.

Another alternative to the Wire library is SoftwareWire. This one allows choosing the GPIO pins for the I2C bus at runtime, and it is still reasonably small and fast. It works only for AVR MCUs, though.

SoftWire and SlowSoftWire are both libraries that use only the Arduino built-ins digitalRead, digitalWrite, and pinMode to control the GPIOs, and for this reason are completely portable. However, they are also a bit slow.

These alternative libraries usually use only one buffer of 32 bytes for reading, which in most circumstances is sufficent. However, one can imagine situations, where one buffer is not enough. If one starts a write transaction using beginTransmission followed by a read transaction using requestFrom before the write transaction has been finished using endTransmission, then things will not work when there is only a read buffer.

How can we replace the Wire library?

While the I2C libraries mentioned above provide (almost) the same interface as the Wire library (restricted to the master mode), it is difficult to use them as a drop-in replacement for the Wire library on a per-sketch basis. The reasons for that have to do with the implementation of the Wire library, the usage of the library by third-party libraries, and the way the Arduino IDE builds a sketch and resolves library dependencies.

When one writes a sketch that needs to talk to an I2C peripheral, and there exists already a library for that, say the sensor library, then one uses the library by including the following directive:

#include <sensor.h>

The sensor library will in turn use the Wire library, using another include directive, as follows:

#include <Wire.h>

Often, the sketch file also uses the Wire library and contains such an include directive as well. So, in general, we have the following structure, where arrows symbolize the usage relationships.

If you want now to replace the usage of Wire in the sensor library for one particular sketch, you could use the compile command of arduino-cli. Assuming you have prepared an alternative Wire library in the folder /AlternativeLibs/Wire/, the following command will use this alternative library instead of the standard one:

arduino-cli compile -b arduino:avr:uno --library "/AlternativeLibs/Wire"

When using the Arduino IDE, things are less straightforward. You have a number of options that range from cumbersome over impossible to ugly, where I prefer the last.

Cumbersome: modify the sensor library

Probably the cleanest solution is to adapt the sensor library and copy it to the src sub-folder in the sketch folder. You can then use the sensor library in your sketch by employing the following include directive:

#include "src/sensor.h"

Assuming that we are using the SoftwareWire library, you need to make the following changes in the sensor library:

  1. Replace #include <Wire.h> by #include <SoftwareWire.h>
  2. Replace any mention of TwoWire by SoftwareWire
  3. If the global Wire instance is used, then you need to introduce it in the header file of the sensor library: extern SoftwareWire Wire;

In the sketch file, you need to create a SoftwareWire instance named Wire, e.g.:

SoftwareWire Wire(A4, A5);

If the class declared in the sensor library has a parameter where a SoftwareWire instance is expected, you can pass this object as a parameter. If the sensor library uses simply the global Wire instance, then everything is handled by the extern declaration in the header file mentioned above.

With that, everything is set up and the sensor library can be used with the SoftwareWire library. However, this solution implies that you are cut off from any further development of the sensor library. This can be bad since you will not profit from bug fixes. It is still possible to manually track the development of the library by repeating the above process for new versions of the library, though.

Impossible: using inheritance and overriding

A more elegant way seems to use the inheritance mechanism of C++. Since libraries are basically C++ classes, it should be possible to use the object-oriented way to specialize the TwoWire class defined in the Wire library to a sub-class that implements the I2C protocol in a different way. As a matter of fact, in one version of the SoftwareWire library, the SoftwareWire class was defined to be derived from the TwoWire class. It did not work out, though.

A necessary prerequisite for that to work would be that the sensor library does not use the global Wire object anymore, but accepts an additional parameter of type TwoWire, which actually some libraries already do. However, even then there are insurmountable obstacles.

One (that means Arduino.cc) would need to declare all TwoWire methods to be virtual, meaning that methods in derived classes can override them. However, that would mean that the code from the Wire library could not be optimized away by link-time optimization. Even worse, one would still need to instantiate the global Wire instance in order to avoid breaking any sketches relying on the Wire library. All in all, a total waste of flash memory and SRAM. So, this is not the way to go.

Ugly hack: relying on Arduino’s dependency resolution process

When studying the way the Arduino IDE builds a sketch, the natural question is, at which point you can override the way libraries are searched? There is a long description of how dependencies are resolved. However, even after reading it twice, I only figured that the --library option described earlier would work. Then, in a long discussion about an issue of the SoftwareWire library, I spotted an interesting piece of information.

Apparently, the dependency resolution process is executed step-wise. When one step provides a solution to a dependency to be resolved later on, then in the later step, this dependency will not be considered anymore. So, if during the recursive scan of import dependencies, the SoftwareWire (or any other Wire-compatible) library is included, and the library folder contains also a file named Wire.h, then any dependence on the Wire library will be considered as already resolved.

This means that including the following header file as Wire.h in the library folder will solve our problem.

#ifndef Wire_h
#define Wire_h

#include <SoftwareWire.h>

extern SoftwareWire Wire;

typedef SoftwareWire TwoWire;

#endif 

Indeed, this works beautifully! And the disadvantage mentioned by the author of the above-mentioned post, namely, that the original Wire library gets compiled and linked in, does not happen!

Further, there should not be any negative side effects when we have different configurations of including header files. If the Wire library is not used at all, there is no problem. Wire.h will not be included. If only the Wire library is used, but not the SoftwareWire library, then the latter will be considered as a candidate for the Wire library (because of the Wire.h header file). However, because of the folder name priority, the standard library will always win. Finally, if the Wire library gets included before the SoftwareWire library, then you get multiple errors, among them, Multiple libraries were found for "Wire.h". In other words, you should always include SoftwareWire before you include any sensor libraries that use the Wire library.

This means that after having put the header file Wire.h into the library folder of SoftwareWire, it is enough to include the SoftwareWire library in the sketch file. Any imported sensor libraries that include Wire.h will then use the SoftwareWire library instead. Magic!

There are, as always, caveats. First, SoftwareWire (and all the other replacement libraries) are not 100 % Wire-compatible and there can be situations, where this shows. Second, all the replacement libraries have usually a much lower communication speed. Third, if you try to compensate for that by using the original Wire library for one I2C device and, e.g., Softwarewire for all the others, you will notice that you cannot use both the Wire library (with high speed) and SoftwareWire library (with moderate speed) in parallel. If you need high-speed communication, it is much better to use SPI instead of I2C in such a case.

Summary & Outlook

If you want to replace the Wire library on a per-project/sketch base, you have different options. The one that requires the least amount of change is an ugly hack, which relies on Arduino’s dependency resolution mechanism. However, it works beautifully. I plan to come up with my own version of a replacement library, called FlexWire, that will incorporate this hack as well as a way to change the GPIO pins dynamically in order to switch between different I2C buses easily. This will make it possible to implement a software I2C multiplexer, about I will report in the next blog post.

Views: 516