Getting Started with the micro:bit IO Pin Service

The intention of the Bluezero library is that it will allow
people to interact with over Bluetooth without necessarily
knowing too much about the low-level details. However, I also want people to be able to learn more about Bluetooth.

What I wanted to capture in this post was the process I went through to understand the IO Pin Service from the
micro:bit BLE Profile
when I was implementing this is Bluezero.

The most difficult pieces for me to get my head around were:

the Pulse Width Modulation (PWM) output

change pins to be digital or analogue inputs

The first thing needed is some way of sending the low-level commands to the micro:bit.
Below are some details of the apps I used and how I broke the problem up.

BLE Apps:

Before I start to add functionality to the Bluezero library I like to test my understanding of the
service/characteristics with as direct a method as possible.
This means working out what hexadecimal octets need to be sent.
Doing this type of work normally means using the
nRF Connect app
from Nordic Semiconductor on a mobile platform. If you are going to be experimenting with BLE and you only want one
app, then this is the one to have.

For Linux and Chromebook it is bluetoothctl that I use for this testing.
bluetoothctl is really helpful both for testing what data is sent and received. On Linux it is also useful to have
it running in a different shell when I’m testing Bluezero to see messages from the Bluetooth daemon.
One of the problems with bluetoothctl is that there isn’t lots of internet resources you can refer to about it.
Apart from typing ‘bluetoothctl’ and then typing ‘help’ to get a list of the commands, you need to work things
out for yourself which isn't always easy the first time.

On the Mac I've found
Lightblue to be the best although it isn't
great and I tend not to use it as often as the others.

PWM Output:

Right, back to those pesky characteristics. Let’s start with the PWM output [UUID: E95DD822-251D-470A-A062-FA1922DFA9A8]
characteristic. It is made up of seven
octets to define three numbers which are:

Pin number (1 octet)

Value is in the range 0 – 1023 (2 octets)

Period is the duration of time of one cycle in microseconds (4 octets)

The final thing we need to be aware of is that fields are transmitted over the air in Little Endian format.

Let’s start with the easy one, the pin number. Take the pin number e.g. 0 and convert it to hexadecimal octet 0x00.
OK, not a great example, let’s try 12 & 19. They would be 0x0C and 0x13 in hexadecimal.

For the value, let’s take four values and convert them into hexadecimal.
256, 512, 768, 1023 become the following in hexadecimal occupying two octets 0x0100, 0x0200, 0x0300 & 0x03FF.

However that isn’t the value we need to send as they are not in the little endian format.
This means that the four values we send are 0x0001, 0x0002, 0x0003 & 0xFF03.

My experiments were to activate the buzzer on the
MI:power board
so I choose the note that is the 49th key from a piano and has a frequency 440 Hz.

This frequency converts into a period of roughly 2273 microseconds which as a 4 octet hexadecimal would be 0x000008E1.
We need to change this to little endian so it becomes 0xE1080000.

Now we have worked out how to handle each of the values let’s put it together.
If we wanted to set pin 0 to a value of 512 with a period of 2273 we would send 0x00 0002 E1080000.

In the nRF Connect app this would look like:

When the send button was pressed this created a suitably annoying beep on the the MI:power board

To stop the beep we can just send 14 zeros.

After my experiments, I summarised what I had done with the IO outputs on a cheat sheet for future reference:

Changing direction and type of Pins

With the outputs taken care of, it was time to move on to understanding inputs.

There are two characteristics that control if pins are input/output and analogue/digital. Depending how these are
set will change the behaviour of pin data.

Reading Digital Values:

To read the value of a pin, the Pin IO Configuration [UUID: E95DB9FE-251D-470A-A062-FA1922DFA9A8] needs to be changed.

This characteristic is 32 bits (4 octets) where the 20 least significant bits represent the 20 pins.
If we set a bit to 0 it represents an output while a 1 represents an input.

If we want to set Pin 0 as an input we would need to set the bit to 1.
This would make the hexadecimal value as 0x00000001.
So again, as the data needs to be little endian we would need to send this as 0x01000000.

Reading Analogue Values:

The Pin AD Configuration [UUID: E95D5899-251D-470A-A062-FA1922DFA9A8] is again a characteristic of 32 bits (4 octets) the same as the Pin IO Configuration
but this time a 0 represents a digital pin and 1 represents analogue.

To read an analogue value on a pin we need set it as an input and analogue.
For example pin 2 would require 0x04000000 to be sent to both the Pin IO Configuration and Pin AD Configuration.

Getting the value

Now the pin is configured as an input, this means when we read the Pin Data
[UUID: E95DB9FE-251D-470A-A062-FA1922DFA9A8] characteristic we will see pairs of octets return that represent the
pin and value. For example if pin 5 is a digital input with a high on it then the hexidecimal that would be returned
would be: 0501.
0x05 is the pin and 0x01 represents a high.

It is similar for the analogue input in that there is still a pin/value pair but the value can be higher than 1.
It can be in the range of 0 to 255 (0xFF).
For example pin 2 at half value would return 0280
0x02 is the pin and 0x80 is a value of 128 in decimal.

After my experiments, I summarised what I had done with the IO inputs on a cheat sheet for future reference: