Search code examples
adabbc-microbit

Ada i2c demo on the microbit with the Ada Drivers Library?


Overview: I'm trying to program a microbit with Ada using the Ada Drivers Library and I can't understand how to use the i2c functions to establish communications with another chip. I'd like to establish a simple demo so I can understand what's happening because the demos in the components directory of the Ada Drivers Library are going over my head (I'm pretty new to Ada too and that doesn't help matters).

The simplest i2c demo in the Ada Drivers Library appears to be for the AK8963 three axis compass (located in /components/src/motion/ak8963/). But that's still going over my head and I don't have the chip to run and debug the code.

Here's what I've tried: I've created two different demos with arduinos. In both demos the transmitter sends an 'A' and then a 'B' all the way to 'Z' and then loops back to 'A'. In the first demo the master transmits the next character every 500 ms and the slave receives it. And in the second demo the master requests the next character every 500 ms and the slave transmits it.

My demos are adapted from the arduino Wire examples found here and here.


Solution

  • I figured it out.

    Let's start with the two Arduino programs to prove that the Arduino code works.

    Arduino Slave transmit:

    /*
    Sends the next letter of the alphabet with each
    request for data from master.
    
    Watch the serial monitor to see what's happening.
    */
    
    #include <avr/wdt.h>
    #include <Wire.h>
    
    // A note about I2C addresses.
    // The Ada program is looking for the slave on address 16
    // but this code says the slave is on 8.
    // What's happening? As best as I can tell it works
    // like this:
    // 16 in binary is 10000. But arduino strips the read/write bit 
    // (which is the last bit) off of the address so it becomes 
    // 1000 in binary. And 1000 in binary is 8.
    const int SLAVE_ADDRESS = 8;
    byte letter = 65; // letter A
    unsigned long counter = 0;
    
    void setup()
    {
      wdt_reset();
      wdt_enable(WDTO_8S);
    
      Serial.begin(9600);
      Serial.println("beginning");
    
      Wire.begin(SLAVE_ADDRESS);        // join i2c bus
      Wire.onRequest(requestEvent);     // register event
    }
    
    void loop()
    {
      wdt_reset();
      counter++;
      if(counter % 1000 == 0)
      {
        // Display a heart beat so we know the arduino has not hung.
        Serial.print("looping: ");
        Serial.println(counter);
      }
      delay(5);
    }
    
    // function that executes whenever data is requested by master
    // this function is registered as an event, see setup()
    void requestEvent()
    {
      // send the current letter on I2C
      Wire.write(letter);
    
      Serial.print("transmitting: ");
      Serial.println(char(letter));
    
      letter++;
      if(letter > 90) // if greater than Z
      {
        letter = 65; // reset to A
      }
    }
    

    Arduino Master receive:

    /*
    Requests a character from the slave every 500 ms and prints it
    to the serial monitor.
    */
    
    #include <avr/wdt.h>
    #include <Wire.h>
    
    const int SLAVE_ADDRESS = 8;
    
    void setup()
    {
      wdt_reset();
      wdt_enable(WDTO_8S);
      Wire.begin();        // join i2c bus
      Serial.begin(9600);
    }
    
    void loop()
    {
      // reset the watchdog timer
      wdt_reset();
    
      // request one byte from the slave
      Wire.requestFrom(SLAVE_ADDRESS, 1);
    
      while(Wire.available()) // slave may send less than requested
      {
          // receive a byte as character
        char c = Wire.read();
        Serial.println(c);
      }
      delay(500);
    }
    

    These two Arduino sketches will happily pass characters all day. Now replace the Arduino master receiver with the Ada version below and physically disconnect the Arduino master receiver.

    Ada master receiver (main.abd):

    --  Request a character from the I2C slave and
    --  display it on the 5x5 display in a loop.
    
    with HAL.I2C;          use HAL.I2C;
    with MicroBit.Display; use MicroBit.Display;
    with MicroBit.I2C;
    with MicroBit.Time;
    
    procedure Main is
       Ctrl   : constant Any_I2C_Port := MicroBit.I2C.Controller;
       Addr   : constant I2C_Address := 16;
       Data   : I2C_Data (0 .. 0);
       Status : I2C_Status;
    begin
    
       MicroBit.I2C.Initialize (MicroBit.I2C.S100kbps);
       if MicroBit.I2C.Initialized then
          --  Successfully initialized I2C
          Display ('I');  
       else
          --  Error initializing I2C
          Display ('E');  
       end if;
       MicroBit.Time.Delay_Ms (2000);
       MicroBit.Display.Clear;
    
       loop
          --  Request a character
          Ctrl.Master_Receive (Addr => Addr, Data => Data, Status => Status);
    
          --  Display the character or the error
          if Status = Ok then
             Display (Character'Val (Data (0)));
          else
             MicroBit.Display.Display (Status'Image);
          end if;
    
          --  Give the user time to read the display
          MicroBit.Time.Delay_Ms (1000);
          MicroBit.Display.Clear;
          MicroBit.Time.Delay_Ms (250);
       end loop;
    end Main;
    

    And here is the Ada project file for completeness:

    with "..\..\Ada_Drivers_Library\boards\MicroBit\microbit_zfp.gpr";
    
    project I2C_Master_Receive_Demo is
    
       for Runtime ("ada") use Microbit_Zfp'Runtime ("Ada");
       for Target use "arm-eabi";
       for Main use ("main.adb");
       for Languages use ("Ada");
       for Source_Dirs use ("src");
       for Object_Dir use "obj";
       for Create_Missing_Dirs use "True";
    
       package Compiler renames Microbit_Zfp.Compiler;
    
       package Linker is
          for Default_Switches ("ada") use Microbit_Zfp.Linker_Switches & ("-Wl,--print-memory-usage", "-Wl,--gc-sections", "-U__gnat_irq_trap");
       end Linker;
    
       package Ide is
          for Program_Host use ":1234";
          for Communication_Protocol use "remote";
          for Connection_Tool use "pyocd";
       end Ide;
    
    end I2C_Master_Receive_Demo;
    

    Tips:

    • you need to observe the I2C address offsets (16 in Ada = 8 on Arduino in my case). See the explanation in the comments of the slave transmit arduino code above. It took me a long time to figure that out.
    • nothing worked with three devices connected to the I2C bus, even if one of them was not powered. I don't know exactly what's happening there but I suspect its related to documentation stating that the I2C bus cannot pull its lines back to HIGH. Some documentation recommends placing a resistor on both I2C lines connected to your source voltage so the line voltages return to HIGH after the devices pulls them low.
    • this work would be easier with an oscilloscope. I could have figured out this problem much more quickly if I had had one.