Embox on the EFM32ZG_STK3200 board. How to fit RTOS into 4kB RAM

image

Embox is a highly configurable RTOS. The main idea of ​​Embox is to transparently run Linux software everywhere, including on microcontrollers. 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 these projects and only the options were used when configuring the original projects and the parameters set in the Embox configuration itself. But a natural question arises to what extent Embox saves resources in comparison with the same Linux? After all, the latter is also fairly well configurable.



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



To work with peripherals and the board itself, you need drivers and other system code. This code can be taken from examples provided by the chip manufacturer itself. In our case, the manufacturer suggests using SimplifyStudio. There are also open repository on GitHub ). We will use this code.



Embox has mechanisms to use the manufacturer's BSP when creating drivers. To do this, you need to download the BSP and build it as a library in Embox. In this case, you can specify various paths and flags required to use this library in drivers.



Example Makefile for downloading BSP:



PKG_NAME := Gecko_SDK
PKG_VER := v5.1.2
PKG_ARCHIVE_NAME := $(PKG_NAME)-$(PKG_VER).tar.gz

PKG_SOURCES := https://github.com/SiliconLabs/$(PKG_NAME)/archive/v5.1.2.tar.gz

PKG_MD5     := 0de78b48a8da80931af1a53d401e74f5

include $(EXTBLD_LIB)
      
      





Mybuild to build 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 such fairly simple steps, you can use the code from the manufacturer. Before you start working with drivers, you need to understand the development tools and architectural parts. Embox uses the usual development tools gcc, gdb, openocd. When starting openocd, you need to indicate that we are using the efm32 platform:



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





There are no special architectural parts for our scarves, 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 things, for example, working with floating point.



     @Runlevel(0) include embox.arch.generic.arch
    include 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 walk through the steps using the debugger, thereby checking if we have correctly set 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 any board in Embox is usually the UART. Our board has LEUART. It is enough for the driver to implement several functions. 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 for the BSP functions to be available, you just need to indicate this in the driver description, the 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, not only the output is available to you, but also the console where you can call your custom commands. To do this, you just need to add a small command interpreter to the Embox configuration file:



    include embox.cmd.help
    include embox.cmd.sys.version

    include embox.lib.Tokenizer
    include embox.init.setup_tty_diag
    @Runlevel(2) include embox.cmd.shell
    @Runlevel(3) include embox.init.start_script(shell_name="diag_shell")
      
      





And also indicate that you need to use not a full-fledged tty available through devfs, but a stub that allows you to access the specified device. The device 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
      
      





Another very simple driver is GPIO. To implement it, we can also use calls from the BSP. To do this, in the driver description, we will indicate that it 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 itself:



static int efm32_gpio_setup_mode(unsigned char port, gpio_mask_t pins, int mode) {
...
}

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

#if (_SILICON_LABS_32B_SERIES < 2) \
  || defined(_SILICON_LABS_32B_SERIES_2_CONFIG_2)
  CMU_ClockEnable(cmuClock_GPIO, true);
#endif
    return gpio_register_chip((struct gpio_chip *)&efm32_gpio_chip, EFM32_GPIO_CHIP_ID);

}
      
      





This is enough to use the 'pin' command from Embox. This command allows you to control the GPIO. And in particular, it can be used to check the blinking of an LED.



Add the command itself to mods.conf:



include embox.cmd.hardware.pin
      
      





And let's make it run at startup. To do this, add one of the lines in the start_sctpt.inc configuration file:



<source ">" 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 calls again. To do this, we only need to add them 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 call related to the display, for example DISPLAY_Init, our .bss section increases by more than 2 kB, with a RAM size of 4 kB, this is very significant. After studying this issue, it turned out that in the BSP itself a framebuffer is allocated for the display. That is, 128x128x1 bits or 2048 bytes.



At this point, I even wanted to stop there, because it is an achievement in itself to fit the call of user commands with some simple command interpreter in 4kB RAM. But I decided to try it.



First, I removed the shell and left only the call to the already mentioned pin command. To do this, I modified the mods.conf config file 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)
      
      





Since I was using a different module for the custom start, I moved the command launch to a different config file. I used system_start.inc instead of start_script.inc.



Then, since I no longer needed to use inodes in the shell, as well as timers, I got rid of them using the options in mods.config:



    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)
      
      





Since I was calling commands directly and not through the shell, I was 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)
      
      





Finally, I got the LED flashing and started up, and inside there was a call to initialize the display.



I wanted to display something on the display. I thought the Embox logo would be indicative. On a good level, you need to use a full-fledged framebuffer driver and output an image from a file, because all this is in Embox. But there was not enough space. And for demonstration, I decided to display the logo directly in the initialization function of the framebuffer driver. Moreover, the data is converted directly into a bitmap. Thus, I needed exactly 2048 bytes in ROM.



The code itself, as before, uses BSP:



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. In a short video you can see the result.





All code is available on GitHub . If there is a board, the same can be reproduced on it using the instructions described on the wiki .



The result exceeded my expectations. After all, we managed to run Embox on essentially 2kB RAM. This means that with options in Embox, OS overhead can be minimized. Moreover, the system has multitasking. Even if it is cooperative. After all, timer handlers are not called directly in the interrupt context, but from their own context. Which is naturally a plus of using the OS. Of course, this example is largely artificial. Indeed, with such limited resources, the functionality will be limited. The benefits of Embox are starting to take their toll on more powerful platforms. But at the same time, this can be considered the limiting case of Embox.



All Articles