Porting EspMon to ZephyrOS on a STM32 Boards (or other boards, with a little work)

Updated on 2023-07-24

A crash course in using Zephyr RTOS with my IoT ecosystem

Introduction

Here there be dragons.

Please read this article carefully as it's critical. For example, in the Submanifests section, there is content you must copy out of the article and paste into a file off of your root zephyr directory.

If you've never done any IoT or embedded coding, or even if you've used Arduino but are not comfortable with it, yet this article isn't for you. You won't be able to use a fancy IDE, or lean on a large, helpful community although at least you can use VS Code's Intellisense with a bit of mangling of the .cpp_properties file. Nothing here comes without work. You've been warned.

If you want to continue development on the Zephyr platform, consider joining the zephyrproject Discord server and the /r/embedded subreddit. Both of these venues have some helpful people in an otherwise sparsely documented arena with a small (at least public) userbase and community.

Prerequisites

Get yourself some VS Code goodness. If you prefer another editor, you can use that but this article assumes VS Code.

VS Code

Pick up a STM32 Nucleo H723GZ board. You can try this with other boards by creating a new overlay file with the appropriate device references and bus settings. I tried this with my Nucleo H745ZI but sadly, Zephyr doesn't include a complete board port for it yet, so it doesn't currently support SPI - necessary for our display. Update: The Github repo also contains support for a Nucleo F767ZI which I believe is much more popular. The wiring is the same - as it is for all Nucleo 144 (larger form factor) boards.

Find an ILI9341 display in 3 or 4-wire SPI format (MISO won't be used).

You'll need the companion app from the EspMon Reboot article here.

EspMon Reboot article here

As a test of your mettle, set up the Zephyr development environment and West build system. If you can't, consider this a captcha of sorts for the rest of the article. None shall pass unless you can cross this particular bridge. Be advised that I ran into an error installing the "dtc-msys2" component via Chocolatey on Windows 11. It continued the installation, and I get a warning during subsequent full builds of projects, but I've not seen it break anything yet. On an unrelated note, pip or other components may complain at you that there's a newer version, but I'd leave it. Same with CMake and whatever else you install with Zephyr and/or West. Note that disturbingly, it seems to create what looks like a copy of itself nested within its root under "zephyr" such that it would be zephyrproject/zephyr if you followed all the instructions. It's not actually a copy, although it might be a partial. Leave it. You might want to create a batch file that invokes python with the build environment shell. I created zephyr.cmd under zephyrproject.

set up the Zephyr development environment and West build system

To acquaint yourself, read the EspMon Reboot article here (also linked above) but note that will be following more closely with the GitHub code, since it's newer. I didn't update the article and the code with the latest bits because they are more complicated. The concepts and flow are the same. It's just structured somewhat differently, and factored to support more platforms and frameworks.

here

That said, we will not be extending that project directly, but rather starting a new zephyr project under zephyrproject/zephyr/applications/zepyhr_mon and basically copying code into it from the other project - I've already done this and provided the complete project with the article. You'll probably have to create the applications folder. I put my projects underneath the zephyr structure because the docs suggested it and I don't want to confuse the already cryptic build system. You can download the zip at the top of this article but I recommend cloning from the provided Github link into zephyrproject/zephyr/applications/zephyr_mon.

Understanding This Mess

We won't go over the code much as it's largely the same as the esp_mon2 Github code, itself closely derived from the associated article's code, but simplified slightly if anything compared to the previous GitHub code.

Devicetree

Sitting at the core of any project is Devicetree - represented in .dts files as a weird sort of pidgin combination of JSON and C++. There's a master device tree file called zephyr.dts under build/zephyr in your project once you've successfully built once. You'll find yourself referring to it to get device names and settings primarily.

Devicetree

You can override, augment and even erase portions of the master file with "overlays". This is the canonical way in Zephyr to initialize and configure the devices you need. By default, much of your board will be disabled, waiting for you to enable it with a devicetree entry in which you configure the properties of your devices as well as make them accessible to your code.

#include <zephyr/dt-bindings/display/ili9xxx.h>
/ {
    aliases {
        command-serial = &usart3;
    };
    chosen {
        zephyr,display = &ili9340;
    };
    zephyr,user {
        // backlight
        bl_gpios = <&gpioc 6 GPIO_ACTIVE_HIGH>;
    };
};
&usart3 {
    compatible = "st,stm32-usart", "st,stm32-uart";
    status = "okay";
    current-speed = <115200>;
};
&arduino_spi {
    status = "okay";
    cs-gpios = <&gpioa 15 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
    ili9340: ili9340@0 {
        compatible = "ilitek,ili9340";
        spi-max-frequency = <20000000>;
        reg = <0>;
        reset-gpios = <&gpiof 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
        cmd-data-gpios = <&gpiod 15 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
        width = <320>;
        height = <240>;
        pixel-format = <ILI9XXX_PIXEL_FORMAT_RGB565>;
        rotation = <270>;
        frmctr1 = [00 18];
        pwctrl1 = [23 00];
        vmctrl1 = [3e 28];
        vmctrl2 = [86];
        pgamctrl = [0f 31 2b 0c 0e 08 4e f1 37 07 10 03 0e 09 00];
        ngamctrl = [00 0e 14 03 11 07 31 c1 48 08 0f 0c 31 36 0f];
    };
};

The first line is an include and it works just like it does in C++. It's actually copied into the C++ generated from this devicetree infoset.

After that, we augment the root / node with a few things.

First, we declare an alias for our serial UART - which we'll call command-serial. This makes it easy to refer to in code, while also making it configurable in devicetree for different boards.

The second block sets up the display subsystem with the ili9340 device which is declared further down the file. We could have also used the console subsystem to wrap the UART, but since we're using binary data, and I've run into problems with them not implementing stdio.h to spec in at least one area I went straight to the UART itself.

The third block is a section for user defined properties. Since GPIOs are not devices, we couldn't use any of the aforementioned blocks to declare it. We create a user property for it instead and refer to it in the code by that property.

Before we set up the display, we must configure our UART. We're using usart3 because on this particular board that connects to the virtual COM port exposed by the embedded ST-Link hardware. The only real significant bits other than that are the part where we enable the device (status = "okay") and set the current baud. The compatible property must match the given device it references or the node gets ignored (I think - I've never actually had occasion to test that assumption.)

Now onto two things - configuring our SPI device as well as the ILI9340 device driver. You'll see we've referred to &arduino-spi which on this board is an alias for the SPI bus that's exposed on the Arduino shield header. The main bit is setting the CS line, because it can be one of two possible values on this board as well as setting the speed to communicate over the bus.

You'll see the ILI9340 driver is nested inside the SPI bus declaration. That's how you tell devicetree that a given device is connected via a given bus. Within the ILI9340 driver are several properties that configure various aspects of the display. Which do what are mostly self-explanatory but outside the scope here. Many should be familiar to anyone that's used these displays before.

several properties

You can create a new overlay for any given board, and the code should work with it as long as it has the requisite hardware. The code shouldn't have to change unless you do something like change the display controller hardware you are using.

I figured this all out by looking at samples online, as well as some help on the Discord server I mentioned. There are significant holes in my knowledge for now so I say the above so you know where to find out more. Google just won't turn up a whole lot by itself, so the Discord is invaluable.

Configuring Your Project using prj.conf

Aside from the overlay when you create a project, the prj.conf file is very important. This has a ton of potential properties starting with the CONFIG_ prefix and typically suffixed with =y. These enable various features of the project. It's not documented extremely well, and part of that is because these properties are defined by individual modules which are arbitrary, rendering the properties themselves arbitrary, similar to #defines in C headers. I'd suggest copying the files from this project into any new project you start, and then removing what you don't need.

Some of these are important but not obvious like CONFIG_NEWLIB_LIBC. This one in particular is critical, along with the C++ related ones in order to use my graphics library. By default, the C library used is minimal and doesn't conform to C99 because a lot of it is stripped out. The aforementioned configuration property enables the full spec C library. Also, my libraries require C++17.

Here's what you'd need to enable my entire zephyrized ecosystem, including UIX which requires the rest:

CONFIG_HTCW_BITS=y
CONFIG_HTCW_IO=y
CONFIG_HTCW_ML=y
CONFIG_HTCW_DATA=y
CONFIG_HTCW_GFX=y
CONFIG_HTCW_UIX=y
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_PICOLIBC=y

A Segue Into Submanifests

There's really no place to put this because it's out of band, so it shall be in the article as well.

Some libraries (like my htcw IoT and embedded ecosystem) are compatible with Zephyr but they are not in the official module repositories. Ergo, in order to use them, you must add them by creating a .yaml file to zephyrproject/zephyr/submanifests. You'll have to create the submanifests folder. These yaml files are used by the build system to fetch local copies of the libraries. To use my ecosystem, you need the following which I name htcw.yaml:

manifest:
  projects:
    - name: htcw_bits
      url: https://github.com/codewitch-honey-crisis/htcw_bits
      revision: master
    - name: htcw_io
      url: https://github.com/codewitch-honey-crisis/htcw_io
      revision: master
    - name: htcw_ml
      url: https://github.com/codewitch-honey-crisis/htcw_ml
      revision: master
    - name: htcw_data
      url: https://github.com/codewitch-honey-crisis/htcw_data
      revision: master
    - name: htcw_gfx
      url: https://github.com/codewitch-honey-crisis/gfx
      revision: master
    - name: htcw_uix
      url: https://github.com/codewitch-honey-crisis/uix
      revision: master

To actually download them, you need to call

west update

Do that from inside zephyrproject/zephyr.

Unfortunately, VS Code will not automatically find includes for these files. Ergo, you must add the paths to each of your submodule's include folders to .cpp_properties, like so:

{
  "configurations": [
    {
      "name": "Win32",
      "includePath": ["${workspaceFolder}/**"],
      "defines": ["_DEBUG", "UNICODE", "_UNICODE"],
      "windowsSdkVersion": "10.0.22000.0",
      "compilerPath": "cl.exe",
      "cStandard": "c17",
      "cppStandard": "c++17",
      "intelliSenseMode": "windows-msvc-x64"
    },
    {
      "name": "ARM",
      "includePath": [
        "C:/Users/username/zephyrproject/**",
        "C:/Users/username/zephyrproject/zephyr/**",
        "${workspaceFolder}/**",
        "C:/Users/username/zephyrproject/htcw_bits/include",
        "C:/Users/username/zephyrproject/htcw_io/include",
        "C:/Users/username/zephyrproject/htcw_ml/include",
        "C:/Users/username/zephyrproject/htcw_data/include",
        "C:/Users/username/zephyrproject/htcw_gfx/include",
        "C:/Users/username/zephyrproject/htcw_uix/include",
        "${workspaceFolder}"
      ],
      "defines": [],
      "windowsSdkVersion": "10.0.22000.0",
      "compilerPath": "C:/mingw64/bin/g++.exe",
      "cStandard": "c99",
      "cppStandard": "gnu++17",
      "intelliSenseMode": "windows-gcc-arm64",
      "compileCommands": "C:/Users/username/zephyrproject/zephyr/build/compile_commands.json"
    }
  ],
  "version": 4
}

You can see all the paths under includePath. If there's a better way, someone please let me know.

In order to actually load the modules and use them in your project, use the following command before the first time you build - in your virtual environment, and from your project directory:

west update

CMakeLists.txt

You'll need one of these in your project folder. The contents are something like this:

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zephyr_mon)
target_sources(app PRIVATE src/main.cpp src/ui.cpp)

Most of this is boilerplate. Just make sure to set the project name in project() and keep your list of C and C++ source files up to date in target_sources().

Using this Mess

Wire the project as in wiring.txt.

Download the project from the EspMon Reboot article linked to earlier. You will need the .NET companion application to use this.

Then flash the board from the project directory:

(.venv) C:\Users\username\zephyrproject\zephyr\applications\zephyr_mon\west build -b nucleo_h723zg
(.venv) C:\Users\username\zephyrproject\zephyr\applications\zephyr_mon\west flash

It should show a disconnected icon on a white screen - actually it's an SVG document but that's neither here nor there.

Run the PC companion application (it will ask for elevated privileges - grant them) and check/select the COM port for the ST-Link virtual COM port associated with the board. Check the "started" checkbox, and you should see the screen on the device change to a monitor screen that is updating with the usage and temps of your CPU and GPU.

Bugs

There are bugs in newlib which can be used by Zephyr. For example, the console subsystem's getchar() function returns 0 on no input instead of -1. I switched the project over to picolib, even though I already worked around the issue. Picolib is maintained by the Zephyr project. As I said, dragons.

Conclusion

Zephyr has quite the learning curve, and the documentation could be better. That said, it's the best option for changing up boards more rapidly which can help dramatically with sourcing hardware.

History

  • 25th July, 2023 - Initial submission