11  Digital Communications - I2C

This chapter is based on the projectlet:

- 09_i2c

and supported by:

- 98_Utils/09_i2c

11.1 Projectlet Goals

Exploration of digital communication discipline using USARTs exposed a significant weakness - the fact that there is no clock or timing as part of the physical layer. This could lead to inconsistencies between the cooperating devices. The invention of protocols such as Inter-Integrated Circuit and other similar derivatives (e.g. I2S, smbus) addressed this challenge amazingly well; these protocols have become an essential part of most embedded systems for integrating sensors, actuators and the like.

The architecture has further benefited from the qwiic ecosystem with standardized connectors and the large number of devices. In this projectlet, 2 such devices - a temperature sensor and a digital display are integrated with our microcontroller.

In addition the binary logging discipline developed earlier is applied to transmit the temperature feedback to a (presumably) supervisory level system. Examples of receiving these packages, unpacking using the nanoprotobuf library completes the system.

11.1.1 The roadmap

At a high level we will build the system to the following architecture.

Sensor Network Architecture

The temperature sensor will be used to measure the ambient temperature. The temperature converted into centigrade and timestamped will be transmitted over a binary serial interface.

The display is primarily an output device, used as a rotating display of alphanumeric characters or as a minute, second clock.

I2C being an extensible network, additional sensors or other compatible devices can be added with ease.

Further to form a complete system we will add a higher layer system, connecting to the microcontroller using the Serial protocol.

Host system based on linux

The serial interface will be used to transmit the temperature readings packed into a nanoprotobuf packet. The host will then receive the binary data, unpack and print out the temperature. It is of course feasible to have the host be on the I2C network; most real life devices prefer this approach or an ethernet network to integrate the supervisory systems.

11.2 Sensor Network

The microcontroller serves as the master of our I2C sensor network. The controller interacts with each of the nodes of the network as the application demands. The base protocol dictates the line discipline and the steps of reading and writing.

Listing 11.1: Attach and Initialize I2C3
File : toolkit-i2c.adb

0034 |    procedure Attach is
0035 |       cfg : stm32.i2c.I2C_Configuration;
0036 |    begin
0037 |       pragma Debug (stm32.GPIO.Set (stm32.board.Blue_LED));
0038 |       Enable_Clock (GPIO_A);
0039 |       Enable_Clock (GPIO_C);
0040 |       Enable_Clock (I2C_3);
0041 |       Init_I2c3_GPIO;
0042 |       cfg.Clock_Speed := 100_000;
0043 |       cfg.Mode := stm32.i2c.I2C_Mode;
0044 |       cfg.Addressing_Mode := stm32.i2c.Addressing_Mode_7bit;
0045 |       cfg.Own_Address := 0;
0046 |       stm32.i2c.Configure (stm32.Device.i2c_3, cfg);
0047 |       stm32.i2c.Set_State (stm32.Device.i2c_3, true);
0048 |    end Attach;

The microcontroller utilizes one of the I2C devices on the board (I2C3) and sets itself up as the master of the network as shown above. The master then determines the data rates 100 Kbps as indicated above. Since the clock is part of the wire, the sensors dont get to choose the speed though not all sensors may be able to support the data rates depending on their function.

Once attached, reading and writing become pretty straightforward given the extensive support provided by the ADL through the packages stm32.i2c.

Listing 11.2: Read a register (memory) from device
File : toolkit-i2c.adb

0052 |    procedure Read
0053 |      (stat   : out hal.i2c.I2C_Status;
0054 |       addr   : hal.i2c.I2C_Address;
0055 |       reg    : hal.UInt16;
0056 |       result : in out hal.i2c.i2c_data)
0057 |    is
0058 |       Status : hal.i2c.i2c_status;
0059 |    begin
0060 |       stm32.i2c.Mem_Read
0061 |         (This          => port,
0062 |          Addr          => addr,
0063 |          Mem_Addr      => Reg,
0064 |          Mem_Addr_Size => HAL.I2C.Memory_Size_8b,
0065 |          Data          => result,
0066 |          Status        => status);
0067 |       stat := Status;
0068 |    end Read;

and for writing:

Listing 11.3: Write to a register (memory) on the device device
File : toolkit-i2c.adb

0072 |    procedure Write
0073 |      (stat : out hal.i2c.I2C_Status;
0074 |       addr : hal.i2c.I2C_Address;
0075 |       reg  : hal.UInt16;
0076 |       data : in out hal.i2c.i2c_data) is
0077 |    begin
0078 |       stm32.i2c.Mem_Write
0079 |         (port,
0080 |          addr,
0081 |          Mem_Addr      => reg,
0082 |          Mem_Addr_Size => HAL.I2C.Memory_Size_8b,
0083 |          Data          => data,
0084 |          Status        => Stat);
0085 |    end Write;

Each member usually has its own extensive specifications to be found in the data sheets.

11.2.1 Temperature Sensor stts22h

The stts22h temperature sensor, simple though it may seem, has an elaborate datasheet based on which the initializations, configurations are to be achieved before data acquisition. The raw data acquired may require further processing eg in this case conversion to centigrade before applications can utilize those values.

With the toolkit support above, the specific sensor is initialized:

Listing 11.4: Temperature sensor configuration
File : stts22h.adb

0072 |    procedure Setup (stat : out hal.I2C.I2c_Status) is
0073 |       CTRLVAL : constant :=
0074 |         SETGET_CTRL_FREERUN + SETGET_CTRL_IF_ADD_INC + SETGET_CTRL_AVG0
0075 |         + SETGET_CTRL_AVG1;
0076 |       pkt : HAL.I2C.I2C_Data (1 .. 1);
0077 |    begin
0078 |       pkt (1) := CTRLVAL;
0079 |       toolkit.i2c.Write (stat, addr, REG_CTRL, pkt);
0080 |    end Setup;

and the feedback read:

Listing 11.5: Temperature sensor configuration
File : stts22h.adb

0045 |    function Read (stat : out hal.i2c.I2C_Status) return Temperature_Type is
0046 |       rawresult : hal.i2c.i2c_data (1 .. 2);
0047 |       result    : Temperature_Type;
0048 |    begin
0049 |       toolkit.i2c.Read (stat, Addr, REG_TEMP_L_OUT, rawresult);
0050 |       result :=
0051 |         Temperature (Unsigned_8 (rawresult (1)), Unsigned_8 (rawresult (2)));
0052 |       return result;
0053 |    end Read;

The 2 raw bytes provided by the sensor are to be converted into a temperature as the datasheets specify:

Listing 11.6: Temperature Compute from raw values
File : stts22h.adb

0020 |    function Temperature (L, H : Unsigned_8) return Temperature_Type is
0021 |       valfull : aliased Unsigned_16 :=
0022 |         Shift_Left (Unsigned_16 (H), 8) + Unsigned_16 (L);
0023 |       valint  : Short_Integer;
0024 |       for valint'Address use valfull'Address;
0025 |    begin
0026 |       return Temperature_Type (valint) / Temp_Mult;
0027 |    end Temperature;

The application then may choose to convert the data into other units such as fahrenheit for logging as shown:

Listing 11.7: Application usage of temperature
File : i2cnet.adb

0059 |       t := stts22h.Read (s);
0060 |       tf := toolkit.Convert (centigrade_type (t));
0061 |       toolkit.logs.logger.Put_Line
0062 |         ("STTS22H Status "
0063 |          & s'Image
0064 |          & " value "
0065 |          & t'Image
0066 |          & " or in deg F "
0067 |          & tf'image,
0068 |          source => myname);

This pattern is pretty common in most sensors. In some extreme cases, applications may have to calibrate the sensors and incorporate those into the final calculations.

11.2.1.1 Temperature applications

The primary application temp reads the temperature from the device, converts it to fahrenheit, packs it into a nanoprotobuf packet and transmits over a binary serial port. A companion app capture runs on a host, receiving the packet, unpacking and printing out the temperature.

Listing 11.8: Unpack the nanoprotobuf buffer and extract the temperature
File : capture.adb

0042 |    procedure Packet_Body is
0043 |       Status : Int;
0044 |       yy , mm , dd , hh , min , ss  : Int ;
0045 |       value : Interfaces.C.double ;
0046 |    begin
0047 |       buffer(1) := mtype ;
0048 |       buffer(2) := packetlen ;
0049 |       pktlen := 2 ;
0050 |       for numbytes in 1..Integer(packetlen)
0051 |       loop
0052 |          Byte_Io.Read( ttybytesF , byte );
0053 |          pktlen := pktlen + 1 ;
0054 |          buffer(pktlen) := byte ;
0055 |          Put(hex.Image(Unsigned_8(byte))) ;
0056 |       end loop ;
0057 |       Put(" Checksum : "); Put(hex.Image(Unsigned_8(byte))) ; Put(" "); Put_Line(">");
0058 |       status := getset.GetTemperature (buffer(3)'Address , Int(pktlen-3) , 
0059 |                                        yy , mm , dd , hh , min , ss , value);

The following log from the utility shows the timestamp and the temperature readings:

MType 11 Len 14 3a0c081a1005180120002807301645efa79242ff Checksum : ff >
 0: 7: 22:  Temp 73.33
MType 11 Len 14 3a0c081a100518012000280730174596c39242c3 Checksum : c3 >
 0: 7: 23:  Temp 73.38
MType 11 Len 14 3a0c081a10051801200028073018456c6792423e Checksum : 3e >
 0: 7: 24:  Temp 73.20
MType 11 Len 14 3a0c081a1005180120002807301945b89e9242c2 Checksum : c2 >
 0: 7: 25:  Temp 73.31
MType 11 Len 14 3a0c081a1005180120002807301a454a8c924243 Checksum : 43 >
 0: 7: 26:  Temp 73.27

11.2.2 Alphanumeric Display ht16k33

The alphanumeric display ht16k33 is an intricate device in itself incorporating distinct LEDs for each segment of the character requiring review of the schematic, data sheets and most importantly application examples. In the prototyping stage, the usual first attempt may well be with the Arduino system and often the associated library could serve as a model for our own efforts.

In fact due to the many variations on the display device, the Arduino library was the primary reference source. The driver was migrated to Ada and ADL using a variety of tools ChatGPT, Claude and manual conversion.

The interaction with the device follows the usual pattern. Once the network is Attached any of the devices on the network can be supported. However the design of the display leads to a layer between the application and the hardware.

Listing 11.9: Write a string to the display
File : ht16k33_alpha.adb

0333 |    procedure Write_String (This : in out HT16K33_Display; S : String) is
0334 |    begin
0335 |       for D in 0 .. 3 loop
0336 |          This.RAM (D * 2) := 0;
0337 |          This.RAM (D * 2 + 1) := 0;
0338 |       end loop;
0339 | 
0340 |       for I in S'Range loop
0341 |          exit when I - S'First > 3;
0342 |          Illuminate_Char (This, I - S'First, S (I));
0343 |       end loop;
0344 |    end Write_String;

The above segment transmits a set of characters to the device which in turn proceeds by maintaining a temporary buffer translating each character into a pattern:

Listing 11.10: Character to illumination pattern
File : ht16k33_alpha.adb

0015 |    function Segment_Pattern (Ch : Character) return UInt16 is
0016 |    begin
0017 |       case Ch is
0018 |          when '0' =>
0019 |             return 2#00000000111111#; --, // '0'
0020 | 
0021 |          when '1' =>
0022 |             return 2#00010000000110#; -- , // '1'
0023 | 
0024 |          when '2' =>
0025 |             return 2#00000101011011#; -- , // '2'
0026 | 
0027 |          when '3' =>
0028 |             return 2#00000101001111#; --, // '3'
0029 | 
0030 |          when '4' =>
0031 |             return 2#00000101100110#; --, // '4'
0032 | 
0033 |          when '5' =>
0034 |             return 2#00000101101101#; --, // '5'
0035 | 
0036 |          when '6' =>
0037 |             return 2#00000101111101#; --, // '6'
0038 | 
0039 |          when '7' =>
0040 |             return 2#01010000000001#; --, // '7'
0041 | 
0042 |          when '8' =>
0043 |             return 2#00000101111111#; --, // '8'
0044 | 
0045 |          when '9' =>
0046 |             return 2#00000101100111#; --, // '9'
0047 | 

before it is transmitted to the device. The pattern is directly translated from the Arduino library referenced above.

11.2.2.1 Display Applications

There are 2 different applications in this projectlet:

  • watch - every second updates the display with the minute and second
  • disp - displays a rotating display of a long string