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.

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.