The BBC Microbit V2 and OpenThread

This is an initial posting about early progress I have made with the BBC Microbit V2 and OpenThread. Nordic Semiconductor has posted some good example code for the NRF52840 dongle and development kit. These examples involve the Zephyr operating system and work pretty well. In particular, the echo server example is easy enough to build and deploy on an NRF52840 dongle or XIAO NRF52840 board. This can then be controlled over IPv6 as I mentioned in a previous post.

Compiling the same code for the BBC Microbit V2 initially did not work. Setting the board type to nrf52833dk_nrf52833 (the same IC that is in the Microbit) allows compilation to work but flash programming the device is difficult. I was looking for a way to do this by setting the board type to bbc_microbit_v2. The code would build, flash but not run. It seems that the configuration files for the Microbit V2 in Zephyr do not enable the 802.15.4 radio required by the Thread network. I discovered that this could be enabled by adding an app.overlay file to the project root directory with the following contents:

/ {
	chosen {
		
		zephyr,ieee802154 = &ieee802154;
	};
};
&ieee802154 {
	status = "okay";
};

Compiling, flashing and running the echo_server example worked after adding this.

The next part of the journey was to add some Microbit specific I/O. I thought it would be nice to control the onboard LED matrix over the network. The echo_server code is a little complex and perhaps daunting for people starting out. I modified it a little so that a beginner could concentrate on a single C file which would handle UDP packets and I/O. This file is called usb_processor.c and is shown below:

#include <stdint.h>
#include <zephyr/logging/log.h>
#include <zephyr/kernel.h>
#include <errno.h>
#include <stdio.h>
#include <zephyr/net/socket.h>
#include <zephyr/drivers/gpio.h>
#include "matrix.h"

int initIO()
{
    int ret=0;
    ret=matrix_begin();
    return ret;
}
void udp_send_receive(uint8_t *buffer, uint32_t len)
{
    // Message is assumed to be at least 4 bytes long (a kluge for now!)
    // print the message out for debugging purposes
    if (len)
    {
        int index=0;
        while(index < len)
        {
            printk("%x ",buffer[index]);
            index++;
        }
    }
    matrix_put_pattern(buffer[0],buffer[1]);
    // pass some data back to the sender
    buffer[2]='a';
    buffer[3]='b';
}

The function initIO configures I/O devices (the LED matrix in this case – see matrix.c in the github link provided below). The function udp_send_receive is called when a UDP packet is received. In this primitive example, the first two bytes are treated as row and column bit masks for the LED matrix. The values are passed on to matrix_put_pattern. Just before returning, two characters are placed in the return packet just to verify that communications is bidirectional.

The NodeJS code that sends data to the Microbit is shown below:


var udp=require('dgram');
// -------------------- udp client ----------------

var buffer = require('buffer');

// creating a client socket
var client = udp.createSocket('udp6');

//buffer msg

client.on('message',function(msg,info){
  console.log('Data received from server : ' + msg.toString());
  console.log('Received %d bytes from %s:%d\n',msg.length, info.address, info.port);
});
//sending msg
    
var data = Buffer.from([0x1f,0x0,0x32,0x33]);

client.send(data,4242,'fd96:5e1e:4749:1:1fdb:ff05:1113:b755',function(error){
    if(error){
        client.close();
    } else{
        console.log('Data sent !!!');
    }
});

The IPv6 address of the Microbit has been hard-coded for now (working on network discovery next). The payload received by the Microbit is prepared in the data Buffer object. The first byte selects which rows are to be activated in the LED matrix (there are 5 of them). The second byte is selects which columns are active. In the case of columns, a ‘0’ in a particular bit activates that column.

Code (such as it is) is over here on github

A closing note for now: This is tricky stuff to set up and get working. I should probably put together a post that details the entire process of setting this up and running. In the meantime, if you have questions send me an email

Driving a silicon carbide MOSFET

A colleague and I are starting to experiment with silicon carbide MOSFETs. The one we have chosen to go with is from Wolfspeed. We want to drive this using an STM32G431 MCU so we opted to use an STGAP2HD isolated gate drive. This is a surface mount part so, for prototyping and evaluation purposes we build a breakout PCB

I wrote some code to generate a 130kHz PWM output signal and the driver PCB was hooked up to the power MOSFET. A long-distance shot of the system operating is show below. The blue trace on the oscilloscope is the gate drive signal while the yellow trace is the drain-source voltage. It has lots of overshoots and oscillations because of the poor layout and the inductance of the load resistor. Anyway, we are now happy that the STGAP2HD driver can actually drive our MOSFET. Next step: MUCH better layout and assemble/test one arm of an inverter bridge.

OpenThread experiments

I have been experimenting with OpenThread using a RaspberryPi with Nordic 52840 Dongle and a pair of Xiao BLE modules.

The topology looks like this:

What does this let me do? As it stands I can ping either of the Xiao BLE devices from any computer on my network using IPv6. The RaspberryPi+NRF52840 dongle behave as a border router and bridges between the OpenThread/6LowPan network and the wired Ethernet.

This has not been entirely straightforward so far. The XIAO-BLE devices are running Zephyr’s sample echo_server built with this command line:

west build -b xiao_ble echo_server -- -DCONF_FILE="prj.conf overlay-ot.conf" 

Prior to this, the project configuration file (prj.conf) was modified as follows:

# Generic networking options
CONFIG_NETWORKING=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_IPV6=y
CONFIG_NET_IPV4=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_SOCKETS_POSIX_NAMES=y
CONFIG_POSIX_MAX_FDS=6
CONFIG_NET_CONNECTION_MANAGER=y

# Kernel options
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_ENTROPY_GENERATOR=y
CONFIG_TEST_RANDOM_GENERATOR=y
CONFIG_INIT_STACKS=y

# Logging
CONFIG_NET_LOG=y
CONFIG_LOG=y
CONFIG_NET_STATISTICS=y
CONFIG_PRINTK=y

# Network buffers
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_PKT_TX_COUNT=16
CONFIG_NET_BUF_RX_COUNT=64
CONFIG_NET_BUF_TX_COUNT=64
CONFIG_NET_CONTEXT_NET_PKT_POOL=y

# IP address options
CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3
CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4
CONFIG_NET_MAX_CONTEXTS=10

# Network shell
CONFIG_NET_SHELL=y
CONFIG_SHELL=y

# Network application options and configuration
CONFIG_NET_CONFIG_SETTINGS=y
CONFIG_NET_CONFIG_NEED_IPV6=y
#CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::3"
#CONFIG_NET_CONFIG_PEER_IPV6_ADDR="2001:db8::1"
#CONFIG_NET_CONFIG_NEED_IPV4=y
#CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1"
#CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2"

# Number of socket descriptors might need adjusting
# if there are more than 1 handlers defined.
CONFIG_POSIX_MAX_FDS=12

# How many client can connect to echo-server simultaneously
CONFIG_NET_SAMPLE_NUM_HANDLERS=1

CONFIG_OPENTHREAD_DHCP6_SERVER=y
CONFIG_OPENTHREAD_SLAAC=y
CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=6
# need to add this so that the module can join the thread network
CONFIG_OPENTHREAD_JOINER=y

This creates a file called Zephyr.uf2 which is copied to the XIAO devices when they are in UF2 bootloader mode (press the button on them twice).

The NRF52840 dongle code was obtained from https://github.com/openthread/ot-nrf528xx/blob/main/src/nrf52840/README.md

This was compiled as follows:

./script/build nrf52840 USB_trans -DOT_BOOTLOADER=USB
arm-none-eabi-objcopy -O ihex build/bin/ot-rcp ot-rcp.hex
nrfutil pkg generate --hw-version 52 --sd-req=0x00 --application ot-rcp.hex --application-version 1 ot-rcp.zip
nrfutil dfu usb-serial -pkg ot-rcp.zip -p /dev/ttyACM0

(The NRF52840 dongle has to be in DFU mode for the last line to work)

Finally, the raspberry pi 3 had to be set up. This is running Raspbian and the OpenThread border router services were obtained by cloning https://github.com/openthread/ot-br-posix, building and compiling (and much fiddling about!).

Where to from here? Well, the whole point of this experiment is to compare the BLE/GATT/GAP approach to IoT to one using IPv6 and “traditional” network function calls.

Breadboard Games 2022 Assembly

Note on board co-ordinates:

There are two sets of co-ordinates shown on the board. We will be using the ones that are the “right way up” i.e. the ones shown on the left side of the image. Some of the images can be a little misleading because of the angle the photos were taken. This is particularly true for the red and black wires at the top left of the image. The red wire connects to 3V3 to any hole just above the red line. The black wire connects GND to any hole just below the blue wire.

Display wiring:

White : Column 32, Row A to any hole just above the red line (keep it left of Column 30)

Purple : Column 31, Row A to GP22

Pink : Column 30, Row A to GP20

Blue : Column 29, Row A to GP21

Orange : Column 28, Row A to GP19

Yellow : Column 27, Row A to GP18

Red : Column 26, Row A to any hole just above the red line (3.3V)

Black : Column 25, Row A to any hole just below the blue line (0V)

Buttons: Place as shown.

Black wires (0V) These are used connect GND (0V) signals together.

Link wires between buttons

Column 42, Row E to Column 43, Row D

Column 42, Row F to Column 43, Row G

Button wiring. Try to get at least some of the button wires into the channel between the top and bottom halves of the breadboard.

Brown : GP26 to Column 55, Row A

Grey : GP27 to Column 50, Row A

Purple : GP17 to Column 14, Row E

Buzzer: GP9 to any hole just below the blue line

Yellow: GP16 to Column 36, Row C

White : GP15 to Column 45, Row C

Green: GP14 to Column 40, Row F

WARNING : ASK FOR ASSISTANCE WITH THIS. DISPLAY ARE FRAGILE!

Fit the display as shown in the row of holes just below the display wires. The rightmost pin of the display should line up with the small white wire.

Breaboard games 2022

Breadboard Games 2022 is approaching. This year we will have 16 students assemble the game console shown above. The console consists of a Raspberry Pi Pico linked to an ST7735, some buttons and a piezo speaker. The schematic is shown below

Two simple games are featured: A version of break out and a simple Santa Clause game. These games are written in Micropython with an emphasis on ease of understanding rather than game play. Nevertheless, the graphics module is reasonably optimized with an inline assembler module that executes very quickly and which is used by API functions as much as possible. Furthermore, the sound module makes use of the second core in the Pico which allows sound to be be played at the same time as the game.

The game sprites are edited in a bitmap editor such as Windows Paint or KolourPaint etc. The sprites are created as 24 bit bitmap (bmp) files. A python script converts these bitmaps to python arrays on the host computer. These arrays are coded as 16 bit RGB (5-6-5) colour values which are compatible with the ST7735. These arrays are then included in a sprite module in the game.

Programs were edited using Thonny which is a nice cross-platform editor that works with a number of operating systems. It can be installed without administrator permissions on Windows and Linux systems.

Code is over here on github.

Speeding up some micropython with a touch of inline assembly on the Raspberry Pi Pico

I have been working on a graphics library for the ST7735 and the Raspberry Ri Rico. The first version was written in pure micropython and worked well enough but was quite slow – especially when writing out blocks of colour (fillRectangle). This was the original code for fillRectangle:

def fillRectangle(self,x1,y1,w,h,colour):
        self.openAperture(x1,y1,x1+w-1,y1+h-1)        
        pixelcount=h*w
        self.command(0x2c)
        self.a0.value(1)        
        msg=bytearray()
        while(pixelcount >0):
            pixelcount = pixelcount-1          
            msg.append(colour >> 8)
            msg.append(colour & 0xff)
        self.spi.write(msg)

Not only was this slow, it also required that a buffer be created that held the filled rectangle in RAM. This was slow and memory intensive.

The new version looks like this:

def fillRectangle(self,x1,y1,w,h,colour):
        self.openAperture(x1,y1,x1+w-1,y1+h-1)        
        pixelcount=h*w
        self.command(0x2c)
        self.a0.value(1)
        self.fill_block(colour,pixelcount) 

It makes use of an inline assembler function the source code of which is as follows:

@micropython.asm_thumb
    def fill_block(r0,r1,r2):
        # pointer to self passed in r0
        # r1 contains the 16 bit data to be written
        # r2 countains count
        # Going to use SPI0.
        # Base address = 0x4003c000
        # SSPCR0 Register OFFSET 0
        # SSPCR1 Register OFFSET 4
        # SSPDR Register OFFSET 8
        # SSPSR Register OFFSET c
        push({r1,r2,r3,r4,r7})
        # Convoluted load of a 32 value into r7
        mov(r7,0x40)
        lsl(r7,r7,8)
        add(r7,0x03)
        lsl(r7,r7,8)
        add(r7,0xc0)
        lsl(r7,r7,8)
        add(r7,0x00)
        mov(r4,2)        
        label(fill_block_loop_start)
        cmp(r2,0)
        beq(fill_block_exit)        
        mov(r3,r1) # read next byte
        lsr(r3,r3,8)
        strb(r3,[r7,8]) # write to SPI
        label(fill_block_spi_wait1)        
        ldr(r3,[r7,0xc]) # read next byte
        and_(r3,r4)
        beq(fill_block_spi_wait1)
        
        mov(r3,r1) # read next byte        
        strb(r3,[r7,8]) # write to SPI        
        sub(r2,r2,1) # decrement count                
        label(fill_block_spi_wait2)        
        ldr(r3,[r7,0xc]) # read next byte
        and_(r3,r4)
        beq(fill_block_spi_wait2)
        b(fill_block_loop_start)
        
        label(fill_block_exit)
        pop ({r1,r2,r3,r4,r7})

This writes the colour value directly to the SPI port the required number of times. It needs to pause when the SPI FIFO fills up (hence he need for the labels fill_block_spi_wait1/2).

The performance improvement is about a factor of 20!

Code is available over on gihub and is likely to change lots in the next couple of weeks while I prepare for a STEM event.

Missing interrupts with Zephyr OS on the Microbit V2.21

The case of the missing interrupts

Recently, we asked our students to buy BBC Microbits for our Internet of Things module. Most of them received version 2.0 of the board. Some however received version 2.21. The main difference between the boards is the USB interface MCU. Version 2.00 used an NXP MKL27Z256VFM4. The 2.21 version changed this to a Nordic Semiconductor NRF52820 device. In most cases, users will not notice the difference between the two boards. However, if you are programming hardware registers directly there are some differences. We use Zephyr OS in our IoT module and work at the hardware level so, for us, this showed up as missing interrupts from the LSM303 Accelerometer/Magnetometer.

Our sample code programs the LSM303 as a step counter. When the accelerometer experiences low acceleration in all 3 axes (i.e. in freefall) it outputs an interrupt signal. This signal pulls the I2C_INT_INT line low (falling edge interrupt trigger). On Version 2.00 boards this worked fine, not so on V2.21 boards. The problem turned out to be that the interface MCU was holding the I2C_INT_INT line low permanently which prevented falling edge interrupts.

The solution

I contacted microbit.org and quickly received a response from Carlos. He pointed me at this site which documents the I2C interface protocol implemented by the interface MCU. This looks quite interesting and will need further exploration at a later date. Carlos suggest that I program the target to perform a dummy read of the interface MCU over the I2C bus. I tried this and it almost solved the problem. Just after programming or after pressing the reset button, the Microbit processed interrupts correctly. After a power on reset however interrupts did not take place. Again Carlos came to the rescue and suggested that I pause the target boot for 1 second before performing the dummy read. This time allows the interface MCU to complete boot up before the dummy read. The result? Interrupts were processed correctly!

The initialization code for the LSM303 motion sensor was modified as shown below:

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)
	{
		printk("Error acquiring i2c1 interface\n");
		return -1;
	}	
	// Fix for version 2.21 of the Microbit.  
	// This code resets the I2C_INT_INT signal coming out of the interface IC (DAPLink)
	// There is an acknowledged bug in the firmware for this IC which leave the interrupt
	// line asserted under certain conditions.  This prevents the LSM303 from raising interrupts
	// A dummy read of the interface IC (I2C address 0x70) deasserts this signal
	// Thanks to Carlos in microbit for this help.
	k_msleep(1000); // allow interface MCU complete booting before dummy read
	uint8_t dummy_value[5];
	nack=i2c_read(i2c,dummy_value,1,0x70);	
	printk("nack=%x\n",nack);
	
	
	// 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)
	{
		printk("Error finding LSM303 on the I2C bus\n");
		return -2;
	}
	else	
	{
		printk("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;
}

Thanks to Carlos Pereira Atencio from the Microbit foundation for lots of help solving this problem.

Full schematics are available here:https://tech.microbit.org/hardware/schematic/

Early experiments with Raspberry Pi Pico, Micropython and an ST7735 LCD

The Raspberry Pi Pico and micropython are new to me. I was curious to see how it would perform with graphics on an ST7735. From what I can see, the SPI output is just as fast as any other language however the preparation of SPI data is slower. I tried two approaches for writing filled rectangles to the display. The first prepared a bytearray filled with the desired colour values. This bytearray is then sent to the SPI bus with a single write command. From what I could see, the write is at full SPI speed with no gaps between bytes. The filling of the bytearray was a little slow.

The second approach involved sending the colour to the display in 2 byte blocks SPI. This eliminated the in-memory bytearray however it added a lot of CPU overhead. In the end I stayed with the first approach for now. I expect that I will be coming back to this in the future.

My initial demo was developed using the Thonny editor which is quite nice and looks like it may be suitable for STEM workshops.

The main function looks like this:

from machine import Pin, Timer,SPI
import urandom
import time
import st7735 
display=st7735.st7735()

while(1):
    x1=urandom.randint(0,120)
    y1=urandom.randint(0,150)
    x2=urandom.randint(0,120)
    y2=urandom.randint(0,150)
    w=urandom.randint(1,127-x1)
    h=urandom.randint(1,159-y1)
    colour=urandom.randint(0,65535)    
    display.drawLine(x1,y1,x2,y2,colour)
    



And the ST7735 library (for now) is this:

from machine import Pin, Timer,SPI
import time
class st7735:
    def __init__(self):        
        # configure pins for output
        self.reset=Pin(21,Pin.OUT)
        self.cs=Pin(22,Pin.OUT)
        self.a0=Pin(20,Pin.OUT)
        self.spi=SPI(0,baudrate=20000000,polarity=1,phase=1,bits=8,firstbit=SPI.MSB,sck=Pin(18),mosi=Pin(19))        
        self.cs.value(1)
        self.reset.value(1)
        time.sleep_ms(10)
        
        self.reset.value(0)
        time.sleep_ms(200)
        
        self.reset.value(1)
        time.sleep_ms(200)
        self.cs.value(200)
        time.sleep_ms(200)
        
        self.cs.value(0)
        time.sleep_ms(20)
        
        self.command(1) # software reset
        time.sleep_ms(100)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0x11)
        time.sleep_ms(120)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)        
        time.sleep_ms(1)
        
        self.command(0xb1)
        self.data(0x05)
        self.data(0x3c)
        self.data(0x3c);
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0xb2)
        self.data(0x05)
        self.data(0x3c)
        self.data(0x3c);        
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0xb3)
        self.data(0x05)
        self.data(0x3c)
        self.data(0x3c)
        self.data(0x05)
        self.data(0x3c)
        self.data(0x3c)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0xb4)
        self.data(0x03)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0x36)
        self.data(0xc8)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0x3a)
        self.data(0x05)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0x29)
        time.sleep_ms(100)
        self.cs.value(1)
        time.sleep_ms(1)
        self.cs.value(0)
        time.sleep_ms(1)
        
        self.command(0x2c)          
        self.fillRectangle(0,0,128,160,0)
        
    def command(self,cmd):
        self.a0.value(0)
        msg=bytearray()
        msg.append(cmd)
        self.spi.write(msg)
        
    def data(self,cmd):
        self.a0.value(1)
        msg=bytearray()
        msg.append(cmd)
        self.spi.write(msg)        
    def openAperture(self,x1,y1,x2,y2):
        self.command(0x2a)
        self.data(x1 >> 8)
        self.data(x1 & 0xff)
        self.data(x2 >> 8)
        self.data(x2 & 0xff)
        self.command(0x2b)
        self.data(y1 >> 8)
        self.data(y1 & 0xff)
        self.data(y2 >> 8)
        self.data(y2 & 0xff)        
        self.command(0x2c)
    def fillRectangle(self,x1,y1,w,h,colour):
        self.openAperture(x1,y1,x1+w-1,y1+h-1)        
        pixelcount=h*w        
        self.command(0x2c)
        self.a0.value(1)
        msg=bytearray()
        while(pixelcount >0):
            pixelcount = pixelcount-1           
            msg.append(colour >> 8)
            msg.append(colour & 0xff)
        self.spi.write(msg)
    def putPixel(self,x,y,colour):
        self.openAperture(x,y,x+1,y+1)        
        self.a0.value(1)
        msg=bytearray()
        msg.append(colour >> 8)
        msg.append(colour & 0xff)
        self.spi.write(msg)
    def drawLine(self,x0,y0,x1,y1,colour):
        if ( (abs(y1-y0) < abs(x1-x0))):
             if (x0 > x1):
                 self.drawLineLowSlope(x1,y1,x0,y0,colour)
             else:
                 self.drawLineLowSlope(x0,y0,x1,y1,colour)
        else:
            if (y0 > y1):
                self.drawLineHighSlope(x1,y1,x0,y0,colour)
            else:
                self.drawLineHighSlope(x0,y0,x1,y1,colour)
            
    def drawLineLowSlope(self,x0,y0,x1,y1,colour):
        # Reference : https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm    
        dx = x1 - x0
        dy = y1 - y0
        yi = 1
        if (dy < 0):
            yi = -1
            dy = -dy
        D = 2*dy - dx
        y = y0
        for x in range(x0,x1+1):
            self.putPixel(x,y,colour)
            if (D > 0):
                y = y + yi
                D = D - 2*dx
            D=D + 2*dy
    def drawLineHighSlope(self,x0,y0,x1,y1,colour):
        # Reference : https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm    
        dx = x1 - x0
        dy = y1 - y0
        xi = 1
        if (dx < 0):
            xi = -1
            dx = -dx
        D = 2*dx - dy
        x = x0
        for y in range(y0,y1+1):
            self.putPixel(x,y,colour)
            if (D > 0):
                x = x + xi
                D = D - 2*dy
            D=D + 2*dx
            

Update on porting DM Badge 2022 to Zephyr 3.1

My Dublin Maker Badge 2022 ran on top of Zephyr 2.6. I’ve been working on porting it to Zephyr 3.1. This involved some superficial changes to header files. It also required some deeper changes to the way hardware was accessed (as a result of the requirement to make use of Device Tree). Anyway, I got through all of this and it works fine. I was not so lucky with the Bluetooth mesh communications however. It seems that there are some application key binding differences between 2.6 and 3.1 that escape me. Code over on github has been updated and I will keep working on the mesh communications problem when I get the time.