So this post describes more or less in detail how to build a small Zephyr RTOS project using as a target the famous and cheap STM32 blue pill board that has a ST32F103 ARM processor onboard an it is supported by Zephyr RTOS.
The project is quite simple, but it will show how to:
- Create a project from scratch.
- Create RTOS tasks
- Enable USB console
Creating a Zephyr RTOS project
Has documented in my previous post Zephyr RTOS – Initial setup and some tests with Platformio and the NRF52840 PCA10059 dongle and also on Zephyr documentation, we need to download, install and configure the Zephyr RTOS sources, the west tool and the supporting Zephyr SDK. This is explained on the above post.
We can create our project under the zephyr workspace directory, but then we will have trouble if we want to use git to manage our project since the zephyr workspace directory is already a git repository. So we will create our own directory outside of the zephyr workspace directory and work from there.
To do this we need to set the ZEPHYR_BASE environment variable to point to the zephyr workspace, otherwise the west tool that will compile and flash our project will fail. Since west is a python command and we are using virtual environments, as discussed on the previous post we need to first change to the virtual env that has west installed:
workon zephyr
We can now setup our project:
mkdir zSTM32usb
export ZEPHYR_BASE=/opt/Develop/zephyrproject/zephyr
cd zSTM32usb
Because I don’t want to create all the necessary files from scratch, mainly the CMakeLists.txt file, I just copy from the zephyr samples repository the simplest of the projects, blinky:
cp -R /opt/Develop/zephyrproject/zephyr/samples/basic/blinky/* .
At this point we should be able to compile and flash the STM32 blue pill board, but before that we can change the name of the project on the CMakeLists.txt file just for consistency:
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.13.1)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zstm32usb)
target_sources(app PRIVATE src/main.c)
We can now compile the project:
west build -b stm32_min_dev_blue
or if we want to do a clean build we add the -p (pristine) flag:
west build -b stm32_min_dev_blue -p
And it should compile without any issues since this is still the basic blinky project.
...
...
-- Configuring done
-- Generating done
-- Build files have been written to: /opt/Develop/zSTM32usb/build
-- west build: building application
[1/138] Preparing syscall dependency handling
[133/138] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region Used Size Region Size %age Used
FLASH: 27064 B 64 KB 41.30%
SRAM: 12392 B 20 KB 60.51%
IDT_LIST: 184 B 2 KB 8.98%
[138/138] Linking C executable zephyr/zephyr.elf
If we didn’t set correctly the ZEPHYR_BASE environment variable, we will get some errors. For example for listing out the available target boards, we can do a west boards command:
west boards
usage: west [-h] [-z ZEPHYR_BASE] [-v] [-V] ...
west: error: argument : invalid choice: 'boards' (choose from 'init', 'update', 'list', 'manifest', 'diff', 'status', 'forall', 'help', 'config', 'topdir', 'selfupdate')
With the variable ZEPHYR_BASE (and virtual environment) correctly set, we get:
west boards | grep stm32
...
...
stm32373c_eval
stm32_min_dev_black
stm32_min_dev_blue
stm32f030_demo
...
...
So make sure the environment is correctly set.
Flashing the board:
Flashing the board is as easy as doing:
west flash
To be able to do this is necessary to have a ST-Link programmer and that it is properly connected to the STM32 blue pill board. Any issues here are probably not related with Zephyr or the west tool, since west only calls openocd to flash the board.
west flash
-- west flash: rebuilding
[0/1] cd /opt/Develop/zSTM32USB/build/zephyr/cmake/flash && /usr/bin/cmake -E echo
-- west flash: using runner openocd
-- runners.openocd: Flashing file: /opt/Develop/zSTM32USB/build/zephyr/zephyr.hex
Open On-Chip Debugger 0.10.0+dev-01341-g580d06d9d-dirty (2020-05-16-15:41)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : clock speed 1000 kHz
Info : STLINK V2J36S7 (API v2) VID:PID 0483:3748
Info : Target voltage: 3.212648
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections
TargetName Type Endian TapName State
-- ------------------ ---------- ------ ------------------ ------------
0* stm32f1x.cpu hla_target little stm32f1x.cpu running
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08002378 msp: 0x20002768
Info : device id = 0x20036410
Info : flash size = 64kbytes
auto erase enabled
wrote 27648 bytes from file /opt/Develop/zSTM32USB/build/zephyr/zephyr.hex in 1.769166s (15.261 KiB/s)
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08002378 msp: 0x20002768
verified 27064 bytes in 0.404006s (65.419 KiB/s)
shutdown command invoked
And now we should have a blinking led.
Creating a project from scratch – Conclusion:
So we now have a project base that we can use that it’s outside of the zephyr workspace directory, and hence, can have it’s own git repository without clashing with the zephyr workspace repository.
We can now move to add functionality to our project.
Creating the RTOS tasks
The basic blinky sample program uses a simple main() entry point and does not create any tasks so it is as simple as it can get.
A single threaded program, like it is blinky, has a main function, and it might have other tasks, either created dynamically or statically.
In our example, we will create the tasks statically. As we can see in the main.c file for our example at the zSTM32usb Github repository we define tasks by using the predefined macro K_THREAD_DEFINE:
// Task for handling blinking led.
K_THREAD_DEFINE(blink0_id, STACKSIZE, blink0, NULL, NULL, NULL, PRIORITY, 0, 0);
// Task to initialize the USB CDC ACM virtual COM port used for outputing data.
// It's a separated task since if nothing is connected to the USB port the task will hang...
K_THREAD_DEFINE(console_id, STACKSIZE, usb_console_init, NULL, NULL, NULL, PRIORITY, 0, 0);
According to K_THREAD_DEFINE Zephyr documentation the parameters are as follows:
K_THREAD_DEFINE(name, stack_size, entry, p1, p2, p3, prio, options, delay)
Parameters
name: Name of the thread.
stack_size: Stack size in bytes.
entry: Thread entry function.
p1: 1st entry point parameter.
p2: 2nd entry point parameter.
p3: 3rd entry point parameter.
prio: Thread priority.
options: Thread options.
delay: Scheduling delay (in milliseconds), or K_NO_WAIT (for no delay).
Based on this, we can then fine tune the task parameters, for example the stack size that is globally defined as 1024 bytes (way too much), and produces an image that takes around 12K of SRAM:
[133/138] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region Used Size Region Size %age Used
FLASH: 27064 B 64 KB 41.30%
SRAM: 12392 B 20 KB 60.51%
IDT_LIST: 184 B 2 KB 8.98%
[138/138] Linking C executable zephyr/zephyr.elf
where if we cut the stacksize to 512 bytes, if frees up SRAM, which is now arounf 11K:
[133/138] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region Used Size Region Size %age Used
FLASH: 27064 B 64 KB 41.30%
SRAM: 11368 B 20 KB 55.51%
IDT_LIST: 184 B 2 KB 8.98%
[138/138] Linking C executable zephyr/zephyr.elf
So while in this example the stack size is equal to both tasks, in a reality each task should have it’s stack adjusted to make the most of the available SRAM/RAM.
Also since the tasks are cooperative they need to release the processor to other tasks so they can run, hence instructions that wait for resources, or just a simple sleep are required to let all tasks to run cooperatively.
In our example this is achieved by the sleep instruction k_msleep that sleeps the tasks for the miliseconds that is passed as the parameter.
For example for blinking the Led, we have:
// Blink for ever.
while (1) {
gpio_pin_set(gpio_dev, led->gpio_pin, (int)led_is_on);
led_is_on = !led_is_on;
k_msleep(SLEEP_TIME_MS); // We should sleep, otherwise the task won't release the cpu for other tasks!
}
Tasks description:
Not too much to say about them, except if they do not enter a infinite loop, like the above led blinking while loop, the task does what it has to do and it ends.
Specifically for our example the led blinking task uses the Zephyr Device Tree to retrieve the onboard led configuration, and then, with that configuration it can start blinking the led. This opens the possibility of handling multiple blinking leds with the same code, just by creating a new task for each led.
The usb console init task, initiates the USB console port and waits for a port connection, after the connection happens, it starts printing to the console using the printk function. If we connect to the USB port of the STM32 blue pill board we get:
miniterm2.py /dev/ttyACM0
Hello from STM32 CDC Virtual COM port!
Hello from STM32 CDC Virtual COM port!
....
By experience console output that isn’t used for debugging purposes and/or while in development should be centralized on a single task because: first it will avoid concurrency issues between multiple tasks, and second if nothing is connected to the USB port to consume the console data, the tasks won’t hang waiting for a USB terminal connection to consume the console output.
USB Console Output configuration:
The end this already long post, we need to configure the console output to goto the USB virtual com port. This USB com port is only used for the console output, not for bidirectional communication such as an user and the device using a terminal program.
The configuration is done on the Zephyr configuration project file prj.conf, and the necessary information to enable USB console output was gathered from a series of different sources….
CONFIG_GPIO=y
CONFIG_USB=y
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Zephyr Console"
CONFIG_USB_UART_CONSOLE=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_UART_LINE_CTRL=y
CONFIG_UART_CONSOLE_ON_DEV_NAME="CDC_ACM_0"
A simple and quick description to the above file is that this file enables a set of modules, and provides some configuration to those modules to be able to use them. An example, to use the Led, the Led is connected to a GPIO pin, so it is necessary to enable the GPIO module: CONFIG_GPIO=y.
The same is true to enable USB. It’s necessary to enable USB support and the USB stack. Some console configuration is needed such as the CONFIG_USB_UART_CONSOLE=y, since the original, it seems, console output is to an UART port.
We can see the USB port connected when reseting the board after flashing:
[30958.705584] usb 1-1.2: new full-speed USB device number 7 using xhci_hcd
[30958.818608] usb 1-1.2: New USB device found, idVendor=2fe3, idProduct=0100, bcdDevice= 2.04
[30958.818610] usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[30958.818611] usb 1-1.2: Product: Zephyr Console
[30958.818611] usb 1-1.2: Manufacturer: ZEPHYR
[30958.818612] usb 1-1.2: SerialNumber: 8701241654528651
[30958.880665] cdc_acm 1-1.2:1.0: ttyACM0: USB ACM device
and on the device list:
....
Bus 001 Device 003: ID 0483:3748 STMicroelectronics ST-LINK/V2
Bus 001 Device 007: ID 2fe3:0100 NordicSemiconductor STM32 STLink
...
where the first entry is indeed the ST-Link programmer, and the the second entry is our USB console port.
As a final note, during compilation, a warning about the device id 2fe3:0100 is given, since for production use we need to change the default:
CMake Warning at /opt/Develop/zephyrproject/zephyr/subsys/usb/CMakeLists.txt:22 (message):
CONFIG_USB_DEVICE_VID has default value 0x2FE3.
This value is only for testing and MUST be configured for USB products.
CMake Warning at /opt/Develop/zephyrproject/zephyr/subsys/usb/CMakeLists.txt:28 (message):
CONFIG_USB_DEVICE_PID has default value 0x100.
This value is only for testing and MUST be configured for USB products.
As usual we can change this by changing the VID on the prj.conf file.
CONFIG_USB_DEVICE_VID=4440
Conclusion:
And that’s it. We now have a minimal skeleton where we can start build some applications and have some console output either for tracing or general information.
The neat part is while I’ve tested this with a STM32 Blue pill board, the same code works without any modification on other boards such as the NRF52840 dongle, which shows that with the same code base we can target different boards.