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 добавим одну из строчек:
"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.