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.
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
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.