Connecting I2C Devices to the BBC micro:bit

APDS-9960 BBC micro:bit DFRobot I2C micro:bit microbit MicroPython Python Sensors

The BBC micro:bit supports the I²C bus protocol, for communicating with other devices. In this post, I've used MicroPython to demonstrate, but the principles can easily be translated to lower and higher level languages.

Objectives: To the micro:bit's minimal I2C commands to interact with a device eg a sensor - so we can use other configurable sensors with the micro:bit, even when there's not already an appropriate MicroPython module available.

Requirements:

There's a huge range of devices that work with Arduino micro controllers, with a variety of libraries that provide commands to make it easier. Among these devices, a subset will have stringent timing requirements, which a micro:bit may not be able to fulfil in some translated programming languages, such as Python. However, there's sometimes ways to optimise things and enable more functionality.

Similarly, some of the hardware on top (HAT) devices for the Raspberry Pi, can also be adapted to work with the micro:bit. To make it easier to connect to these devices, there's the 4Tronix bit2pi board. Note that this still requires the correct jumper lead connections and code to be running on the micro:bit, as discussed below.

Here's a link to some great micro:bit references that include some MicroPython packages that already support various devices on the micro:bit: https://github.com/carlosperate/awesome-microbit

Most devices have data sheets that assist a programmer in writing code that will interact with it. Picking the DFRobot SEN0187 RGB and Gesture Sensor, reveals the corresponding datasheet after a quick search.

We can see that there's a number of different modes that can be enabled on this sensor:

  • Proximity
  • ALS (colour and ambient light sensor)
  • Gesture

Each device will generally have a unique address, and there's some Python code here to scan for connected devices. This reveals that the device is at hexidecimal (hex) address 0x39. According to the data sheet, there's memory addresses on that device, AKA registers, that need to be written to and read from, in order to configure and retrieve information back from the sensor. However, the micro:bit has a more limited MicroPython implementation of I²C commands than other devices.

A comparison of MicroPython I²C commands with the subset of commands on the BBC micro:bit

A quick search of the Internet comes up with this and shows:

Commands to read and write to the device:

i2c.init(I2C.MASTER)
i2c.writeto(0x42, '123')        # send 3 bytes to slave with address 0x42
i2c.writeto(addr=0x42, b'456')  # keyword for address

Commands to read and write to memory locations on the device:

i2c.scan()                          # scan for slaves on the bus, returning
                                    #   a list of valid addresses
i2c.readfrom_mem(0x42, 2, 3)        # read 3 bytes from memory of slave 0x42,
                                    #   starting at address 2 in the slave
i2c.writeto_mem(0x42, 2, 'abc')     # write 'abc' (3 bytes) to memory of slave 0x42
                                    # starting at address 2 in the slave, timeout after 1 second

As mentioned previously, the micro:bit only has write and read commands. This is fine for a simple sensor that just returned values, but as previously mentioned, our sensor has multiple modes that each need to be enabled/disabled, and configured!

There's a number of posts on the Internet about how to write to, and read from, specific registers, but for understanding, it made sense to actually be able to visualise exactly what information the device needed to see.

Wiring the sensor to the BBC micro:bit for I2C communications

Firstly, here's the connections required for this device:

  • 3.3 volts (positive power terminal coming from the micro:bit, ie pin 17)
  • ground (ground from the micro:bit, ie pin 22)
  • SCL (clock - ie pin 19 on the micro:bit)
  • SDA (data - ie pin 20 on the micro:bit)
  • interrupt (optionally used to send a pulse back to the micro:bit to signal when the sensor has data, so this can just be any digital pin on the micro:bit, eg pin 16)

The pinouts for the micro:bit are available here.

In the image below, you can see the micro:bit connected to the sensor on a bread board, as described above.

 Theres lots of great tutorials online about how I²C works. Here's some more links if you'd like to read more:

  • http://www.iot-programmer.com/index.php/books/27-micro-bit-iot-in-c/chapters-micro-bit-iot-in-c/49-micro-bit-iot-in-c-i2c-bus
  • https://learn.sparkfun.com/tutorials/i2c

However, this post is more about the journey to create the required communications with multi-register sensor device. An easy way to do this, is to look at a real I²C conversation between master and slave devices, and then look at what comes out when we try to emulate this.

Analyse working I2C communications

So the steps could be:

  1. Hook up an Arduino-compatible micro controller, running a working sketch that uses existing libraries, and observe the working conversation
  2. Map the observed conversation back to the data sheet for the device and try to map what is seen, to what is written in the data sheet
  3. Try and do the same using the superset of MicroPython commands on another I²C micro controller master device
  4. See if we can get the micro:bit to emit the same required I²C conversation and see if the sensor device responds correctly
  5. Keep going and realise that it's pretty easy ;) and contribute to the community!

At this stage, I'll point out that there doesn't seem to be any example code for this specific sensor, so a good by-product of this tutorial might be that we'll have that soon. Since not everyone may have access to a logic analyser/oscilloscope/bus pirate, here's some observed decodes of a conversation between the devices:

The master sends a command to write to the device, which has the ID of 0x39 in hex (ACK is the acknowledgement received back from the slave device)

Setup Write to [9 (0x39)] + ACK

This can then followed by the register (memory address 0x80) to be written to, on the device:

'128' (0x80) + ACK

The next number to be sent will be written to that address:

'5' (0x05) + ACK

Writing to specific sensor memory addresses (registers) using the BBC micro:bit

So, on some MicroPython devices, we can just do a write to a memory location, like so:

i2c.writeto_mem(42, 2, b'\x10') # write the byte '\x10' to device ID 42, at location 2 (b'' is the raw format of a byte array)

Notice that there's three parameters, but on the micro:bit we only have two; the device ID and the data to be sent (which can be a byte array). The easy way to emulate the previously-observed conversation on the micro:bit would be:

i2c.write(0x39, b'\x80\x05')

The other thing we'll want to do, is read back data from a register that might have useful information, eg a sensor reading, as explained by the sensor's data sheet.

Reading from specific sensor registers using the BBC micro:bit

Examining a read of a register value, from a working conversation, we see:

Setup Read to [9 (0x39)] + ACK

..followed by the register address on the sensor, to read from:

'156' (0x9C) + ACK

 On the micro:bit, we can just set the register to read from, like this:

i2c.write(0x39, b'\x9c')

Working example Python script for the BBC micro:bit

In the context of a working script for the micro:bit, that shows a happy face when we move nearer, we have:

# Configure and read proximity values from DFRobot SEN0187 RGB and Gesture Sensor (APDS-9960)
from microbit import *

i2c.write(0x39, b'\x80\x05') # write 0000101 to register 0x80 on device 0x39 to power on and enable proximity mode (keeping it simple here)

while True:
    i2c.write(0x39, b'\x9C') # point to register 0x9C
    i = i2c.read(0x39, 1) # read one byte
    print(i) # write the value to the console (REPL)
    if i[0] > 2:
        display.show(Image.HAPPY)
    else:
        display.show(Image.SAD) 

 

It's also useful to look at the Arduino libraries, as they will show that there's actually a few more things that should be done to set up the sensor - including setting lots of registers to default values to make it work consistently. For clarity, I've stripped this example down to the most basic elements.

You could easily go and create byte arrays with the required payloads for manipulation, and use Python's struct functions to convert output from little-endian format (low byte then high byte) into integers and so on.

 

Psst: We've also got a bunch of sensors in the Arduino section of the store - check out the data sheets and perhaps there's now a few more you can get working with the humble BBC micro:bit! :)


Older Post Newer Post