image
Embox является сильно конфигурируемой RTOS. Основная идея Embox — прозрачный запуск Linux программного обеспечения везде, в том числе и на микроконтроллерах. Из достижений стоит привести OpenCV, Qt, PJSIP, запущенные на микроконтроллерах STM32F7. Конечно, запуск подразумевает, что в данные проекты не вносились изменения и использовались только опции при конфигурации оригинальных проектов и параметры задаваемые в самой конфигурации Embox. Но возникает естественный вопрос насколько Embox позволяет экономить ресурсы по сравнению с тем же Linux? Ведь последний также достаточно хорошо конфигурируется.

Для ответа на этот вопрос можно подобрать минимально возможную для запуска Embox аппаратную платформу. В качестве такой платформы мы выбрали EMF32ZG_STK3200 от компании SiliconLabs. Данная платформа имеет 32kB ROM и 4kB RAM память. А также процессорное ядро cortex-m0+. Из периферии доступны UART, пользовательские светодиоды, кнопки, а также 128x128 монохромный дисплей. Нашей целью является запуск любого пользовательского приложения, позволяющего убедиться в работоспособности Embox на данной плате.

Для работы с периферией и самой платой нужны драйвера и другой системный код. Данный код можно взять из примеров предоставляемых самим производителем чипа. В нашем случае производитель предлагает использовать SimplifyStudio. Есть также открытый репозиторий на GitHub). Этот код и будем использовать.

В Embox есть механизмы позволяющие использовать BSP производителя при создании драйверов. Для этого нужно скачать BSP и собрать его как библиотеку в Embox. При этом можно указать различные пути и флаги, необходимые для использования данной библиотеки в драйверах.

Пример Makefile для скачивания 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 для сборки 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 для платы EFM32ZG_STK3200:

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",

...

}

После таких достаточно простых действий можно использовать код от производителя. Прежде чем приступить к работе с драйверами необходимо разобраться со средствами разработки и архитектурными частями. В Embox используются обычные средства разработки gcc, gdb, openocd. При запуске openocd нужно указать что мы используем платформу efm32:

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

Для нашей платки нет каких-то специальных архитектурных частей, только специфика cortex-m0+. Это задается компилятором. Поэтому мы можем задать общий код для cotrex-m0 отключив все лишнее, например, работу с плавающей точкой.

     @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

После этого можно попробовать скомпилить Embox и походить с помощью отладчика по шагам, проверив тем самым правильно ли мы задали параметры в линкер скрипте

/* region (origin, length) */
ROM (0x00000000, 32K)
RAM (0x20000000, 4K)

/* section (region[, lma_region]) */
text   (ROM)
rodata (ROM)
data   (RAM, ROM)
bss    (RAM)

Первым драйвером реализуемым для поддержки какой-нибудь платы в Embox обычно является UART. На нашей плате есть LEUART. Для драйвера достаточно реализовать несколько функций. При этом мы можем использовать функции из 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);

Для того чтобы функции BSP были доступны нужно просто указать это в описании драйвера, файл Mybuild:

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
}

После реализации драйвера UART вам доступны не только вывод, но и консоль где вы можете вызвать свои пользовательские команды. Для этого вам достаточно добавить в конфигурационный файл Embox маленький командный интерпретатор:

    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")

А также указать, что нужно использовать не полноценный tty доступный через devfs, а заглушку, которая позволяет обращаться к заданному устройству. Устройство задается также в конфигурационном файле mods.conf:

    @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. Для его реализации мы также можем воспользоваться вызовами из BSP. Для этого в описании драйвера укажем что он зависит от 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
}

Сама реализация:

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);

}

Этого достаточно чтобы использовать команду ‘pin’ из Embox. Данная команда позволяет управлять GPIO. И в частности, может использоваться для проверки мигания светодиодом.

Добавляем саму команду в mods.conf:

include embox.cmd.hardware.pin

И сделаем так, чтобы она запускалась при старте. Для этого в конфигурационном файле start_sctpt.inc добавим одну из строчек:

<source">«pin GPIOC 10 blink»,
Или

"pin GPIOC 11 blink",

Команды одинаковые, просто номера светодиодов разные.

Попробуем запустить еще и дисплей. Сначала все просто. Ведь мы опять можем использовать вызовы BSP. Для этого нам нужно только добавить их в описание драйвера фреймбуфера:

package embox.driver.video

@BuildDepends(platform.efm32.efm32_bsp)
module efm32_lcd {
...

    source "efm32_lcd.c"
    @NoRuntime depends platform.efm32.efm32_bsp
}

Но как только мы делаем любой вызов связанный с дисплеем например DISPLAY_Init у нас секция .bss увеличивается больше чем на 2 kB, при размерах RAM 4 kB, это очень существенно. После изучения данного вопроса, выяснилось, что в самом BSP выделен фреймбуфер размером под дисплей. То есть 128x128x1 бит или 2048 байт.

В этот момент я даже хотел остановиться на достигнутом, ведь уместить в 4kB RAM вызов пользовательских команд с каким-то простым командным интерпретатором само по себе достижение. Но все-таки решил попробовать.

Первым я убрал командный интерпретатор и оставил только вызов уже упомянутой команды pin. Для этого я изменил конфигурационный файл mods.conf следующим образом:

    //@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)

Поскольку я использовал другой модуль для пользовательского старта, я перенес запуск команд в другой конфигурационный файл. Вместо start_script.inc использовал system_start.inc.

Затем, поскольку уже не требовалось использовать индексные дескрипторы в командном интерпретаторе, а также таймеры, я с помощью опций в 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)

Поскольку я вызывал команды напрямую, а не через командный интерпретатор, я смог уменьшить размер стека:

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

Наконец, у меня собралось и запустилось мигание светодиодом, и при этом внутри был вызов инициализации дисплея.

Мне хотелось вывести что нибудь на дисплейю Я подумал, что логотип Embox будет показателен. По хорошему нужно использовать полноценный драйвер фреймбуфера и выводить изображение из файла, ведь все это есть в Embox. Но места совсем не хватало. И для демонстрации я решил вывести логотип прямо в функции инициализации драйвера фреймбуфера. Причем данные конвертировав напрямую в битовый массив. Таким образом мне потребовалось ровно 2048 байт в ROM.

Сам код, как и ранее, использует 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;
}

Собственно все. На коротком видео можно увидеть результат.


Весь код доступен на GitHub. Если есть плата, то же самое можно воспроизвести на ней с помощью инструкции описанной на wiki.

Результат превзошел мои ожидания. Ведь удалось запустить Embox по сути на 2kB RAM. Это означает, что с помощью опций в Embox накладные расходы на использование ОС можно свести к минимуму. При этом в системе присутствует многозадачность. Пусть даже она и кооперативная. Ведь обработчики таймеров вызываются не напрямую в контексте прерывания, а из собственного контекста. Что естественно является плюсом использования ОС. Конечно, данный пример во многом искусственный. Ведь при столь ограниченных ресурсах и функциональность будет ограниченной. Преимущества Embox начинают сказываться на более мощных платформах. Но в то же время это можно считать предельным случаем работы Embox.