IO Expander

Now we can move on to the IO Expander, which is located on our mainboard. The IO Expander is an I2C interface to which our sensors are mainly connected. The IO Expander extends the pins of the micro:bit so that it is possible to use all the sensors of the Joy-Car.

You can find out more about the IO Expander here.

In the following example, you can read out the IO Expander and write to the available pins accordingly. It is important to note the differences between the various revisions of the IO Expander. Further information on the revisions can be found here.

Differences between the revisions:

  • Revision 1.3: Pins 0, 1, and 7 are free and can be written to
  • Older revisions: Only pin 7 is free and can be used

 

In the example, pin 7 of the IO Expander is first switched to LOW and then reset to HIGH. The IO expander is then read out every second in an endless loop in order to continuously monitor the current status of the pins.

Revision

This code block defines the revision number of your Joy-Car, which you can find on the underside of the mainboard. You can find more information here.

Defining the revision number ensures that the code can be used for different revisions of the Joy-Car by automatically adapting to the specific differences of the respective hardware version. This makes the same code block universally usable and facilitates the handling of different Joy-Car versions.

Read IO Expander

This code block reads the IO expander and returns the values determined as a string. In this string, the first number represents the status of pin 0. The remaining numbers in the string correspond to the states of the other pins in the order in which they are numbered.

The returned data has the following structure:

  1. Left speed sensor
  2. Right speed sensor
  3. Left line tracker
  4. Middle line tracker
  5. Right line tracker
  6. Left obstacle sensor
  7. Right obstacle sensor
  8. Free pin 7
  9. Free pin 0 (only available from revision 1.3)
  10. Free pin 1 (only available from revision 1.3)

 

This makes it easy to check and process the pin statuses in a readable and structured form.

Write to IO Expander

This code block can be used to write to the free pins of the IO expander by setting them to HIGH or LOW. Please note that the availability of the free pins depends on the revision of the Joy-Car. Further information on the differences between the revisions can be found here. The code takes these differences into account and ensures that only the pins that are released for the respective revision are written.

Code example

JoyCar.initJoyCar(RevisionMainboard.OnepThree)
JoyCar.writeIOExpander(IOExpanderPin.Pin7, 0)
serial.writeLine(JoyCar.readIOExpander())
basic.pause(1000)
JoyCar.writeIOExpander(IOExpanderPin.Pin7, 1)
serial.writeLine(JoyCar.readIOExpander())
basic.pause(1000)
basic.forever(function () {
    serial.writeLine(JoyCar.readIOExpander())
    basic.pause(1000)
})

Revision & initialization

First, the revision of the Joy-Car must be defined to ensure that the following methods are correctly adapted to the respective version of the Joy-Car. You can find the revision number of your Joy-Car here. This revision number is simply entered in the joycar_rev variable.

The IO Expander is then initialized so that it can be used for the next steps in the program. Initialization enables communication with the IO Expander and ensures that the pins can be configured according to the defined revision.

# Define your Joy-Car mainboard revision
joycar_rev = 1.3

# Initialization of the I2C interface for the Joy-Car mainboard
i2c.init(freq=400000, sda=pin20, scl=pin19)

Read IO Expander

The fetchSensorData method is used to read out the IO Expander and write the captured data to an array called bol_data_dict. This array is then returned. The method adapts the assignment of the data to the revision of the Joy-Car.

In revisions as from 1.3, the speed sensors are no longer connected to the IO expander. Instead, the 8th and 9th positions in the array represent the states of the free pins pin 0 and pin 1. The 0th and 1st positions remain assigned to the speed sensors.

The returned data in the array has the following structure:

  1. Left speed sensor
  2. Right speed sensor
  3. Left line tracker
  4. Middle line tracker
  5. Right line tracker
  6. Left obstacle sensor
  7. Right obstacle sensor
  8. Free pin 7
  9. Free pin 0 (only available from revision 1.3)
  10. Free pin 1 (only available from revision 1.3)

This structure ensures that all sensor data and free pins are clearly and unambiguously assigned, regardless of the revision used.

# retrieve all sensor data
def fetchSensorData():
    # Since the zfill function is not included in micro:bit Micropython, 
    # it must be inserted as a function
    def zfill(s, width):
        return '{:0>{w}}'.format(s, w=width)

    # Read hexadecimal data and convert to binary data
    data = "{0:b}".format(ord(i2c.read(0x38, 1)))
    # Fill in the data to 8 digits if required
    data = zfill(data, 8)
    # declare bol_data_dict as dictionary
    bol_data_dict = {}
    # Counter for the loop that enters the data from data into bol_data_dict
    bit_count = 7
    # Transfer the data from data to bol_data_dict
    for i in data:
        if i == "0":
            bol_data_dict[bit_count] = False
            bit_count -= 1
        else:
            bol_data_dict[bit_count] = True
            bit_count -= 1

    # As of mainboard revision 1.3, the speed sensors are on separate pins
    if joycar_rev >= 1.3:
        bol_data_dict[8], bol_data_dict[9] = bol_data_dict[0], bol_data_dict[1]
        bol_data_dict[0] = bool(pin14.read_digital())
        bol_data_dict[1] = bool(pin15.read_digital())

    # bit 0 = SpeedLeft, bit 1 = SpeedRight, bit 2 = LineTrackerLeft,
    # bit 3 = LinetrackerCenter, bit 4 = LinetrackerRight,
    # bit 5 = ObstacleLeft, bit 6 = ObstacleRight, bit 7 = free pin(7)
    # (bit 8 = free (pin0) bit 9 = free (pin1)) - only with revision 1.3 or newer
    return bol_data_dict

Write to IO Expander

 

The writeIOExpander method allows you to set the free pins of the IO expander to HIGH or LOW. Thereby:

  • pin: is used to select the specific pin to be written to.
  • value: specifies whether the pin is set to HIGH (1) or LOW (0).

 

The method takes into account the respective revision of the Joy-Car to ensure that only the permitted pins can be written to:

  • From revision 1.3: Pins 0, 1 and 7 can be written to.
  • Before revision 1.3: Only pin 7 is available.

 

This method ensures that no unauthorized pins are changed and at the same time allows flexible and safe control of the pins independently of the Joy-Car revision.

# Write data to IO expander as byte
def writeIOExpander(pin, value):
    # Read the current status from the IO expander
    expander_state = ord(i2c.read(0x38, 1))
    # Newer versions than 1.3 have 3 possible pins to use (0, 1 and 7)
    # With older versions, only pin 7 can be used freely.
    # Input pins are set to the value 1.
    if joycar_rev < 1.3:
        if pin == 7:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x80 | 0x7F)), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0x7F) | 0x7F))
                                      , 'ascii'))
    if joycar_rev >= 1.3:
        if pin == 0:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x01 | 0x7C)), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0xFE) | 0x7C))
                                      , 'ascii'))
        elif pin == 1:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x02) | 0x7C), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0xFD) | 0x7C))
                                      , 'ascii'))
        elif pin == 7:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x80 | 0x7C)), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0x7F) | 0x7C))
                                      , 'ascii'))

Code example

# Import necessary libraries
from microbit import *

# Define your Joy-Car mainboard revision
joycar_rev = 1.3

# Initialization of the I2C interface for the Joy-Car mainboard
i2c.init(freq=400000, sda=pin20, scl=pin19)

# retrieve all sensor data
def fetchSensorData():
    # Since the zfill function is not included in micro:bit Micropython, 
    # it must be inserted as a function
    def zfill(s, width):
        return '{:0>{w}}'.format(s, w=width)

    # Read hexadecimal data and convert to binary data
    data = "{0:b}".format(ord(i2c.read(0x38, 1)))
    # Fill in the data to 8 digits if required
    data = zfill(data, 8)
    # declare bol_data_dict as dictionary
    bol_data_dict = {}
    # Counter for the loop that enters the data from data into bol_data_dict
    bit_count = 7
    # Transfer the data from data to bol_data_dict
    for i in data:
        if i == "0":
            bol_data_dict[bit_count] = False
            bit_count -= 1
        else:
            bol_data_dict[bit_count] = True
            bit_count -= 1

    # As of mainboard revision 1.3, the speed sensors are on separate pins
    if joycar_rev >= 1.3:
        bol_data_dict[8], bol_data_dict[9] = bol_data_dict[0], bol_data_dict[1]
        bol_data_dict[0] = bool(pin14.read_digital())
        bol_data_dict[1] = bool(pin15.read_digital())

    # bit 0 = SpeedLeft, bit 1 = SpeedRight, bit 2 = LineTrackerLeft,
    # bit 3 = LinetrackerCenter, bit 4 = LinetrackerRight,
    # bit 5 = ObstacleLeft, bit 6 = ObstacleRight, bit 7 = free pin(7)
    # (bit 8 = free (pin0) bit 9 = free (pin1)) - only with revision 1.3 or newer
    return bol_data_dict

# Write data to IO expander as byte
def writeIOExpander(pin, value):
    # Read the current status from the IO expander
    expander_state = ord(i2c.read(0x38, 1))
    # Newer versions than 1.3 have 3 possible pins to use (0, 1 and 7)
    # With older versions, only pin 7 can be used freely.
    # Input pins are set to the value 1.
    if joycar_rev < 1.3:
        if pin == 7:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x80 | 0x7F)), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0x7F) | 0x7F))
                                      , 'ascii'))
    if joycar_rev >= 1.3:
        if pin == 0:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x01 | 0x7C)), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0xFE) | 0x7C))
                                      , 'ascii'))
        elif pin == 1:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x02) | 0x7C), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0xFD) | 0x7C))
                                      , 'ascii'))
        elif pin == 7:
            if value == 1:
                i2c.write(0x38, bytes(str(chr(expander_state | 0x80 | 0x7C)), 'ascii'))
            elif value == 0:
                i2c.write(0x38, bytes(str(chr((expander_state & 0x7F) | 0x7C))
                                      , 'ascii'))

# Set additional pin 7 to low
writeIOExpander(7, 0)
print(fetchSensorData())
sleep(1000)

# Set additional pin 7 to high
writeIOExpander(7, 1)
print(fetchSensorData())
sleep(1000)

while True:
    # Output retrieved data
    print(fetchSensorData())
    sleep(1000)