Software I2C master implementation
Some time ago I was spending evening with a friend and it came up that I had not ever written software I2C master routines, even though I have used I2C quite much in various projects.
So, to correct this omission, we armed ourselves with the I2C specification, sit down for few hours, and composed together a simple C implementation for I2C master using GPIO pins.
The code was for Atmel SAMD21 and as such could not be used with AVR-Ada. This led me to rewrite the code in Ada.
I abstracted the core logic into a generic package, which can be used on any platform as long as few procedures are provided by the user:
generic
with procedure Pull_SDA_Down;
with procedure Release_SDA_Up;
with procedure Pull_SCL_Down;
with procedure Release_SCL_Up;
with procedure Delay_T_HD_STA; -- 4.0
with procedure Delay_T_SU_STO; -- 4.0
with procedure Delay_T_Buf; -- 4.7..5
with procedure Delay_T_Low_Half; -- 2.4
with procedure Delay_T_High; -- 5
with function SDA_State return Boolean;
package Soft_I2C is
type Byte_Array is
array (Interfaces.Unsigned_8 range <>) of Interfaces.Unsigned_8;
type Error_Status is
(SOFT_I2C_OK,
SOFT_I2C_NACK,
SOFT_I2C_FAILED);
procedure Start;
procedure Stop;
procedure Write_Byte (Byte : Interfaces.Unsigned_8;
Status : out Error_Status);
procedure Read_Byte (Byte : out Interfaces.Unsigned_8; Ack : Boolean);
procedure Write (Address : Interfaces.Unsigned_8; Bytes : Byte_Array;
Status : out Error_Status);
procedure Read (Address : Interfaces.Unsigned_8; Bytes : in out Byte_Array);
end Soft_I2C;
The user need to provide GPIO pin manipulation procedures/functions:
with procedure Pull_SDA_Down; with procedure Release_SDA_Up; with procedure Pull_SCL_Down; with procedure Release_SCL_Up; ... with function SDA_State return Boolean;
And procedures which implement the required delays between pin changes:
with procedure Delay_T_HD_STA; -- 4.0 usecs with procedure Delay_T_SU_STO; -- 4.0 usecs with procedure Delay_T_Buf; -- 4.7..5 usecs with procedure Delay_T_Low_Half; -- 2.4 usecs with procedure Delay_T_High; -- 5 usecs
An implementation for AVR-Ada and Arduino UNO (atmega328p) is provided in uno_i2c.ads and uno_i2c.adb.
Following example code shows how to read TMP102 temperature sensor value using the package:
procedure Test_I2C is
use type Interfaces.Unsigned_8;
TMP102_Address : constant := 16#90#;
Data : Uno_I2C.I2C.Byte_Array (1..2) := (0, 0);
Cmd : Uno_I2C.I2C.Byte_Array (1..1) := (1 => 16#00#);
Status : Uno_I2C.I2C.Error_Status;
Temp_Value : Integer;
begin
AVR.UART.Init (103);
loop
Data := (0, 0);
Uno_I2C.Write (Address => TMP102_Address, Bytes => Cmd, Status => Status);
Uno_I2C.Read (Address => TMP102_Address, Bytes => Data);
Temp_Value := Integer (Data (1)) * 256;
Temp_Value := Temp_Value + Integer (Data (2));
Temp_Value := Temp_Value / 256; -- Basically we ignore the second byte
AVR.UART.Put ("T:");
if Temp_Value > 0 then
Data (1) := Interfaces.Unsigned_8 (Temp_Value);
else
AVR.UART.Put ("-");
Data (1) := Interfaces.Unsigned_8 (-Temp_Value);
end if;
AVR.UART.Put (Data (1), Base => 10);
AVR.UART.Put (" C");
AVR.UART.CRLF;
delay 2.0;
end loop;
end Test_I2C;
Full code is available under ISC license at my arduino-blog Bitbucket repository.
As usual, some caveats:
- Code doesn't implement all I2C master features, like clock stretching.
- The example code for Arduino UNO uses 80kHz I2C bus speed. Faster is not possible easily.
- Read and Write procedures expect 8-bit I2C addresses.