Embox on EFM32ZG_STK3200 board. How to run RTOS on MCU with 4kB RAM.

Embox is a highly configurable RTOS. The main idea of ​​Embox is to transparently run Linux software everywhere, including MCUs. Among the achievements it is worth mentioning OpenCV, Qt, PJSIP running on STM32F7 microcontrollers. Of course, the launch implies that no changes were made to the source code of these projects and only options are used to configure them and the actual parameters set up in a Embox configuration. But a natural question arises to what extent Embox helps to reduce resources footprint in comparison with the same Linux? After all, Linux is also well configurable.

To answer this question, one can choose the minimum hardware platform for running Embox. We chose EFM32ZG_STK3200 from SiliconLabs as such a platform. This platform has 32kB ROM and 4kB RAM memory and the cortex-m0+ processor core. Some peripherals are available, among them: UARTs, custom LEDs, buttons, and a 128x128 monochrome display. Our goal is to launch any custom application to make sure that Embox is working on this board.

To work with any boards and peripherals, you need drivers and other system parts. This code can be got from examples provided by the chip manufacturer itself. In our case, the manufacturer suggests using ‘SimplifyStudio’. There is also an open repository on GitHub. We use this repo.

There is a mechanism to use the manufacturer’s BSP to create drivers in Embox. You need to download BSP and build it as a library in Embox. In this case, you can specify various paths and flags required to use it in drivers. Example

Makefile for downloading BSP

PKG_NAME := Gecko_SDKPKG_VER := v5.1.2PKG_ARCHIVE_NAME := $(PKG_NAME)-$(PKG_VER).tar.gzPKG_SOURCES := https://github.com/SiliconLabs/$(PKG_NAME)/archive/v5.1.2.tar.gzPKG_MD5 := 0de78b48a8da80931af1a53d401e74f5include $(EXTBLD_LIB)

Mybuild for building BSP

package platform.efm32...@BuildArtifactPath(cppflags="-I$(EXTERNAL_BUILD_DIR)/platform/efm32/bsp_get/Gecko_SDK-5.1.2/hardware/kit/common/bsp/")module bsp_get { }@BuildDepends(bsp_get)@BuildDepends(efm32_conf)static module bsp extends embox.arch.arm.cmsis {    ...    source "platform/emlib/src/em_timer.c",            "platform/emlib/src/em_adc.c",    ...    depends bsp_get    depends efm32_conf}

Mybuild for EFM32ZG_STK3200 board

package platform.efm32.efm32zg_stk3200@BuildArtifactPath(cppflags="-I$(EXTERNAL_BUILD_DIR)/platform/efm32/bsp_get/Gecko_SDK-5.1.2/platform/Device/SiliconLabs/EFM32ZG/Include")@BuildArtifactPath(cppflags="-I$(EXTERNAL_BUILD_DIR)/platform/efm32/bsp_get/Gecko_SDK-5.1.2/hardware/kit/EFM32ZG_STK3200/config")...@BuildArtifactPath(cppflags="-D__CORTEX_SC=0")@BuildArtifactPath(cppflags="-DUART_COUNT=0")@BuildArtifactPath(cppflags="-DEFM32ZG222F32=1")module efm32zg_stk3200_conf extends platform.efm32.efm32_conf {    source "efm32_conf.h"}@BuildDepends(platform.efm32.bsp)@BuildDepends(efm32zg_stk3200_conf)static module bsp extends platform.efm32.efm32_bsp {    @DefineMacro("DOXY_DOC_ONLY=0")    @AddPrefix("^BUILD/extbld/platform/efm32/bsp_get/Gecko_SDK-5.1.2/")    source 
"platform/Device/SiliconLabs/EFM32ZG/Source/system_efm32zg.c",
"hardware/kit/common/drivers/displayls013b7dh03.c", ...}

After these simple steps, you can use the code from the manufacturer.

Before you start working with drivers, you need get familiar with tools and architectural parts. The usual development tools gcc, gdb, openocd are available for Embox. When starting openocd, you need to specify that we are using efm32:

sudo openocd -f /usr/share/openocd/scripts/board/efm32.cfg

There are no special architectural parts for our board, only the cortex-m0+ specifics. This is set by the compiler. Therefore, we can set the general code for cotrex-m0 by disabling all unnecessary parts, for example, FPU

@Runlevel(0) include embox.arch.generic.archinclude embox.arch.arm.libarch@Runlevel(0) include embox.arch.arm.armmlib.locore@Runlevel(0) include embox.arch.system(core_freq=8000000)@Runlevel(0) include embox.arch.arm.armmlib.exception_entry(irq_stack_size=256)@Runlevel(0) include embox.kernel.stack(stack_size=1024,alignment=4)@Runlevel(0) include embox.arch.arm.fpu.fpu_stub

After that, you can try to compile Embox and go inside with a debugger, thereby making sure that you have correctly set up the parameters in the linker script

/* region (origin, length) */ROM (0x00000000, 32K)RAM (0x20000000, 4K)/* section (region[, lma_region]) */text   (ROM)rodata (ROM)data   (RAM, ROM)bss    (RAM)

The first driver implemented to support a new board in Embox is usually the UART driver. Our board has LEUART. It is enough to implement several functions for the driver. In doing so, we can use functions from the BSP.

static int efm32_uart_putc(struct uart *dev, int ch) {    LEUART_Tx((void *) dev->base_addr, ch);    return 0;}static int efm32_uart_hasrx(struct uart *dev) {...}static int efm32_uart_getc(struct uart *dev) {    return LEUART_Rx((void *) dev->base_addr);}static int efm32_uart_setup(struct uart *dev, const struct uart_params *params) {    LEUART_TypeDef      *leuart = (void *) dev->base_addr;    LEUART_Init_TypeDef init    = LEUART_INIT_DEFAULT;    /* Enable CORE LE clock in order to access LE modules */    CMU_ClockEnable(cmuClock_HFPER, true);...    /* Finally enable it */    LEUART_Enable(leuart, leuartEnable);    return 0;}...DIAG_SERIAL_DEF(&efm32_uart0, &uart_defparams);

In order to make BSP functions available, you just need to add ‘@BuildDepends’ annotation into the module description.

Mybuild file

package embox.driver.serial@BuildDepends(platform.efm32.efm32_bsp)module efm32_leuart extends embox.driver.diag.diag_api {    option number baud_rate    source "efm32_leuart.c"    @NoRuntime depends platform.efm32.efm32_bsp    depends core    depends diag}

After implementing the UART driver, a console becomes available to you where you can call your custom commands. To do this, you just need to add a small command interpreter to mods.conf:

include embox.cmd.helpinclude embox.cmd.sys.versioninclude embox.lib.Tokenizerinclude embox.init.setup_tty_diag@Runlevel(2) include embox.cmd.shell@Runlevel(3) include embox.init.start_script(shell_name="diag_shell")

And you also have to indicate that you do not want to use regular ‘tty’ available through devfs (‘/dev/ttySx’). That is also specified in the mods.conf configuration file

@Runlevel(1) include embox.driver.serial.efm32_leuart@Runlevel(1) include embox.driver.diag(impl="embox__driver__serial__efm32_leuart")include embox.driver.serial.core_notty

GPIO is another simple driver. We can also use functions from the BSP to implement it if we indicate that the driver depends on the BSP

package embox.driver.gpio@BuildDepends(platform.efm32.efm32_bsp)module efm32_gpio extends api {    option number log_level = 0    option number gpio_chip_id = 0    option number gpio_ports_number = 2    source "efm32_gpio.c"    depends embox.driver.gpio.core    @NoRuntime depends platform.efm32.efm32_bsp}

The implementation, only with outputs now

static int efm32_gpio_setup_mode(unsigned char port, gpio_mask_t pins, int mode) {...    return 0;}static void efm32_gpio_set(unsigned char port, gpio_mask_t pins, char level) {    if (level) {        GPIO_PortOutSet(port, pins);   } else {        GPIO_PortOutClear(port, pins);    }}static gpio_mask_t efm32_gpio_get(unsigned char port, gpio_mask_t pins) {      return GPIO_PortOutGet(port) & pins;}...static int efm32_gpio_init(void) {#if (_SILICON_LABS_32B_SERIES < 2)    CMU_ClockEnable(cmuClock_HFPER, true);#endif...}

This is enough to use the ‘pin’ command. This command allows you to control GPIOs. In particular, it can be used to blink an LED.

Add the command into mods.conf

include embox.cmd.hardware.pin

And let’s make it run at startup, adding one of the lines into ‘start_sctpt.inc’ configuration file

"pin GPIOC 10 blink",

or

“pin GPIOC 11 blink”,

The commands are the same, just the LED numbers are different.

Let’s try to start the display as well. It’s simple at first. After all, we can use BSP functions again. We only need to add a dependence to the description of the framebuffer driver.

package embox.driver.video@BuildDepends(platform.efm32.efm32_bsp)module efm32_lcd {...     source "efm32_lcd.c"     @NoRuntime depends platform.efm32.efm32_bsp}

But as soon as we make any calls related to the display, for example, DISPLAY_Init, our .bss section increases by more than 2 kB. It is a lot for our board which has only 4 kB RAM. After investigating this issue, we found out that the BSP has a video buffer that is 128x128x1 bits or 2048 bytes.

At that point, we even wanted to stop. Because running user commands in a shell within 4Kb RAM is great even without working display. But all the same, we decided to continue.

First, I got rid of the shell and left only the direct call to the already mentioned ‘pin’ command. I modified the ‘mods.conf’ as follows

//@Runlevel(2) include embox.cmd.shell//@Runlevel(3) include embox.init.start_script(shell_name="diag_shell")@Runlevel(3) include embox.init.system_start_service(cmd_max_len=32, cmd_max_argv=6)

Second, since we used a different module to launch user commands, we moved the command lines to a different config file. We used system_start.inc. instead of start_script.inc

Third, by setting options in ‘mods .conf’, we got rid of some objects: timer, index descriptors and so on

include embox.driver.common(device_name_len=1, max_dev_module_count=0)include embox.compat.libc.stdio.file_pool(file_quantity=0)...include embox.kernel.task.resource.idesc_table(idesc_table_size=3)include embox.kernel.task.task_no_table@Runlevel(1) include embox.kernel.timer.sys_timer(timer_quantity=1)...@Runlevel(1) include embox.kernel.timer.itimer(itimer_quantity=0)

Finally, because we called commands directly (not through the shell) we were able to reduce the stack size

@Runlevel(0) include embox.arch.arm.armmlib.exception_entry(irq_stack_size=224)@Runlevel(0) include embox.kernel.stack(stack_size=448,alignment=4)

At last, we were able to build and launch Embox with a blinking LED and display initialization (DISPLAY_Init).

We wanted to draw something on the display. We thought the Embox logo would be indicative. Of course, it is better to use a regular framebuffer driver and file image, all these are available in Embox. But there was not enough space. And we decided to display the logo directly in the initialization function of the framebuffer driver. Moreover, the data is converted into a bitmap. Thus, we needed exactly 2048 bytes in ROM.

The driver uses the BSP as usual

extern const uint8_t demo_image_mono_128x128[128][16];static int efm_lcd_init(void) {    DISPLAY_Device_t      displayDevice;    EMSTATUS status;    DISPLAY_PixelMatrix_t pixelMatrixBuffer;    /* Initialize the DISPLAY module. */    status = DISPLAY_Init();    if (DISPLAY_EMSTATUS_OK != status) {        return status;    }    /* Retrieve the properties of the DISPLAY. */    status = DISPLAY_DeviceGet(DISPLAY_DEVICE_NO, &displayDevice);    if (DISPLAY_EMSTATUS_OK != status) {        return status;    }    /* Allocate a framebuffer from the DISPLAY device driver. */    displayDevice.pPixelMatrixAllocate(&displayDevice,    displayDevice.geometry.width,    displayDevice.geometry.height,    &pixelMatrixBuffer);#if START_WITH_LOGO    memcpy(pixelMatrixBuffer, demo_image_mono_128x128,    displayDevice.geometry.width * displayDevice.geometry.height / 8 );    status = displayDevice.pPixelMatrixDraw(&displayDevice,
pixelMatrixBuffer, 0, displayDevice.geometry.width,
0, displayDevice.geometry.height);
#endif return 0;}

That’s all. You can see the result in this short video.

All code is available on GitHub. If you have EFM32ZG_STK3200 board you can reproduce the results using the instructions described on the wiki.

The result exceeded my expectations. We managed to run Embox essentially on 2KB RAM. It means that you can minimize OS overhead using options in Embox. What’s more, the system which is presented in the article is multitasking. Yes, it is non-preemptive, just cooperative, but multitasking. For example, timer handlers are called from their own context, not from interrupts directly. It is a big benefit of using the OS. Of course, this example is largely artificial. Indeed, with such restricted resources, functionality will be limited. The benefits of Embox really appear on more powerful platforms. But at the same time, this can be considered the limiting case of Embox.