Skip to content

I2C Communication

jbaumann edited this page Oct 19, 2020 · 8 revisions

I2C communication with small micro controllers is hard, and it is very astonishing that the ATTiny can do it, especially when being woken from deep sleep. Furthermore, communicating with the Raspberry Pi‘s I2C interface seems to add to the complexity as well, so I started out hoping for nothing.

But it works really well, even though there are read errors from time to time. But by introducing delays between command and actual read, and by simply retrying a few times if an error occurs, the communication is astonishingly stable.

Bit errors during transmission are seldom, but from time to time the result of reading from the I2C bus on the Raspberry side is only 0xFF and 0XFFFF, respectively. The explanation for these is simple: The ATTiny hasn‘t been fast enough to wake up, process the register that the Raspberry wants to read and to send back the actual data. On the ATTiny side the same can happen when the Raspberry tries to write a new value.

The simplest way to do this is to add something to the end of the transmission data that allows us to verify that the data has been transferred correctly. This way we can catch many of the communication erros and do not have to compute any checksums. In fact this is the approach used in earlier versions of the ATTiny daemon. After the data itself was transferred, another byte containing the register accessed was appended.

The problem with this approach is that bit errors or single byte errors cannot be detected. So, if e.g., the first byte is transferred as 0xFF and the remaining bytes including the register number were transferred correctly then the transmission was seen as correct.

This led to adding a checksum instead. Each transmission now includes a checksum that guarantees that nearly all errors can be identified, in which case the communication is repeated. This leads to a vastly improved communication which on average is faster as well, because we can use much lower delays.

Using a checksum has disadvantages though. Computing the checksum on the ATTiny costs valuable time, power and space in the flash rom. While time is no problem in our case, power consumption and flash space are to be considered. The additional power consumption is offset by the, on average, shorter delays. This means that the ATTiny can go to sleep sooner and this way consumes only a fraction of the power to stay awake. The flash space is a real challenge though.

Introducing the checksum exceeded the available flash space of the ATTiny45. While everything is fine with the ATTiny85, the ATTiny45 simply does not have the flash space available to hold all the functionality. So, to conserve flash memory on the ATTiny45, the oversampling of temperature and voltage has been removed. So, instead of 5 readings that are averaged on the ATTiny85, on the ATTiny45 we only take one reading and accept the lower accuracy.

Since the recommendation is to use an ATTiny85, this should not be a problem (and they cost nearly same anyways). But I still have a few ATTiny45 lying around, which motivated me to create these two versions.

The Protocol

Low-level (Voltage Levels)

The Raspberry is the I2C Master and the ATTiny is the I2C slave. I2C has an open-drain architecture that uses pull-up resistors to Vcc of the bus, which means that every participant signals a 0 by driving the line to GND, and a 1 by leaving the line open (letting the resistors pulling it to Vcc). This means that in principle our ATTiny could act as a master and request data from the Geekworm UPS HAT, but since we can measure the voltage of the battery on our own we do not need to do that.

Detail: The size of the resistor and the length of the wire connecting master and slave determines the maximum speed on the bus. In our case the resistor is pretty low and the connection is very short (at least on the PCB), so we won‘t have any problems regarding the transmission speed.

A potential problem are the different voltages of master and slave. Even though no device will be damaged by being connected to an I2C bus (provided that the pull-up resistors are connected to the lowest Vcc), communication might not be stable or possible at all if the different Vcc‘s are too different. The I2C bus defines that 0.7Vcc is interpreted as a logical 1. If we have the pull-up resistors connected to 3.3V and a 5V device connected to the bus, then a logical 1 from the 3.3V-device will be signalled (obviously) by 3.3V. This is only 66% of 5V and thus is not guaranteed to be interpreted as a logical 1. IF we look at the datasheet for the ATTiny, on p.161, Ch. 21.2 we find that the ATTiny interprets 0.6Vcc as a logical 1, so even if we used 5V on our ATTiny the communication should work. Additionally we see that the ATTiny accepts 0.5V overvoltage on its input pins (this will be the breakdown voltage of its ESD diodes), which means that even when the battery goes down to 3V (and the Raspberry still is on 3.3V) we have only a voltage difference of 0.3V and are on the safe side.

TL;DR: Everything is fine and dandy with our setup.

I2C Protocol

The USI (Universal Serial Interface) implementation of the ATTiny is absolutely fantastic. It offloads most of the protocol implementation from the processor into the hardware (including clock stretching), and it even provides an interrupt capability for the start condition on the I2C bus that can wake the processor from deep sleep while delaying the I2C communication until it is awake. This makes it possible to do I2C communication directly from deep sleep and is the main reason that we can reach our low power consumption levels.

But seldomly we still get read errors. One current hypothesis: The Raspberry implementation of I2C does not support clock stretching, and if the USI does that, then the Raspberry, ignoring it, would simply read 0xFF values.

Our Communication Protocol

The protocol we use is so simple that I hesitate to call it a protocol at all. Every time data is sent a final byte is appended to that data containing the checksum for the data. This holds true for both directions. While very simple and trivial, it still catches nearly all (in my observation, all) of the transmission errors on the bus.

When the Raspberry wants to read a register and the final data byte does not hold the correct checksum, then it retries to read after a timeout.

When the Raspberry wants to write a register, the ATTiny, upon receiving the data, verifies that the last byte of the data contains the correct checksum. If this is not the case, the attempt to write is ignored. The Raspberry, after writing, tries to read the same register to verify that the write was successful. If the value is not the same as the one written (assuming the checksum of the read data is correct), it retries the write after a timeout.

The number of retries and the length of the timeout are the same for read and write operations and can be changed in the daemon source.

Clone this wiki locally