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.

Dublin Maker Badge PCB design

Well, Dublin Maker is coming up on the 23rd of July this year. The unofficial electronic badge design is coming along well. I think (hope!) that the PCB design is fine and it will be sent off for manufacture soon. You can see the 3D viewer output from KiCad below. U3 is the pin header for the display, U2 is a pin header for the boost converter. U1 is an NRF52833 module. SW7 is shown as a pin header but is in fact an on/off switch with the same pin pitch. A “Simple Add-On” connector (SAO1) is also provided. This nearly conforms to the BadgeLife SAO 1.69 standard. It provides power, I2C and a single GPIO (rather than 2). I ran out of GPIO port bits in the design as I will not be using the pads underneath the NRF52833 module (can’t hand solder them). The IDC socket for the SAO interface will not be populated in the final badge but will instead be left to anyone who cares to solder one on.

All components for the build have now arrived except for the battery cases. These can hold a single AA battery and are mounted on the back of the badge. One last check and it will be off to PCBWay with the design!

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

DMB2022 Graphics subsystem

DMB2022 (Dublin Maker Badge for 2022 (not official)) is an electronic badge consisting of an NRF52833 module and a display. The display for the badge is an ST7789 module. It has an SPI interface that is operated at 32MHz. It is configured to use 16 bit RGB colour values arranged as follows:

5 most significant bits : Red

6 middle bits : Green

5 least significant bits : Blue

The NRF52833’s SPI3 is used to drive the display as SPI0 and SPI1 are limited to 8Mbs. SPI3 can operate up to 32Mbs.

The display.cpp module handles all interaction with the ST7789. Lots of LCD displays of this type have a similar pattern of operations.

At startup, the display is first configured which typically involves bringing the device out of low power mode and configuring its colour mode, orientation and optionally its colour palette. The initialization code achieves this by sending commands and data to the display. The D/C signal tells the display whether a command or data byte is being transmitted. If it is Low then a command is being sent; a High level implies data is being sent.

In order to put pixels on the display, the controlling program must first open up an aperture for drawing in. This aperture can range from the entire display to just a single pixel. Once opened, data can be written to the aperture as a continuous stream of bytes. The display performs a raster like operation on the incoming data with display lines auto-wrapping when the data stream reaches the right-hand side of the aperture.

All of the remaining graphic primitives are built on top of this mechanism (with a few performance optimizations). The display class includes the following functions (for now):

int begin();
	void command(uint8_t cmd);
	void data(uint8_t data);
	void openAperture(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
	void putPixel(uint16_t x, uint16_t y, uint16_t colour);
	void putImage(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t *Image);
	void 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 drawRectangle(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t Colour);
	void fillRectangle(uint16_t x,uint16_t y,uint16_t width, uint16_t height, uint16_t colour);
	void drawCircle(uint16_t x0, uint16_t y0, uint16_t radius, uint16_t Colour);
	void fillCircle(uint16_t x0, uint16_t y0, uint16_t radius, uint16_t Colour);
	void print(const char *Text, uint16_t len, uint16_t x, uint16_t y, uint16_t ForeColour, uint16_t BackColour);
	void print(uint16_t number, uint16_t x, uint16_t y, uint16_t ForeColour, uint16_t BackColour);
	uint16_t RGBToWord(uint16_t R, uint16_t G, uint16_t B);
	void drawLineLowSlope(uint16_t x0, uint16_t y0, uint16_t x1,uint16_t y1, uint16_t Colour);
	void drawLineHighSlope(uint16_t x0, uint16_t y0, uint16_t x1,uint16_t y1, uint16_t Colour);    

I will not do a deep dive on all of the functions, instead I will just focus on two of them : putPixel and fillRectangle.

Getting a dot on the screen : putPixel

This badge uses Zephyr OS as the underlying operating system (principally to make use of its Bluetooth Low Energy features). As a result it makes use of Zephyr’s peripheral device drivers to interface with the onboard peripherals of the NRF52833.

// Configuration for the SPI port.  Note the 32MHz clock speed possible only on SPI 3
// Pin usage by SPI bus defined in app.overlay.
static const struct spi_config cfg = {
	.frequency = 32000000,
	.operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB |  SPI_MODE_CPOL | SPI_MODE_CPHA,
	.slave = 0,
};
void display::putPixel(uint16_t x, uint16_t y, uint16_t colour)
{
    this->openAperture(x, y, x + 1, y + 1);
	struct spi_buf tx_buf = {.buf = &colour, .len = 2};
	struct spi_buf_set tx_bufs = {.buffers = &tx_buf, .count = 1};
	DCHigh();
	spi_write(spi_display, &cfg, &tx_bufs);
}

The SPI configuration structure cfg sets the SPI mode, bit order and role. It also specifies the transfer speed of 32Mbps. This is used in calls to spi_write.

Inside putPixel we see that a 1 pixel size aperture is opened on the display. Next, an spi_buf structure is prepared which contains a pointer to the data being written as well as it’s length. This is wrapped in an spi_buf_set structure required by the spi_write function. Finally, the D/C line is driven high indicating to the ST7789 that data is being written. The spi_write function handles the actual write operation. The first parameter for spi_write is a Zephyr device structure called spi_display which identifies the SPI device being used. This was obtained when the SPI interface was initialized and is used in a manner similar to a FILE structure in C file operations

As you can see from the above there is a bit of overhead associated with writing a pixel to the display. Functions in display.cpp that write multiple pixels don’t necessarily call putPixel as this would be too slow. Instead they interface directly with Zephyr’s I/O functions. An example of this is fillRectangle.

Filling large areas of the screen: fillRectangle

void display::fillRectangle(uint16_t x,uint16_t y,uint16_t width, uint16_t height, uint16_t colour)
{
	// This routine breaks the filled area up into sections and fills these.
	// This allows it to make more efficient use of the control lines and SPI bus
#define PIXEL_CACHE_SIZE 64
	static uint16_t fill_cache[PIXEL_CACHE_SIZE]; // use this to speed up writes
	uint32_t pixelcount = height * width;
	uint32_t blockcount = pixelcount / PIXEL_CACHE_SIZE;
	
	this->openAperture(x, y, x + width - 1, y + height - 1);
	DCHigh();
	struct spi_buf tx_buf = {.buf = &colour, .len = 2};
	struct spi_buf_set tx_bufs = {.buffers = &tx_buf, .count = 1};   

	if (blockcount)
	{
  		  for (int p=0;p<PIXEL_CACHE_SIZE;p++)
		  {
			fill_cache[p]=colour;
		  }
	}
	while(blockcount--)
	{
	   tx_buf.buf=fill_cache;
	   tx_buf.len = PIXEL_CACHE_SIZE*2;
	   spi_write(spi_display, &cfg, &tx_bufs);
	}

	pixelcount = pixelcount % PIXEL_CACHE_SIZE;
	while(pixelcount--) 
	{
	  tx_buf.buf = &colour;
 	  tx_buf.len = 2;		
		  spi_write(spi_display, &cfg, &tx_bufs);
	}	
}

Filling a rectangle on screen consists of two steps:

Open an aperture for the rectangle

Write pixel values to the display.

In an attempt to reduce the number of Zephyr API function calls this function divides the pixel data into chunks of size PIXEL_CACHE_SIZE. This allows that number of pixels to be written in a single call to spi_write (and maybe to leverage any optimization within the driver such as DMA). For example, suppose you want to fill an area of the screen consisting of 200 pixels. Using the value of 64 as a block size this would require the writing of three 64 pixel block and 8 individual pixel writes. So 200 calls to spi_write have been reduced to 11. If the function were to use putPixel then it would suffer from the additional overhead of opening an aperture for each individual pixel as well as controlling the D/C line for each of them. The above mechanism is a lot faster.

Code for all of this is very much a work in progress and is available over here on github. It will be changing a lot over the next few 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.

Dublin maker badge 2022

It looks like Dublin Maker will actually happen this year :). I had planned a second badge for 2020 but that never happened. I initially thought that I might simply move that plan to this year however testing of the radio link proved to be inadequate for my needs (it used an NRF24L01 module). In the meantime I have been working with Zephyr OS and the BBC Microbit V2. The Microbit is based on an NRF52833 MCU which is capable of doing BLE Mesh networking. This looks quite attractive so I have begun moving my design to this platform. Basic Mesh networking seems to be ok and the graphics routines are working. I need to do a full hardware prototype next using some NRF52833 modules before a batch of badges is produced.

This is the first in a set of posts that will follow the development of the badge.

Zephyr on the NRF52840 dongle

I got an NRF52840 dongle at Christmas and have been trying to work with Zephyr on it. An initial set of examples is over here. These examples include a simple “Hello World” with a blinking LED, a minimal BLE example and an example that drives an ST7735 display.

Nrf52840 dongle with an ST7735 display

A couple of points about the wiring:

The ST7735 has a built-in 3.3V regulator so it is supplied from the 5V VBus pin.

The serial interface is connected to a USB-Serial converter (not shown). The TX pin (from the NRF) is on P0.20. The RX pin (in to the NRF) is on pin P0.24

Getting to know CORDIC on the STM32G491

The STM32G491 has an interesting mathematical acceleration subsystem called CORDIC (coordinate rotation digital computer). It implements the following mathematical operations in hardware: cos, sin, phase, mod, atan, cosh, sinh, atanh, ln, sqrt. It uses fixed point q31 or q15 calculations for all of these operations. These functions can be used to speed up DSP calculations while saving energy at the same time. I had an idea that they may be useful for abc-dq0 and dq0-abc calculations in power electronics also but before I tried to tackle that I thought I should do some basic experiments. First on the list is a signal generator.

cordic Cordic;
int32_t args[2];
int32_t results[2];
int main()
{
    char c;
	int16_t s_int=0;		
	unsigned output = 0;    
    setup();  
	initDACS();
    Serial.begin();
	Cordic.begin();
	Cordic.set_scale(0);
	Cordic.set_precision(5);
	Cordic.set_n_arg(2);
	Cordic.set_n_res(2);
	Cordic.set_arg_size(32);
	Cordic.set_res_size(32);
	Cordic.set_function(Cordic.sin);
	args[0]=0;
	args[1]=0x7fffff00;
    enable_interrupts();
    Serial.print("Cordic signal generator\r\n");	
	while(1)
    {        
		s_int++;
		output = (s_int-(int16_t)0x8000); 
		args[0]=output<<16;
		Cordic.write_args(args);
		Cordic.get_results(results);		
        DAC1->DAC_DHR12R1 = (results[0]+0x80000000)>>20;
        DAC1->DAC_DHR12R2 = output>>4;
    }    
}

The code uses a counter (s_int) as an angle input to the CORDIC system which has been programmed to perform a sin calculation. The output from the CORDIC system is then passed to DAC1, channel1. The “angle” variable is passed to DAC1, channel2. The resultant analogue outputs are shown in the image below (green = “angle”, yellow = “sin”)

Some scaling and adjusting of values is necessary for this to work properly. First of all, the CORDIC output is 32 bits wide and so must be shifted right by 20 bits to suit the 12 bit DAC. Secondly, the zeros for the input and output to the CORDIC system must be shifted (with an overflow) to cater for the fact that the DAC is unsigned while the CORDIC system is signed.

Code is available over here on github.

Sinusoidal voltage control of a stepper motor

Permanent magnet stepper motors consist of a rotor which is permanently magnetized and a stator that houses a set of electromagnets. The diagram below shows a very simply motor with a single pole pair in the rotor. In practice, there are lots of pole pairs which reduces the mechanical step size and hence increases the resolution of the machine.

The electromagnets (coils) in the stator can be energized in sequence as shown above. This causes the rotor to rotate. The image above is a simplified electromagnetic view of the machine. The stator coils for a bipolar motor are driven as shown below.

The full bridge connected to motor terminals A,B allow current to be driven through the coil in either direction. A second full bridge drives motor terminals C,D. These electronic bridges could be built using individual transistors but in this case an SN754410NE was used as shown below.

Control pulses for the motor were generated using an STM32L432 Nucleo board which is equipped with a sophisticated motor control timer.

The the motor is driven using a simple sequence of pulses it will indeed rotate however it will exhibit torque pulsations as the motor steps between the stator magnetic poles. These pulsations can be reduced if sinusoidal PWM is used to drive the stator coils. A phase difference of 90 degrees is required between each of the motor coil waveforms.

In order to generate the sinusoidal PWM signal a lookup table was constructed using the following Octave code

clear
scalefactor=1999;
ts=1000;
anglestep=2*pi/ts;
angles=0:anglestep:2*pi-anglestep;
waveform=sin(angles);
lookup=scalefactor+(scalefactor*waveform);
fid=fopen('lookup.h','w');
fprintf(fid,'const uint16_t ScaleFactor=%d;\n',2+2*scalefactor);
fprintf(fid,'const uint16_t SineArray[]={');
for i=1:length(lookup)
  fprintf(fid,'%d',floor(lookup(i)+1));
  fprintf(fid,',\n');
end
fprintf(fid,'};');
fclose(fid);

This code creates a file called lookup.h which is included in a C file that controls the timer. A section of this C file is shown below.

const uint32_t SampleCount = sizeof(SineArray)/sizeof(uint16_t);
volatile uint32_t SampleCounter1 = 0;
volatile uint32_t SampleCounter2 = (SampleCount/4); // start SampleCounter2 a quarter cycle (90) ahead of SampleCounter1
void initTimer()
{
  // see github link for this code
}
void TIM1_UE_Handler(void)
{
/*
 * Warning: it is really important to do something that consumes a few clock cycles in this ISR after the interrupt flags are cleared 
 * see : https://developer.arm.com/documentation/ka003795/latest
 */
	
	TIM1->SR =0; // 
	TIM1->CCR1 = SineArray[SampleCounter1];
	SampleCounter1++;
	if (SampleCounter1 >= sizeof(SineArray)/2)
	{
		SampleCounter1 = 0;
	}
	TIM1->CCR2 = SineArray[SampleCounter2];
	SampleCounter2++;
	if (SampleCounter2 >= sizeof(SineArray)/2)
	{
		SampleCounter2 = 0;
	}
	GPIOB->ODR ^= BIT3; // Toggle green LED
}

At the end of each PWM interval, a new value is loaded into the counter compare register for each of the two channels used. Both counter compare channels reference the same sine lookup table using separate indices which are shifted the equivalent of 90 degrees apart.

The current drawn by the SN754410 driver is shown below:

This current waveform is effectively the absolute a value of the current in each stator coil plus quiescent current. Due to the overlap it appears to be 4 times faster than the actual motor coil currents which run at approx 2Hz. The motor runs without any significant torque pulsations. If current control were used these pulsations would probably be reduced further.

Full source code can be found over here on github.

Zephyr and the BBC Microbit V2 Tutorial Part 4: BLE

In this example a BBC Microbit V2 will be programmed to behave as a step counter and it will report these over its BLE interface. This will not be a primer on Bluetooth. There are plenty of those to be found on the Internet. This post will be specifically about how you work with Zephyr and BLE on the Microbit V2.


BLE Roles for the BBC Microbit and a mobile phone

When your phone connects to a bluetooth device such as a pedometer it takes on a role defined by the Bluetooth SIG as “Central”. The pedometer or other device’s role is “Peripheral”. The central device initiates a connection with the peripheral. The peripheral device can advertise services which contain characteristics that can be read or written. They can also send notifications to the central device should a sensor value change. These services are identified using numeric values. In this example a 128 bit UUID of 00000001-0002-0003-0004-000000000000 is used to identify a step-count service which has a single characteristic (the actual step-count value) with a UUID of 00000001-0002-0003-0004-000000000005

This step count service is defined in Zephyr as follows:

// ********************[ 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 characteristic contained within this service is defined as follows:

// ********************[ Start of First characteristic ]**************************************
#define BT_UUID_STEPCOUNT_ID  BT_UUID_128_ENCODE(1, 2, 3, 4, (uint64_t)5)
static struct bt_uuid_128 stepcount_id=BT_UUID_INIT_128(BT_UUID_STEPCOUNT_ID); // the 128 bit UUID for this gatt value
uint32_t stepcount_value=0; // the gatt characateristic value that is being shared over BLE	
static ssize_t read_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset);
static ssize_t write_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags);
// Callback that is activated when the characteristic is read by central
static ssize_t read_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
{
	printk("Got a read %d\n",stepcount_value);
	const char *value = (const char *)&stepcount_value; // point at the value in memory
	return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(stepcount_value)); // pass the value back up through the BLE stack
}
// Callback that is activated when the characteristic is written by central
static ssize_t write_stepcount(struct bt_conn *conn, const struct bt_gatt_attr *attr,
			 const void *buf, uint16_t len, uint16_t offset,
			 uint8_t flags)
{
	uint8_t *value = attr->user_data;
	printk("Got a write %d\n",len);
	if (len == sizeof(stepcount_value)) // check that the incoming data is the correct size
	{
		memcpy(value, buf, len); // copy the incoming value in the memory occupied by our characateristic variable
	}
	else
	{
		printk("Write size is incorrect.  Received %d bytes, need %d\n",len,sizeof(stepcount_value));
	}
	return len;
}
// Arguments to BT_GATT_CHARACTERISTIC = _uuid, _props, _perm, _read, _write, _value
#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)
// ********************[ End of First characteristic ]****************************************

As you can see from both of these definitions there are lots of C-macros involved. These are included to help the developer avoid some of the lower level details. The characteristic stores its value in the integer stepcount_value. The characteristic is defined as having the following properties: Read, Write and Notify.

If the central device issues a read requent then the function read_stepcount is called. This function copies the the contents of stepcount_value to a buffer (buf) which is then passed back to the central device.

If the central device writes to the microbit the function write_stepcount will be called. This copies data from a buffer passed by the lower level of the BLE stack into the memory occupied by stepcount_value.

void main(void)
{
	int err;
	int old_stepcount = 0;
	err = lsm303_ll_begin();
	if (err < 0)
	{
		 printk("\nError initializing lsm303.  Error code = %d\n",err);  
         while(1);

	}
	err = bt_enable(NULL);
	if (err) {
		printk("Bluetooth init failed (err %d)\n", err);
		return;
	}
	bt_ready(); // This function starts advertising
	bt_conn_cb_register(&conn_callbacks);
	printk("Zephyr Microbit V2 minimal BLE example! %s\n", CONFIG_BOARD);
	if (lsm303_countSteps(&stepcount_value) < 0)
	{
		printk("Error starting step counter\n");
		while(1);
	}

	while (1) {
		k_msleep(100);		
		// int bt_gatt_notify(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *data, u16_t len)
		// conn: Connection object. (NULL for all)
		// attr: Characteristic Value Descriptor attribute.
		// data: Pointer to Attribute data.
		// len: Attribute value length.				
		if (active_conn)
		{
			if (stepcount_value != old_stepcount) // only send a notification if the data changes
			{
				old_stepcount = stepcount_value;
				bt_gatt_notify(active_conn,&my_service_svc.attrs[2], &stepcount_value,sizeof(stepcount_value));			
			}
		}	
	}
}

It begins by initializing the LSM303 accelerometer on the Microbit. It then starts bluetooth advertising. The main loop sleeps for 100ms and then sends an update to the central device if there is an active BLE connection and if there has been a change to the step-count value.

Some significant changes need to be made to prj.conf to enable Bluetooth. On such change is this:

CONFIG_BT_DEVICE_NAME=”Microbit V2 BLE”

This allows you set the name that the microbit will broadcast when advertising its presence.

Full code for this example is over here on github.