Был один из короновирусных вечеров проводимых мной в самоизоляции. На столе лежала плата STM32F769I-Discovery. Я посмотрел на нее и подумал, ведь это же смартфон. Есть экран c тачскрином 800x480, есть аудио интерфейс, есть сетевой интерфейс, пусть даже и не беспроводной. Все это основано на микроконтроллере, поэтому более надежно с точки зрения температурных режимов. И имеет меньшее потребление. Не хватает только программного обеспечения. Конечно, никакой Android даже близко не встанет на данную плату. И я решил попробовать насколько быстро требуемый для телефона функционал может быть разработан под данную плату на Embox.


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

Телефон на плате STM32F769I-Discovery


Embox конфигурируемая ОС для встроенных систем. Отличительной чертой является то, что она позволяет использовать ПО Linux без изменения исходных кодов на системах с ограниченными ресурсами.

Одним из самых популярных проектов VOIP телефонов является PJSIP. Его и будем использовать для наших целей.

Сборка PJSIP на Linux


Для начала нужно скачать, собрать и запустить главную часть — PJSIP, SIP стек с открытым исходным кодом. Скачиваем последнюю версию. На данный момент это версия 2.10.

Далее нужно собрать проект. Это легко сделать под вашу хостовую ОС. В моем случае это Linux.

$ ./configure --prefix=~/pj_build

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

Далее выполняем

$ make dep
$ make

Запуск PJSIP на Linux


Если все завершилось успешно, значит мы собрали PJSIP, а также demo приложения.

Давайте запустим что-то простое, но функциональное. Нам нужен звонок в обе стороны, возьмем pjsip-apps/src/samples/simple_pjsua.c. Это простое приложение с автоматическим ответом на звонок. Редактируем выбранный пример simple_pjsua.c для того чтобы указать SIP аккаунт. За это отвечают следующие строки:

 #define SIP_DOMAIN   "example.com"
 #define SIP_USER     "alice"
 #define SIP_PASSWD   "secret"

Пересобираем и запускаем:

$ ./pjsip-apps/bin/samples/x86_64-unknown-linux-gnu/simple_pjsua

Должно появиться что-то похожее:

15:21:22.181        	pjsua_acc.c  ....SIP outbound status for acc 0 is not active
15:21:22.181        	pjsua_acc.c  ....sip:bob@sip.linphone.org: registration success, status=200 (Registration successful), will re-register in 300 seconds
15:21:22.181        	pjsua_acc.c  ....Keep-alive timer started for acc 0, destination:91.121.209.194:5060, interval:15s

Теперь можно принимать входящие звонки.

Сборка PJSIP на Embox


Давайте соберем то же самое на Embox. Сначала, чтобы не заботиться о количестве памяти сделаем сборку под эмулятор Qemu.

В Embox есть механизм позволяющий подключать внешние проекты. Он позволяет задать ссылку для скачивания проектов, наложить патчи если требуется, и задать правила для трех стадий: configure, build, install.

Для того чтобы использовать этот механизм достаточно указать что в аннотации ‘@Build’ нужно использовать 'script=$(EXTERNAL_MAKE)'.

@Build(stage=2,script="$(EXTERNAL_MAKE) PJSIP_ENABLE_CXX=false")
@BuildArtifactPath(cppflags="-I$(abspath $(EXTERNAL_BUILD_DIR))/third_party/pjproject/core/install/include/")
module core_c extends core {
	depends pjsip_dependencies
}

Вот такой Makefile используется для переноса сборки под Embox:

PKG_NAME := pjproject
PKG_VER  := 2.10

PKG_SOURCES := https://github.com/pjsip/pjproject/archive/$(PKG_VER).tar.gz
PKG_MD5 	:= 13e5c418008ae46c4ce0c1e27cdfe9b5

include $(EXTBLD_LIB)

PKG_PATCHES := pjproject-$(PKG_VER).patch 	sha256_error_fix-$(PKG_VER).patch 	addr_resolv_sock-$(PKG_VER).patch
…

DISABLE_FEATURES := 	l16-codec   	ilbc-codec  	speex-codec 	speex-aec   	gsm-codec   	g722-codec  	g7221-codec 	libyuv 	libwebrtc

$(CONFIGURE) :
	export EMBOX_GCC_LINK=full; 	cd $(BUILD_ROOT) && (     	./configure         	CC=$(EMBOX_GCC)         	CXX=$(EMBOX_GXX)         	--host=$(AUTOCONF_TARGET_TRIPLET)         	--target=$(AUTOCONF_TARGET_TRIPLET)         	--prefix=$(PJSIP_INSTALL_DIR)         	$(DISABLE_FEATURES:%=--disable-%)         	--with-external-pa; 	)
	touch $@

$(BUILD) :
	cd $(BUILD_ROOT) && (     	     $(MAKE) dep;     	     $(MAKE) MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)'; 	)
	touch $@

$(INSTALL) :
...

Как видно, это те же configure, make dep, make что и для Linux. Конечно при конфигурировании мы указываем что нужно использовать кросс-компиляцию ( --host, --target, CC, CXX) для целевой платформы.

Кроме этого, можно заметить еще одно отличие. Мы указываем --with-external-pa, то есть говорим, что для аудио нужно использовать драйвера из Embox. Audio драйвера в Embox предоставляют интерфейс portaudio, который также доступен на Linux.

Как видно, мы отключили сборку библиотек libyuv и libwebrtc. Также заранее отключим все лишние аудио кодеки кроме PCMA/PCMU. Проверяем корректность конфигурации на Линуксе:

$ ./configure 	--prefix=$PREFIX 	--disable-l16-codec   	--disable-ilbc-codec  	--disable-speex-codec 	--disable-speex-aec   	--disable-gsm-codec   	--disable-g722-codec  	--disable-g7221-codec 	--disable-libyuv 	--disable-libwebrtc
$ make dep && make

Для более простой работы с приложением simple_pjsua перенесем код в Embox. Из модификаций просто вынесем задание параметров SIP аккаунта из Си-шного кода в файл ‘simple_pjsua_sip_account.inc’, который положим в конфигурационные файлы. То есть для сборки приложения с другим аккаунтом вам нужно поменять только этот файл. Содержимое осталось прежним:

#define SIP_DOMAIN 	<sip_domain>
#define SIP_USER   	<sip_user>
#define SIP_PASSWD 	<sip_passwd>

Запускаем приложение simple_pjsua как и раньше на Linux. Работает — значит PJSIP сконфигурирован правильно. Эти параметры configure затем легко переносятся обратно в Makefile на Embox.

Итоговый Makefile под спойлером

PKG_NAME := pjproject
PKG_VER  := 2.10

PKG_SOURCES := https://github.com/pjsip/pjproject/archive/$(PKG_VER).tar.gz
PKG_MD5 	:= 13e5c418008ae46c4ce0c1e27cdfe9b5

include $(EXTBLD_LIB)

PKG_PATCHES := pjproject-$(PKG_VER).patch     sha256_error_fix-$(PKG_VER).patch     addr_resolv_sock-$(PKG_VER).patch

ifeq ($(PJSIP_ENABLE_CXX),false)
PKG_PATCHES    += pjsua2_disable-$(PKG_VER).patch
endif

DISABLE_FEATURES :=     l16-codec       ilbc-codec      speex-codec     speex-aec       gsm-codec       g722-codec      g7221-codec     libyuv     libwebrtc     #g711-codec

BUILD_ROOT  := $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VER)
PJSIP_INSTALL_DIR := $(EXTERNAL_BUILD_DIR)/third_party/pjproject/core/install

$(CONFIGURE) :
    export EMBOX_GCC_LINK=full;     cd $(BUILD_ROOT) && (    	 ./configure    		 CC=$(EMBOX_GCC)    		 CXX=$(EMBOX_GXX)    		 --host=$(AUTOCONF_TARGET_TRIPLET)    		 --target=$(AUTOCONF_TARGET_TRIPLET)    		 --prefix=$(PJSIP_INSTALL_DIR)    		 $(DISABLE_FEATURES:%=--disable-%)    		 --with-external-pa;     )
    cp ./config_site.h $(BUILD_ROOT)/pjlib/include/pj/config_site.h
    touch $@

$(BUILD) :
    cd $(BUILD_ROOT) && (    	 $(MAKE) -j1 dep;    	 $(MAKE) -j1 MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)';     )
    touch $@

$(INSTALL) :
    cd $(BUILD_ROOT) && $(MAKE) install
    # Remove AUTOCONF_TARGET_TRIPLET from file names to use them in Mybuild
    for f in $(PJSIP_INSTALL_DIR)/lib/*-$(AUTOCONF_TARGET_TRIPLET).a; do    	 fn=$$(basename $$f);    	 cp $$f $(PJSIP_INSTALL_DIR)/lib/$${fn%-$(AUTOCONF_TARGET_TRIPLET).a}.a;     done
    # Copy binaries and
    # remove AUTOCONF_TARGET_TRIPLET from file names to use them in Mybuild
    for f in $(BUILD_ROOT)/pjsip-apps/bin/samples/$(AUTOCONF_TARGET_TRIPLET)/*; do    	 cp $$f $(PJSIP_INSTALL_DIR)/$$(basename $$f).o;     done
    for f in $(BUILD_ROOT)/pjsip-apps/bin/*-$(AUTOCONF_TARGET_TRIPLET); do    	 fn=$$(basename $$f);    	 cp $$f $(PJSIP_INSTALL_DIR)/$${fn%-$(AUTOCONF_TARGET_TRIPLET)}.o;     done
    touch $@


Итоговый Mybuild под спойлером
package third_party.pjproject

module pjsip_dependencies {
    depends embox.net.lib.getifaddrs

    depends embox.compat.posix.pthreads
    depends embox.compat.posix.pthread_key
    depends embox.compat.posix.pthread_rwlock
    depends embox.compat.posix.semaphore
    depends embox.compat.posix.fs.fsop
    depends embox.compat.posix.idx.select
    depends embox.compat.posix.net.getaddrinfo
    depends embox.compat.posix.net.gethostbyname
    depends embox.compat.posix.util.gethostname

    depends embox.compat.posix.proc.pid
    depends embox.compat.posix.proc.exit
    depends embox.compat.libc.stdio.fseek
    depends embox.compat.posix.time.time

    depends embox.kernel.thread.thread_local_heap

    depends embox.driver.audio.portaudio_api
}

@DefaultImpl(core_c)
abstract module core { }

@Build(stage=2,script="$(EXTERNAL_MAKE) PJSIP_ENABLE_CXX=false")
@BuildArtifactPath(cppflags="-I$(abspath $(EXTERNAL_BUILD_DIR))/third_party/pjproject/core/install/include/")
module core_c extends core {
    depends pjsip_dependencies
}

/* Currently not used. It will be used for PJSUA2 if required. */
@Build(stage=2,script="$(EXTERNAL_MAKE) PJSIP_ENABLE_CXX=true")
@BuildArtifactPath(cppflags="-I$(abspath $(EXTERNAL_BUILD_DIR))/third_party/pjproject/core/install/include/")
@BuildDepends(third_party.STLport.libstlportg)
module core_cxx extends core {
    depends pjsip_dependencies
    depends third_party.STLport.libstlportg
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpjsip {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libpjsip.a",
   		 "libpjsip-simple.a",
   		 "libpjsip-ua.a"

    @NoRuntime depends core
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpjsua {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libpjsua.a"

    @NoRuntime depends core
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpjlib_util {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libpjlib-util.a"

    @NoRuntime depends core
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpj {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libpj.a"

    @NoRuntime depends core
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpjmedia {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libpjmedia.a",
   		 "libpjmedia-codec.a",
   		 "libpjmedia-audiodev.a"

    @NoRuntime depends core
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpjnath {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libpjnath.a"

    @NoRuntime depends core
}

@BuildDepends(core)
@Build(stage=2,script="true")
static module libpj_third_party {
    @AddPrefix("^BUILD/extbld/third_party/pjproject/core/install/lib")
    source "libresample.a",
   		 "libsrtp.a"

    @NoRuntime depends core
}

@BuildDepends(libpjsua)
@BuildDepends(libpjsip)
@BuildDepends(libpjmedia)
@BuildDepends(libpj)
@BuildDepends(libpjlib_util)
@BuildDepends(libpjnath)
@BuildDepends(libpj_third_party)
@Build(stage=2,script="true")
static module libpj_all {
    @NoRuntime depends libpjsua,
   		 libpjsip,
   		 libpjmedia,
   		 libpj,
   		 libpjlib_util,
   		 libpjnath,
   		 libpj_third_party
}

@AutoCmd
@Cmd(name="streamutil", help="", man="")
@BuildDepends(core)
@Build(stage=2,script="true")
module streamutil {
    source "^BUILD/extbld/third_party/pjproject/core/install/streamutil.o"
    depends core
}

@AutoCmd
@Cmd(name="pjsua", help="", man="")
@BuildDepends(core)
@Build(stage=2,script="true")
module pjsua {
    source "^BUILD/extbld/third_party/pjproject/core/install/pjsua.o"
}

@AutoCmd
@Cmd(name="pjsip_simpleua", help="", man="")
@BuildDepends(core)
@Build(stage=2,script="true")
module simpleua {
    source "^BUILD/extbld/third_party/pjproject/core/install/simpleua.o"
    depends core
}


Теперь можно принимать входящие звонки, но уже на Embox.

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


Осталось подменить конфигурацию Embox c PJSIP для QEMU на конфигурацию для конкретной платы — STM32F769I-Discovery. Для конфигурации Embox нужно несколько составляющих:

  • Файл с флагами компиляции (build.conf).
  • Файл линкера, в котором описано какие доступны памяти и как в них будет расположен финальный образ (lds.conf).
  • Файл конфигурации модулей Embox (mods.conf).
  • Конфигурация PJSIP.

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

Для начала рассмотрим конфигурацию Embox. В чем здесь отличие от запуска на Линукс? На Линуксе у нас было почти бесконечно количество памяти, мы не заботились о числе задач, размере выделяемой памяти и т.д. Теперь же у нас есть всего 2Мб ROM и 512Мб RAM, если не считать внешнюю память. Соответственно, необходимо задать сколько ресурсов нам требуется под конкретные нужды.

К примеру, PJSIP исполняется в своем потоке. На каждое новое соединение еще поток. И еще один поток для работы с аудио. Таким образом, даже при одном соединении нам нужны, как минимум, 3 потока. Далее, хотим добавить DHCP — выделяем еще один поток. Итого, уже 4. Все это естественным образом переносится в конфигурацию:

include embox.kernel.thread.core(thread_pool_size=5,thread_stack_size=12000)

Мы задали стеки фиксированного размера. Можно задать и разного. Все зависит от задачи.

Далее выделяем число необходимых пакетов:

	include embox.net.skbuff(amount_skb=28)
	include embox.net.skbuff_data(amount_skb_data=28)

Устанавливаем размер кучи (откуда malloc() работает):

	include embox.mem.heap_bm
	include embox.mem.static_heap(heap_size=0x3C000)

В остальном конфигурация остается такой же как на QEMU.

Выясняем размер кучи


Основной вопрос, который возникает при составлении конфигурации, как же подобрать нужные параметры? К примеру, почему куча 0x3C000, число сетевых пакетов 28, а стек у потока 12Кб? Я часто применяю следующий подход. Первым делом разбираемся со стеками и кучей. Кучу можно для начала исследовать на Линуксе при помощи Valgrind. Для этого можно использовать профилировщик Valgrind — Massif. Он работает по “снимкам” в определенные моменты времени и показывает какая функция сколько памяти запросила.

Запускаем valgrind с нашим приложением:

$ valgrind --tool=massif --time-unit=B --massif-out-file=pjsip.massif ./pjsip-apps/bin/samples/x86_64-unknown-linux-gnu/simple_pjsua

После работы приложения, визуализируем данные при помощи massif-visualizer:

$ massif-visualizer pjsip.massif



Здесь видно что память расходуется не только на PJSIP, но и на стандартную библиотеку, а также libasound (это хостовый звук — ALSA). Сам PJSIP запрашивает себе размеры из нижнего красного подграфика. Это в пике 600 Кб, а во время соединения около 320 Кб. На нашей плате 512кБ RAM. Поэтому пробуем сконфигурировать PJSIP, уменьшив расход памяти…

Я составил следующую конфигурацию:

#define PJ_LOG_USE_STACK_BUFFER    	0

#define PJ_LOG_MAX_LEVEL 6

#define PJ_POOL_DEBUG    	0
#define PJ_HAS_POOL_ALT_API  0

/* make PJSUA slim */
#define PJSUA_MAX_ACC 3
#define PJSUA_MAX_CALLS 1
#define PJSUA_MAX_VID_WINS 0
#define PJSUA_MAX_BUDDIES 1
#define PJSUA_MAX_CONF_PORTS 4
#define PJSUA_MAX_PLAYERS 1
#define PJSUA_MAX_RECORDERS 1

/* Changing to #if 0 will increase memory consumption
 * but insreases communication speed. */
#if 1
	/* This sample derived from pjlib/include/pj/config_site_sample.h: */
	#define PJ_OS_HAS_CHECK_STACK	0
	#define PJ_ENABLE_EXTRA_CHECK	0
	#define PJ_HAS_ERROR_STRING  	0
	#undef PJ_IOQUEUE_MAX_HANDLES
	#define PJ_IOQUEUE_MAX_HANDLES   8
	#define PJ_CRC32_HAS_TABLES  	0
	#define PJSIP_MAX_TSX_COUNT  	15
	#define PJSIP_MAX_DIALOG_COUNT   15
	#define PJSIP_UDP_SO_SNDBUF_SIZE 4000
	#define PJSIP_UDP_SO_RCVBUF_SIZE 4000
	#define PJMEDIA_HAS_ALAW_ULAW_TABLE  0
#endif

Копируем ее в PJSIP в файл pjlib/include/pj/config_site.h. Пересобираем и запускаем. Анализируем результат:



В пике уже около 300 Кб, что может влезть на плату.

Далее, я выставил кучу порядка 300 Кб в Embox и выставил отладку пулов, чтобы увидеть если что-то переполнится (отмечу, что в итоге размер кучи удалось сократить до 240 Кб). Отладка пулов включается опцией:

#define PJ_POOL_DEBUG    	1

в том же pjlib/include/pj/config_site.h.

Хорошо, остается настроить стеки потоков и число сетевых пакетов. Здесь нужно грамотно распределить оставшиеся ресурсы. К примеру, если сетевых пакетов будет слишком мало, то звук просто “захлебнется”. Если же пакетов выделить слишком много, то под стеки ничего не останется. Приоритет отдается конечно же стекам. Если стек испортился — все пропало.

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

@Runlevel(0) include embox.arch.arm.armmlib.exception_entry(irq_stack_size=1024)

После этого оставшиеся ресурсы отдаем на сетевые пакеты. Как я уже приводил выше, их у нас получилось 28 штук.

Все, первая часть успешно выполнена. Приложение simple_pjsia успешно работает на STM32F769I-Discovery во внутренней памяти 512 Кб.

Дорабатываем SIP-телефон. Добавляем пользовательский интерфейс.


После успешного запуска консольной версии нужно как-то добавить пользовательский интерфейс. Для простоты будем считать, что он включает в себя следующее. При старте приложения на экране должна быть какая-то поясняющая надпись. К примеру, “PJSIP DEMO”. Если есть входящий звонок, то на экран выводится откуда звонок, и появляются две кнопки с иконками — “Accept”, “Decline”. Звонок можно либо принять либо отклонить. В случае если звонок принят, то начинается разговор, выводится контактная информация об абоненте, и на экране остается одна кнопка — “Hang”. В случае если звонок изначально отклонен — здесь все тривиально — возвращаемся к начальной картинке с “PJSIP DEMO”.

Вот пример того, как это должно будет выглядеть.



Разработка прототипа на Линуксе


Так как в Embox уже была поддержка Nuklear, я решил использовать этот проект. Хотя на микроконтроллере у нас уже имеется работающая консольная версия телефона, здесь важно, что доработать UI сильно проще на Линуксе, как это уже было с настройкой PJSIP выше.

Чтобы это сделать, берем два примера. Первый пример это simple_pjsua из PJSIP. Второй пример это demo/x11_rawfb/ из Nuklear. Теперь наша задача в том, чтобы они заработали вместе под Линукс.

Первым делом я заменил автоматический ответ на звонок в PJSIP на ответ по внешнему событию (например, нажатие кнопки). Далее, я написал логику на Nuklear.

В процессе выяснилось, что иконки почему-то не прорисовались внутри кнопок. На картинке ниже вы можете видеть иконки телефона внутри зеленой и красной кнопок. Это обычные картинки, на которых прозрачно все на 100% кроме телефонной трубки. При этом изначально вместо них рисовались лишь белые квадраты. Дело было в реализации плагина rawfb. Видимо, он не очень популярен, поэтому в нем отрисовывается только курсор. Я добавил код, который просто копирует содержимое изображения в нужный регион памяти Nuklear.

В итоге после дня работы на проектом, я получил следующее:



Зная, что у STM32F76I-Discovery размер экрана 800x480, а в QEMU 800x600 я сразу выставил нужные размеры в Nuklear, чтобы проще было ориентироваться в создании динамических надписей и кнопок. Итоговый код получился следующим:

	if (nk_begin(ctx, "Demo", nk_rect(0, 0, WIN_WIDTH, WIN_HEIGHT),
        	NK_WINDOW_NO_SCROLLBAR)) {
    	int answer_pressed = 0, decline_pressed = 0;

    	if (!draw_mouse) {
        	nk_style_hide_cursor(ctx);
    	}

    	nk_layout_row_static(ctx,
        	(WIN_HEIGHT - CALL_BTN_HEIGHT - 2 * CALL_INFO_TEXTBOX_HEIGHT - WIN_HEIGHT / 4), 15, 1);

    	nk_layout_row_dynamic(ctx, CALL_INFO_TEXTBOX_HEIGHT, 1);

    	nk_style_set_font(ctx, &rawfb_fonts[RAWFB_FONT_DEFAULT]->handle);

        switch (call_info->state) {
        case CALL_INACTIVE:
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 56;
            nk_label(ctx, "PJSIP demo", NK_TEXT_CENTERED);
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 32;
            break;
        case CALL_INCOMING:
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 32;
            nk_label(ctx, "Incoming call from:", NK_TEXT_CENTERED);
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 38;
            nk_label(ctx, call_info->incoming, NK_TEXT_CENTERED);
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 32;
            break;
        case CALL_ACTIVE:
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 32;
            nk_label(ctx, "Active call:", NK_TEXT_CENTERED);
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 38;
            nk_label(ctx, call_info->remote_uri, NK_TEXT_CENTERED);
            rawfb_fonts[RAWFB_FONT_DEFAULT]->handle.height = 32;
            break;
        }

        if (call_info->state != CALL_INACTIVE) {
            nk_layout_row_static(ctx, (WIN_WIDTH - 9 * 4) / 9, (WIN_WIDTH - 9 * 4) / 9, 9);

            switch (call_info->state) {
            case CALL_INCOMING:
                nk_spacing(ctx, 2);
                demo_nk_accept_btn(ctx, im_accept, &answer_pressed);
                nk_spacing(ctx, 3);
                demo_nk_decline_btn(ctx, im_decline, &decline_pressed);
                nk_spacing(ctx, 2);
                break;
            case CALL_ACTIVE:
                nk_spacing(ctx, 4);
                demo_nk_decline_btn(ctx, im_decline, &decline_pressed);
                nk_spacing(ctx, 4);
                break;
            default:
                break;
            }
        }

    	if (answer_pressed && call_info->state == CALL_INCOMING) {
        	demo_pj_answer();
    	}
    	if (decline_pressed) {
        	demo_pj_hang();
    	}
	}
	nk_end(ctx);

Запуск на плате


Остается перенести проект сначала на QEMU, а затем на плату. У нас уже все подготовлено для консольной версии, поэтому просто перенесем новое приложение с Линукса. Для этого достаточно создать Mybuild файл в системе сборки Embox:

@AutoCmd
@Cmd(name="sip_nuklear", help="", man="")
@BuildDepends(third_party.pjproject.libpj_all)
@BuildDepends(third_party.lib.nuklear)
@Build(stage=2)
module sip_nuklear {
	@InitFS
	source "icons/phone-accept-80.png",
       	"icons/phone-decline-80.png",
       	"fonts/Roboto-Regular.ttf"

	source "main.c"

	source "nuklear_main.c"

	@IncludePath("$(CONF_DIR)")
	@DefineMacro("PJ_AUTOCONF=1")
	source "pjsua.c"

	@NoRuntime depends third_party.pjproject.libpj_all
	@NoRuntime depends third_party.lib.nuklear
	depends embox.driver.input.core
	depends rawfb_api
}

Как видно, перечисляются исходники. Иконки и шрифты размещаются во внутренней файловой системе и будут доступны как обычные read-only файлы. Кроме того, добавлены зависимости от библиотек pjsip и nuklear.

Запустив приложение на плате, я заметил что шрифт по умолчанию из Nuclear ужасно выглядит на экране STM32F769I. Часть букв просто была потеряна. К примеру “1” выглядела как “|”, а “m” как “n”. Пришлось подключить шрифты из ttf файлов — Roboto-Regular.ttf. Этот шрифт занимает порядка 150 Кб флэш-памяти, зато текст разборчив.

Проверив на Линукс я решил, что это небольшая плата. И попробовал использовать разные размеры шрифтов 32 и 38. Но получил segfault. В итоге я отказался от идеи загрузки нескольких размеров шрифта из файла, а загрузил только 32-ой и масштабировал его.

Особенности запуска на железе


Вернемся к запуску на плате. Здесь важно понимать, что в случае с UI необходим фреймбуфер. Так как мы хотим полноэкранный режим, а экран 800x480, то даже при 1-байтовой RGB палитре нам потребовалось 800 * 480 * 1 = 384000 байт, то есть 375 Кб. Учитывая, что мы уже заняли почти всю внутреннюю память 512 Кб под нужды PJSIP, найти место под фреймбуфер не получится. По этой причине будем задействовать SDRAM. На STM32F76I-Discovery доступно 16 Мб. Раз мы уже задействуем внешнюю память, не будем сильно экономить и поставим RGBA 32 бит. Таким образом, фреймбуфер будет иметь размер 800 * 480 * 4 = 1536000 байт или 1.5 Мб.

В нашей конфигурации SDRAM находится на адресе 0x60000000. Указываем его как адрес фреймбуфера.

	@Runlevel(1) include embox.driver.video.stm32f7_lcd(
    	fb_base=0x60000000, width=800, height=480, ltdc_irq=88, bpp=32
	)
	include embox.driver.video.fb

Я уже рассказывал про эффекты мерцания при использовании одного буфера в другой статье. Поэтому будем учитывать что в системе используется двойная буферизация, а, следовательно, нужна дополнительная память под еще один буфер размером 1.5 Мб. Кроме того, шрифты потребуют еще порядка 256 Кб. Итого, нужно увеличивать кучу на 2 Мб. Также размещаем ее во внешней памяти:

	@Runlevel(2) include embox.driver.input.touchscreen.stm32f7cube_ts
	@Runlevel(2) include embox.driver.input.input_dev_devfs

Теперь тачскрин будет устройство /dev/stm32-ts в файловой системе devfs в Embox, и с ним можно работать через обычные open()/read().

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

	@Runlevel(0) include embox.arch.arm.armmlib.armv7m_cpu_cache(
    	log_level=4,
    	sram_nocache_section_size=0x10000
	)

В Embox дескрипторы и данные сетевых пакетов, а также аудио буферы, с которыми работает DMA располагаются в специальной секции памяти, помеченной как некэшируемая память в MPU. Это требуется для того, что состояние объектов, находящихся в этой памяти было всегда корректным как для CPU так и для DMA.

В итоге мы получаем очень простой работающий SIP-телефон с UI c кнопками, который достаточно хорошо работает.

Ниже я постарался изобразить итоговое распределение памяти:



Разработка на хостовой системе


Мой процесс разработки свелся к тому что приведен на рисунке.



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

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