The JeeNode Ports library has supported software-based I2C right from the start. There are some limitations to this approach, but it’s been very useful to allow connecting lots of I2C-based plugs on any of the 4 JeeNode “ports”.
I’ve always suspected that the timing of this software “bit-banging” was off. Well… it’s waaaaay too slow in fact:
That’s a JeeNode talking to an RTC Plug over I2C, set to 400 KHz. Except that it’s talking at a mere 40 KHz!
The code works, of course. It’s just far from optimal and wasting time. The main reason for this is that the Arduino’s digitalWrite(), digitalRead(), and pinMode() calls take a huge amount of time – due to their generality. A second problem is that the delayMicroSeconds() call actually has a granularity of 4 µs.
As a quick test, I hard-coded the calls in the Ports.h header to use much more efficient code:
The result is that I2C can now run at over 300 KHz, and it still works as expected:
It can even run at over 600 KHz, which is beyond the official I2C specs, but it works fine with many I2C chips:
Except that this now requires a pull-up resistor on SDA (i.e. the AIO pin). The rise-time of the data pulses is now too low to work reliably off the internal ATmega pull-up resistors. I used a 1 kΩ resistor, as pull-up to 3.3V:
Note the glitch at the end – it’s probably from emulating open-collector logic with the ATmega’s three-state pins.
Pull-ups are also a very good idea with shorter bus lines, because they also lower the impedance of the wire, reducing noise. These tests were all done with the RTC Plug stuck directly into the Port 1 header, BTW.
Here’s the SDA signal on a 5 µs/div scale – via 4 Extension Cables back to back, i.e. 65 cm – with 1 kΩ pull-up:
And without – showing abysmal rise times and lots of crosstalk, making I2C on this hookup totally useless:
I’ll need to figure out how to properly implement these software optimizations one day, since that means we can’t just use the generic I/O pin calls anymore. There are several cases: different speeds as well as different ports (including “port 0” which uses A5 and A6, the official I2C pins – but still in bit-banged mode).
All in all, this software-based I2C works fine, with the advantage of supporting it on any two I/O pins – but it could be optimized further. The other thing to keep in mind is that the SCL clock line is not open-collector, but driven by a normal totem pole output pin, so this doesn’t support I2C clock stretching for slow (or busy) slaves.