Skip to content

felias-fogg/SoftI2CMaster

Repository files navigation

SoftI2CMaster

License: GPL v3 Commits since latest Build Status Hit Counter

Why another I2C library?

The standard I2C library for the Arduino is the Wire Library. While this library is sufficient most of the time when you want to communicate with devices, there are situations when it is not applicable:

  • the I2C pins SDA/SCL are in use already for other purposes,
  • the code shall run on an ATtiny processor with 1 MHz on arbitrary pins,
  • you are short on memory (flash and RAM), or
  • you do not want to use the implicitly enabled pull-up resistors because your devices are run with 3.3 volts.

I adapted Peter Fleury's I2C software library that is written in AVR assembler, extremely light weight (just under 500 byte in flash) and very fast. Even on an ATtiny running with 1MHz, one can still operate the bus with 33 kHz, which implies that you can drive slave devices that use the SMBus protocol (which timeout if the the bus frequency is below 10 kHz).

If you want a solution running on an ARM MCU (Due, Zero, Teensy 3.x), you want to use pins on port H or above on an ATmega256, or you want to use many different I2C buses, this library is not the right solution for you. In these cases, another bit-banging I2C library written in pure C++ could perhaps help you: SlowSoftI2CMaster.

Features

This library has the following features:

  • supports only master mode
  • compatible with all 8-bit AVR MCUs
  • no bus arbitration (i.e., only one master allowed on bus)
  • clock stretching (by slaves) supported
  • timeout on clock stretching
  • timeout on ACK polling for busy devices (new!)
  • internal MCU pullup resistors can be used (new!)
  • can make use of almost any pin (except for pins on port H and above on large ATmegas)
  • very lightweight (roughly 500 bytes of flash and 0 byte of RAM, except for call stack)
  • it is not interrupt-driven
  • very fast (standard and fast mode on ATmega328, 33 kHz on ATtiny with 1 MHz CPU clock)
  • can be easily used in multi-file projects (new)
  • Optional Wire library compatible interface
  • GPL license

Installation

The simplest way to install this library is to use the library manager. Alternatively, one can download the zip file from GitHub, uncompress, rename the directory to SoftI2CMaster and move it into the libraries folder.

Importing the library

In order to use the library, you have to import it using the include statement:

#include <SoftI2CMaster.h>

In the program text before the include statement, some compile-time parameters have to be specified, such as which pins are used for the data (SDA) and clock (SCL) lines. These pins have to be specified in a way so that port manipulation commands can be used. Instead of specifying the number of the digital pin (0-19), the port (PORTB, PORTC, PORTD) and the port pin has to be specified. The mapping is explained here. For example, if you want to use digital pin 2 for SCL and digital pin 14 (= analog pin 0) for SDA, you have to specify port pin 2 of PORTD for SCL and port pin 0 of PORTC for SDA:

#define SCL_PIN 2
#define SCL_PORT PORTD
#define SDA_PIN 0
#define SDA_PORT PORTC
#include <SoftI2CMaster.h>

Using the library in multi-file projects

If you want to use the library in a larger project consisting of multiple cpp files, then you may wonder how the library should be imported. Since the header file contains all the source code, importing the library in more than one source file will lead to linkage errors. In order to support the usage of the library in such a context, ArminJo came up with the solution of using another compile time constant that controls of whether only the function declarations are imported. If you put the following compile time constant definition before the #include directive

#define USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE

then only the function declarations are included. In other words, in such a large project, you once include the header file without this definition and all the other times, you have to put the above definition before the #include directive.

Configuration

There are a few other constants that you can define in order to control the behavior of the library. You have to specify them before the include statement so that they can take effect. Note that this is different from the usual form of using libraries! This library is always compiled with your sketch and therefore the defines need to be specified before the inclusion of the library!

#define I2C_HARDWARE 1

Although this is basically a bit-banging library, there is the possibility to use the hardware support for I2C, if you happen to run this library on an MCU such as the ATmega328 that implements this. If this constant is set to 1, then the hardware registers are used (and you have to use the standard SDA and SCL pins).

#define I2C_PULLUP 1

With this definition you enable the internal pullup resistors of the MCU. Note that the internal pullups have around 50kΩ, which may be too high. This slows down the bus speed somewhat. Furthermore, when switching from HIGH to LOW (or the other way around), the bus lines will temporarily in a high impedance state. With low I2C frequencies, things will probably work. However, be careful when using this option and better check with a scope that things work out.

#define I2C_TIMEOUT ...

Since slave devices can stretch the low period of the clock indefinitely, they can lock up the MCU. In order to avoid this, one can define I2C_TIMEOUT. Specify the number of milliseconds after which the I2C functions will time out. Possible values are 0 (no time out) to 10000 (i.e., 10 seconds). Enabling this option slows done the bus speed somewhat.

#define I2C_MAXWAIT ...

When waiting for a busy device, one may use the function i2c_start_wait(addr) (see below), which sends start commands until the device responds with an ACK. If the value of this constant is different from 0, then it specifies the maximal number of start commands to be sent. Default value is 1000.

#define I2C_NOINTERRUPT 1

With this definition you disable interrupts between issuing a start condition and terminating the transfer with a stop condition. Usually, this is not necessary. However, if you have an SMbus device that can timeout, one may want to use this feature. Note however, that in this case interrupts become unconditionally disabled when calling i2c_start(...) und unconditionally enabled after calling i2c_stop().

#define I2C_CPUFREQ ...

If you are changing the CPU frequency dynamically using the clock prescaler register CLKPR and intend to call the I2C functions with a frequency different from F_CPU, then define this constant with the correct frequency. For instance, if you used a prescale factor of 8, then the following definition would be adequate: #define I2C_CPUFREQ (F_CPU/8)

#define I2C_FASTMODE 1

The standard I2C bus frequency is 100kHz. Often, however, devices permit for faster transfers up to 400kHz. If you want to allow for the higher frequency, then the above definition should be used.

#define I2C_SLOWMODE 1

In case you want to slow down the clock frequency to less than 25kHz, you can use this definition (in this case, do not define I2C_FASTMODE!). This can help to make the communication more reliable.

I have measured the maximal bus frequency under different processor speeds. The results are displayed in the following table. The left value is with I2C_TIMEOUT and I2C_PULLUP disabled. The right value is the bus frequency with both options enabled. Note that there is a high clock jitter (roughly 10-15%) because the clock is implemented by delay loops. This is not a problem for the I2C bus, though. However, the throughput might be lower than one would expect from the numbers in the table.

1 MHz 2 MHz 4 MHz 8 MHz 16 MHz
Slow mode (kHz) 23 21 24 22 24 23 24 23 24 23
Standard mode (kHz) 45 33 90 72 95 83 95 89 90 88
Fast mode (kHz) 45 33 90 72 180 140 370 290 370 330

Interface

The following functions are provided by the library:

i2c_init()

Initialize the I2C system. Must be called once in setup. Will return false if SDA or SCL is on a low level, which means that the bus is locked. Otherwise returns true.

i2c_start(addr)

Initiates a transfer to the slave device with the 8-bit I2C address addr. Note that this library uses the 8-bit addressing scheme different from the 7-bit scheme in the Wire library. In addition the least significant bit of addr must be specified as I2C_WRITE (=0) or I2C_READ (=1). Returns true if the addressed device replies with an ACK. Otherwise false is returned.

i2c_start_wait(addr)

Similar to the i2c_start function. However, it tries repeatedly to start the transfer until the device sends an acknowledge. It will timeout after I2C_MAXWAIT failed attempts to contact the device (if this value is different from 0). By default, this value is 1000.

i2c_rep_start(addr)

Sends a repeated start condition, i.e., it starts a new transfer without sending first a stop condition. Same return value as i2c_start().

i2c_stop()

Sends a stop condition and thereby releases the bus. No return value.

i2c_write(byte)

Sends a byte to the previously addressed device. Returns true if the device replies with an ACK, otherwise false.

i2c_read(last)

Requests to receive a byte from the slave device. If last is true, then a NAK is sent after receiving the byte finishing the read transfer sequence. The function returns the received byte.

Example

As a small example, let us consider reading one register from an I2C device, with an address space < 256 (i.e. one byte for addressing)

// Simple sketch to read out one register of an I2C device
#define SDA_PORT PORTC
#define SDA_PIN 4 // = A4
#define SCL_PORT PORTC
#define SCL_PIN 5 // = A5
#include <SoftI2CMaster.h>

#define I2C_7BITADDR 0x68 // DS1307
#define MEMLOC 0x0A 

void setup(void) {
    Serial.begin(57600);
    if (!i2c_init()) // Initialize everything and check for bus lockup
        Serial.println("I2C init failed");
}

void loop(void){
    if (!i2c_start((I2C_7BITADDR<<1)|I2C_WRITE)) { // start transfer
        Serial.println("I2C device busy");
        return;
    }
    i2c_write(MEMLOC); // send memory address
    i2c_rep_start((I2C_7BITADDR<<1)|I2C_READ); // restart for reading
    byte val = i2c_read(true); // read one byte and send NAK to terminate
    i2c_stop(); // send stop condition
    Serial.println(val);
    delay(1000);
}

I2CShell

In the example directory, you find a much more elaborate example: I2CShell. This sketch can be used to interact with I2C devices similar in the way you can use the Bus Pirate. For example, you can type:

[ 0xAE 0 0 [ 0xAF r:5 ]

This will address the I2C device under the (8-bit) address 0xAE in write mode, set the reading register to 0, then opens the same device again in read mode and read 5 registers. A complete documentation of this program can be found in the I2CShell example folder.

Alternative Interface

Meanwhile, I have written a wrapper around SoftI2CMaster that emulates the Wire library (master mode only). It is another C++-header file called SoftWire.h, which you need to include instead of SoftI2CMaster.h. The ports and pins have to be specified as described above:

#define SDA_PORT ...
...
#include <SoftWire.h>
...
setup() {
    Wire.begin()
...
}

This interface sacrifices some of the advantages of the original library, in particular its small footprint, but comes handy if you need a replacement of the original Wire library. The following section sketches the memory footprint of different I2C libraries.

There are a few constants that you can define in order to control the behavior of the library. You have to specify them before the include statement so that they can take effect. Note that this is different from the usual form of libraries! This library is always compiled with your sketch and therefore the defines need to be specified before the inclusion of the library!

#define I2C_RX_BUFFER_LENGTH 48

The default buffer length is 32 byte like in the standard Arduino Wire or TWI library. But if some I2C device sends more then 32 byte, you can use this definition to increase the receiver buffer size.

Finally, you can use this wrapper library in multi-file projects. By putting the following directive

#define USE_SOFTWIRE_H_AS_PLAIN_INCLUDE

before including the library, only the declaration part is included.

Memory requirements

In order to measure the memory requirements of the different libraries, I wrote a baseline sketch, which contains all necessary I2C calls for reading and writing an EEPROM device, and compiled it against a library with empty functions. This sketch was compared to sketches that imported all the other libraries. For the Wire-like libraries, I had to rewrite the sketch, but it has the same functionality. The memory requirements differ somewhat from ATmega to ATtiny, but the overall picture is similar. The take-home message is: If you are short on memory (flash or RAM), it makes sense to use the SoftI2CMaster library.

ATmega328
LibrarySoftI2C-SoftI2C-SoftI2C-Soft-SlowSoft-SlowSoft-USI-Tiny-Wire
MasterMasterMasterWireI2CMasterWireWireWire
OptionPullup+TimeoutHardware
Flash48256443410669741556--1972
RAM00066470--210
ATtiny85
LibrarySoftI2C-SoftI2C-SoftI2C-Soft-SlowSoft-SlowSoft-USI-Tiny-Wire
MasterMasterMasterWireI2CMasterWireWireWire
OptionPullup+TimeoutHardware
Flash428510-1002732129211081834-
RAM00-664704586-

Shortcomings

One shortcoming is that one cannot use port H and above on an ATmega256. The reason is that these ports are not addressable as I/O registers.

Another shortcoming is, as mentioned, that the code runs only on AVR MCUs (because it uses assembler). If you want to use a software I2C library on the ARM platform, you could use https://github.com/felias-fogg/SlowSoftI2CMaster, which uses only C++ code. Because of this, it is much slower, but on a Genuino/Arduino Zero, the I2C bus runs with roughly 100kHz. There is also a Wire-like wrapper available for this library: https://github.com/felias-fogg/SlowSoftWire.