(full description and code for the libraries available on github - actually some parts of this section will be the same as the ones in the repo readme!)
While working with some NES Mini Controllers I started tinkering with the I2C protocol in order to connect them into some attiny85's micros I usually use on my projects (I managed to do something interesting here).
First I used a pure software implementation approach and made a bit-banging small driver to act as a primary device and being able to control multiple I2C devices I had (those controllers, some ssd1306 oled screens and so on); but then I started reading about the "native" AVR approach: the Two-Wire mode.
After more reading, poking and trying lots of external references (there're lots of I2C implementations out there: some of them worked, others didn't, another ones are more Arduino-intended, maybe a bunch of them are made on C++...) I wrote this pair of C basic libraries that allows me to manage the I2C protocol on my attiny85's both from primary and secondary sides.
Here's a simple demonstration that uses the example code and the three attiny85's mini devices I made for it:
Introducing a couple of C-written libraries (primary and secondary) to allow I2C communication as a primary or secondary device!
Primary devices will perform the usual calls (as seen on multiple micros, Arduino-like devices, etc.): start transaction, send address, read/write some bytes and stop transaction.
Secondary devices may "listen" to write operations from the primary one (primary writes) or prepare some buffer to be "consumed" (primary reads).
(also a secondary one can perform both read and write operations, obviously!)
Please refer to the github repo to know more about the libs.
The "hardware part" of all of this is a super standard proof of concept made of three different attiny85's built as independent devices and then hooked up to a common bus (there's a pdf with the schematic in the files section - or you can download it from here):
- The primary device has a control led (+ it's own resistor) on PB4. It's main mission is to read from the button device and then write some commands to the leds device. It's also connected to the bus (SDA and SCL) via the proper pins (PB0 and PB2 in the attiny85). This bus also have two 10k resistors for pulling up the lines.
- The first secondary device has a control led and a button. This one is designed to handle a buffer that will be read by the primary one when performing a read operation. It's a super simple example, but enough to show the functionality: the buffer reads 0x00 if there's no button pressed; otherwise some specific values are set there - the main idea is to perform a write operation on multiple bytes to make sure the (n)ack signals are working fine.
- The second secondary one has three leds and will turn them on or off based on some commands that are being read when the primary write there some data.
- Bonus! There's also a ssd1306 oled screen attached to the bus that displays a static image at the beginning (just to show the primary library handling not only custom-made devices but also other I2C things out there).
Needless to say the example code follows the default pin configuration I used, but it can be easily changed and adapted to different components, address values, commands...
TODOs, work in progress...
- Test on different micros (working on attiny85's only, at least for now)
- There's no arbitration implemented for primary devices
- Testing, testing, testing (and bugfixing)
- More learning: why is this exactly happening at that moment, etc.
Here's a bunch of links I found useful when writing the libraries. Some of them pointed me in the right direction while from other examples I grab some core ideas (such as the state machine on the secondary devices):
- attiny85 datasheet, check the two-wire mode section with the registers list!
- Unipolar Stepper Driver, I...
It is a pretty ugly implementation of i2c. The problem is that due to specification "I2C-bus specification and user manual UM10204" paragraph 3.1.1: "
The output stages of devices connected to the bus must have an open-drain or
open-collector to perform the wired-AND function."
So doing this:
// release SCL
PORTB |= (1<<PIN_SCL);
is not recommended. However the same paragraph also says:
"For a single controller application, the controller’s SCL output can be a push-pull driver
design if there are no devices on the bus which would stretch the clock." so it is major limitation.Proper implemetation on devices that do not have hardware i2c interface is by switching direction of the port. A port output is set permanently to LOW and external pull-up is applied. Switching PORT to input releases the correspoding line, setting it to output sets zero - no risk shorting outputs of opposite levels.