Moving a BLE application from Zephyr 2.6.0 to version 3.1.0

I have been using the Microbit V2 as a BLE development tool with our engineering students. Part of this involves looking below the covers at what is happening on the I2C bus. Students interact with the built-in accelerometer using I2C reads and writes (i.e. not using Zephyr’s high level driver). Version 2.6.0 of Zephyr allowed us to do this without many problems. Students were given a basic BLE step counter application which they analysed and extended in a practical session. Zephyr OS is under constant development of course and it has moved on to version 3.1 over the past few months. Naturally this broke all of my sample code 🙂

So what’s changed. Well, first of all, the Zephyr include path has gone from

#include <bluetooth/bluetooth.h>

to

#include <zephyr/bluetooth/bluetooth.h>

Now there is a project configuration variable that allows legacy include directory names but I decided that I would go in and make the changes to the various source files. This turned out to be the easiest change to make.

The next hurdle turned out to be the PINCTRL system built in to Device Tree. This is something I avoided in Zephyr 2.6 as it has a big enough learning curve, lacks some documentation and is very different to the bare-metal microcontroller way of managing I/O pins. The approach taken was instead to assign pins to functions in the app.overlay file as follows:


&i2c1 { 
	compatible = "nordic,nrf-twim"; 
	status = "okay"; 
	sda-pin = < 16 >;// P0.16 = I2C_INT_SDA 
	scl-pin = < 8 >; // P0.8 = I2C_INT_SCL 
}; 
&spi2 { 
	compatible = "nordic,nrf-spi"; 
	status = "okay"; 
	sck-pin = <17>; 
	mosi-pin = <13>; 
/* Redirecting MISO to a pin that is not connected on the microbit v2 board */ 
	miso-pin = <27>; 
	clock-frequency = <1000000>; 
}; 
&adc { 
	status = "okay"; 
}; 
&pwm0 { 
	status = "okay"; 
// P0.3 is labelled RING1 on the microbit. 
	ch0-pin = <3>; 
};


Attempting to do this with version 3.1 results in lots of errors of the following form:

static assertion failed: “/soc/spi@40023000 has legacy *-pin properties defined although PINCTRL is enabled

Why does this happen? When I build the code I use the following command:

west build -b bbc_microbit_v2 ble_stepcount -p

This uses the board definitions for the bbc_microbit_v2 device as defined in the directory zephyr/boards/arm/bbc_microbit_v2/

This directory contains a number of files that define the device tree and various project build options. The result is that the build system requires all pin definitions to be carried out using the PINCTRL mechanism. So what’s different? Here’s the PINCTRL version of my app.overlay file.

&pinctrl {
   /* IMPORTANT!  There should not be a space before the : in the next line (and 	similar below) */
    spi2_default_alt: spi2_default_alt {
        group1 {
            psels = <NRF_PSEL(SPIM_MOSI,0,17)>,
                    <NRF_PSEL(SPIM_SCK,0,13)>;
        };
        group2 {
        	psels = <NRF_PSEL(SPIM_MISO, 0, 27)>;
	      bias-pull-down;
        };
        
    };
    spi2_sleep_alt: spi2_sleep_alt {
        group1 {
            psels = <NRF_PSEL(SPIM_MOSI,0,17)>,
                    <NRF_PSEL(SPIM_SCK,0,13)>,
                    <NRF_PSEL(SPIM_MISO, 0, 27)>;
            low-power-enable;
        };
    };
    i2c1_default_alt: i2c1_default_alt {
        group1 {
            psels = <NRF_PSEL(TWIM_SDA,0,16)>,
                    <NRF_PSEL(TWIM_SCL,0,8)>;
            bias-pull-up;
        };
    };
    i2c1_sleep_alt: i2c1_sleep_alt {
        group1 {
            psels = <NRF_PSEL(TWIM_SDA,0,16)>,
                    <NRF_PSEL(TWIM_SCL,0,8)>;
            low-power-enable;
        };
    };
 };
 
 &spi2 {
    compatible = "nordic,nrf-spi";
    status = "okay";
    pinctrl-0 = <&spi2_default_alt>;
    pinctrl-1 = <&spi2_sleep_alt>;
    pinctrl-names = "default", "sleep";

    clock-frequency = <1000000>;
};
&i2c1 {
	compatible = "nordic,nrf-twim";
	status = "okay";
	pinctrl-0 = <&i2c1_default_alt>;
	pinctrl-1 = <&i2c1_sleep_alt>;
	pinctrl-names = "default", "sleep";
	label = "I2C_1";
};
&i2c0 {
    status="disabled";
};
&gpio0 {
    status="okay";
    label="GPIO_0";
};
&adc {
        status = "okay";
};
&pwm0 {
        status = "okay";
// P0.3 is labelled RING1 on the microbit. 
        ch0-pin = <3>; 
};


		

As you can see, there are lots of changes. The pinctrl section is new. This section allows us to redefine the pins used by the microcontroller peripherals spi2 and i2c1. We write definitions for each of these for the default powered up state and for the sleep state. (Definitions for sleep state are not required if you disable power management in the project configuration file). States contain one or more groups of pin definitions. These groups allocate actual pin numbers to peripheral functions and can set group properties such as whether there will be pull-up resistors etc. Pin assignment for SPI2 is follows:

psels = <NRF_PSEL(SPIM_MOSI,0,17)>,
<NRF_PSEL(SPIM_SCK,0,13)>,
<NRF_PSEL(SPIM_MISO, 0, 27)>;

This states that MOSI is on GPIO0, bit 17, SCK GPIO0, bit 13 etc. The trickiest part of this is finding out what are the correct names of the pins e.g. SPIM_MOSI, TWIM_SDA and so on. I found them in

zephyr/boards/arm/bbc_microbit_v2/bbc_microbit_v2-pinctrl.dtsi and

zephyr/boards/arm/nrf52833dk_nrf52833/nrf52833dk_nrf52833-pinctrl.dtsi

Having defined the pin assignments the next sections (&spi2, &i2c1) allow you to apply them to the hardware in this project. In the case of spi2, the pinctrl settings for state 0 (default) are remapped to spi2_default_alt with this line:

pinctrl-0 = <&spi2_default_alt>;

The configuration for sleep mode are configured by assigning a value to pinctrl-1 as follows:

pinctrl-1 = <&i2c1_sleep_alt>;

In my C code I had acquired a device handle I2C1 and GPIO0 using the names “I2C_1” and “GPIO_0” respectively. This did not work with Zephyr 3.1 but inserting label directives as shown above fixed this error.

At this point, the code would build and appear to work with one exception: Bluetooth notify failed to work correctly. I saw lots of warnings of the following form in a serial terminal monitoring the microbit:

<wrn> Device is not subscribed to characteristic

This happened when the bt_gatt_notify function was called in response to a change in step count value. Fixing this error took a while! The original BLE characteristic and service definition was as follows:

#define BT_GATT_CHAR1 \ BT_GATT_CHARACTERISTIC(&stepcount_id.uuid,BT_GATT_CHRC_READ | \  BT_GATT_CHRC_WRITE |  BT_GATT_CHRC_NOTIFY, 	BT_GATT_PERM_READ | \ BT_GATT_PERM_WRITE, read_stepcount, write_stepcount, &stepcount_value)


// ********************[ Service definition ]********************
#define BT_UUID_CUSTOM_SERVICE_VAL	\
	BT_UUID_128_ENCODE(1, 2, 3, 4, (uint64_t)0)
static struct bt_uuid_128 my_service_uuid = BT_UUID_INIT_128( BT_UUID_CUSTOM_SERVICE_VAL);

BT_GATT_SERVICE_DEFINE(my_service_svc,
	BT_GATT_PRIMARY_SERVICE(&my_service_uuid),
		BT_GATT_CHAR1
);

The new definition and additional code is as follows:

#define BT_GATT_CHAR1 BT_GATT_CHARACTERISTIC(&stepcount_id.uuid,BT_GATT_CHRC_READ | \ BT_GATT_CHRC_WRITE |  BT_GATT_CHRC_NOTIFY | BT_GATT_CCC_NOTIFY, \ BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, read_stepcount, write_stepcount, \ &stepcount_value)

static void step_changed(const struct bt_gatt_attr *attr,
                                 uint16_t value)
{   if (value==BT_GATT_CCC_NOTIFY)
    {
       printk("Subscribed\n");
       Subscribed = 1;
    }
    else
    {
        printk("Not Subscribed\n");
        Subscribed = 0;
    }
}



// ********************[ Service definition ]********************
#define BT_UUID_CUSTOM_SERVICE_VAL	BT_UUID_128_ENCODE(1, 2, 3, 4, (uint64_t)0)
static struct bt_uuid_128 my_service_uuid = BT_UUID_INIT_128( BT_UUID_CUSTOM_SERVICE_VAL);
BT_GATT_SERVICE_DEFINE(my_service_svc,
	BT_GATT_PRIMARY_SERVICE(&my_service_uuid),
		BT_GATT_CHAR1,
        BT_GATT_CCC(step_changed,BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)

Changes are highlighted in bold. What are the changes all about?

BT_GATT_CCC_NOTIFY : configures the attribute to send notifications if there are changes to attribute values.

Function step_changed : This function is a callback that is activated when there is a change to the subscription status of a BLE client. It updates a global variable called Subscribed which is used to later to determine whether a ble_gatt_notify event is sent.

BLE_GATT_CCC : This macro defines what looks like a new attribute of the characteristic BT_GAT_CHAR1 (the step counter). This attribute can be read and written by the client device to enable or disable notifications. When a BLE client subscribes or unsubscribes from notifications for this characteristic, the function step_changed is called (CCC stands for Client Characteristic Configuration).

All of the above took quite some time and I hope the description is of some help to other lost souls. Code is available over here on github. This will grow as a port the rest of my examples.

Sprites, tiles, motion and transparency for DMB2022

All of the games I have written to date for embedded systems such as Breadboard Games and badges featured a uniform background which was usually black. This was simple to implement . If a character is to be moved, it is first overwritten with the background colour and then redrawn at a new location. More complex backgrounds that featured terrain such as grass or rock were not so easy to deal with. In a desktop programming environment I would probably tackle the problem as follows:

(1) Make a copy of the area that the character will obscure

(2) Draw the character

If a character is then to be moved, you simply write the copy of the obscured area to the screen to hide the character and then repeat the process at a new location.

Step (1) requires a readable display buffer. The ST7789 in this project is a write-only device. In theory I could make a frame buffer big enough to hold the entire screen and then repeatedly write this out over the SPI interface. The display has 240×240 pixels, each pixel encoded in 16 bits. A complete framebuffer for this requires 240x240x2 = 115200 bytes. The NRF52833 MCU driving the display has 128kB of RAM so not much would be left over for stack and variables. Dividing the display up into “tiles” can greatly reduce the RAM requirements.

Let’s divide the screen up into 30×30 pixel tiles. This may seem a little large however the ST7789 screen is very small and has a high pixel density. A 30×30 tile represents an area of 3mm x 3mm approx within which a texture or character is drawn. Let’s also use the following two tiles:

The left tile represents a character in a game (surrounded by transparency), the right one represents grass on the ground. We can completely fill the screen with 64 grass tiles. When our character moves across this background it can cover (at least partially) at most 4 grass tiles. This means that we can make use of a framebuffer in RAM that is 2 tiles x 2 tiles or 60x60x2 bytes in size (7200 bytes). We can move the character across the framebuffer in RAM and then write the framebuffer to the display. If the character moves beyond the edge of the framebuffer we can move where we write the framebuffer to the display and adjust the character’s position within the framebuffer. The image below shows how the framebuffer moves when the character moves diagonally across the screen (the background was not drawn to emphasize the movement).

The character moves across screen as shown in the following video:

https://youtube.com/shorts/gbuUdUqhvyA?feature=share

As can be seen, the movement is smooth and quite fast (it is actually artificially slowed down). There is however a problem: The bounding square of the character is drawn as white which overwrites the background. It would be much better if the background bounding rectangle for the character was treated as transparent.

A slight detour for PNG files.

I’m using KolourPaint in KDE (Kubuntu) to produce the tiles. These images are saved as PNG files with 4 channels: Red, Green, Blue and Alpha. The Alpha channel represents the transparency of a pixel. In this case I’m concerned with two levels of this: completely opaque (Alpha=255) and completely transparent (Alpha = 0). These PNG files are converted to C header files which encode the RGB values into a 16 bit colour value suitable for use with the ST7789. As mentioned in a previous blog post, the ST7789 uses 565 (RGB) encoding. A value of 11111 111111 11111 represents white. A slightly different value of 11111 111110 11111 (the LSB of the green channel is set to 0) looks nearly the same on the screen. I decided that the colour value of 11111 111111 11111 (65535) would represent “transparent” while any value that was meant to be white would be re-encoded as 11111 111110 11111 (65503). The following python script was then used to convert the png file to a C header file.

# Want to deal with transparency.  Need to nominate a particular colour as "transparent"
# Going to go with 0xffff as being transparent
# if a pixel is designed to be this colour it will be changed to 
# 0b11111 111110 11111 
# i.e. the least significant green bit will be set to 0.  This is slightly off the intended white
# but not by much.
import sys
Filename=sys.argv[1]
Forename=Filename.split(".")[0]
from PIL import Image
img=Image.open(Filename)
width, height = img.size
pixels = list(img.getdata())
print("#define ",end="")
print(Forename,end="")
print("_width ",end="")
print(width)
print("#define ",end="")
print(Forename,end="")
print("_height ",end="")
print(height)
print("static const uint16_t ",end="")
print(Forename,end="")
print("[]={")
for x in range(0,width):
	
	for y in range (0, height):		
		(Red,Green,Blue,Alpha) = pixels[(x*height)+y]
		# Colour format : Red : 5 bits, Green 6 bits, Blue 5 bits
		# Assuming all components are in range 0 - 255
		if (Alpha == 255):
			Red = Red >> 3 # discard 3 bits
			Blue = Blue >> 3 # discard 3 bits
			Green = Green >> 2 # discard 2 bits
			st7789_16 = (Red << 11) + (Green << 5) + Blue
			low_byte = st7789_16 & 0xff
			# have to do an endian swap
			high_byte = st7789_16 >> 8
			st7789_16 = (low_byte << 8) + high_byte		
			if (st7789_16 == 0xffff):
				st7789_16 = 0b1111111111011111
			print(st7789_16, end="")			
		else:
			print("65535", end="")
		print(",")		
print("};")	

The framebuffer output functions were adapted to take this special transparent colour into account and the new motion now looks like this:

https://youtube.com/shorts/Vsr7n7rsNRI?feature=share

Once again, motion looks smooth (and has been artificially slowed down).

Code for all of this is in quite an untidy state for now but will be uploaded to github over the next couple of weeks

Testing an NRF52833 Module

The (unofficial) Dublin Maker Badge for 2022 will hopefully be based on an NRF52833 module. I got hold of couple and asked a colleague to make a breakout PCB for it to allow me experiment.

The PCB is a little rough but is OK for evaluation purposes. I won’t be using the inner layer of contacts for the module as I can’t solder on to them manually however I should have enough pins in what remains. Luckily the extremely flexible NRF52833 allows for routing of signals to just about any pin. Next step is to the module to the board.

The drill holes for the pin headers have more or less wiped out all of the pads for the pin headers but with some careful soldering I think I got everything wired up. Next step Blinky!

VDD and VDDH are connected to a 3.3V DC supply; the SWD interface is connected to a JLink EDU probe. A simple Zephyr based LED Blinky program was downloaded and tested. The JLink software complained a little about the SWD interface being unstable and it automatically dropped its speed to a lower value. Blinky seemed to work fine; how about a simple BLE example I previously used on the BBC Microbit V2? Well that worked fine too without any changes 🙂

Next step: Interface with a display to see if the SPI interface can be operated at full speed.

Zephyr and the BBC Microbit V2 Tutorial Part 3: I2C

The BBC Microbit V2 includes an LSM303AGR IC. This can sense acceleration and magnetic fields in three dimensions. It’s typical use is for tracking orientation and direction of motion. It communicates with the NRF52833 using an I2C (Inter-Integrated Circuit) serial bus. This has two signal lines as shown in the extract from the schematic below:

I2C_INT_SCL : Serial clock line for internal I2C communications

I2C_INT_SDA : Serial data line for internal I2C communications.

The phrase “internal” is used as these signals are not brought out to the edge connector and are used for on-board communications only.

A third connection called I2C_INT_INT allows the LSM303 interrupt the NRF52833 when something “interesting” happens e.g. the device is dropped or tapped.

The trace below shows the a data exchange between the NRF52833 and the LSM303. The I2C_INT_SCL line is used to pace the transmission and reception of data. This signal is generated by the NRF52833 which is playing the role of a master or controller device; the LSM303 is a slave or peripheral device. The trace shows two signals and also a higher level interpretation of what is going between the devices.

The I2C transaction begins with a Start signal. This is a High-Low transition of the SDA line when the SCL is high. Next follows an address value. Each I2C device has a manufacturer set 7 or 10 bit address, this discussion will deal with 7 bit addresses only. Several different devices can exist on the same I2C bus. When a controller wishes to communicate with a peripheral it must first send the peripheral’s address. Other I2C devices on the bus effectively disconnect from the bus for the remainder of that transaction. An additional bit is added just after the 7 bit address called the Read/Write bit. If this bit is a 0 then a write transaction is about to take place; if it’s a 1 then a read operation will follow. Altogether then the controller sends 8 bits at the start of a transaction : the 7 bit address and an R/W bit. It then sets it’s SDA signal to a high impedance state so that it can listen for a returning signal from the peripheral.

If a peripheral on the I2C bus recognizes the 7 bit address it pulls the SDA line to 0. This is an Acknowledge signal. If there is no peripheral with that address then the SDA line will remain high.

The LSM303 has a number of internal registers. Typical interactions with it involve reading from and writing to these registers. Immediately following the I2C addressing phase the register number of interest is transmitted. In the above trace this value is hex AA. The slave acknowledges this write. The transaction shown above actually consists of two parts. The first part writes the register number of interest to the LSM303, the second part reads data from that register (and the one following). The controller indicates this by sending a start signal again (labelled Sr = repeated start). If that had been the end of the transaction the controller would have sent a stop signal.

The last phase of this transaction is a read of the registers within the LSM303. Once again the I2C address is sent but this time the least significant bit i.e. the R/W bit is a 1 to indicate that a register read is required. Following the Ack from the peripheral the controller puts its SDA line into a high impedance or listening state and outputs 16 clock pulses. The peripheral takes control of the SDA line and sends back two bytes of data. A Negative Acknowledge or NACK signal is sent by the controller to indicate that the transaction is done. It follows this by sending a Stop signal (a low to high transition of the SDA line when the SCL line is high).

Overall the transaction in the graph above is a read of registers 0x2A and 0x2B in the LSM303. These contain the low and high byte values for the Y acceleration. Each byte could have been read separately (slower) but the LSM303 allows you read multiple bytes in successive registers by setting the most significant bit of the register number to a ‘1’ so register number 0xAA represents a transaction involving register number 0x2A and subsequent registers within the LSM303.

The code for interfacing with the LSM303 is shown below. Inside the function lsm303_ll_begin a pointer to the I2C device structure for device I2C_1 is obtained. The code then attempts to read a particular register within the LSM303 which contains a known value. This is a mechanism to allow us check to see whether the device is actually on the bus. In this case, the register number in question is 0x0f and it should contain the value 0x33 (decimal 51). If all of this works ok the function then sets the operating mode of the accelerometer : +/- 2g range with 12 bit resolution.

The function lsm303_ll_readAccelY performs the transaction shown in the I2C trace above. It makes use of the Zephyr I2C API function i2c_burst_read. This function takes the following arguments:

A pointer to the I2C device

The address of the I2C peripheral

The number of the register from which you want to read

A pointer to a receive buffer

A count of the number of bytes required.

Two other function lsm303_ll_readRegister and lsm303_ll_writeRegister facilitate reading and writing of single bytes from/to registers.

Note the way lsm303_ll_readAccelY combines the low and high bytes and scales them to a value of acceleration scaled up by a factor of 100.

static const struct device *i2c;
int lsm303_ll_begin()
{
	int nack;
	uint8_t device_id;
	// Set up the I2C interface
	i2c = device_get_binding("I2C_1");
	if (i2c==NULL)
	{
		printf("Error acquiring i2c1 interface\n");
		return -1;
	}	
	// Check to make sure the device is present by reading the WHO_AM_I register
	nack = lsm303_ll_readRegister(0x0f,&device_id);
	if (nack != 0)
	{
		printf("Error finding LSM303 on the I2C bus\n");
		return -2;
	}
	else	
	{
		printf("Found LSM303.  WHO_AM_I = %d\n",device_id);
	}
	lsm303_ll_writeRegister(0x20,0x77); //wake up LSM303 (max speed, all accel channels)
	lsm303_ll_writeRegister(0x23,0x08); //enable  high resolution mode +/- 2g
	
	return 0;
}

int lsm303_ll_readAccelY() // returns Temperature * 100
{
	int16_t accel;
	uint8_t buf[2];
	buf[0] = 0x80+0x2a;	
	i2c_burst_read(i2c,LSM303_ACCEL_ADDRESS,0xaa, buf,2);
	accel = buf[1];
	accel = accel << 8;
	accel = accel + buf[0];
	accel = accel / 16; // must shift right 4 bits as this is a left justified 12 bit result
	// now scale to m^3/s * 100.
	// +2047 = +2g
	int accel_32bit = accel; // go to be wary of numeric overflow
	accel_32bit = accel_32bit * 2*981 / 2047;
    return accel_32bit;    
}

int lsm303_ll_readRegister(uint8_t RegNum, uint8_t *Value)
{
	    //reads a series of bytes, starting from a specific register
    int nack;   
	nack=i2c_reg_read_byte(i2c,LSM303_ACCEL_ADDRESS,RegNum,Value);
	return nack;
}
int lsm303_ll_writeRegister(uint8_t RegNum, uint8_t Value)
{
	//sends a byte to a specific register
    uint8_t Buffer[2];    
    Buffer[0]= Value;    
    int nack;    
	nack=i2c_reg_write_byte(i2c,LSM303_ACCEL_ADDRESS,RegNum,Value);
    return nack;
}

These functions can be used as follows to send the Y acceleration to the serial port

void main(void)
{
	int ret;
		
	ret = lsm303_ll_begin();
	if (ret < 0)
	{
		printf("\nError initializing lsm303.  Error code = %d\n",ret);	
		while(1);
	}
	while(1)
	{    
        printf("Accel Y (x100) = %d\n",lsm303_ll_readAccelY());
         k_msleep(100);

	}
}

This particular example illustrates a low level (hence the “ll” in the function names) transaction with the LSM303 over the I2C bus. Zephyr includes a driver for the LSM303 (and may other I2C devices) which performs initialization and scaling.

The following modifications have to be made to prj.conf to trigger the inclusion of the I2C driver into the program:

CONFIG_STDOUT_CONSOLE=y
CONFIG_GPIO=y
CONFIG_I2C=y

Also, app.overlay needs to be modified so that the I2C interface is enabled and pins are assigned to it as follows:

&i2c1 {
        compatible = "nordic,nrf-twim";
        status = "okay";
        sda-pin = < 16 >;   // P0.16 = I2C_INT_SDA
        scl-pin = < 8 >;    // P0.8 = I2C_INT_SCL
};

Full code for this example can be found over here on github.

Zephyr and the BBC Microbit V2 Tutorial Part 2 : Analogue input and output

The NRF52833 has a 12 bit, 8 channel Analogue to Digital Converter (ADC) which allows it to convert signals from analogue sensors into numbers that can be used in calculations. The NRF52833 does not have a Digital to Analogue Converter (DAC) (this is quite common for microcontrollers). Instead it fakes an analogue output capability by doing Pulse Width Modulation (PWM) i.e. by sending a square wave to an output pin and varying the percentage time the pin is high. This allows it to control the average output voltage on that pin. A simple RC filter can be used to filter out the pulses and leave a variable output voltage.

Reading an analogue input

In this example an analogue input is sent to RING0 (there are 5 holes on the Microbit that are designed to accept banana plugs. Ring 0 is the left-most hole when the board is viewed from the speaker side). The RING0 input is connected to Port 0 bit 2 (P0.2) which is also referred to as AIN0 (analogue input 0) in the NRF52833 data sheet. This pin must be configured for operation as an analogue input. This is done using an adc_channel_cfg as shown below.

/*
 * The internal voltage reference is 0.6V
 * The permissable gains are 1/6, 1/5, 1/4, 1/3, 1/2, 1, 2 and 4
 * If a gain of 1/5 is selected then the ADC range becomes 0 to 3V
 */
static const struct device *adc;
// Will read from analog input on P0.2 which is RING 0 on the microbit v2
#define ADC_PORT_BIT 2
struct adc_channel_cfg channel_cfg = {
		/* gain of 1/5 */
		.gain = ADC_GAIN_1_5,
		/* 0.6V reference */
		.reference = ADC_REF_INTERNAL,
		/* sample and hold time of 3us is the default (0 setting) on the NRF52833 */
		.acquisition_time = ADC_ACQ_TIME_DEFAULT,
		/* Channel 0 */
		.channel_id = 0,
		/* AIN0 is specified by setting input_positive to 0+1 i.e. an offset of 1  */
		/* This is as a result of the way the PSELP and PSELN registers work in the NRF52 series of devices */
		/* see page 375 of the NRF52 product specificatoin version : 4452_021 v1.3 */
		.input_positive = 1,
		/* Using single ended conversions */
        .differential = 0
};

int adc_begin()
{
	int ret;
	// Configure the GPIO's 	
	adc=device_get_binding("ADC_0");
	if (adc == NULL)
	{
		printf("Error acquiring ADC \n");
		return -1;
	}
	ret = adc_channel_setup(adc, &channel_cfg);
	if (ret < 0)
	{
		printf("Error configuring ADC channel 0\n");
		return -2;
	}		
	return 0;
}

The NRF52833 can use an internal voltage reference of 0.6V as a basis for ADC conversions as well as fractions of the supply voltage. We will use 0.6V as this is independent of the power supply voltage. Each ADC channel can be scaled by an amplifier. This scaling factor is called “gain” and allows us to control the measurable input voltage range. For example, with a gain of 1 and a voltage reference of 0.6 the ADC will produce its maximum digital output value ((2^12) -1 = 4095) when the input is just 0.6V. If we apply a gain of 1/5 then the measurable input voltage range extends to 3V.

The ADC is of the successive-approximation variety and as such, it requires a stable input voltage during the conversion process. A sample-and-hold circuit (a little capacitor) is used to take a snapshot of the input voltage which is then converted. Capacitors take time to charge and it can happen that insufficient time is allowed for this in which case the snapshot will be different to the actual input voltage at that instant. We can avoid this by allowing a long charging period however this reduces the maximum sampling rate. The acquisition_time field of the adc_channel_config structure allows you control this charging period. It is set to the default of 3 microseconds above.

The channel_id field of the adc_channel_config is used to “name” a particular ADC channel. It is a logical name as opposed to a physical channel in the case of the NRF52833. We associate this adc channel with a particular analogue input using the input_positive field. If we want to use AIN0 this field should be set to ‘1’, for AIN1 this should be 2 etc. i.e. one more than the analogue input channel number as described in the NRF52833 datasheet. The reason for the addition of ‘1’ is to do with the way registers are programmed in this particular microcontroller.

The adc_begin function gets a device structure pointer for the ADC and configures a single channel for use.

To make a reading from the ADC we have to pass an adc_sequence structure to the adc_read

static int16_t channel_0_data;  // This will hold the adc result

struct adc_sequence sequence = {        
		/* This is a bitmask that tells the driver which channels to convert : bit n = 1 for channel n */		
		.channels    = (1 << 0),
		/* Where will the data be stored (could be an array if there are multiple channels to convert */
		.buffer      = &channel_0_data,
		/* buffer size in bytes, not number of samples */
		.buffer_size = sizeof(channel_0_data),
		/* 12 bit resolution */
		.resolution  = 12,
		/* nulls for the rest of the fields */
		.options = NULL,
		.calibrate = 0,
		.oversampling = 0,        
};
int adcread()
{
	int ret;
	ret = adc_read(adc, &sequence);	
	return channel_0_data;
}

In our case we are doing a sequence of 1 conversion so a single 16 bit result is stored to the channel_0_data variable. The address and size of an array can be passed here instead if multiple samples are to be taken.

Analogue output

As mentioned above, the NRF52833 does not have a DAC so it uses PWM instead to simulate a continuously variable analogue output. This requires us to add a couple of elements to our project. We need C functions to initialize the PWM output and also to send values to it as shown below

static const struct device *pwm;
int pwm_begin()
{
	int ret;
	// Configure the GPIO's 	
	pwm=device_get_binding("PWM_0");
	if (pwm == NULL)
	{
		printf("Error acquiring PWM interface \n");
		return -1;
	}
	return 0;
}
int pwm_write(uint16_t value)
{
	
	return pwm_pin_set_usec(pwm,3,PWM_PERIOD_US,value,0);
}

The pwm_begin function acquires a pointer to the device structure for the PWM_0 device. The pwm_write function takes a single argument which is the number of microseconds the associated output pin should be high in each PWM cycle. The constant PWM_PERIOD_US in this example is set to 100 so the incoming parameter to this function should be in the range 0 to 100. The pwm_pin_set_usec function takes 5 arguments:

A pointer to the PWM device structure

The pin number that is to be controlled

The PWM period expressed in microseconds

The PWM high-time expressed in microseconds

A “flags” value which can be used to set the PWM output polarity (0 works fine here)

I have chosen to use P0.3 as the PWM output pin. This is connected to RING1 on the BBC microbit which makes it easy to use with banana plugs. The PWM output can be routed to other pins but I have found that not all of them work (probably due to being configured for use with other peripherals by the OS).

The pwm_begin function acquires a pointer to the device structure for the PWM_0 device. The pwm_write function takes a single argument which is the number of microseconds the associated output pin should be high in each PWM cycle. The constant PWM_PERIOD_US in this example is set to 100 so the incoming parameter to this function should be in the range 0 to 100. The pwm_pin_set_usec function takes 5 arguments:

A pointer to the PWM device structure

The pin number that is to be controlled

The PWM period expressed in microseconds

The PWM high-time expressed in microseconds

A “flags” value which can be used to set the PWM output polarity (0 works fine here)

I have chosen to use P0.3 as the PWM output pin. This is connected to RING1 on the BBC microbit which makes it easy to use with banana plugs. The PWM output can be routed to other pins but I have found that not all of them work (probably due to being configured for use with other peripherals by the OS).

The app.overlay file.

The analogue input and output routines shown above require an additional file be created in the project directory: app.overlay. This file can override and add to settings in the default device tree (dts) file for this device which is to be found in zephyr/boards/arm/bbc_microbit_v2/bbc_microbit_v2.dts. In this file, the adc and pwm devices are disabled. Also, there are no pins assigned to the PWM subsystem. We can fix all of this with the following app.overlay file:

&adc {
	status = "okay";
};
&pwm0 {
	status = "okay";
	ch0-pin = <3>; // P0.3 is labelled RING1 on the microbit. (connected to pin 1 in breakout board)
};

Putting it all together

The following main function reads a value from the ADC and writes a proportional value to the PWM system. The average output voltage should therefore track the input voltage (it will be a little higher because the output switches between 0 and 3.3V. If the input voltage is 3V then the duty will be 100% resulting in an output voltage of 3.3V)

void main(void)
{
	int ret;
	ret = adc_begin();	
	if (ret < 0)
	{
		printf("\nError initializing adc.  Error code = %d\n",ret);	
		while(1);
	}
	ret = pwm_begin();	
	if (ret < 0)
	{
		printf("\nError initializing PWM.  Error code = %d\n",ret);	
		while(1);
	}
	while(1)
	{       
		uint32_t adcvalue = adc_readDigital();
		printf("ADC Digital = %u\n",adcvalue);
		/* The default version of printf does not support floating point numbers so scale up to an integer */
		printf("ADC Voltage (mV) = %d\n",(int)(1000*adc_readVoltage()));
		pwm_write((adcvalue * PWM_PERIOD_US)/4095);
		k_msleep(100);
	}
}

Full source code is available over here on github

Using a BMP280 with the BBC Microbit V2 and Zephyr OS

I wanted to continue my investigation of the Microbit V2 and Zephyr by adding an external I2C device. The BMP280 module I chose is able to report back temperature and atmospheric pressure. I thought it would be nice to combine this with the earlier ST7789 example to produce a live reading of temperature and pressure on the display. This required a slight modification to the ST7789 setup as it is not possible to use both I2C1 and SPI1 in the same NRF52833 (microbit) project. This was easily fixed as the NRF52833 has SPI interfaces 0 to 2. I chose SPI1 which led me to write the following app.overlay file:

&spi2 {
 compatible = "nordic,nrf-spi";
 status = "okay";
 sck-pin = <17>;
 mosi-pin = <13>;
 /* Redirecting MISO to a pin that is not connected on the microbit v2 board */
 miso-pin = <27>;
 clock-frequency = <1000000>;
};
&i2c1 {
	compatible = "nordic,nrf-twim";
	status = "okay";
	sda-pin = < 0x20 >; // P1.0 = pin reference 32+0 = I2c_EXT_SDA
	scl-pin = < 0x1a >; // P0.26 = pin reference 0x1a = I2C_EXT_SCL
};

Code is available over here on github.

Adding an ST7789 display to my Microbit V2 and Zephyr setup

I wanted to learn about using an external SPI device with the BBC Microbit V2. I ported my ST7789 library over to a Zephyr based program shown running on the Microbit and it is shown in operation above. The SPI interface runs at a fairly slow 8MHz which I believe (for now) is the maximum for this interface. As a result, screen updates are not super quick but probably good enough for a simple user interface.

The display library supports the following functions:

int display_begin();
void display_command(uint8_t cmd);
void display_data(uint8_t data);
void display_openAperture(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
void display_putPixel(uint16_t x, uint16_t y, uint16_t colour);
void display_putImage(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t *Image);
void display_drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t Colour);
int iabs(int x); // simple integer version of abs for use by graphics functions
void display_drawRectangle(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t Colour);
void display_fillRectangle(uint16_t x,uint16_t y,uint16_t width, uint16_t height, uint16_t colour);
void display_drawCircle(uint16_t x0, uint16_t y0, uint16_t radius, uint16_t Colour);
void display_fillCircle(uint16_t x0, uint16_t y0, uint16_t radius, uint16_t Colour);
void display_print(const char *Text, uint16_t len, uint16_t x, uint16_t y, uint16_t ForeColour, uint16_t BackColour);
uint16_t display_RGBToWord(uint16_t R, uint16_t G, uint16_t B);

Code is available over here on github.

Zephyr OS on the BBC Microbit V2

In previous years I used mbed OS to program the BBC Microbit (V1). As far as I can tell, the V2 board is not supported in mbed’s web compiler (yet?). So I began to look around at alternatives operating systems that would help me develop BLE peripheral applications. I considered install size and system requirements and decided that Zephyr looked like a good fit. I have begun writing examples for the various peripherals on the microbit v2 source code for which is over on github.

You will need to install zephyr to compile these. I found that the Getting started guide worked well. I compile my examples within the zephyrproject/zephyr directory (copy them from github to here) with the following command:

west build -b bbc_microbit_v2 magnetometer_serial_microbit_v2 –pristine

This will wipe the build directory and recompile the magnetometer example. Change “magnetometer_serial_microbit_v2” to one of the other directory names when you want to try them out. The output from the application on the microbit is sent to UART at 115200 bps.

I’m using Zephyr SDK version 0.12.4