Made a soft i2c lib. Supports multimaster + multislave, repstart. Seeking input.

The library and example project can be found there.

I am seeking input on size/robustness/performance improvement and other general input. Feel free to contact via this thread, github, or email.

There are many things yet to do with this code (clock-stretching and more testing), but it can be useful. My motivation in writing this was to emulate an eeprom which used more than one address.

The big update: much higher speeds than when originally posted. Multimaster is now supported as is the repeated start command.

On a regular 16MHz atmega, this should be happy operating with the standard 100KHz IIC speed in either master or slave mode.

On my 8MHz atmega168, I can act as a 50KHz master (haven't tested faster, will do so soon).
As a slave on a 13MHz atmega168, IIC speed of 94KHz (what my test master uses) is OK.

Not sure if it matters, but I did my testing on avr-gcc 5.2 with link time optmization enabled and -Os optimization setting.

Thanks,
cirthix

The library now can handle an IIC bus at 100KHz. Bumping the thread for some input.

License is almost longer than the code ...

Looks like a decent piece of work (quick glance),

some remarks

  • add an I2C scanner build in / example
  • keeping the interface consistent with Wire would make it easy to switch between HW & SW I2C
  • You have calls to Serial hardcoded, better provide a stream class as parameter so I could Serial or SWSerial etc as dumpbin.

can you tell something about the footprint?

For which ATmega chips is it ? For the ATmega2560 and ATmega32U4 as well ?

This is the SoftwareWire : GitHub - Testato/SoftwareWire: Creates a software I2C/TWI bus on every pins
It is a drop-in replacement for the normal Wire library. It is Master only, no timers are used, and should work for all ATmega chips of about 1MHz to 16MHz.
It is available in the Library Manager in the Arduino IDE.

You could have used it for the Master section.

How do I know at what speed the Slave will work?
Suppose an Arduino uses the normal Wire library for a Slave on one I2C bus, and the SoftIIC for a Slave on another I2C bus. Will the interrupts slow down the maximum allowed speed ?

@robtillaart:

Thanks :slight_smile:

IIC scanner is built-in
IIC sniffer is also built-in (though due to printing, it will not work at 100KHz, and is not well-tested)

I did not try to keep consistency with the original library because it behaves too differently, particularly in slave-mode.

Do you have an example what you're recommending with the prints?

For footprint, I have a rather complicated (can't post it, sorry) sketch that uses slave mode only. With hardware iic, the size is 12206, with this softiic lib, 14288 bytes. Not sure how it would impact other projects.

With the example:
All code : 8.2k
none: 3.2k
masterdumpall only: 6.2k
snoop only: 4.7k
slavehandletransaction only: 4.8k

So roughly 1.5k for slave and 3k for master, or 5k for both (bigger than sum? weird).

@koepel:
All of my testing was done on an atmega168p. Given the similarities of the chips, it would probably work on the 2560. No experience with the 32u4.

I have an asic that reads a 24c16 eeprom (only partially full of data) and also talks to another iic chip continuously. Having the atmega168 emulate the eeprom in addition to it's other functionality makes the system simpler to configure and more flexible to use.

I did lots of testing with a logic analyzer to verify functionality in my system.

Having interrupts enabled when trying to do time-critical things like reading in a serialized byte will of course not work...

In your example, the soft transaction will be interrupted, the timeout check will fail, and the handletransaction function will exit promptly after ensuring that the bus is not pulled-down. The hardware transaction which called the interrupt would succeed.

cirthix, I'm sorry, but I can't make it work. I have an Arduino Leonardo as Master with the normal Arduino Wire library and Wire.setClock(50000L), and an Arduino Uno as Slave with your code at 50kHz.
All I get is errors from the Wire library and the i2c_scanner can't see the Slave. Sometimes the I2C halts the Master and the error depends on the I2C speed that I select. I don't know what the 'snoop' function is for, and I don't know at what moment the Slave is acting as a Slave.

Could you provide code for Master and Slave ? The Master with the Arduino Wire library if possible.

Koepel:
cirthix, I'm sorry, but I can't make it work. I have an Arduino Leonardo as Master with the normal Arduino Wire library and Wire.setClock(50000L), and an Arduino Uno as Slave with your code at 50kHz.
All I get is errors from the Wire library and the i2c_scanner can't see the Slave. Sometimes the I2C halts the Master and the error depends on the I2C speed that I select. I don't know what the 'snoop' function is for, and I don't know at what moment the Slave is acting as a Slave.

Could you provide code for Master and Slave ? The Master with the Arduino Wire library if possible.

The slave is only acting as a slave when the SlaveHandleTransaction function is running.

Try this example for the slave:

#include <avr/pgmspace.h>
#include <SoftIIC.h>

// Note: these can be any pins, not just a4/a5.
#define SDA_PIN A4
#define SCL_PIN A5

#define SERIAL_PORT_SPEED 38400
#define IIC_SPEED 50

static uint8_t current_register_address_for_50 = 0x00;
static uint8_t current_register_address_for_51 = 0x00;

PROGMEM const uint8_t MY_VIRTUAL_EEPROM50[] = {0x00, 0x0F, 0x01, 0x02, 0x03, 0xab};
PROGMEM const uint8_t MY_VIRTUAL_EEPROM51[] = {0xF0, 0xFF, 0xF1, 0xAA, 0x09, 0xa4};

SoftIIC my_SoftIIC = SoftIIC(SCL_PIN, SDA_PIN, true, IIC_SPEED, true);

void setup() {
Serial.begin(SERIAL_PORT_SPEED);
noInterrupts();
}

void loop() {

// Last, act as A 24c04 eeprom (read-only) slave
uint8_t successful_bytes = 0;
uint16_t TOTAL_EXPECTED_BYTES = 512;
while (successful_bytes < TOTAL_EXPECTED_BYTES) {
successful_bytes = successful_bytes + my_SoftIIC.SlaveHandleTransaction(respond_to_address, respond_to_command, respond_to_data, get_current_register_address, set_current_register_address, read_iic_slave, write_iic_slave);
}

// delay(10000);
}

//////////////////////////////////////////////////////////// These functions should be edited to give the iic slave a 'personality'. ////////////////////////////////////////////////////////////////

uint8_t virtualeeprom(uint8_t chipaddress, uint8_t registeraddress) {
uint8_t retval = 0xFF;
if (chipaddress == 0x50 && registeraddress < (sizeof(MY_VIRTUAL_EEPROM50) / sizeof(uint8_t))) { retval = pgm_read_byte_near(MY_VIRTUAL_EEPROM50 + registeraddress); }
if (chipaddress == 0x51 && registeraddress < (sizeof(MY_VIRTUAL_EEPROM51) / sizeof(uint8_t))) { retval = pgm_read_byte_near(MY_VIRTUAL_EEPROM51 + registeraddress); }
return retval;
}

uint8_t respond_to_address(uint8_t chipaddr){
if((chipaddr>>1)==0x50) {return 0x01;}
if((chipaddr>>1)==0x51) {return 0x01;}
return 0x00;
}

uint8_t respond_to_command(uint8_t commandaddr){
return 0x01;
}

uint8_t respond_to_data(uint8_t commandaddr){
return 0x01;
}

uint8_t get_current_register_address(uint8_t chipaddr) {
if (chipaddr == 0x50) { return current_register_address_for_50; }
if (chipaddr == 0x51) { return current_register_address_for_51; }
return 0x00;
}

uint8_t set_current_register_address(uint8_t chipaddr, uint8_t regaddr) {
if (chipaddr == 0x50) { current_register_address_for_50 = regaddr; }
if (chipaddr == 0x51) { current_register_address_for_51 = regaddr; }
return 0x00;
}

uint8_t read_iic_slave(uint8_t chipaddress, uint8_t* value) {
uint8_t registeraddress = get_current_register_address(chipaddress);
*value = virtualeeprom( chipaddress, registeraddress);
return 0x00;
}

uint8_t write_iic_slave(uint8_t chipaddr, uint8_t value) {
// Don't do anything with writes for this demo.
return 0x00;
}

It is working 8)
I can get the data from 0x50 and 0x51.

The limit for highest speed is about 80kHz. That is with the default Arduino option to optimize for size: -Os
When I add "#pragma GCC optimize("O3")" to the sketch and the "SoftIIC.cpp" file, then the maximum is 100kHz.

When I set the Master (Arduino Wire) at 50kHz and the Slave (SoftIIC) at 60kHz, then it is often not recognized. But when it is recognized, I can request 32 bytes without error.
I guess, there is some kind of resync for every byte ?

After using SoftwareWire for the Master, it is no longer working. The SoftwareWire does not use a precise clock. I suppose your SoftIIC Master does have a steady clock ?

Cool that it is working :slight_smile:

I have updated the github page with a master example and a slave example.

I suspect that without lots of optimization (either -O3 or LTO), there is function call overhead that limits speed. Maybe someone with more experience with this sort of thing could take a look.

In my particular system, there are only a couple of IIC clocks between each byte within a transaction as well as between transactions, so in order to not get desynchronized, timeouts are set to only one extra clock worth of time per byte. This means that if you start the slave with iic clock of 60KHz and have a master running at 50KHz, you will likely have timeouts start to fail. Both at 50 is OK. For more timeout time, Master at 50 and slave at 30 is probably good. Maybe I should add a function to adjust the number of timeout clocks allowed?

If you are uncertain about your clocks, there is no downside to specifying a slower-than-real clock for the slave aside from this precise timeout behavior.

Another thing that you may be running into is that after a transaction (timeout or stop condition), SlaveHandleTransaction exits, and if you get another transaction beginning before calling SlaveHandleTransaction, you will miss it.

It is not reliable with the Master using the SoftwareWire library. The SoftwareWire does not have that strict timing and it probably never will. I think you don't have to adjust your SoftIIC for the SoftwareWire, the two software I2C libraries are not compatible :frowning:

Using compiler flags -O3 and -flto in "platform.local.txt" did not change a lot. It is working at 110kHz. So the -flto option increased the maximum speed only from 100kHz to 110kHz.
I used the Arduino Wire library for the Master.
Almost 120kHz with "-O3 -flto -fipa-pta -fira-loop-pressure -mcall-prologues", but that fails sometimes, so it is still 110kHz maximum.

The global optimization -flto optimizes calls to functions in other files. Perhaps the compiler did already optimize the calls to functions in the same file.

I removed the pgm_read_byte_near(), and changed the data into normal ram data.
Still using the -O3 and -flto, but it failed at 120kHz, so the maximum is still 110 kHz.

Then I filled the data with analogRead(), so I didn't have an emulated eeprom anymore, but an emulated sensor :slight_smile:
I don't understand the functions well enough to emulate other I2C devices.

Thanks for the testing of those options.

I was under the impression that there were problems with flto under the shipped version of gcc, so I did my testing with avr-gcc 5.2.

You can get it here:
avr-gcc-5.2.1_2015-08-18_mingw32.zip

Changes to platform.txt:

##############################################################

Lines below added for avr-gcc 5.1.0 with link time optimization plugin support.

See this for more info: Enable link-time optimization (after switching to avr-gcc 4.5 or greater) [imported] · Issue #660 · arduino/Arduino · GitHub

Default "compiler.path" is correct, change only if you want to overidde the initial value

#compiler.path={runtime.tools.avr-gcc.path}/bin/
compiler.rootdir=c:/avrgcc521
compiler.path={compiler.rootdir}/bin/
compiler.ar.extra_flags=--plugin={compiler.rootdir}/libexec/gcc/avr/5.2.1/liblto_plugin-0.dll
##############################################################

compiler.optimization_level=-Os

compiler.c.cmd=avr-gcc
compiler.c.flags=-c -g {compiler.optimization_level} {compiler.warning_flags} -std=gnu11 -ffunction-sections -fdata-sections -MMD

-w flag added to avoid printing a wrong warning 59396 – [avr] Wrong warning with ISR() and -flto

This is fixed in gcc 4.8.3 and will be removed as soon as we update the toolchain

compiler.c.elf.flags={compiler.warning_flags} {compiler.optimization_level} -Wl,--gc-sections
compiler.c.elf.cmd=avr-gcc
compiler.S.flags=-c -g -x assembler-with-cpp
compiler.cpp.cmd=avr-g++
compiler.cpp.flags=-c -g {compiler.optimization_level} {compiler.warning_flags} -std=gnu++11 -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -MMD
compiler.ar.cmd=avr-ar
compiler.ar.flags=rcs
compiler.objcopy.cmd=avr-objcopy
compiler.objcopy.eep.flags=-O ihex -j .eeprom --set-section-flags=.eeprom=alloc,load --no-change-warnings --change-section-lma .eeprom=0
compiler.elf2hex.flags=-O ihex -R .eeprom
compiler.elf2hex.cmd=avr-objcopy
compiler.ldflags=
compiler.size.cmd=avr-size

This can be overriden in boards.txt

build.extra_flags=

##############################################################

Lines below added for avr-gcc 5.1.0 with link time optimization plugin support.

See this for more info: Enable link-time optimization (after switching to avr-gcc 4.5 or greater) [imported] · Issue #660 · arduino/Arduino · GitHub

compiler.c.extra_flags=-Wextra -flto
compiler.c.elf.extra_flags=-w -flto -fuse-linker-plugin
compiler.S.extra_flags=-flto
compiler.cpp.extra_flags=-Wextra -flto
compiler.objcopy.eep.extra_flags=
compiler.elf2hex.extra_flags=
##############################################################

These can be overridden in platform.local.txt

#compiler.c.extra_flags=
#compiler.c.elf.extra_flags=
#compiler.S.extra_flags=
#compiler.cpp.extra_flags=
#compiler.ar.extra_flags=
#compiler.objcopy.eep.extra_flags=
#compiler.elf2hex.extra_flags=

The -flto is working for the Uno, but not for the Mega 2560 if the code size is too big. It does not produce wrong code, but the linker can no longer link everything because the addresses have been shortened. I did not try the linker-plugin.
I use the -flto as a last option to squeeze code into ATmega8 chips.

Hello cirthix,

I'm very new to Arduino and need your help please.

I've to send I2C data without the ACK bits. Because the chip NJU72343 does not send ACK.
It looks your lib is able to do so. But I'm unable to adapt your code.

Please can you give me an example of doing so?
The slave adress is 82h, register from 00h to 08h and data from 00h to FFh.

Kind Regards
dremeier