Русский
Русский
English
Статистика
Реклама

Embox

Разрабатываем web-site для микроконтроллера

09.02.2021 18:11:49 | Автор: admin

С приходом в нашу жизнь различного рода умных розеток, лампочек и других подобных устройств, необходимость наличия веб-сайтов на микроконтроллерах стала неоспоримой. А благодаря проекту lwIP (и его младшему брату uIP) подобным функционалом никого не удивишь. Но поскольку lwIP направлен на минимизацию ресурсов, то с точки зрения дизайна, функционала, а также удобства использования и разработки, подобные сайты сильно отстают от тех, к которым мы привыкли. Даже для встроенных систем, сравнить, например, с сайтом для администрирования на самых дешевых роутерах. В данной статье мы попробуем разработать сайт на Линуксе для какого-нибудь умного устройства и запустить его на микроконтроллере.

Для запуска на микроконтроллере будем использовать Embox. В состав этой RTOS входит HTTP сервер с поддержкой CGI. В качестве HTTP сервера на Linux будем использовать встроенный в python HTTP сервер.
python3 -m http.server -d <site folder>


Статический сайт


Начнем с простого статического сайта, состоящего из одной или нескольких страниц.
Тут все просто, давайте создадим папку и в ней index.html. Этот файл будет скачиваться по умолчанию, если в браузере задан только адрес сайта.
$ ls website/em_big.png  index.html


Сайт еще будет содержать логотип Embox, файл em_big.png, который мы встроим в html.

Запустим http сервер
python3 -m http.server -d website/


Зайдем в браузере на localhost:8000


Теперь добавим наш статический сайт в файловую систему Embox. Это можно сделать скопировав нашу папку в папку rootfs/ темплейта (текущий темплейт в папке conf/rootfs). Или создать модуль указав в нем файлы для rootfs.
$ ls website/em_big.png  index.html  Mybuild


Содержимое Mybuild.
package embox.demomodule website {    @InitFS    source "index.html",        "em_big.png",}

Для простоты мы положим наш сайт прямо в корневую папку (аннотация @InitFs без параметров).

Нам также нужно включить наш сайт в конфигурационном файле mods.conf и туда же добавить сам httd сервер

    include embox.cmd.net.httpd        include embox.demo.website


Кроме того, давайте запустим сервер с нашим сайтом во время старта системы. Для этого добавим строчку в файл conf/system_start.inc

"service httpd /",


Естественно все эти манипуляции нужно делать с конфигом для платы. После этого собираем и запускаем. Заходим в браузере на адрес вашей платы. В моем случае это 192.168.2.128

И имеем такую же картинку как и для локального сайта


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

Какой-бы статический контент не положили в папку с сайтом, например, js или css файлы, мы можем его использовать без каких-либо дополнительных усилий.

Добавим в наш сайт app.js (сайт на angular) и в нем пару вкладок. Страницы для этих вкладок положим в папку partials, изображения в папку images/, а css файлы в css/.

$ ls website/app.js  css  images  index.html  Mybuild  partials


Запустим наш сайт.


Согласитесь, сайт выглядит гораздо привычнее и приятнее. Причем все это сделано на стороне браузера. Как мы и сказали, весь контекст все еще статический. И мы его можем разрабатывать на хосте как обычный сайт.

Естественно, при этом можно использовать все средства разработки обычных веб-девелоперов. Так, открыв консоль в браузере, мы обнаружили сообщение об ошибке, о том что не хватает favicon.ico:


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

Поиск в интернете сразу выдал, что можно обойтись и без файла, нужно всего лишь добавить строку в head секцию html. Хотя ошибка не мешала, но сделать сайт чуть лучше всегда приятно. И главное, что мы убедились, что обычные средства разработчика вполне применимы при предлагаемом подходе.

Динамический контент


CGI


Перейдем к динамическому контенту. Common Gateway Interface (CGI) интерфейс взаимодействия web-сервера с утилитами командной строки, позволяющий создавать динамический контент. Иными словами, CGI позволяет использовать вывод утилит для генерации динамического контента.

Давайте взглянем на какой-нибудь CGI скрипт
#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: application/json\r\n"echo -ne "Connection: Connection: close\r\n"echo -ne "\r\n"tm=`LC_ALL=C date +%c`echo -ne "\"$tm\"\n\n"


Вначале в стандартный output печатается http заголовок, а затем печатаются данные самой страницы. output может быть перенаправлен куда угодно. Можно просто запустить этот скрипт из консоли. Увидим следующее:
./cgi-bin/gettimeHTTP/1.1 200 OKContent-Type: application/jsonConnection: Connection: close"Fri Feb  5 20:58:19 2021"


А если вместо стандартного output это будет socket то браузер получит эти данные.

CGI часто реализуют с помощью скриптов, даже говорят cgi scripts. Но это не обязательно, просто на скриптовых языках подобные вещи делать быстрее и удобнее. Утилита предоставляющая CGI может быть реализована на любом языке. И так как мы ориентируемся на микроконтроллеры, следовательно, стараемся заботиться об экономии ресурсов. Давайте то же самое реализуем на С.
#include <stdio.h>#include <unistd.h>#include <string.h>int main(int argc, char *argv[]) {    char buf[128];    char *pbuf;    struct timeval tv;    time_t time;    printf(        "HTTP/1.1 200 OK\r\n"        "Content-Type: application/json\r\n"        "Connection: Connection: close\r\n"        "\r\n"    );    pbuf = buf;    pbuf += sprintf(pbuf, "\"");    gettimeofday(&tv, NULL);    time = tv.tv_sec;    ctime_r(&time, pbuf);    strcat(pbuf, "\"\n\n");    printf("%s", buf);    return 0;}


Если скомпилировать данный код и запустить, мы увидим точно такой же вывод как и в случае со скриптом.

В наш app.js добавим обработчик для вызова CGI скрипта для одной из нашей вкладки
app.controller("SystemCtrl", ['$scope', '$http', function($scope, $http) {    $scope.time = null;    $scope.update = function() {        $http.get('cgi-bin/gettime').then(function (r) {            $scope.time = r.data;        });    };    $scope.update();}]);


Небольшой нюанс по запуску на Linux с помощью встроенного сервера python. В нашу строку запуска нужно добавить аргумент --cgi для поддержки CGI:
python3 -m http.server --cgi -d .




Автоматическое обновление динамического контента


Теперь давайте разберемся с еще одним очень важным свойством динамического сайта автоматическим обновлением содержимого. Есть несколько механизмов для его реализации:
  • Server Side Includes (SSI)
  • Server-sent Events (SSE)
  • WebSockets
  • И так далее


Server Side Includes (SSI).


Server Side Includes (SSI). Это несложный язык для динамического создания веб-страниц. Обычно файлы использующие SSI имеют формат .shtml.

Сам SSI имеет даже директивы управления, if else и так далее. Но в большинстве примеров для микроконтроллеров, которые мы находили, он используется следующим образом. В .shtml страницу вставляется директива, которая периодически перегружает всю страницу. Это может быть, например
<meta http-equiv="refresh" content="1">

Или
<BODY onLoad="window.setTimeout("location.href='runtime.shtml'",2000)">


И тем или иным образом происходит генерация контента, например, с помощью задания специального обработчика.

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



Обновление страницы (см. вкладку) сильно заметно. И перезагружать всю страницу, выглядит как чрезмерно избыточное действие.

Приведен стандартный пример из FreeRTOS https://www.freertos.org/FreeRTOS-For-STM32-Connectivity-Line-With-WEB-Server-Example.html

Server-sent Events


Server-sent Events (SSE) это механизм, который позволяет установить полудуплексное (одностороннее) соединение между клиентом и сервером. Клиент в этом случае открывает соединение, и сервер использует его для передачи данных клиенту. При этом, в отличие от классических CGI скриптов, цель которых сформировать и отправить ответ клиенту, после чего завершиться, SSE предлагает непрерывный режим. То есть сервер может отправлять сколько угодно данных до тех пор пока либо не завершится самостоятельно, либо клиент не закроет соединение.

Есть несколько небольших отличий от обычных CGI скриптов. Во-первых, http заголовок будет немного другой:
        "Content-Type: text/event-stream\r\n"        "Cache-Control: no-cache\r\n"        "Connection: keep-alive\r\n"


Connection, как видно, не close, а keep-alive, то есть продолжающееся соединение. Чтобы браузер не кешировал данные нужно указать Cache-Control no-cache. Ну и наконец, нужно указать что используется специальный тип данных Content-Type text/event-stream.

Этот тип данных представляет из себя специальный формат для SSE:
: this is a test streamdata: some textdata: another messagedata: with two lines


В нашем случае данные нужно упаковать в следующую строку
data: { time: <real date>}


Наш CGI скрипт будет выглядеть
#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: text/event-stream\r\n"echo -ne "Cache-Control: no-cache\r\n"echo -ne "Connection: keep-alive\r\n"echo -ne "\r\n"while true; do    tm=`LC_ALL=C date +%c`    echo -ne "data: {\"time\" : \"$tm\"}\n\n" 2>/dev/null || exit 0    sleep 1done


Вывод если запустить скрипт
$ ./cgi-bin/gettimeHTTP/1.1 200 OKContent-Type: text/event-streamCache-Control: no-cacheConnection: keep-alivedata: {"time" : "Fri Feb  5 21:48:11 2021"}data: {"time" : "Fri Feb  5 21:48:12 2021"}data: {"time" : "Fri Feb  5 21:48:13 2021"}


И так далее раз в секунду

Тоже самое на С
#include <stdio.h>#include <unistd.h>#include <string.h>int main(int argc, char *argv[]) {    char buf[128];    char *pbuf;    struct timeval tv;    time_t time;    printf(        "HTTP/1.1 200 OK\r\n"        "Content-Type: text/event-stream\r\n"        "Cache-Control: no-cache\r\n"        "Connection: keep-alive\r\n"        "\r\n"    );    while (1) {        pbuf = buf;        pbuf += sprintf(pbuf, "data: {\"time\" : \"");        gettimeofday(&tv, NULL);        time = tv.tv_sec;        ctime_r(&time, pbuf);        strcat(pbuf, "\"}\n\n");        if (0 > printf("%s", buf)) {            break;        }        sleep(1);    }    return 0;}


И наконец, нужно еще сообщить angular, что у нас SSE, то есть модифицировать код для нашего контроллера
app.controller("SystemCtrl", ['$scope', '$http', function($scope, $http) {    $scope.time = null;    var eventCallbackTime = function (msg) {        $scope.$apply(function () {            $scope.time = JSON.parse(msg.data).time        });    }    var source_time = new EventSource('/cgi-bin/gettime');    source_time.addEventListener('message', eventCallbackTime);    $scope.$on('$destroy', function () {        source_time.close();    });    $scope.update = function() {    };    $scope.update();}]);


Запускаем сайт, видим следующее:


Заметно, что в отличие от использования SSI, страница не перегружается, и данные плавно и приятно для глаза обновляются.

Демо


Конечно приведенные примеры не реальные поскольку очень простые. Их цель показать разницу между используемыми на микроконтроллерах и в остальных системах подходов.

Мы сделали небольшую демонстрацию с реальными задачами. Управлением светодиодами, получением данных в реальном времени с датчика угловой скорости (гироскопа) и вкладкой с системной информацией.

Разработка сайта велась на хосте. Нужно было только сделать маленькие заглушки для эмуляции светодиодов и данных с датчика. Данные с датчика это просто случайные значения получаемые через стандартный RANDOM
#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: text/event-stream\r\n"echo -ne "Cache-Control: no-cache\r\n"echo -ne "Connection: keep-alive\r\n"echo -ne "\r\n"while true; do    x=$((1 + $RANDOM % 15000))    y=$((1 + $RANDOM % 15000))    z=$((1 + $RANDOM % 15000))    echo -ne "data: {\"rate\" : \"x:$x y:$y z:$z\"}\n\n" 2>/dev/null || exit 0    sleep 1done


Состояние светодиодов просто храним в файле.
#!/bin/python3import cgiimport sysprint("HTTP/1.1 200 OK")print("Content-Type: text/plain")print("Connection: close")print()form = cgi.FieldStorage()cmd = form['cmd'].valueif cmd == 'serialize_states':    with open('cgi-bin/leds.txt', 'r') as f:        print('[' + f.read() + ']')elif cmd == 'clr' or cmd == 'set':    led_nr = int(form['led'].value)    with open('cgi-bin/leds.txt', 'r+') as f:        leds = f.read().split(',')        leds[led_nr] = str(1 if cmd == 'set' else 0)        f.seek(0)        f.write(','.join(leds))


То же самое тривиально реализовано и в C варианте. При желании можно посмотреть код в репозитории папка (project/website).

На микроконтроллере конечно используются реализации взаимодействующие с реальной периферией. Но так как это просто команды и драйвера, они были отлажены отдельно. Поэтому сам перенос сайта на микроконтроллер, не занял времени.

Скриншот запущенный на хосте выглядит так


На коротком видео можно увидеть работу на реальном микроконтроллере. Отмечу, что происходит не только общение по http, но и например установка даты с помощью ntp из командной строки в Embox, и конечно обращение с периферией.


Самостоятельно все приведенное в статье можно воспроизвести по инструкции на нашем вики

Заключение


В статье мы показали, что возможно разрабатывать красивые интерактивные сайты и запускать их на микроконтроллерах. Причем делать это легко и быстро используя все средства разработки под хост и затем запускать из на микроконтроллерах. Естественно, разработкой сайта может заниматься профессиональный веб-дизайнер, в то время как embedded разработчик будет реализовывать логику работы устройства. Что очень удобно и сильно экономит время выхода на рынок.

Естественно за это придется расплачиваться. Да SSE потребует немного больше ресурсов чем SSI. Но мы с помощью Embox легко вместились в STM32F4 причем без оптимизации и использовали всего 128 кб ОЗУ. Меньше просто проверять не стали. Так что накладные расходы не такие уж большие. А удобство разработки и качество самого сайта сильно выше. И при этом конечно не стоит забывать, что современные микроконтроллеры заметно подросли и продолжают это делать. Ведь от устройств требуют быть все более умными.
Подробнее..

Добавляем modbus в Embox RTOS и используем на STM32 и не только

17.03.2021 18:23:31 | Автор: admin
image
Нас часто спрашивают, чем Embox отличается от других ОС для микроконтроллеров, например, FreeRTOS? Сравнивать проекты между собой, конечно, правильно. Но параметры, по которым порой предлагают сравнение, лично меня повергают в легкое недоумение. Например, сколько нужно памяти для работы Embox? А какое время переключения между задачами? А в Embox поддерживается modbus? В данной статье на примере вопроса про modbus мы хотим показать, что отличием Embox является другой подход к процессу разработки.

Давайте разработаем устройство, в составе которого будет работать в том числе modbus server. Наше устройство будет простым. Ведь оно предназначено только для демонстрации modbus, Данное устройство будет позволять управлять светодиодами по протоколу Modbus. Для связи с устройством будем использовать ethernet соединение.

Modbus открытый коммуникационный протокол. Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232 и сети TCP/IP (Modbus TCP).

Протокол modbus достаточно простой чтобы реализовать его самостоятельно. Но поскольку любая новая реализация функциональности может содержать ошибки, давайте используем что-нибудь готовое.

Одной из самых популярных реализаций протокола modbus является открытый проект libmodbus. Его и будем использовать. Это позволит сократить время разработки и уменьшить количество ошибок. При этом мы сможем сосредоточиться на реализации бизнес логики, а не на изучении протокола.

Наш проект будем вести в отдельном репозитории. При желании все можно скачать и воспроизвести самостоятельно.

Разработка прототипа на Linux


Начнем с разработки прототипа на хосте. Для того чтобы можно было использовать libmodbus в качестве библиотеки его нужно скачать, сконфигурировать и собрать.
Для этих целей я набросал Makefile

libmodbus-$(LIBMODBUS_VER).tar.gz:    wget http://libmodbus.org/releases/libmodbus-$(LIBMODBUS_VER).tar.gz$(BUILD_BASE)/libmodbus/lib/pkgconfig/libmodbus.pc : libmodbus-$(LIBMODBUS_VER).tar.gz    tar -xf libmodbus-$(LIBMODBUS_VER).tar.gz    cd libmodbus-$(LIBMODBUS_VER); \    ./configure --prefix=$(BUILD_BASE)/libmodbus --enable-static --disable-shared; \    make install; cd ..;


Собственно из параметров конфигурации мы используем только prefix чтобы собрать библиотеку локально. А поскольку мы хотим использовать библиотеку не только на хосте, соберем ее статическую версию.

Теперь нам нужен modbus сервер. В проекте libmodbus есть примеры, давайте на основе какого-нибудь простого сервера сделаем свою реализацию.

    ctx = modbus_new_tcp(ip, port);    header_len = modbus_get_header_length(ctx);    query = malloc(MODBUS_TCP_MAX_ADU_LENGTH);    modbus_set_debug(ctx, TRUE);    mb_mapping = mb_mapping_wrapper_new();    if (mb_mapping == NULL) {        fprintf(stderr, "Failed to allocate the mapping: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    listen_socket = modbus_tcp_listen(ctx, 1);    for (;;) {        client_socket = modbus_tcp_accept(ctx, &listen_socket);        if (-1 == client_socket) {            break;        }        for (;;) {            int query_len;            query_len = modbus_receive(ctx, query);            if (-1 == query_len) {                /* Connection closed by the client or error */                break;            }            if (query[header_len - 1] != MODBUS_TCP_SLAVE) {                continue;            }            mb_mapping_getstates(mb_mapping);            if (-1 == modbus_reply(ctx, query, query_len, mb_mapping)) {                break;            }            leddrv_updatestates(mb_mapping->tab_bits);        }        close(client_socket);    }    printf("exiting: %s\n", modbus_strerror(errno));    close(listen_socket);    mb_mapping_wrapper_free(mb_mapping);    free(query);    modbus_free(ctx);


Здесь все стандартно. Пара мест, которые представляют интерес, это функции mb_mapping_getstates и leddrv_updatestates. Это как раз функционал, который и реализует наше устройство.

static modbus_mapping_t *mb_mapping_wrapper_new(void) {    modbus_mapping_t *mb_mapping;    mb_mapping = modbus_mapping_new(LEDDRV_LED_N, 0, 0, 0);    return mb_mapping;}static void mb_mapping_wrapper_free(modbus_mapping_t *mb_mapping) {    modbus_mapping_free(mb_mapping);}static void mb_mapping_getstates(modbus_mapping_t *mb_mapping) {    int i;    leddrv_getstates(mb_mapping->tab_bits);    for (i = 0; i < mb_mapping->nb_bits; i++) {        mb_mapping->tab_bits[i] = mb_mapping->tab_bits[i] ? ON : OFF;    }}


Таким образом, нам нужны leddrv_updatestates, которая задает состояние светодиодов, и leddrv_getstates, которая получает состояние светодиодов.
static unsigned char leddrv_leds_state[LEDDRV_LED_N];int leddrv_init(void) {    static int inited = 0;    if (inited) {        return 0;    }    inited = 1;    leddrv_ll_init();    leddrv_load_state(leddrv_leds_state);    leddrv_ll_update(leddrv_leds_state);    return 0;}...int leddrv_getstates(unsigned char leds_state[LEDDRV_LED_N]) {    memcpy(leds_state, leddrv_leds_state, sizeof(leddrv_leds_state));    return 0;}int leddrv_updatestates(unsigned char new_leds_state[LEDDRV_LED_N]) {    memcpy(leddrv_leds_state, new_leds_state, sizeof(leddrv_leds_state));    leddrv_ll_update(leddrv_leds_state);    return 0;}


Так как мы хотим чтобы наше ПО работало и на плате и на хосте, нам понадобятся разные реализации функций установки и получения состояния светодиодов. Давайте для хоста хранить состояние в обычном файле. Это позволит получать состояние светодиодов в других процессах.

Например, если мы захотим проверить состояния через вебсайт мы запустим вебсайт и укажем в качестве источника данных этот файл.

void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];        for (i = 0; i < LEDDRV_LED_N; i++) {        char state = !!leds_state[i];        fprintf(stderr, "led(%03d)=%d\n", i, state);        buff[i * 2] = state + '0';        buff[i * 2 + 1] = ',';    }    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    write(idx, buff, (LEDDRV_LED_N * 2) - 1);    close(idx);}...void leddrv_load_state(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    read(idx, buff, (LEDDRV_LED_N * 2));    close(idx);        for (i = 0; i < LEDDRV_LED_N; i++) {        leds_state[i] = buff[i * 2] - '0';    }}


Нам нужно указать файл где будет сохранено начальное состояние светодиодов. Формат файла простой. Через запятую перечисляются состояние светодиодов, 1 светодиод включен, а 0 -выключен. В нашем устройстве 80 светодиодов, точнее 40 пар светодиодов. Давайте предположим, что по умолчанию четные светодиоды будут выключены а нечетные включены. Содержимое файла

0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1


Запускаем сервер
./led-serverled(000)=0led(001)=1...led(078)=0led(079)=1


Теперь нам нужен клиент для управления нашим устройством. Его тоже очень просто разрабатываем на базе примера из libmodbus

ctx = modbus_new_tcp(ip, port);    if (ctx == NULL) {        fprintf(stderr, "Unable to allocate libmodbus context\n");        return -1;    }    modbus_set_debug(ctx, TRUE);    modbus_set_error_recovery(ctx,            MODBUS_ERROR_RECOVERY_LINK |            MODBUS_ERROR_RECOVERY_PROTOCOL);    if (modbus_connect(ctx) == -1) {        fprintf(stderr, "Connection failed: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    if (1 == modbus_write_bit(ctx, bit_n, bit_value)) {        printf("OK\n");    } else {        printf("FAILED\n");    }    /* Close the connection */    modbus_close(ctx);    modbus_free(ctx);


Запускаем клиент. Установим 78 светодиод, который по умолчанию выключен

./led-client set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


На сервере увидим
...led(076)=0led(077)=1led(078)=1led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


То есть светодиод установлен. Давайте выключим его.
./led-client clr 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><00><00>OK


На сервере увидим сообщение об изменении
...led(076)=0led(077)=1led(078)=0led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


Запустим http сервер. О разработке веб-сайтов мы рассказывали в статье. К тому же веб-сайт нам нужен только для более удобной демонстрации работы modbus. Поэтому не буду сильно вдаваться в подробности. Сразу приведу cgi скрипт

#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: application/json\r\n"echo -ne "Connection: close\r\n"echo -ne "\r\n"if [ $REQUEST_METHOD = "GET" ]; then    echo "Query: $QUERY_STRING" >&2    case "$QUERY_STRING" in        "c=led_driver&a1=serialize_states")            echo [ $(cat ../emulate/conf/leds.txt) ]            ;;        "c=led_driver&a1=serialize_errors")            echo [ $(printf "0, %.0s" {1..79}) 1 ]            ;;        "c=led_names&a1=serialize")            echo '[ "one", "two", "WWWWWWWWWWWWWWWW", "W W W W W W W W " ]'            ;;    esacelif [ $REQUEST_METHOD = "POST" ]; then    read -n $CONTENT_LENGTH POST_DATA    echo "Posted: $POST_DATA" >&2fi


И напомню что запустить можно с помощью любого http сервера с поддержкой CGI. Мы используем встроенный в python сервер. Запускаем следующей командой
python3 -m http.server --cgi -d .


Откроем наш сайт в браузере


Установим 78 светодиод с помощью клиента
./led-client -a 127.0.0.1 set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


сбросим 79 светодиод
./led-client -a 127.0.0.1 clr 79Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>OK


На сайте увидим разницу


Собственно все, на Linux наша библиотека прекрасно работает.

Адаптация к Embox и запуск на эмуляторе


Библиотека libmodbus


Теперь нам нужно перенести код в Embox. начнем с самого проекта libmodbus.
Все просто. Нам нужно описание модуля (Mybuild)
package third_party.lib@Build(script="$(EXTERNAL_MAKE)")@BuildArtifactPath(cppflags="-I$(ROOT_DIR)/build/extbld/third_party/lib/libmodbus/install/include/modbus")module libmodbus {    @AddPrefix("^BUILD/extbld/^MOD_PATH/install/lib")    source "libmodbus.a"    @NoRuntime depends embox.compat.posix.util.nanosleep}


Мы с помощью аннотации Build(script="$(EXTERNAL_MAKE)") указываем что используем Makefile для работы с внешними проектами.

С помощью аннотации BuildArtifactPath добавляем пути для поиска заголовочных файлов к тем модулям, которые будут зависеть от этой библиотеки.

И говорим что нам нужна библиотека source libmodbus.a

PKG_NAME := libmodbusPKG_VER  := 3.1.6PKG_SOURCES := http://libmodbus.org/releases/$(PKG_NAME)-$(PKG_VER).tar.gzPKG_MD5     := 15c84c1f7fb49502b3efaaa668cfd25ePKG_PATCHES := accept4_disable.patchinclude $(EXTBLD_LIB)libmodbus_cflags = -UHAVE_ACCEPT4$(CONFIGURE) :    export EMBOX_GCC_LINK=full; \    cd $(PKG_SOURCE_DIR) && ( \        CC=$(EMBOX_GCC) ./configure --host=$(AUTOCONF_TARGET_TRIPLET) \        prefix=$(PKG_INSTALL_DIR) \        CFLAGS=$(libmodbus_cflags) \    )    touch $@$(BUILD) :    cd $(PKG_SOURCE_DIR) && ( \        $(MAKE) install MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)'; \    )    touch $@


Makefile для сборки тоже простой и очевидный. Единственное, отмечу что используем внутренний компилятор ($(EMBOX_GCC) ) Embox и в качестве платформы (--host) передаем ту, которая задана в Embox ($(AUTOCONF_TARGET_TRIPLET)).

Подключаем проект к Embox


Напомню, что для удобства разработки мы создали отдельный репозиторий. Для того чтобы подключить его к Embox достаточно указать Embox где лежит внешний проект.
Делается это с помощью команды
make ext_conf EXT_PROJECT_PATH=<path to project> 

в корне Embox. Например,
 make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrol


modbus-server


Исходный код modbus сервера не требует изменений. То есть мы используем тот же код, который разработали на хосте. Нам нужно добавить Mybuild
package iocontrol.modbus.cmd@AutoCmd@Build(script="true")@BuildDepends(third_party.lib.libmodbus)@Cmd(name="modbus_server")module modbus_server {    source "modbus_server.c"    @NoRuntime depends third_party.lib.libmodbus}


В котором с помощью аннотаций мы укажем что это у нас команда, а также что она зависит от библиотеки libmodbus.

Нам также понадобятся библиотеки эмуляции. Не буду приводить Mybuild для них, они тривиальны, лишь отмечу, что исходники также используются без изменений.

Нам также нужно собрать нашу систему вместе с modbus сервером
Добавляем наши модули в mods.conf
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv_ll_stub


А наш файл leds.txt со статусами светодиодов кладем в корневую файловую систему. Но так как нам нужен изменяемый файл, давайте добавим RAM disk и скопируем наш файл на этот диск. Содержимое system_start.inc
"export PWD=/","export HOME=/","netmanager","service telnetd","service httpd http_admin","ntpdate 0.europe.pool.ntp.org","mkdir -v /conf","mount -t ramfs /dev/static_ramdisk /conf","cp leds.txt /conf/leds.txt","led_driver init","service modbus_server","tish",


Этого достаточно запустим Embox на qemu
./scripts/qemu/auto_qemu

modbus и httpd сервера запускаются автоматически при старте. Установим такие же значения с помощью modbus клиента, только указав адрес нашего QEMU (10.0.2.16)
./led-client -a 10.0.2.16 set 78Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


и соответственно
./led-client -a 10.0.2.16 clr 79Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>


Откроем браузер


Как и ожидалось все тоже самое. Мы можем управлять устройством через modbus протокол уже на Embox.

Запуск на микроконтроллере


Для запуска на микроконтроллере будем использовать STM32F4-discovery. На вышеприведенных скриншотах страниц браузера, видно что используется 80 ног вывода, объединенные в пары, и еще можно заметить что у этих пар есть другие свойства, например можно задать имя, или пара может подсвечиваться. На самом деле, код был взят из реального проекта и из него для упрощения были убраны лишние части. 80 выходных пинов было получено с помощью дополнительных микросхем сдвиговых регистров.

Но на плате STM32F4-discovery всего 4 светодиода. Было бы удобно задавать количество светодиодов, чтобы не модифицировать исходный код В Embox есть механизм позволяющий параметризировать модули. Нужно в описании модуля (Mybuild) добавить опцию
package iocontrol.modbus.libstatic module libleddrv {    option number leds_quantity = 80...}


И можно будет использовать в коде
#ifdef __EMBOX__#include <framework/mod/options.h>#include <module/iocontrol/modbus/lib/libleddrv.h>#define LEDDRV_LED_N OPTION_MODULE_GET(iocontrol__modbus__lib__libleddrv,NUMBER,leds_quantity)#else#define LEDDRV_LED_N 80#endif


При этом менять этот параметр можно будет указав его в файле mods.conf
    include  iocontrol.modbus.lib.libleddrv(leds_quantity=4)


если параметр не указывается, то используется тот который задан в модуле по умолчанию, то есть 80.

Нам нужно еще управлять реальными линиями вывода. Код следующий
struct leddrv_pin_desc {    int gpio; /**< port */    int pin; /**< pin mask */};static const struct leddrv_pin_desc leds[] = {    #include <leds_config.inc>};void leddrv_ll_init(void) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_setup_mode(leds[i].gpio, leds[i].pin, GPIO_MODE_OUTPUT);    }}void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_set(leds[i].gpio, leds[i].pin,                leds_state[i] ? GPIO_PIN_HIGH : GPIO_PIN_LOW);    }}


В файле mods.conf нам нужна конфигурация для нашей платы. К ней добавляем наши модули
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv(leds_quantity=4)    include iocontrol.modbus.lib.libleddrv_ll_stm32_f4_demo


По сути дела, те же модули как и для ARM QEMU, за исключением конечно драйвера.

Собираем, прошиваем, запускаем. И с помощью того же modbus клиента управляем светодиодами. Нужно только поставить правильный адрес, и не забыть что у нас всего 4 светодиода на плате.

Работу на плате stm32f4-discovery можно увидеть на этом коротком видео


Выводы


На этом простом примере мы постарались показать, в чем же основное отличие Embox от других ОС для микроконтроллеров. В том числе которые имеют POSIX совместимость. Ведь мы по сути дела, взяли готовый модуль, разработали бизнес логику на Linux используя при этом несколько приложений. И запустили все это на нашей целевой платформе. Тем самым существенно упростив и ускорив саму разработку.

Да, конечно, приложение демонстрационное и не сложное. Сам протокол modbus так же можно было бы реализовать самостоятельно. Но в этом случае нам нужно было бы разбираться в протоколе modbus. А наш подход позволяет сосредоточится каждому специалисту на своей части. Ну и конечно, большинство проблем решаются на хосте, что гораздо более удобно чем разрабатывать напрямую на плате.
Подробнее..

Запуск QT на STM32. Часть 2. Теперь с псевдо 3d и тачскрином

05.04.2021 20:15:29 | Автор: admin
Мы в проекте Embox некоторое время назад запустили Qt на платформе STM32. Примером было приложение moveblocks анимация с четырьмя синими квадратами, которые перемещаются по экрану. Нам захотелось большего, например, добавить интерактивность, ведь на плате доступен тачскрин. Мы выбрали приложение animatedtiles просто потому, что оно и на компьютере круто смотрится. По нажатию виртуальных кнопок множество иконок плавно перемещаются по экрану, собираясь в различные фигуры. Причем выглядит это вполне как 3d анимация и у нас даже были сомнения, справится ли микроконтроллер с подобной задачей.

Сборка


Первым делом начинаем со сборки. Это проще всего делать на эмуляторе QEMU, т.к. там все точно влезает по памяти, и поэтому легко проверить всех ли компонентов хватает для сборки приложения. Проверив, что все animatedtiles успешно собралось, я легко перенес его в конфигурацию для нужной мне платы.

Первый запуск на плате


Размер экрана у STM32F746G-Discovery 480x272, при запуске приложение нарисовалось только в верхнюю часть экрана. Нам естественно захотелось выяснить в чем дело. Конечно можно уйти в отладку прямо на плате, но есть более простое решение. запустить приложение на Линукс с теми же самыми размерами 480x272 с виртуальным фреймбуфером QVFB.

Запускаем на Линукс


Для запуска на Linux нам потребуется три части QVFB, библиотека Qt, и само приложение.

QVFB это обычное приложение, которое предоставит нам виртуальный экран для работы Qt. Собираем его как написано в официальной документации.

Запускаем с нужным размером экрана:
./qvfb -width 480 -height 272 -nocursor


Далее, собираем библиотеку Qt как embedded, т.е. С указанием опции -embedded. Я еще отключил разные модули для ускорения сборки, в итоге конфигурация выглядела вот так:
./configure -opensource -confirm-license -debug \    -embedded -qt-gfx-qvfb -qvfb \    -no-javascript-jit -no-script -no-scripttools \    -no-qt3support -no-webkit -nomake demos -nomake examples


Далее собираем приложение animatedtiles (qmake + make). И запускаем скомпилированное приложение, указав ему наш QVFB:
./examples/animation/animatedtiles/animatedtiles -qws -display QVFb:0


После запуска я увидел, что на Линуксе также рисуется только в часть экрана. Я немного доработал animatedtiles, добавив опцию -fullscreen, при указании которой приложение стартует в полноэкранном режиме.

Запуск на Embox


Модифицированный исходный код приложения будем использовать в Embox. Пересобираем и запускаем. Приложение не запустилось, при этом появились сообщения о нехватке памяти в Qt. Смотрим в конфигурацию Embox и находим что размер кучи установлен 2Мб и его явно не хватает. Опять же, можно попробовать выяснить этот момент с прямо на плате, но давайте сделаем это с удобствами в Линукс.

Для этого запускаем приложение следующим образом:
$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


В файле animatedtiles.out видим максимальное значение заполненности кучи порядка 2.7 Мб. Отлично, теперь можно не гадать, а вернуться в Embox и поставить размер кучи 3Мб.

Animatedtiles запустилось.

Запуск на STM32F769I-Discovery.


Давайте попробуем еще усложнить задачу, и запустим тот же пример на подобном микроконтроллере, но только с большим разрешением экрана STM32F769I-Discovery (800x480). То есть теперь под фреймбуфер потребуется в 1.7 раз больше памяти (напомню, что у STM32F746G экран 480x272), но это компенсируется в два раза большим размером SDRAM (16 Мб против 8Мб доступной памяти SDRAM у STM32F746G).

Для оценки размера кучи, как и выше, сначала запускаем Qvfb и наше приложение на Линуксе:
$ ./qvfb -width 800 -height 480 -nocursor &$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


Смотрим расход памяти в куче около 6 МБ (почти в два раза больше, чем на STM32F746G).

Осталось выставить нужный размер кучи в mods.conf и пересобрать. Приложение запустилось сразу и без проблем, что и продемонстрировано этом коротком видео


Традиционно можно воспроизвести результаты самостоятельно. Как это сделать описано у нас на wiki.

Данная статья впервые была нами опубликована на английском языке на embedded.com.
Подробнее..

Запуск сложных C приложений на микроконтроллерах

27.01.2021 20:23:33 | Автор: admin
image Сегодня никого не удивить возможностью разрабатывать на C++ под микроконтроллеры. Проект mbed полностью ориентирован на этот язык. Ряд других RTOS предоставляют возможности разработки на С++. Это удобно, ведь программисту доступны средства объектно-ориентированного программирования. Вместе с тем, многие RTOS накладывают различные ограничения на использование C++. В данной статье мы рассмотрим внутреннюю организацию C++ и выясним причины этих ограничений.

Сразу хочу отметить, что большинство примеров будут рассмотрены на RTOS Embox. Ведь в ней на микроконтроллерах работают такие сложные C++ проекты как Qt и OpenCV. OpenCV требует полной поддержки С++, которой обычно нет на микроконтроллерах.

Базовый синтаксис


Синтаксис языка C++ реализуется компилятором. Но в рантайм необходимо реализовать несколько базовых сущностей. В компиляторе они включаются в библиотеку поддержки языка libsupc++.a. Наиболее базовой является поддержка конструкторов и деструкторов. Существуют два типа объектов: глобальные и выделяемые с помощью операторов new.

Глобальные конструкторы и деструкторы


Давайте взглянем на то как работает любое C++ приложение. Перед тем как попасть в main(), создаются все глобальные C++ объекты, если они присутствуют в коде. Для этого используется специальная секция .init_array. Еще могут быть секции .init, .preinit_array, .ctors. Для современных компиляторов ARM, чаще всего секции используются в следующем порядке .preinit_array, .init и .init_array. С точки зрения LIBC это обычный массив указателей на функции, который нужно пройти от начала и до конца, вызвав соответствующий элемент массива. После этой процедуры управление передается в main().

Код вызова конструкторов для глобальных объектов из Embox:

void cxx_invoke_constructors(void) {    extern const char _ctors_start, _ctors_end;    typedef void (*ctor_func_t)(void);    ctor_func_t *func = (ctor_func_t *) &_ctors_start;    ....    for ( ; func != (ctor_func_t *) &_ctors_end; func++) {        (*func)();    }}

Давайте теперь посмотрим как устроено завершение C++ приложения, а именно, вызов деструкторов глобальных объектов. Существует два способа.

Начну с наиболее используемого в компиляторах через __cxa_atexit() (из C++ ABI). Это аналог POSIX функции atexit, то есть вы можете зарегистрировать специальные обработчики, которые будут вызваны в момент завершения программы. Когда при старте приложения происходит вызов глобальных конструкторов, как описано выше, там же есть и сгенерированный компилятором код, который регистрирует обработчики через вызов __cxa_atexit. Задача LIBC здесь сохранить требуемые обработчики и их аргументы и вызвать их в момент завершения приложения.

Другим способом является сохранение указателей на деструкторы в специальных секциях .fini_array и .fini. В компиляторе GCC это может быть достигнуто с помощью флага -fno-use-cxa-atexit. В этом случае во время завершения приложения деструкторы должны быть вызваны в обратном порядке (от старшего адреса к младшему). Этот способ менее распространен, но может быть полезен в микроконтроллерах. Ведь в этом случае на момент сборки приложения можно узнать сколько обработчиков потребуется.

Код вызова деструкторов для глобальных объектов из Embox:

int __cxa_atexit(void (*f)(void *), void *objptr, void *dso) {    if (atexit_func_count >= TABLE_SIZE) {        printf("__cxa_atexit: static destruction table overflow.\n");        return -1;    }    atexit_funcs[atexit_func_count].destructor_func = f;    atexit_funcs[atexit_func_count].obj_ptr = objptr;    atexit_funcs[atexit_func_count].dso_handle = dso;    atexit_func_count++;    return 0;};void __cxa_finalize(void *f) {    int i = atexit_func_count;    if (!f) {        while (i--) {            if (atexit_funcs[i].destructor_func) {                (*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);                atexit_funcs[i].destructor_func = 0;            }        }        atexit_func_count = 0;    } else {        for ( ; i >= 0; --i) {            if (atexit_funcs[i].destructor_func == f) {                (*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);                atexit_funcs[i].destructor_func = 0;            }        }    }}void cxx_invoke_destructors(void) {    extern const char _dtors_start, _dtors_end;    typedef void (*dtor_func_t)(void);    dtor_func_t *func = ((dtor_func_t *) &_dtors_end) - 1;    /* There are two possible ways for destructors to be calls:     * 1. Through callbacks registered with __cxa_atexit.     * 2. From .fini_array section.  */    /* Handle callbacks registered with __cxa_atexit first, if any.*/    __cxa_finalize(0);    /* Handle .fini_array, if any. Functions are executed in teh reverse order. */    for ( ; func >= (dtor_func_t *) &_dtors_start; func--) {        (*func)();    }}

Глобальные деструкторы необходимы, чтобы иметь возможность перезапускать C++ приложения. Большинство RTOS для микроконтроллеров предполагает запуск единственного приложения, которое не перезагружается. Старт начинается с пользовательской функции main, единственной в системе. Поэтому в небольших RTOS зачастую глобальные деструкторы пустые, ведь их использование не предполагается.

Код глобальный деструкторов из Zephyr RTOS:

/** * @brief Register destructor for a global object * * @param destructor the global object destructor function * @param objptr global object pointer * @param dso Dynamic Shared Object handle for shared libraries * * Function does nothing at the moment, assuming the global objects * do not need to be deleted * * @return N/A */int __cxa_atexit(void (*destructor)(void *), void *objptr, void *dso){    ARG_UNUSED(destructor);    ARG_UNUSED(objptr);    ARG_UNUSED(dso);    return 0;}

Операторы new/delete


В компиляторе GCC реализация операторов new/delete находится в библиотеке libsupc++, А их декларации в заголовочном файле .

Можно использовать реализации new/delete из libsupc++.a, но они достаточно простые и могут быть реализованы например, через стандартные malloc/free или аналоги.

Код реализации new/delete для простых объектов Embox:

void* operator new(std::size_t size)  throw() {    void *ptr = NULL;    if ((ptr = std::malloc(size)) == 0) {        if (alloc_failure_handler) {            alloc_failure_handler();        }    }    return ptr;}void operator delete(void* ptr) throw() {    std::free(ptr);}

RTTI & exceptions


Если ваше приложение простое, вам может не потребоваться поддержка исключений и динамическая идентификация типов данных (RTTI). В этом случае их можно отключить с помощью флагов компилятора -no-exception -no-rtti.

Но если эта функциональность С++ требуется, ее нужно реализовать. Сделать это куда сложнее чем new/delete.

Хорошая новость заключается в том что эти вещи не зависят от ОС и уже реализованы в кросс-компиляторе в библиотеке libsupc++.a. Соответственно, самый простой способ добавить поддержку это использовать библиотеку libsupc++.a из кросс компилятора. Сами прототипы находятся в заголовочных файлах и .

Для использования исключений из кросс-компилятора есть небольшие требования, которые нужно реализовать при добавлении собственного метода загрузки C++ рантайма. В линкер скрипте должна быть предусмотрена специальная секция .eh_frame. А перед использованием рантайма эта секция должна быть инициализирована с указанием адреса начала секции. В Embox используется следующий код:

void register_eh_frame(void) {    extern const char _eh_frame_begin;    __register_frame((void *)&_eh_frame_begin);}

Для ARM архитектуры используются другие секции с собственной структурой информации .ARM.exidx и .ARM.extab. Формат этих секция определяется в стандарте Exception Handling ABI for the ARM Architecture EHABI. .ARM.exidx это таблица индексов, а .ARM.extab это таблица самих элементов требуемых для обработки исключения. Чтобы использовать эти секции для обработки исключений, необходимо включить их в линкер скрипт:

    .ARM.exidx : {        __exidx_start = .;        KEEP(*(.ARM.exidx*));        __exidx_end = .;    } SECTION_REGION(text)    .ARM.extab : {        KEEP(*(.ARM.extab*));    } SECTION_REGION(text)

Чтобы GCC мог использовать эти секции для обработки исключений, указывается начало и конец секции .ARM.exidx __exidx_start и __exidx_end. Эти символы импортируются в libgcc в файле libgcc/unwind-arm-common.inc:
extern __EIT_entry __exidx_start;extern __EIT_entry __exidx_end;

Более подробно про stack unwind на ARM написано в статье.

Стандартная библиотека языка (libstdc++)


Собственная реализация стандартной библиотеки


В поддержку языка C++ входит не только базовый синтаксис, но и стандартная библиотека языка libstdc++. Ее функциональность, так же как и для синтаксиса, можно разделить на разные уровни. Есть базовые вещи типа работы со строками или C++ обертка setjmp . Они легко реализуются через стандартную библиотеку языка C. А есть более продвинутые вещи, например, Standard Template Library (STL).

Стандартная библиотека из кросс-компилятора


Базовые вещи реализованы в Embox. Если этих вещей достаточно, то можно не подключать внешнюю стандартную библиотеку языка C++. Но если нужна, например, поддержка контейнеров, то самым простым способом является использование библиотеки и заголовочных файлов из кросс-компилятора.

При использовании стандартной библиотеки С++ из кросс-компилятора существует особенность. Взглянем на стандартный arm-none-eabi-gcc:

$ arm-none-eabi-gcc -vUsing built-in specs.COLLECT_GCC=arm-none-eabi-gccCOLLECT_LTO_WRAPPER=/home/alexander/apt/gcc-arm-none-eabi-9-2020-q2-update/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapperTarget: arm-none-eabiConfigured with: ***     --with-gnu-as --with-gnu-ld --with-newlib   ***Thread model: singlegcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)

Он собран с поддержкой --with-newlib.Newlib реализация стандартной библиотеки языка C. В Embox используется собственная реализация стандартной библиотеки. Для этого есть причина, минимизация накладных расходов. И следовательно для стандартной библиотеки С можно задать требуемые параметры, как и для других частей системы.

Так как стандартные библиотеки C отличаются, то для поддержки рантайма нужно реализовать слой совместимости. Приведу пример реализации из Embox одной из необходимых но неочевидных вещей для поддержки стандартной библиотеки из кросс-компилятора

struct _reent {    int _errno;           /* local copy of errno */  /* FILE is a big struct and may change over time.  To try to achieve binary     compatibility with future versions, put stdin,stdout,stderr here.     These are pointers into member __sf defined below.  */    FILE *_stdin, *_stdout, *_stderr;};struct _reent global_newlib_reent;void *_impure_ptr = &global_newlib_reent;static int reent_init(void) {    global_newlib_reent._stdin = stdin;    global_newlib_reent._stdout = stdout;    global_newlib_reent._stderr = stderr;    return 0;}

Все части и их реализации необходимые для использования libstdc++ кросс-компилятора можно посмотреть в Embox в папке third-party/lib/toolchain/newlib_compat/

Расширенная поддержка стандартной библиотеки std::thread и std::mutex


Стандартная библиотека C++ в компиляторе может иметь разный уровень поддержки. Давайте еще раз взглянем на вывод:

$ arm-none-eabi-gcc -v***Thread model: singlegcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)

Модель потоков Thread model: single. Когда GCC собран с этой опцией, убирается вся поддержка потоков из STL (например std::thread и std::mutex). И, например, со сборкой такого сложного С++ приложение как OpenCV возникнут проблемы. Иначе говоря, для сборки приложений, которые требуют подобную функциональность, недостаточно этой версии библиотеки.

Решением, которые мы применяем в Embox, является сборка собственного компилятора ради стандартной библиотеки с многопоточной моделью. В случае Embox модель потоков используется posix Thread model: posix. В этом случае std::thread и std::mutex реализуются через стандартные pthread_* и pthread_mutex_*. При этом также отпадает необходимость подключать слой совместимости с newlib.

Конфигурация Embox


Хотя пересборка компилятора и является самым надежным и обеспечивает максимально полное и совместимое решение, но при этом оно занимает достаточно много времени и может потребовать дополнительных ресурсов, которых не так много в микроконтроллере. Поэтому данный метод не целесообразно использовать везде.

В Embox для оптимизации затрат на поддержку введены несколько абстрактных классов (интерфейсов) различные реализации которых можно задать.

  • embox.lib.libsupcxx определяет какой метод для поддержки базового синтаксиса языка нужно использовать.
  • embox.lib.libstdcxx определяет какую реализацию стандартной библиотеки нужно использовать

Есть три варианта libsupcxx:

  • embox.lib.cxx.libsupcxx_standalone базовая реализация в составе Embox.
  • third_party.lib.libsupcxx_toolchain использовать библиотеку поддержки языка из кросс-компилятора
  • third_party.gcc.tlibsupcxx полная сборка библиотеки из исходников

Минимальный вариант может работать даже без стандартной библиотеки С++. В Embox есть реализация базирующаяся на простейших функциях из стандартной библиотеки языка С. Если этой функциональности не хватает, можно задать три варианта libstdcxx.

  • third_party.STLport.libstlportg стандартная библиотека вслкючающая STL на основе проекта STLport. Не требует пересборки gcc. Но проект давно не поддерживается
  • third_party.lib.libstdcxx_toolchain стандартная библиотека из кросс-компилятора
  • third_party.gcc.libstdcxx полная сборка библиотеки из исходников

Если есть желание у нас на wiki описано как можно собрать и запустить Qt или OpenCV на STM32F7. Весь код естественно свободный.
Подробнее..

Разбираемся в особенностях графической подсистемы микроконтроллеров

08.09.2020 14:23:50 | Автор: admin
Привет!

В этой статье я бы хотел рассказать об особенностях реализации графического пользовательского интерфейса с виджетами на микроконтроллере и при этом иметь и привычный пользовательский интерфейс и приличный FPS. Внимание я хотел бы акцентировать не на какой-то конкретной графической библиотеке, а на общих вещах память, кэш процессора, dma и так далее. Поскольку я являюсь разработчиком команды Embox, приведенные примеры и эксперименты будут на данной ОС РВ.


Ранее мы уже рассказывали про запуск библиотеки Qt на микроконтроллере. Получилась достаточно плавная анимация, но при этом затраты по памяти даже на хранение прошивки был существенными код исполнялся из внешней флэш памяти QSPI. Конечно, когда требуется сложный и многофункциональный интерфейс, который еще и анимацию какую-то умеет делать, то затраты по аппаратным ресурсам могут быть вполне оправданы (особенно если у вас уже есть этот код, разработанный под Qt).

Но если вам не нужна вся функциональность Qt? Что если у вас четыре кнопки, один регулятор громкости и пара popup меню? При этом хочется, чтобы выглядело красиво и работало быстро :) Тогда будет целесообразным использовать более легковесные средства, например библиотеку lvgl или аналогичную.

У нас в проекте Embox некоторое время назад был портирован Nuklear проект по созданию очень легковесной библиотека, состоящая из одного хедера и позволяющий легко создавать несложный GUI. Его мы и решили использовать для создания небольшого приложения в котором будет виджет с набором графических элементов и которым можно было бы управлять через touchscreen.

В качестве платформы выбрали STM32F7-Discovery c Cortex-M7 и сенсорным экраном.

Первые оптимизации. Экономия памяти


Итак, графическая библиотека выбрана, платформа тоже. Теперь поймем что по ресурсам. Тут стоит отметить, что основная память SRAM в разы быстрей внешней SDRAM, поэтому если вам позволяют размеры экраны, то конечно лучше положить фреймбуфер в SRAM. Наш экран имеет разрешение 480x272. Если мы хотим цвет в 4 байта на пиксель, то получается порядка 512 Кб. При этом размер внутреннего RAM всего 320 и сразу понятно, что видеопамять будет внешней. Другой вариант уменьшить битность цвета до 16 (т.е. 2 байта), и таким образом сократить расход памяти до 256 Кб, что уже может влезть в основную RAM.

Первое что можно попробовать сэкономить на всем. Сделаем видео буфер на 256 Кб, разместим его в RAM и будем в него же и рисовать. Проблема, с которой сразу же столкнулись это мерцание сцены возникающее если рисовать напрямую в видеопамять. Nuklear перерисовывает всю сцену с нуля, поэтому каждый раз сначала выполняется заливка всего экрана, далее рисуется виджет, потом в него кладется кнопка, в которую помещается текст и так далее. Как следствие, невооруженным взглядом заметно как вся сцена перерисовывается и картинка мигает. То есть, простое помещение во внутреннюю память, не спасает.

Промежуточный буфер. Компиляторные оптимизации. FPU


После того как мы немного повозился с предыдущим способом (размещением во внутренней памяти), в голову сразу стали приходить в воспоминания об X Server и Wayland. Да, действительно, по сути оконные менеджеры и занимаются тем, что обрабатывают запросы от клиентов (как раз наше пользовательское приложение), и далее собирают элементы в итоговую сцену. К примеру, ядро Линукса посылает серверу события от input устройств через драйвер evdev. Сервер, в свою очередь, определяет кому из клиентов адресовать событие. Клиенты, получив событие (например, нажатие на сенсорном экране) выполняют свою внутреннюю логику подсвечивают кнопку, отображают новое меню. Далее (немного по-разному для X и Wayland) либо сам клиент, либо сервер производит отрисовку изменений в буфер. И затем компоновщик (compositor) уже соединяет все кусочки воедино для отрисовки на экран. Достаточно просто и схематичное объяснение вот здесь.

Стало ясно, что нам нужна похожая логика, вот только запихивать X Server в stm32 ради маленького приложения очень уж не хочется. Поэтому попробуем просто рисовать не в видео память, а в обычную память. После отрисовки всей сцены будет копировать буфер в видео память.

Код виджета
        if (nk_begin(&rawfb->ctx, "Demo", nk_rect(50, 50, 200, 200),            NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|            NK_WINDOW_CLOSABLE|NK_WINDOW_MINIMIZABLE|NK_WINDOW_TITLE)) {            enum {EASY, HARD};            static int op = EASY;            static int property = 20;            static float value = 0.6f;            if (mouse->type == INPUT_DEV_TOUCHSCREEN) {                /* Do not show cursor when using touchscreen */                nk_style_hide_cursor(&rawfb->ctx);            }            nk_layout_row_static(&rawfb->ctx, 30, 80, 1);            if (nk_button_label(&rawfb->ctx, "button"))                fprintf(stdout, "button pressed\n");            nk_layout_row_dynamic(&rawfb->ctx, 30, 2);            if (nk_option_label(&rawfb->ctx, "easy", op == EASY)) op = EASY;            if (nk_option_label(&rawfb->ctx, "hard", op == HARD)) op = HARD;            nk_layout_row_dynamic(&rawfb->ctx, 25, 1);            nk_property_int(&rawfb->ctx, "Compression:", 0, &property, 100, 10, 1);            nk_layout_row_begin(&rawfb->ctx, NK_STATIC, 30, 2);            {                nk_layout_row_push(&rawfb->ctx, 50);                nk_label(&rawfb->ctx, "Volume:", NK_TEXT_LEFT);                nk_layout_row_push(&rawfb->ctx, 110);                nk_slider_float(&rawfb->ctx, 0, &value, 1.0f, 0.1f);            }            nk_layout_row_end(&rawfb->ctx);        }        nk_end(&rawfb->ctx);        if (nk_window_is_closed(&rawfb->ctx, "Demo")) break;        /* Draw framebuffer */        nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);        memcpy(fb_info->screen_base, fb_buf, width * height * bpp);


В этом примере создается окно размером 200 x 200 пикселей, в него отрисовываются графические элементы. Сама итоговая сцена рисуется в буффер fb_buf, который мы выделили SDRAM. А далее в последней строчке просто вызывается memcpy. И все повторяется в бесконечном цикле.

Если просто собрать и запустить этот пример, получим порядка 10-15 FPS. Что конечно не очень хорошо, ведь заметно даже глазом. Причем поскольку в коде рендерера Nuklear много вычислений с плавающей точкой, ее поддержку мы включили изначально, без нее FPS был бы еще ниже. Первая и самая простая (бесплатная) оптимизация конечно флаг компилятора -O2.

Соберем и запустим тот же самый пример получим 20 FPS. Уже лучше, но все равно не достаточно для хорошей работы.

Включение кэшей процессора. Режим Write-Through


Перед тем как перейти к дальнейшим оптимизациям, скажу что мы используем плагин rawfb в составе Nuklear, который как раз и рисует напрямую в память. Соответственно оптимизация работы с памятью выглядит очень перспективно. Первое что приходит на ум это cache.

В старших версиях Cortex-M, таких как Cortex-M7 (наш случай) встроен дополнительный кэш процессора (кэш инструкций и кэш данных). Он включается через регистр CCR блока System Control Block. Но с включением кэша приходят новые проблемы несогласованность данных в кэше и памяти. Есть несколько способов управления кэшем, но в этой статье я не буду на них останавливаться, поэтому перейду к одному из самых простых, на мой взгляд. Чтобы решить проблему несогласованности кэша и памяти можно просто пометить всю доступную нам память как некэшируемую. Это означает, что все записи в эту память будут всегда проходить в память, а не в кэш. Но если мы таким способом пометим всю память, то и от кэша смысла не будет. Есть еще один вариант. Это сквозной режим, при котором все записи в память помеченную как write through попадают одновременно как в кэш, так и в память. Это создает накладные расходы на запись, но с другой стороны, сильно ускоряет чтение, поэтому результат будет зависеть от конкретного приложения.

Для Nuklearа write-through режим оказался очень хорош производительность поднялась с 20 FPS до 45 FPS, что само по себе уже достаточно хорошо и плавно. Эффект конечно интересный, мы даже пробовали отключать write through режим, не обращая внимания на несогласованность данных, но FPS поднимался лишь до 50 FPS, то есть значительного прироста по сравнению с write through не наблюдалось. Отсюда мы сделали вывод, что для нашего приложения требуются много именно операций чтения, а не записи. Вопрос конечно откуда? Возможно, из-за количества преобразований в коде rawfb, которые часто обращаются в память за чтением очередного коэффициента или что-то в этом роде.

Двойная буферизация (пока с промежуточным буфером). Включение DMA


Останавливаться на 45 FPS не хотелось, поэтому решили поэкспериментировать дальше. Следующей идей была двойная буферизация. Идея широко известная, и в общем то нехитрая. Отрисовываем сцену с помощью одного устройства в один буфер, а другое устройство в это время выводит на экран из другого буфера. Если посмотреть на предыдущий код, то хорошо виден цикл, в котором сначала в буфер рисуется сцена, а затем с помощью memcpy содержимое копируется в видео память. Понятно, что memcpy использует CPU, то есть отрисовка и копирование происходят последовательно. Наша идея была в том, что копирование можно делать параллельно с помощью DMA. Другими словами, пока процессор рисует новую сцену, DMA копирует предыдущую сцену в видеопамять.

Memcpy заменяется следующим кодом:

            while (dma_in_progress()) {            }            ret = dma_transfer((uint32_t) fb_info->screen_base,                    (uint32_t) fb_buf[fb_buf_idx], (width * height * bpp) / 4);            if (ret < 0) {                printf("DMA transfer failed\n");            }            fb_buf_idx = (fb_buf_idx + 1) % 2;

Здесь вводится fb_buf_idx индекс буфера. fb_buf_idx = 0 это front buffer, fb_buf_idx = 1 это back buffer. Функция dma_transfer() принимает destination, source и кол-во 32 битных слов. Далее DMA заряжается требуемыми данными, а работа продолжается со следующим буфером.

Попробовав такой механизм производительность выросла примерно до 48 FPS. Чуть лучше чем с memcpy(), но незначительно. Я не хочу сказать, что DMA оказался бесполезен, просто в этом конкретном примере влияние кэша на общую картину показало себя лучше.

После небольшого удивления, что DMA показал себя хуже чем ожидалось, пришла отличная, как нам тогда казалось, мысль использовать несколько DMA каналов. В чем суть? Число данных, которые можно зарядить в DMA за один раз на stm32f7xx составляет 256 Кб. При этом помним, что экран у нас 480x272 и видеопамять порядка 512 Кб, а значит, казалось бы, что можно первую половину данных положить в один канал DMA, а вторую половину во второй. И все вроде бы хорошо Вот только производительность падает с 48 FPS до 25-30 FPS. То есть возвращаемся к той ситуации, когда еще не включили кэш. С чем это может быть связано? На самом деле с тем, что доступ к памяти SDRAM синхронизируется, даже память так и называется Synchronous Dynamic Random Access Memory (SDRAM), поэтому такой вариант лишь добавляет дополнительную синхронизацию, не делая при этом запись в память параллельной, как хочется. Немного поразмыслив, мы поняли, что ничего удивительного тут нет, ведь память то одна, и циклы записи и чтения генерируются к одной микросхеме (по одной шине), а поскольку добавляется еще один источник/приемник, то арбитру, который и разруливает обращения по шине, нужно смешивать циклы команд от разных DMA каналов.

Двойная буферизация. Работа с LTDC


Копирование из промежуточного буфера конечно хорошо, но как мы выяснили этого недостаточно. Рассмотрим еще одно очевидное улучшение двойную буферизацию. В подавляющем большинстве современных контроллеров дисплея можно задавать адрес на используемую видеопамять. Таким образом можно вообще избежать копирования, и просто переставлять адрес видеопамяти на подготовленный буфер, а контроллер экрана заберет данные оптимальным для него способом самостоятельно по DMA. Это и есть настоящая двойная буферизация, без промежуточного буфера как было до этого. Еще есть вариант когда контроллер дисплея может иметь два и более буферов, что по сути дела тоже самое пишем в один буфер, а другой используется контроллером, при этом копирование не требуется.

У LTDC (LCD-TFT display controller) в составе stm32f74xx есть два аппаратных уровня наложения Layer 1 и Layer 2, где Layer 2 накладывается на Layer 1. Каждый из уровней конфигурируется независимо и может быть включен или отключен отдельно. Мы попробовали включить только Layer 1 и у него переставлять адрес видеопамяти на front buffer или back buffer. То есть один отдаем дисплею, а в другой в это время рисуем. Но получили заметное дрожание картинки при переключении наложений.

Попробовали вариант когда используем оба слоя с включением/отключением одного из них, то есть когда каждый слой имеет свой адрес видеопамяти, который не меняется, а смена буфера осуществляется включением одного из слоев с одновременным выключением другого. Вариант также приводил к дрожанию. И наконец, попробовали вариант, когда слой не отключался, а выставлялся альфа канал либо в ноль 0 либо в максимум (255), то есть мы управляли прозрачностью, делая один из слоев невидимым. Но и этот вариант не оправдал ожидания, дрожание все еще присутствовало.

Причина была не ясна в документации сказано, что обновление конфигурации слоев можно выполнять на лету. Сделали простой тест отключили кэши, плавающую точку, нарисовали статическую картинку с зеленым квадратом в центре экрана, одинаковую для обоих Layer 1 и Layer 2, и стали переключать уровни в цикле, надеясь получить статическую картину. Но снова получили то же самое дрожание.

Стало понятно, что дело в чем то другом. И тут вспомнили про выравнивание адреса фреймбуфера в памяти. Так как буфер выделялись из кучи и их адреса были не выровнены выровнены. Выровняли адреса фреймбуферов на 1 Кб получили ожидаемую картинку без дрожания. Потом нашли в документации, что LTDC вычитывает данные пачками по 64 байта, и что не выравненность данных дает значительную потерю в производительности. При этом выровнены должны быть как адрес начала фреймбуфера, так и его ширина. Для проверки мы изменили ширину 480x4 на 470x4, которая не делится на 64 байта, и получил то же самое дрожание картинки.

В итоге, выровняли оба буфера на 64 байта, убедились что ширина выровнена тоже на 64 байта и запустили nuklear дрожание исчезло. Решение, которое сработало, выглядит так. Вместо переключения между уровнями при помощи полного отключения либо Layer 1 либо Layer используем прозрачность. То есть, чтобы отключить уровень, установим его прозрачность в 0, а чтобы включить в 255.

        BSP_LCD_SetTransparency_NoReload(fb_buf_idx, 0xff);        fb_buf_idx = (fb_buf_idx + 1) % 2;        BSP_LCD_SetTransparency(fb_buf_idx, 0x00);

Получили 70-75 FPS! Значительно лучше, чем изначальные 15.

Стоит отметить, что решение работает через управление прозрачностью, а варианты с отключением одного из уровней и вариант с переставлением адреса уровня дают дрожание картинки при FPS больших 40-50, причина нам на данный момент неизвестна. Также забегая вперед скажу, что это решение для данной платы.

Аппаратная заливка сцены через DMA2D


Но и это еще не предел, последней на текущий момент нашей оптимизацией для увеличения FPS, стала аппаратная заливка сцены. До этого мы делали заливку программно:
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);

Давайте теперь скажем плагину rawfb, что заливать сцену не нужно, а только рисовать поверх:
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 0);

Сцену будем заливать тем же цветом 0xff303030, только аппаратно через контроллер DMA2D. Одна из основных функций DMA2D это копирование или заливка цветом прямоугольника в оперативной памяти. Основное удобство здесь в том, что это не непрерывный отрезок памяти, а именно прямоугольная область, которая в памяти располагается с разрывами, а значит обычным DMA сходу не обойтись. В Embox мы еще не работали с этим устройством, поэтому давайте просто воспользовались средствами STM32Cube функция BSP_LCD_Clear(uint32_t Color). Она программирует в DMA2D цвет заливки и размеры всего экрана.

Vertical Blanking Period (VBLANK)


Но даже при достигнутых 80 FPS осталась заметная проблема части виджета двигались небольшими разрывами при перемещении по экрану. То есть, виджет будто бы делился на 3 (или больше) части, которые двигались рядом, но с небольшой задержкой. Оказалось что причина в неправильном обновления видеопамяти. А точнее, обновления в неправильные интервалы времени.

У контроллера дисплея есть такое свойство как VBLANK, оно же VBI или Vertical Blanking Period. Оно обозначает временной интервал между соседними видео кадрами. Или чуть точнее, время между последней строкой предыдущего видеокадра и первой строкой следующего. В этом промежутке никакие новые данные не передаются на дисплей, картинка статическая. По этой причине обновлять видеопамять безопасно именно внутри VBLANKа.

На практике, у контроллера LTDC есть прерывание, которое настраивается на срабатывание после обработки очередной строки фреймбуфера (LTDC line interrupt position configuration register (LTDC_LIPCR)). Таким образом, если настроить это прерывание на номер последней строки, то мы как раз и получим начало интервала VBLANK. В этом месте и производим необходимое переключение буферов.

В результате таких действий картинка нормализовалась, разрывы ушли. Но при этом FPS упал с 80 до 60. Давайте поймем в чем может быть причина подобного поведения.

В документации можно найти следующую формулу:

          LCD_CLK (MHz) = total_screen_size * refresh_rate,

где total_screen_size = total_width x total_height. LCD_CLK это частота, на которой контроллер дисплея будет загружать пиксели из видеопамяти в экран (к примеру, через Display Serial Interface (DSI)). А вот refresh_rate это уже частота обновления самого экрана, его физическая характеристика. Выходит, зная refresh rate экрана и его размеры, можно сконфигурировать частоту для контроллера дисплея. Проверив по регистрам ту конфигурацию, которую создает STM32Cube, я выяснил что он настраивает контроллер на экран 60 Hz. Таким образом, все сошлось.

Немного об input устройствах в нашем примере


Вернемся к нашему приложению и рассмотрим как происходит работа с touchscreen ведь как вы понимаете современный интерфейс подразумевает интерактивность, то есть взаимодействие с пользователем.

У нас все устроено достаточно просто. События от input устройств обрабатываются в основном цикле программы непосредственно перед отрисовкой сцены:

        /* Input */        nk_input_begin(&rawfb->ctx);        {            switch (mouse->type) {            case INPUT_DEV_MOUSE:                handle_mouse(mouse, fb_info, rawfb);                break;            case INPUT_DEV_TOUCHSCREEN:                handle_touchscreen(mouse, fb_info, rawfb);                break;            default:                /* Unreachable */                break;            }        }        nk_input_end(&rawfb->ctx);

Сама же обработка событий от touchscreen происходит в функции handle_touchscreen():

handle_touchscreen
static void handle_touchscreen(struct input_dev *ts, struct fb_info *fb_info,        struct rawfb_context *rawfb) {    struct input_event ev;    int type;    static int x = 0, y = 0;    while (0 <= input_dev_event(ts, &ev)) {        type = ev.type & ~TS_EVENT_NEXT;        switch (type) {        case TS_TOUCH_1:            x = normalize_coord((ev.value >> 16) & 0xffff, 0, fb_info->var.xres);            y = normalize_coord(ev.value & 0xffff, 0, fb_info->var.yres);            nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 1);            nk_input_motion(&rawfb->ctx, x, y);            break;        case TS_TOUCH_1_RELEASED:            nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 0);            break;        default:            break;        }    }}


По сути, здесь происходит конвертация событий input устройств в формат понятный Nuklearу. Собственно наверное и все.

Запускаем на другой плате


Получив вполне приличные результаты, мы решили воспроизвести их на другой плате. У нас была другая похожая плата STM32F769I-DISCO. Там такой же LTDC контроллер, но другой экран с разрешением 800x480. После запуска на ней получили 25 FPS. То есть заметное падение производительности. Это объясняется размером фреймбуфера он почти в 3 раза больше. Но основная проблема оказалась в другом изображение очень сильно искажалось, статической картинки в момент когда виджет должен быть на одном месте не было.

Причина была не ясна, поэтому мы пошли смотреть стандартные примеры из STM32Cube. Там оказалсяз пример с двойной буферизаций для данной платы. В этом примере разработчики в отличие от метода с изменением прозрачности просто переставляют указатель на фреймбуфер по прерыванию VBLANK Этот способ мы уже пробовали ранее для первой платы, но для нее он не сработал. Но применив этот метод для STM32F769I-DISCO, мы получили достаточно плавное изменение картинки с 25 FPS.

Обрадовавшись, мы еще раз проверили данный метод (с переставлением указателей) на первой плате, но он все так же не работал при больших FPS. В итоге, на одной плате работает метод с прозрачностями слоев (60 FPS), а на другой метод с переставлением указателей (25 FPS). Обсудив ситуацию, мы решили отложить унификацию до более глубокой проработки графического стека.

Итоги


Итак, подведем итоги. Показанный пример представляет простой, но в то же время распространенный паттерн GUI для микроконтроллеров несколько кнопок, регулятор громкости, может что-то еще. В примере отсутствует какая либо логика привязанная к событиям, так как упор был сделан именно на графику. По производительности получилось вполне приличное значение FPS.

Накопленные нюансы для оптимизации производительности подводят к выводу, что в современных микроконтроллерах графика усложняется. Теперь нужно, как и на больших платформах, следить за кэшем процессора, что-то размещать во внешней памяти, а что-то в более быстрой, задействовать DMA, использовать DMA2D, следить за VBLANK и так далее. Все это стало похожим на большие платформы, и быть может поэтому я уже несколько раз сослался на X Server и Wayland.

Пожалуй, одной из самых неоптимизированных частей можно считать сам рендеринг, мы перерисовываем всю сцену с нуля, целиком. Я не могу сказать как сделано в других библиотеках для микроконтроллеров, возможно где-то эта стадия встроена в саму библиотеку. Но по итогам работы с Nuklear кажется что в этом месте нужен аналог X Server или Wayland, конечно, более легковесный, что опять таки уводит нас к мысли, что маленькие системы повторяют путь больших.

Наши контакты:

Github: https://github.com/embox/embox
Рассылка: embox-ru[at]googlegroups.com
Телеграмм чат: t.me/embox_chat
Подробнее..

О кэшах в микроконтроллерах ARM

04.11.2020 18:10:50 | Автор: admin
image Привет!

В предыдущей статье мы для ускорения графики на микроконтроллере применяли процессорный кэш. При этом мы использовали режим write-through. Тогда мы писали о некоторых преимуществах и недостатках связанных с write-through режимом, но это был лишь беглый обзор. В этой статье я, как и обещал, хочу подробней рассмотреть типы кэшей на ARM микроконтроллерах, а также сравнить их. Конечно все это будет рассмотрено с точки зрения программиста, и вдаваться в детали работы контроллера памяти в данной статье мы не планируем.

Начну с того на чем остановился в предыдущей статье, а именно, на разнице между write-back и write-through режимами, поскольку именно эти два режима чаще всего используются. Если кратко, то:
  • Write-back. Данные по записи подают только в кэш. Реальная запись в память откладывается до тех пор пока кэш не переполнится и не потребуется место для новых данных.
  • Write-through. Запись происходит одновременно и в кэш и в память.

Write-through


Преимуществами режима write-through считается простота использования, что потенциально уменьшает количество ошибок. Действительно, в этом режиме память всегда находится в корректном состоянии и не требует дополнительных процедур обновления.

Конечно кажется будто это должно сильно сказаться на производительности, но сам STM в этом документе говорит, что это не так:
Write-through: triggers a write to the memory as soon as the contents on the cache line are written to. This is safer for the data coherency, but it requires more bus accesses. In practice, the write to the memory is done in the background and has a little effect unless the same cache set is being accessed repeatedly and very quickly. It is always a tradeoff.

То есть, изначально мы предполагали, что раз запись происходит в память, то на операциях записи производительность будет примерно такой же как и совсем без кэша, а основной выигрыш происходит за счет повторных чтений. Однако, STM это опровергает, говорится что данные в в память попадают в фоне, поэтому производительность на записи практически такая же как и в режиме write-back. Это, в частности, может зависеть от внутренних буферов контроллера памяти (FMC).

Минусы режима write-through:
  • При последовательном и быстром доступе в одну и ту же память производительность может снижаться. В режиме write-back последовательные частые доступы к одной памяти будут, наоборот, являться плюсом.
  • Как и в случае с write-back все равно нужно делать cache invalidate после окончания DMA операций.
  • Баг Data corruption in a sequence of Write-Through stores and loads в некоторых версиях Cortex-M7. Нам указал на него один из разработчиков LVGL.

Write-back


Как уже говорилось выше, в этом режиме (в отличие от write-through) данные в общем случае не попадают в память по записи, а попадают только в кэш. Как и у write-through, у этой стратегии имеются два под варианта 1) write allocate, 2) no write allocate. Об этих вариантах мы поговорим дальше.

Write Allocate


Как правило, в кэшах всегда используется read allocate то есть по промаху (cache miss) на чтение данные забираются из памяти и размещаются в кэше. Аналогично, при промахе на запись данные могут подгружаться в кэш (write allocate) или не подгружаться (no write allocate). Обычно на практике используются сочетания write-back write allocate или write-through no write allocate. Далее в тестах мы попробуем чуть более детально проверить в каких ситуациях использовать write allocate, а в каких no write allocate.

MPU


Прежде чем переходить к практической части нам необходимо разобраться как же задавать параметры региона памяти. Для выбора режима кэша (или его отключения) для определенного региона памяти в архитектуре ARMv7-M используется MPU (Memory Protection Unit).

В контроллере MPU поддерживается задание регионов памяти. Конкретно в архитектуре ARMV7-M может быть до 16 регионов. Для этих регионов можно независимо устанавливать: стартовый адрес, размер, права доступа (read/write/execute и т.д.), атрибуты TEX, cacheable, bufferable, shareable, а так же и другие параметры. С помощью такого механизма, в частности, можно добиться любого типа кэширования для определенного региона. Например, мы можем избавиться от необходимости вызывать cache_clean/cache_invalidate просто выделив регион памяти под все операции DMA и пометив эту память как не кэшируемую.

Нужно отметить важный момент при работе с MPU:
The base address, size and attributes of a region are all configurable, with the general rule that all regions are naturally aligned. This can be stated as:
RegionBaseAddress[(N-1):0] = 0, where N is log2(SizeofRegion_in_bytes)


Иными словами, стартовый адрес региона памяти должен быть выровнен на его собственный размер. Если у вас, к примеру, регион 16 Кб, то выравнивать нужно на 16 Кб. Если регион памяти 64 Кб, то выравниваем на 64 Кб. И так далее. Если этого не сделать, то MPU может автоматически обрезать регион под размер соответствующий его стартовому адресу (проверено на практике).

Кстати, в STM32Cube есть несколько ошибок. Например:
  MPU_InitStruct.BaseAddress = 0x20010000;  MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;


Видно, что стартовый адрес выровнен на 64 Кб. А размер региона хотим 256 Кб. В этом случае придется создавать 3 региона: первый 64 Кб, второй 128 Кб, и третий 64 Кб.

Задавать нужно только регионы с отличными от стандартных свойствами. Дело в том, что атрибуты всех памятей при включении кэша процессора описаны в архитектуре ARM. Есть стандартный набор свойств (к примеру, поэтому память SRAM STM32F7 имеет режим write-back write-allocate по умолчанию), Поэтому если вам понадобится не стандартный режим для какой-то из памятей, то нужно будет задать его свойства через MPU. При этом внутри региона можно задать подрегион со своими свойствами, Выделив внутри этого региона еще один с большим приоритетом с требуемыми свойствами.

TCM


Как следует из документации (раздел 2.3 Embedded SRAM), первые 64 Кб SRAM в STM32F7 некэшируемые. В самой архитектуре ARMv7-M по адресу 0x20000000 находится память SRAM. TCM тоже относится к SRAM, но находится на другой шине относительно остальных памятей (SRAM1 и SRAM2), и располагается ближе к процессору. Из-за этого данная память очень быстрая, по сути дела, имеет такую же скорость как и кэш. И из за этого кэширование не нужно, и этот регион не возможно сделать кэшируемым. По сути TCM это еще один такой вот кэш.

Instruction cache


Стоит отметить, что все рассмотренное выше относится к кэшу данных (D-Cache). Но кроме кэша данных в ARMv7-M предусмотрен и кэш инструкций Instruction cache (I-Cache). I-Cache позволяет перенести часть исполняемых (и следующих) инструкций в кэш, что может значительно ускорить работу программы. Особенно, в тех случаях, когда код находится в более медленной памяти чем FLASH, к примеру, QSPI.

Чтобы уменьшить непредсказуемость в тестах с кэшем ниже, мы намеренно отключим I-Cache и будем думать исключительно о данных.

При этом хочу отметить, что включается I-Cache достаточно просто и не требует никаких дополнительных действий со стороны MPU в отличие от D-Cache.

Синтетические тесты


После обсуждения теоретической части, давайте перейдем к тестам, чтобы лучше понять разницу и сферы применимости той или иной модели. Как я и говорил выше, отключаем I-Cache и работаем только с D-Cache. Так же я намеренно компилирую с -O0, чтобы циклы в тестах не оптимизировались. Тестировать будем через внешнюю память SDRAM. С помощью MPU я разметил регион 64 Кб, и будем выставлять этому региону нужные нам атрибуты.

Так как тесты с кэшами очень капризные и находятся под влиянием всего и вся в системе сделаем код линейным и непрерывным. Для этого отключаем прерывания. Так же, замерять время будем не таймерами, а DWT (Data Watchpoint and Trace unit), в котором есть 32 битный счетчик процессорных тактов. На его основе (на просторах интернета) люди делают микросекундные задержки в драйверах. Счетчик довольно быстро переполняется на системной частоте 216 МГц, но до 20 секунд померить можно. Просто будем об этом помнить, и сделаем тесты в этом временном интервале, предварительно обнуляя счетчик тактов перед стартом.

Non-cacheable memory VS. write-back


Итак, начнем с совсем простых тестов.

Просто последовательно пишем в память.
    dst = (uint8_t *) DATA_ADDR;    for (i = 0; i < ITERS * 8; i++) {        for (j = 0; j < DATA_LEN; j++) {            *dst = VALUE;            dst++;        }        dst -= DATA_LEN;    }


Так же последовательно пишем в память, но не по одному байту за раз, а немного развернем циклы.
    for (i = 0; i < ITERS * BLOCKS * 8; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            *dst = VALUE;            *dst = VALUE;            *dst = VALUE;            *dst = VALUE;            dst++;        }        dst -= BLOCK_LEN;    }

Так же последовательно пишем в память, но теперь еще и чтение добавим.
    for (i = 0; i < ITERS * BLOCKS * 8; i++) {        dst = (uint8_t *) DATA_ADDR;        for (j = 0; j < BLOCK_LEN; j++) {            val = VALUE;            *dst = val;            val = *dst;            dst++;        }    }


Если запустить все эти три теста, то они дадут абсолютно одинаковый результат какой бы режим вы не выбрали:
mode: nc, iters=100, data len=65536, addr=0x60100000
Test1 (Sequential write):
0s 728ms
Test2 (Sequential write with 4 writes per one iteration):
7s 43ms
Test3 (Sequential read/write):
1s 216ms


И это резонно, SDRAM не такая уж и медленная, особенно если учесть внутренние буферы FMC, через который она подключена. Тем не менее, я ожидал небольшой вариации в цифрах, но оказалось что ее на этих тестах нет. Ну что же, будем думать дальше.

Давайте попробуем подпортить жизнь SDRAM смешивая чтения и записи. Для этого развернем циклы добавим такую распространенную на практике вещь как инкремент элемента массива:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            // 16 lines            arr[i]++;            arr[i]++;***            arr[i]++;        }    }

Результат:
Не кэшируемая память: 4s 743ms
Write-back: : 4s 187ms

Уже лучше с кэшем оказалось на пол секунды быстрей. Давайте попробуем еще усложнить тест добавим доступ по разреженным индексам. К примеру, с одним индексом:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr[i + 0 ]++;            ***            arr[i + 3 ]++;            arr[i + 4 ]++;            arr[i + 100]++;            arr[i + 6 ]++;            arr[i + 7 ]++;            ***            arr[i + 15]++;        }    }

Результат:
Не кэшируемая память: 11s 371ms
Write-back: : 4s 551ms

Теперь разница с кэшем стала более чем заметна! И в довершение введем второй такой индекс:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr[i + 0 ]++;            ***            arr[i + 4 ]++;            arr[i + 100]++;            arr[i + 6 ]++;            ***            arr[i + 9 ]++;            arr[i + 200]++;            arr[i + 11]++;            arr[i + 12]++;            ***            arr[i + 15]++;        }    }

Результат:
Не кэшируемая память: 12s 62ms
Write-back: : 4s 551ms

Видим как время для не кэшируемой памяти подросло еще почти на секунду, в то время как для кэша осталось прежним.

Write allocate VS. no write allocate


Теперь давайте разберемся с режимом write allocate. Тут еще сложней увидеть разницу, т.к. если в ситуации между не кэшируемой памятью и write-back становятся хорошо видны уже начиная с 4-го теста, то различия между write allocate и no write allocate до сих пор тестами не вскрылись. Давайте подумаем когда write allocate будет быстрей? Например, когда у вас есть много записей в последовательные ячейки памяти, а чтений из этих ячеек памяти мало. В этом случае в режиме no write allocate будем получать постоянные промахи, и подгружаться по чтению в кэш будут совсем не те элементы. Давайте смоделируем такую ситуацию:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr[j + 0 ]  = VALUE;            ***            arr[j + 7 ]  = VALUE;            arr[j + 8 ]  = arr[i % 1024 + (j % 256) * 128];            arr[j + 9 ]  = VALUE;            ***            arr[j + 15 ]  = VALUE;        }    }

Здесь в 15 из 16 записей выставляется константа VALUE, в то время как чтение осуществляется из разных (и не связанных с записью) элементов arr[i % 1024 + (j % 256) * 128]. Получается, что при стратегии no write allocate только эти элементы и будут загружаться в кэш. Причина по которой используется такая индексация (i % 1024 + (j % 256) * 128) ухудшение скорости FMC/SDRAM. Так как обращения к памяти по существенно различным (не последовательным) адресам, могут существенно сказываться на скорости работы.

Результат:
Write-back : 4s 720ms
Write-back no write allocate: : 4s 888ms

Наконец-то получили разницу, пусть и не настолько заметную, но уже видимую. То есть наша гипотеза подтвердилась.

И наконец, самый сложный, на мой взгляд, случай. Хотим понять когда no write allocate лучше чем write allocate. Первый лучше если мы часто обращаемся к адресам, с которыми в ближайшее время работать не будем. Такие данные, не нужно заносить в кэш.

В следующем тесте в случае write allocate данные будут заполняться по чтению и по записи. Я сделал массив arr2 на 64 Кб, поэтому кэш будет сбрасываться, чтобы подкачать новые данные. В случае же с no write allocate я сделал массив arr на 4096 байт, и только он попадет в кэш, а значит данные кэша сбрасываться в память не будут. За счет этого и попробуем получить хотя бы небольшой выигрыш.
    arr = (uint8_t *) DATA_ADDR;    arr2 = arr;    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr2[i * BLOCK_LEN            ] = arr[j + 0 ];            arr2[i * BLOCK_LEN + j*32 + 1 ] = arr[j + 1 ];            arr2[i * BLOCK_LEN + j*64 + 2 ] = arr[j + 2 ];            arr2[i * BLOCK_LEN + j*128 + 3] = arr[j + 3 ];            arr2[i * BLOCK_LEN + j*32 + 4 ] = arr[j + 4 ];            ***            arr2[i * BLOCK_LEN + j*32 + 15] = arr[j + 15 ];        }    }

Результат:
Write-back : 7s 601ms
Write-back no write allocate: : 7s 599ms

Видно, что write-back write allocate режим чуть-чуть быстрей. Но главное, что быстрей :) Лучшей демонстрации у меня добиться не получилось, но я уверен, что есть практические ситуации, когда разница более ощутима. Читатели могут предложить свои варианты!

Практические примеры


Давайте перейдем от синтетических примеров к реальным.

ping


Один из самых простых это ping. Его легко запустить, а время можно смотреть прямо на хосте. Embox был собран с оптимизацией -O2. Сразу приведу результаты:

Не кэшируемая память : ~0.246 c
Write-back : ~0.140 c


OpenCV


Еще одним примером реальной задачи на которой мы хотели попробовать работу подсистемы cache это OpenCV на STM32F7. В той статье было показано, что запустить вполне реально, но производительность была довольно низкая. Мы используем для демонстрации стандартный пример, который выделяет границы на основе фильтра Canny. Давайте измерим время работы с кешами (и D-cache и I-cache) и без.
   gettimeofday(&tv_start, NULL);    cedge.create(image.size(), image.type());    cvtColor(image, gray, COLOR_BGR2GRAY);    blur(gray, edge, Size(3,3));    Canny(edge, edge, edgeThresh, edgeThresh*3, 3);    cedge = Scalar::all(0);    image.copyTo(cedge, edge);    gettimeofday(&tv_cur, NULL);    timersub(&tv_cur, &tv_start, &tv_cur);

Без кэша:

> edges fruits.png 20
Processing time 0s 926ms
Framebuffer: 800x480 32bpp
Image: 512x269; Threshold=20

С кэшем:

> edges fruits.png 20
Processing time 0s 134ms
Framebuffer: 800x480 32bpp
Image: 512x269; Threshold=20

То есть, 926ms и 134ms ускорение почти в 7 раз.

На самом деле у нас достаточно часто спрашивают про OpenCV на STM32, в частности какая производительность. Получается FPS конечно не высокий, но 5 кадров в секунду, вполне реально получить.

Не кэшируемая или кэшируемая память, но с cache invalidate


В реальных устройствах повсеместно используется DMA, естественно с ним связаны трудности, ведь нужно синхронизировать память даже для режима write-through. Возникает естественное желание просто выделить кусок памяти который будет не кэшируемый и использовать его при работе с DMA. Немного отвлекусь. В Linux это делается функцию через dma_coherent_alloc(). И да, это очень эффективный метод, например, когда идет работа с сетевыми пакетами в ОС, пользовательские данные проходят большой этап обработки прежде чем дойти до драйвера, а в драйвере подготовленные данные со всем шапками копируются в буферы, которые используют не кэшируемую память.

А есть случаи когда в драйвере с DMA более предпочтителен clean/invalidate? Да, есть. К примеру, видеопамять, которая нас и побудила более подробно разобраться с работой cache (). В режиме двойной буферизации у системы есть два буфера, в которые она поочередно рисует, а потом отдает видеоконтроллеру. Если делать такую память не кэшируемой, то случится падение в производительности. Поэтому лучше сделать clean перед тем как отдать буфер в видеоконтроллер.

Заключение


Мы немного разобрались с разными вида кэшей в ARMv7m: write-back, write-through, а также настроек write allocate и no write allocate. Построили синтетические тесты, в которых попытались выяснить когда один режим лучше другого, а также рассмотрели практические примеры с ping и OpenCV. В Embox мы еще только занимаемся данной тематикой, поэтому соответствующая подсистема пока прорабатывается. Хотя достоинства в использовании кэшей определенно заметны.

Все примеры можно посмотреть и воспроизвести собрав Embox из открытого репозитория.

P.S.
Если вам интересна тема системного программирования и OSDev, то уже завтра будет проходить конференция OS Day! В этом году он проходит в онлайне, так что желающие не пропустите! :) Embox выступает завтра в 12.00
Подробнее..

Embox на плате EFM32ZG_STK3200

14.01.2021 22:19:16 | Автор: admin
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_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 для сборки 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.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru