Итак, это случилось. Я добыл Flipper Zero в бою (ну, на самом деле мне его выдали на хакатоне), и естественно, что интереснее всего не играть в него, используя уже сделанные кем-то функции, а писать что-то новое. Примерно так же я развлекался с Pebble. Тут, кстати, все очень похоже — МК, RTOS, небольшой монохромный экранчик, С (ну, С++ тоже есть, но зачем).

Когда-то надпись была другой, по-русски и не очень цензурная (https://twitter.com/vvzvlad/status/1467813142885548038, подписывайтесь, кстати), но разработчики послушали восхищенные возгласы комьюнити и заменили ее :(
Когда-то надпись была другой, по-русски и не очень цензурная (https://twitter.com/vvzvlad/status/1467813142885548038, подписывайтесь, кстати), но разработчики послушали восхищенные возгласы комьюнити и заменили ее :(

Поэтому давайте попробуем что-нибудь под него написать. Для начала, что-то совсем простое, чтобы освоиться с SDK, не закапываясь в отладку и сложности RTOS, но функциональное. Есть кнопки, есть экранчик, давайте напишем счетчик-кликер. Нажимаете кнопку — число увеличивается. Подойдет считать посетителей, круги на стадионе, взломанные домофоны, молитвы (без шуток, я видел ребят, которые делали электронные четки) или сообщения в чате русскоязычного комьюнити флиппера (по утрам, в особенно активное время там их бывает до 2к, так что я бы на вашем месте подумал, прежде чем нажимать кнопку Join).

Шаг первый: установка нужного софта, проверка сборки и загрузка прошивки

Web-ide, как у Pebble, у флиппера нет, но это вопрос времени. Но пока прошивку надо собирать на своем компьютере.

Обратите внимание, у меня на ноутбуке MacOS, поэтому первые 4 команды я привожу для нее. Для Linux будет отличаться. Там в чем-то проще — можно одной командой запустить докер, у которого внутри все уже будет работать, но могут быть проблемы с пробросом USB-устройств туда. Установить в систему чуть сложнее, но тоже сводится к десятку команд.

Версия оригинальной прошивки (запомните ее)
Версия оригинальной прошивки (запомните ее)

1)Устанавливаем brew и git (если еще не. Но тогда что вы тут делаете?):

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install git

2)Клонируем репозиторий с прошивкой:

git clone https://github.com/flipperdevices/flipperzero-firmware.git
cd flipperzero-firmware

3) Устанавливаем необходимые утилиты (список берется из файла Brewfile в корне репозитория, поэтому не стоит пытаться выполнить эту команду в другом каталоге):

brew bundle --verbose

Если brew после установки bundle скажет "Homebrew Bundle complete! 6 Brewfile dependencies now installed", то все хорошо, ничего делать больше не надо.

Если у вас уже был установлены пакеты, связанные со сборкой под нативный arm (arm-none-eabi-*) какой-то старой версии, то вы получите ошибку при установке. В этом случае стоит посмотреть, что у вас стоит командой brew list | grep arm (в моем случае это был gcc-arm-none-eabi-62, который неведомо как прожил в системе с 2015 года) и удалить его командой brew remove gcc-arm-none-eabi-62 (у вас название пакета может быть другое).

4) Собираем прошивку:

make 

Дожидаемся, пока make скажет нам что-то типа:

	LD	 .obj/f7/bootloader.elf
   text	   data	    bss	    dec	    hex	filename
  30680	    640	   2656	  33976	   84b8	.obj/f7/bootloader.elf
	BIN	 .obj/f7/bootloader.bin
	HEX	 .obj/f7/bootloader.hex
	DFU	 .obj/f7/bootloader.dfu
	JSON	 .obj/f7/bootloader.json
CFLAGS ok

	HEX	 .obj/f7/firmware.hex
	DFU	 .obj/f7/firmware.dfu
	JSON	 .obj/f7/firmware.json
Firmware binaries can be found at:
	/Users/vvzvlad/Documents/Projects/flipper-hack/flipperzero-firmware/dist/f7
Use this file to flash your Flipper:
	flipper-z-f7-full-local.dfu

Готово, вы великолепны! Осталось только загрузить эту прошивку в устройство.

5) Программатор для загрузки прошивки не требуется, только провод со штекером USB Type-C.

Флиппер, подключенный к компьютеру
Флиппер, подключенный к компьютеру

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

Флиппер в режиме DFU
Флиппер в режиме DFU


Для прошивки надо перейти в режим DFU — зажать одновременно кнопку "назад" и "влево"(<+↩), после перезагрузки отпустить "назад"(↩), оставив зажатой "влево"(<). Если отпустить сразу обе, то флиппер просто перезагрузится, а если при перезагрузке оставить <, то войдет в DFU.

Загрузка прошивки делается вот такой командой:

make -C firmware upload

Если make говорит, что "Nothing to be done for upload.", то это странное поведение makefile: два раза прошить одну и ту же прошивку не получится, make считает, что все таргеты выполнены. Обходной путь, если вам надо прошить тоже самое второй раз — сделать что-то вроде touch ./applications/about/about.c, заставив make думать, что вы что-то изменили в одном файле. Ну или make clean && make, но в этом случае он пересоберет всю прошивку заново, что займет ощутимо больше времени.

После этого на экране вы увидите что-то вроде такого:

> make -C firmware upload
CFLAGS ok

dfu-util -D .obj/f7/firmware.bin -a 0 -s 0x08008000 
dfu-util 0.11

dfu-util: Warning: Invalid DFU suffix signature
dfu-util: A valid DFU suffix will be required in a future dfu-util release
Opening DFU capable USB device...
Device ID 0483:df11
Device DFU version 011a
Claiming USB DFU Interface...
Setting Alternate Interface #0 ...
Determining device status...
DFU state(2) = dfuIDLE, status(0) = No error condition is present
DFU mode device DFU version 011a
Device returned transfer size 1024
DfuSe interface name: "Internal Flash   "
Downloading element to address = 0x08008000, size = 728896
Erase   	[=========================] 100%       728896 bytes
Erase    done.
Download	[====================     ]  81%       590848 bytes

Стирание и прошивка занимает где-то минуту. Если в это время отключить флиппер, то прошивка прервется, флиппер работать не будет, но он останется в режиме DFU и повторным запуском той же команды можно довести дело до конца. Если же вы вдруг перезагрузите его после неудачной прошивки (сочетанием <+↩), то вас встретит пустой экран и в DFU с картинкой он больше не зайдет. Но это не значит, что вы его окончательно окирпичили, и вам поможет только программатор.
Нет, достаточно чуть другой комбинации для входа в DFU в этом случае: зажимаем сочетание <+↩+⦿, потом отпускаем < и ↩, оставляя нажатым ⦿(центральную кнопку в джойстике), через секунду отпускаем и ее. Все, можно прошивать.

После прошивки девайс нужно перезагрузить сочетанием <+↩, и попасть уже в свежезалитую прошивку.

Версия прошивки после сборки и загрузки. Последняя строка — это ветка гитхаба, я собирал из своего репозитория, поэтому она "vvzvlad-dev"
Версия прошивки после сборки и загрузки. Последняя строка — это ветка гитхаба, я собирал из своего репозитория, поэтому она "vvzvlad-dev"

Лайфхак: если вам не хочется нажимать каждый раз на сочетание кнопок для перезагрузки в DFU, то вы можете зайти в него, отправив в cli флиппера команду "dfu". Автоматизировать это можно следующим способом:

stty -f /dev/cu.usbmodemflip_* 115200
echo -en "dfu\r\n" > /dev/cu.usbmodemflip_*

Выход из DFU чуть сложнее:

dfu-util -a 0 -s :leave ; sleep 0.5
dfu-util -s 0x08008000 -a 0 -R

Соответственно, собрав это в одну команду, чтобы получить что-то вроде stty -f /dev/cu.usbmodemflip_* 115200 && echo -en "dfu\r\n" > /dev/cu.usbmodemflip_* && make -C firmware upload && dfu-util -a 0 -s :leave ; sleep 0.5 && dfu-util -s 0x08008000 -a 0 -R, у вас получится команда для запуска DFU, прошивки, и перезагрузки в прошивку после прошивки (гм). Естественно, что она не сработает, если вы настолько сломаете прошивку, что она не сможет запуститься или будет уходить в бесконечный цикл с блокированием прерываний и переключений тасков или просто не будет принимать команды в cli. Придется запускать вручную.
Но в принципе, лайфхак вовсе не обязателен, если вам надо собрать одну прошивку и загрузить ее в устройство, хватит qflipper (приложения для компьютера, скачать можно с https://update.flipperzero.one/) или ручного перехода в DFU по сочетанию кнопок. Но в том случае, если во время отладки вы будете прошивать десяток-другой сборок и хотите, чтобы это требовало от вас как можно меньше действий, и у вас нет программатора вроде ST-Link/J-Link, то время это сэкономит.

Теперь, когда мы умеем собирать прошивку, самое время сделать какое-нибудь осмысленное маленькое приложение.

Шаг второй: пишем приложение

Для начала, поймем, а под что мы вообще собрались писать.
Центральный контроллер — STM32WB55, два arm-ядра (одно Cortex-M4, второе Cortex-M0+, заведует всяким радио), 1 мегабайт флеша, 256 кб оперативки, 2.4ггц трансивер (Zigbee, Thread, BLE), SPI, I2C, USB, куча таймеров, криптография (включая хранилище неизвлекаемых ключей). В общем, Linux не поставить, но по меркам микроконтроллеров довольно жирный камень.
Кроме самого микроконтроллера есть еще и субгигагерцовый трансивер — СС1101 от TI, который может работать на основных частотах, 315/433/868/915 МГц. Для работы с NFC используется отдельный ридер, ST25R3916, работа с 125 кГц метками осуществляется силами самого процессора, как и с iButton и IR.

Однако, в отличии от "обычной" разработки под МК, когда на микроконтроллере выполняется исключительно ваша логика, тут не весь камень отдан в ваше распоряжение. Помимо самого контроллера, тут есть куча железок — и трансивер, и NFC, и экран, и всякое-разное на I2C шине, типа микросхемы управления светодиодом и подсветкой. Нормально управлять всем этим в суперцикле не получится (точнее, вы в процессе этого напишите свою RTOS), поэтому тут есть операционная система — FreeRTOS, в которой, во-первых, удобно разграничивать разные логические приложения в таски, а во-вторых, которая сама занимается переключением тасков и их контекста, сама определяет, что именно надо запустить сейчас и так далее.

Отладчик показывает список тасков
Отладчик показывает список тасков

Например, сразу при старте, даже без запуска других приложений, уже становятся активны 19 тасков — начиная от тех, что обслуживают события с кнопок, слежение за зарядом аккумулятора и выдача данных для иконки заряда, рисование интерфейса, который рисует эту иконку и весь остальной интерфейс в придачу, и заканчивая записью-чтением из памяти и реализацией консольного интерфейса. Даже у состояния дельфина есть отдельный таск.
Разумеется, это не означает, что они работают сразу все и активно. Большинство из них находятся в состоянии сна подавляющую часть времени, просыпаясь только тогда, когда им приходит сообщение, из ресурсов занимая только часть оперативки под свой стек.

Зачем нужна такая сложность? Ну, например, для удобства разработки. Вот хочется, например, вам в приложении сделать "дрр" вибромотором. Для этого надо не только его включить, но и выключить в нужное время. Если вы будете делать это внутри своего приложения, которое запускается на контроллере монопольно, то вам придется делать какую-то логику для ожидания перед отключением. А если ваше приложением упадет или зависнет, то вибромотор останется включенным. А это самый простой пример, гораздо сложнее разрулить такого рода штуки с другим железом, требующим жестких таймингов. А если в это время вы еще и хотите что-то на экран выводить.. Поэтому и RTOS. Тут можно просто отправить сообщение процессу, ответственному за работу с вибромотором, в котором сказать "включи его на 300мс", и забыть про это — ОС сама разбудит этот таск, сама позаботится о доставке в него сообщения, и вновь разбудит его через 300мс, чтобы оно могло вибромотор отключить. А в это время другие процессы будут продолжать работать.

Многозадачность тут вытесняющая, так что можно не думать над тем, когда отдавать управление планировщику, если считаете что-то сложное, он сам отберет у вас управление. Но наломать дров все же можно — это не настольная ОС, где до железа пять слоев абстракций, тут можно работать напрямую, например с шиной I2C, и часто эта работа требует реалтайма, поэтому существуют способы сказать RTOS, чтобы во время выполнения этого куска кода вас ни за что не прерывали. Но вот если в процессе выполнения этого кода вы уйдете в бесконечный цикл, девайсу кроме перезагрузки уже ничего не сможет помочь.

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

Сейчас приложения — это часть прошивки, т.е. для добавления нового вам придется добавить его в код прошивки, собрать ее и залить в устройство. Работы над загрузкой приложений без пересборки прошивки и даже с SD-карты ведутся, но пока ситуация такая.

Самое короткое приложение

Итак, учитывая все вышесказанное — какой порог входа? Какое минимальное количество кода надо написать, чтобы запустить на флиппере приложение?
Оказалось, пять строчек. Следите за руками. Три строчки уйдут на само приложение:

long counter_app(void* p) {
    return 0;
}

Эти три строчки надо положить в файл .с, и кинуть его, в целом, где угодно в папке flipperzero-firmware/applications. Я положил в /applications/counter, но в целом не принципиально — компилируется в ходе сборки все подряд, это потом линкер определяет своих. Следите за тем, чтобы название функций были уникальными, а то компилироваться оно будет, а вот собираться в бинарник нет.
Еще одна строчка — объявление имени главной функции приложения в applications/applications.c с ключевым словом extern:

extern int32_t counter_app(void* p);

И еще одна строчка — это создание пункта меню, который будет запускать приложение, там же, в applications.c:

{.app = counter_app, .name = "Counter", .stack_size = 1024, .icon = &A_BadUsb_14},

.app — это имя функции-точки входа в приложение, .name — имя в меню, .stack_size — размер стека для приложения (для нашего многовато как-то, не находите?), .icon — иконка для меню. В главном меню иконки обязательны, поэтому NULL-ом обойтись не получится, и я взял иконку у соседнего приложения. А сама запись — это просто один элемент массива:

const FlipperApplication FLIPPER_APPS[] = {
...
    {.app = counter_app, .name = "Counter", .stack_size = 1024, .icon = &A_BadUsb_14},
...
};

Еще хорошо бы его окружить ifdef/endif (как тут, например) для того, чтобы можно было в applications.mk выбирать (вот тут логика работы ifdef, вот тут сам выбор), с какими приложениями собирать прошивку, а с какими нет, но это уже за пределами минимального ТЗ "лишь бы запускалось". Итак, три строчки нам потребовалось, чтобы добавить новое приложение в прошивку.

Правда, иконка у нее от другого приложения. А еще оно ничего не делает: никакой реакции на нажатие, потому что мы никак не описали интерфейс. Приложение запускается и тут же завершается.

Давайте добавим интерфейс, но прежде всего приведем в порядок уже написанные пять строк, превратив их в 15. Ничего необычного, в приложении заменили long на int32_t (пришлось потратить еще одну строчку на инклуд furi.h, все равно он понадобится дальше), и как описано выше, обвязали пункт меню ifdef для возможности выбрать, включать или нет это приложение ключами при компиляции.

Furi, инклуд которого мы добавили, он же фурри — это наполовину HAL, наполовину системная библиотека, реализующая всякие полезные штуки, которые обычно лежат в папке utils. Когда-то название расшифровывалось как Flipper Universal Registry Interface, но актуальность этого давно потерялась, но разработчики так и не смогли расстаться с приятной их сердцу аббревиатурой, поэтому она тут.

Идем дальше. Соорудим приложению хоть какой-нибудь интерфейс, чтобы при запуске происходило хоть что-то. До обработки кнопок мы еще не дошли, поэтому приложение будет открываться и закрываться само. Но если это сделать без задержки, то процесс будет настолько быстрым, что поведение будет неотличимо от предыдущего варианта, поэтому полезной нагрузкой приложения будет просто delay(2000).
Delay тут можно вставлять в неограниченных количествах, по целым двум причинам: во-первых, вытесняющая многозадачность, которая отберет управление даже у while-цикла (но не отдаст его таскам с более низким приоритетом и не даст процессору уйти в сон, так что лучше бесполезные долгие циклы не плодить), а во-вторых, delay тут вызывает vTaskDelay из FreeRTOS, который лишь сообщает планировщику, что в следующий раз надо вызвать этот таск через определенное количество времени, после чего планировщик передает управление другим таскам или уводит процессор в сон, если таковых нет.

Приложение приобретает следующий вид (коммит):

#include <furi.h>
#include <furi-hal.h>
#include <gui/gui.h>

int32_t counter_app(void* p) {
    ViewPort* view_port = view_port_alloc();
    Gui* gui = furi_record_open("gui");
    gui_add_view_port(gui, view_port, GuiLayerFullscreen);

    delay(2000);

    gui_remove_view_port(gui, view_port);
    furi_record_close("gui");
    view_port_free(view_port);
    return 0;
}

То, что в нем делается, довольно просто: сначала создаем ViewPort — это такая структура, в которую записываются указатели на коллбеки отрисовки экрана и получения эвентов от пользователя, потом вызываем furi_record_open, получая структуру Gui — говорим операционной системе, что у нас тут есть есть некоторый интерфейс и мы его хотим показывать, потом добавляем в полученный Gui созданный ViewPort, ждем две секунды, освобождаем все ресурсы и выходим. Негусто, да. Но работает:

Давайте разнесем выделение ресурсов и их освобождение по отдельным функциям, чтобы они не мешались в main, а сами ресурсы положим в структуру, так код станет читабельнее и переносимее. Создадим структуру с Gui и ViewPort (у нас больше ничего и нет пока):

typedef struct {
    Gui* gui;
    ViewPort* view_port;
} CounterApp;

И перепишем основную функцию вот так (коммит):

void counter_app_free(CounterApp* app) {
    gui_remove_view_port(app->gui, app->view_port);
    view_port_free(app->view_port);
    furi_record_close("gui");
    free(app);
}

CounterApp* counter_app_alloc() {
    CounterApp* app = furi_alloc(sizeof(CounterApp));
    app->view_port = view_port_alloc();
    app->gui = furi_record_open("gui");
    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
    return app;
}

int32_t counter_app(void* p) {
    CounterApp* app = counter_app_alloc();

    delay(2000);

    counter_app_free(app);
    return 0;
}

Приложение, которое хоть что-то делает

Теперь все готово к какому-то осмысленному функционалу. Давайте, наконец, что-то напишем в нашем приложении, хотя бы название.
Как я уже говорил, для вывода чего-то на экран служит функция коллбека, которую вызывает тред Gui. Функция выглядит вот так:

void counter_draw_callback(Canvas* canvas, void* ctx) {
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 2, 10, "Counter application");
}

Она вызывается с двумя аргументами — первый это canvas, на котором мы собственно, и что-то рисуем/пишем, второй служит для передачи туда каких-нибудь дополнительных данных, т.к. как эта функция, хоть и лежит в приложении, де-факто будет выполняться внутри треда Gui, который ее вызвал, и не будет иметь доступа к данным приложения. Поэтому вторым аргументом туда передается контекст приложения — например, та самая структура CounterApp или какая-нибудь другая с данными, которые должны быть отрисованы.

Что делаем дальше — понятно по названиям функций. Устанавливаем шрифт и по координатам 2:10 выводим этим шрифтом надпись. Осталось указать эту функцию в качестве коллбека для отрисовки. Для этого где-нибудь в конце counter_app_alloc вызываем функцию view_port_draw_callback_set вот так:

view_port_draw_callback_set(app->view_port, counter_draw_callback, app);

Первый аргумент — это обьект вьюпорта, второй — непосредственно коллбек, а третий — тот самый контекст, который появляется во втором аргументе коллбека. Я туда уже положил app, хоть он в коллбеке никак еще не используется. Еще для каждой отрисовки надо вызывать функцию view_port_update, которая сообщает треду о том, что надо запросить коллбек, когда у треда интерфейса будет время, но в данном случае ее вызывать не обязательно — после регистрации коллбека он будет в первый раз вызван автоматически.

Работает (коммит):

Но у нас по-прежнему нет обработчиков кнопок, и выходим мы по дилею. Следующая задача — добавить обработку кнопок. Для этого нам понадобятся две вещи: еще один коллбек для инпута и очередь сообщений. Очередь сообщений — потому что коллбек инпута, как и коллбек отрисовки, выполняется в контексте другого треда, и вызвать что-то из нашего треда не имеет возможности. Точнее, вызвать-то может, но если он вызовет, то это будет уже функция, запущенная в другом треде. Но RTOS тем и хороша, что у нее есть встроенные средства отправить сообщение другому треду из любого места в программе. Таким образом, план выглядит вот так: мы создаем коллбек, который будет вызываться из треда пользовательского ввода, и который будет отправлять сообщения в наш основной тред, где они и будут обрабатываться.

Для начала, надо создать очередь сообщений. Добавляем в структуру CounterApp очередь (event_queue с типом osMessageQueueId_t):

  typedef struct {
      Gui* gui;
      ViewPort* view_port;
+     osMessageQueueId_t event_queue;
  } CounterApp;

Создаем при инициализации очередь и удаляем ее при выходе:

CounterApp* counter_app_alloc() {
    ...
+    app->event_queue = osMessageQueueNew(8, sizeof(InputEvent), NULL);
    ...
}

void counter_app_free(CounterApp* app) {
    ...
+    osMessageQueueDelete(app->event_queue);
    ...
}

В osMessageQueueNew "8" — это количество сообщений в очереди, следующий аргумент это размер одного элемента — мы будем класть туда приходящий в коллбек события этого же типа, и последний — это какие-то атрибуты, сейчас нам ненужные.

Создаем коллбек:

void counter_input_callback(InputEvent* input_event, void* ctx) {
    furi_assert(ctx);
    osMessageQueuePut((osMessageQueueId_t)ctx, input_event, 0, 0);
}

Выглядит страшно, но на самом деле не сложнее прошлого, с рисованием на экране. Первый аргумент — это эвент, из которого можно понять, что и как нажалось, второй — такой же контекст. Чтобы функция , отправляющая сообщения могла понять, куда надо эти сообщения отправлять, мы положим в контекст очередь сообщений. furi_assert — это, в данном случае, проверка на существование контекста. Если его не передать, то и отправлять будет некуда, поэтому лучше упасть. Что, собственно, furi_assert и делает. Но делает это правильно, выводя сообщение о падении и уходя после этого в бесконечный цикл с запретом прерываний, чтобы можно было подключить отладчик и посмотреть, а где все-таки все упало.
Дальше — мы, собственно, отправляем сообщение. Приводим контекст к типу osMessageQueueId_t, так как он у нас приходит в виде void, вторым аргументом передаем в него полученный эвент, третим и четвертым — приоритет и таймаут, которые тут никак не используются.

Теперь в counter_app_alloc мы регистрируем этот коллбек:

CounterApp* counter_app_alloc() {
  ...
+    view_port_input_callback_set(app->view_port, counter_input_callback, app->event_queue);
	...
}

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

Теперь самое вкусное — разбираем сообщения в основной функции. Вместо sleep(2000) пишем такое:

while(1) {
    InputEvent input;
    osStatus_t result = osMessageQueueGet(app->event_queue, &input, NULL, osWaitForever);
    furi_check(result == osOK);
    if(input.type == InputTypeShort && input.key == InputKeyBack) {
        break;
    }
}

Функция получения данных из очереди передает нам данные, записывая их по указателю, поэтому сразу создаем структуру InputEvent, такую же, какая приедет нам в коллбеке и будет отправлена через очередь, и вызываем osMessageQueueGet, передавая ей очередь, указатель на то, куда надо записывать полученные данные, приоритет и таймаут. В качестве последнего аргумента указываем osWaitForever, что заставит ОС поставить тред на паузу, пока не придет сообщение. Если бы там было какое-то конкретное значение, то ОС бы будила тред не только по приходу сообщения , но и по истечению этого времени, чтобы он мог сделать что-то другое.

osMessageQueueGet возвращает только маркер статуса, который должен быть "OK", а если он не ОК, то случилось что-то нехорошее и надо упасть и дождаться пользователя с отладчиком, что и делает furi_check. furi_assert и furi_check имеют одинаковое поведение, но с одной разницей — furi_assert предназначен для дебага программы, чтобы не забыть что-то передать, например, и отловить это как можно раньше и при сборке релизной прошивки распадается на плесень и липовый мед, а furi_check никуда не девается, и используется там, где приходящие данные от разработчика не зависят: мы можем делать все правильно, но RTOS может по независящим от нас причинам передавать статус ошибки, поэтому даже в релизной версии имеет смысл упасть и дождаться дебаггера (о настройке железного дебаггера и отладке с ним тоже будет во второй статье).

Код далее — тривиален. input, переданный нам коллбеком и пересланный через очередь сообщений, имеет в себе type и key. Type — это тип нажатия: Press, Release (оба уже после устранения дребезга), Short, Long, Repeat. Тип уже за нас обработался в треде пользовательского ввода, поэтому нам не надо заниматься подсчетом длительности нажатия для определения, короткое оно или длинное и заботиться об устранении дребезга. Key — это идентификатор клавиши (Up, Down, Right, Left, Ok, Back). Нам будут приходить все сообщения от всех всех нажатых клавиш, в том числе и события Press и Release. Но мы их игнорируем и фильтруем только короткие нажатия на кнопку "назад". Как только что-то пройдет фильтр — выходим из цикла, освобождаем ресурсы и выходим из программы. Тут break, а не counter_app_free и return, потому что иметь в программе различное число вызовов аллокации и освобождения — не очень хорошая идея, можно запутаться и либо попытаться освободить что-то второй раз, либо забыть и не освободить.

Работает! (коммит)

Что тут еще можно сделать лучше? Вынести инициализацию input из цикла, чтобы это происходило один раз при старте (можно, например, туда же, где и все остальное, в struct CounterApp), а не при каждом нажатии кнопки. Накладных расходов немного, на как-то неаккуратно. Получение сообщений и проверку кода статуса можно записать в одну строчку:

furi_check(osMessageQueueGet(app->event_queue, &app->event, NULL, osWaitForever) == osOK);

Поехали дальше. Чего не хватает счетчику? Правильно, самого счетчика.

И наконец, счетчик

Добавляем счетчик. Для этого нам нужна, как минимум, переменная счетчика — положим ее туда же, в общую структуру. Думаю, uint16 хватит:

 typedef struct {
     Gui* gui;
     ViewPort* view_port;
     osMessageQueueId_t event_queue;
     InputEvent event;
+    uint16_t counter;
 } CounterApp;

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

 while(1) {
     furi_check(osMessageQueueGet(app->event_queue, &app->event, NULL, osWaitForever) == osOK);
-         if(app->event.type == InputTypeShort && app->event.key == InputKeyBack) {
+    if(app->event.type == InputTypeLong && app->event.key == InputKeyBack) {
         counter_app_free(app);
         return 0;
     }
+    if(app->event.type == InputTypeShort) {
+        app->counter++;
+    }
+    if(app->event.type == InputTypeLong && app->event.key == InputKeyOk) {
+        app->counter = 0;
+    }
+    view_port_update(app->view_port);
 }

Изменения понятны и говорят сами за себя. Отдельно надо отметить только вызов view_port_update в конце — это тот самый вызов, который сообщает процессу Gui, что у нас есть новые данные и хорошо бы их отрисовать. Раньше мы не использовали его, потому что данные на экране рисовались один раз, а теперь мы хотим обновлять экран каждый раз после нажатия кнопки и изменения счетчика. В целом, судя по данным отладчика, view_port_update вызывается сам по себе примерно каждую секунду и так, но гарантировать это вам система не может (как и то, что такое поведение сохранится в будущих версиях), поэтому лучше все-таки вызвать его специально.

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

 void counter_draw_callback(Canvas* canvas, void* ctx) {
+    CounterApp* app = (CounterApp*)ctx;
     canvas_set_font(canvas, FontPrimary);
     canvas_draw_str(canvas, 2, 10, "Counter");
 

+   char string[6];
+   sprintf(string, "%d", app->counter);
+
+   canvas_set_font(canvas, FontBigNumbers);
+   canvas_draw_str(canvas, 53, 38, string);
+
+   canvas_set_font(canvas, FontSecondary);
+   canvas_draw_str(canvas, 15, 60, "Long press back for exit");
}

Первой добавленной строчкой мы приводим переданный контекст к нужному типу. Остальными добавленными — создадим строку, с помощью sprintf запишем в нее текстовое представление переменной счетчика, достав ее из контекста, переключимся на шрифт с большими цифрами, нарисуем цифры, переключимся на обычный шрифт, нарисуем комментарий "Long press back for exit", чтобы не забыть, как выходить, все.

Собственно, счетчик мы сделали (вот коммит):

Минусы — поле фиксировано по левому краю, и при увеличении разрядности немного уползает вбок, некрасиво (но есть canvas_draw_str_aligned, которая делает это красиво). Еще эта реализация не потокобезопасна — операции инкремента не атомарные, и может случиться ситуация, когда основной тред увеличивает число, а в этот момент ОС решает отдать управление треду Gui и тот решает отрисовать экран, и на экране появляется не совсем то, что мы задумали. Могла быть ситуация еще хуже, если бы мы имели какие-то операции с изменением указателя. Стоит смотреть на все действия внутри программы с точки зрения "а что случится, если в произвольный момент вызовется коллбек отрисовки" или любой другой процесс, потенциально конкурирующий за данные. И если вам не нравится, что может произойти в этом случае, не надо оставлять так, стоит обмазать конструкцию мьютексами. Пример этого можно увидеть вот тут.

Заключение

Разработка под флиппер отличается от разработки под обычные МК: в первую очередь тем, что для правильной работы надо соблюдать соглашения о совместном использовании ресурсов, потому что ваш код не выполняется на контроллере единолично, и ему требуется сосуществовать с другой логикой, в том числе системной. 

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

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

Комментарии (72)


  1. LionZXY
    24.12.2021 17:34
    +7

    Очень приятный текст, а главное - постепенное погружение в недры прошивки. Спасибо тебе!


  1. Firsto
    24.12.2021 17:58
    +11

    Прям душу травите)

    Когда же уже мне его отправят.


  1. EnterSandman
    24.12.2021 17:59
    +8

    До этого хотелось купить потому что "ну клёво же", а теперь еще и siemens-like интерфейс накрыл ностальгией. Уговорили)


  1. predator86
    24.12.2021 19:25

    легко может быть такая ситуация, когда забыли освободить что-то вы сейчас, а упадет другое приложение потом.
    Разработчики flipper'а что-то про MPU говорили, или ни как от этого не защитится?


    1. utsu
      24.12.2021 19:28

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


      1. predator86
        24.12.2021 19:32

        Почему бы не освободить это автоматически при закрытии приложения?


        1. utsu
          24.12.2021 20:25

          Оверхед на трекинг ресурсов будет равен бесконечности. В больших ОС для этого есть интерфейс сисколов и MMU, у нас ничего.


          1. predator86
            24.12.2021 20:55

            Так MPU у Вас всё таки используется или нет? Если да, то для чего?


            1. utsu
              24.12.2021 21:26

              MPU используется для защиты 0x0 от записи.


              1. predator86
                24.12.2021 21:38
                +2

                Насколько я понял, используется один большой heap и в нём все программы берут кусочки памяти и затем их возвращают. Почему бы не выделять каждой программе свой heap, и при её завершении освобождать его целиком? Тогда можно защитить эти участки с помощью MPU.


                1. utsu
                  24.12.2021 21:53

                  И получится микроядро.

                  Вы представляете себе какая потребуется сложность аллокатора и оверхед чтобы заставить это работать в реальности? Какой будет сложность взаимодействия между тасками?

                  Даже основной код прошивки не поместится после этого в память.


                  1. predator86
                    24.12.2021 22:05
                    +5

                    Это не я придумал. Вот выдержка из мануала по Cortex M4:

                    If a program accesses a memory location that is prohibited by the MPU, the processor generates a memory management fault. This causes a fault exception, and might cause termination of the process in an OS environment.
                    In an OS environment, the kernel can update the MPU region setting dynamically based on the process to be executed. Typically, an embedded OS uses the MPU for memory protection.

                    Так как вы пользуетесь FreeRTOS, то вынуждены терпеть все её недостатки, которые компенсируются её «универсальностью» ко всем архитектурам, позволяющей лишь видеть знакомые названия API при программировании разных контроллеров. Если хотите чтобы ОС была полноценной, то она должна быть написана конкретно под Cortex M.


                    1. utsu
                      24.12.2021 22:12

                      Напишите как долно быть и сделайте пул реквест.


                      1. predator86
                        24.12.2021 22:17
                        +3

                        У меня нету Flipper'а чтобы его проверить.


                      1. dernuss
                        24.12.2021 22:44
                        -1

                        есть только у избранных)


                      1. predator86
                        24.12.2021 22:58

                        Раздали бы энтузиастам, уже бы и прошивка давно была готова и дружное комьюнити сразу же образовалось.


                      1. vvzvlad Автор
                        24.12.2021 22:59
                        -1

                        Вы «мифический человеко-месяц» читали? :)


                      1. predator86
                        24.12.2021 23:09

                        Не согласен. Новые программисты, если грамотно ими воспользоваться (тем более бесплатные), помогут тестированию, либо образуют альтернативные версии ПО. А какая разница если будет продаваться?


                      1. vvzvlad Автор
                        24.12.2021 23:58
                        +10

                        Не читали. А «как пасти котов»?
                        Нельзя даже просто залить разработку деньгами и набрать программеров, это не работает. А вы предлагаете сделать тоже самое, но без денег?

                        Новые программисты — это расходы на введение в курс дела. Например, я с нуля, имея под рукой людей, которых можно спрашивать, потратил три или четыре дня просто на то, чтобы понять, как именно работает описанный в статье кусок апи, хотя до этого имел ощутимый опыт в разработке под МК. Просто потому что другие подходы, другие абстракции, не особо помогает ни опыт разработки под baremetal, ни под *nix. И это маленький кусочек уже написанной кодовой базы, понимания которого недостаточно, чтобы написать что-то не просто рабочее, а пригодное к использованию и сопровождению. Реального обьема того, что надо понять, читая код, прежде чем человек может писать приложения которые можно вливать в мастер, на порядок больше. Кто будет вводить всех этих разрабов в курс дела, учитывая что полноценной документации на внутренние интерфейсы пока нет?

                        Новые программисты — это расходы на управление и накладные расходы на коммуникацию. Знаете, почему внутренняя ставка разраба в компаниях может быть в два раза больше, чем его зарплата? Не только из-за налогов, но и потому, что занятость разраба тянет за собой занятость менеджеров, им управляющих. Даже не столько на «поставить задачу и проконтролировать», а просто на устранение проблем в коммуникации внутри команды, подпинывание отстающих, чтобы не тормозили команду, чтобы соблюдали кодстайл и так далее. Этих менеджеров надо взять, обучить, платить зарплату. Без них программисты разбредаются и начинают генерировать плюшки для собственного развлечения.

                        Новые опенсорсные программисты — это люди, пришедшие сюда ради интереса (я много писал об этом вот тут: Как Unix-way убивает десктопный Linux). Они не работают за зарплату, им нельзя поставить KPI, их нельзя вызвать на разговор и сказать «ты сделал плохо, переделай». Они не будут в третий раз переписывать реализацию, потому что вдруг изменился подход в какой-то системной библиотеке. А он будет меняться еще десять раз, потому что софт в стадии развития. А раз они не способны на поддержку своего кода, то он — либо бессмысленная трата времени, потому что он не будет использоваться, либо дополнительная нагрузка на текущих разработчиков, которым придется поддерживать код, сделанный людьми, которые не до конца понимают принципы работы и стиль написания (см. п. 1). Зачастую он такого качества, что проще переписать с нуля, чем отлаживать приложения в попытках найти, где же бага.

                        Кроме того, вот именно то, что вы предлагаете — раздать разрабам, уже и было сделано на хакатоне, условия которого были очень лайтовые — заявите интересную идею и хоть что-нибудь сделайте. Что там сделали? Из 30 или-сколько-там-человек что-то продолжают писать в ядро и системный функционал — двое или трое. Остальные написали свою аппу, и на этом их разработка закончилась. А, ну еще один, «образующий альтернативную версию ПО», которая идет вразрез с политикой разработчиков флиппера. И хорошо, что их трое, а не 30, потому что см. п. 1 и п. 2, существующие же разрабы зашились бы.


                      1. predator86
                        25.12.2021 00:21
                        -6

                        Я понял у Вас стереотипное мышление. Но они продают товар для энтузиастов и подход должен быть соответствующий.


                      1. predator86
                        25.12.2021 13:50
                        +1

                        Новые опенсорсные программисты — это люди, пришедшие сюда ради интереса
                        Этот товар делается в основном для них.
                        Из 30 или-сколько-там-человек что-то продолжают писать в ядро и системный функционал — двое или трое.
                        Их должно было быть 3000.
                        А, ну еще один, «образующий альтернативную версию ПО»
                        А этих должно быть не один, а двадцать один.
                        Flipper должен был стать «пластилином» из которого можно легко создать «тулс своей мечты».
                        Кроме того, вот именно то, что вы предлагаете — раздать разрабам
                        Да! Это был бы дружественный шаг, на встречу комьюнити. А сейчас ему просто не из чего образоваться.


                1. Dr_Zlo13
                  24.12.2021 22:30

                  1) Как решать проблему освобождения памяти когда задача аллоцирует через IPC ресурс в памяти другой задачи\сервиса?
                  2) Для разделения куч на каждую задачу надо запланировать ее определенный размер и адрес, обеспечивая неперекрываемость адресов размерами из-за некоторых особенностей, см ниже.
                  3) В MPU может быть только 8 регионов.
                  4) Гранулярность размера региона задается с шагом в степень двойки (32, 64, 128...), например мы не можем задать регион в 10 килобайт, только в 8 или 16, что обеспечивает очень сложное прогнозирование допустимого размера кучи на задачу.
                  5) Адрес участка должен быть кратен его размеру. Нельзя защитить восемь килобайт оперативной памяти начиная с третьего килобайта, только с нулевого или с восьмого. Это дает еще больше ада при прогнозировании размеров и адресов куч.

                  Из-за всех этих особенностей MPU практически неприменим на кортексах-м даже для защиты стека задач.


                  1. predator86
                    24.12.2021 22:55
                    +2

                    1) Переписать её память в память другой задачи внутри SVC, так же часть тесно взаимодействующих задач могут быть объединены в рамках одного процесса и быть в одном защищённом регионе.
                    2) Так это хорошо когда приложению ограничат доступ к общей памяти, оно пусть скажет сколько ему надо, а система подумает пускать его в работу или нет. Иначе оно пойдет в разнос и кому очень надо не достанется.
                    3) Значит можно выделить 8 независимых heap'ов на процесс, очень даже не кисло. А в M7 даже 16 дают. Некоторые даже все 8 пристроить не могут.
                    4) Каждый регион может быть разделён на 8 частей — сразу проще стало.
                    5) Опять же, разделение каждого региона на 8 частей упрощает эту задачу.

                    Из-за этих особенностей MPU может достаточно легко защитить программы от взаимного повреждения памяти (а так же всех ресурсов МК), и полностью передать её контроль ОС.


                    1. Dr_Zlo13
                      24.12.2021 23:19

                      1) Сам IPC пишется больно, не представляю (точнее слишком хорошо представляю) боль при том что еще накладываются ограничения на память и передачу указателей. Кстати как решается то что ресурсом должен владеть (пользоваться, читать\писать) и сервис и наша задача? IPC на каждый чих в каждую сторону? Но для IPC так же нужна общая область памяти.

                      2) Даже прогнозирование стека плохо автоматизируется, а кучу пргнозировать кралтно сложнее. Плюс это невозможно нормально поддерживать в приложении в случае смены размеров каких-то системных обьектов.

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


                      1. predator86
                        24.12.2021 23:44
                        +2

                        1) Для IPC не нужна своя память. Вы свободно пользуетесь вытесняющим менеджером, так вот IPC ест ресурсов меньше. IPC должен работать из SVC и (пользоваться, читать\писать) не будет проблем.

                        IPC на каждый чих в каждую сторону
                        Ну не попиксельно же передавать изображение.
                        2) Если невозможно спрогнозировать то давать с адекватным запасом. Но отдавать всё что можно сразу — это уже крайность.

                        я изучал, очень много изучал разные проекты и подходы к ос
                        Я понимаю, но этого недостаточно, нужно экспериментировать, с первого раза и у меня не получилось, но со 1001…


                      1. Dr_Zlo13
                        24.12.2021 23:58

                        Не понимаю где я говорил что что-то можно изучать без эксперементирования.

                        Ну и если у вас удалось реализовать такой подход - поделитесь им, код сразу решит многие возникшие вопросы.


                      1. predator86
                        25.12.2021 00:02
                        -2

                        код сразу решит многие возникшие вопросы
                        Даже не знаю. Я уже всё расписал как делать. А flipper'а дадите?


                      1. dernuss
                        25.12.2021 00:04

                        только купить, и то не всем)

                        у меня есть купон, могу продать по себестоимости

                        как реализовать купон, тема мутная


  1. predator86
    24.12.2021 19:27
    -1

    И всё же FreeRTOS кажется сложноватой, а как там дела с поддержкой Arduino IDE?


    1. predator86
      24.12.2021 23:04
      +1

      Я считаю что поддержка Arduino IDE очень важна для тех кто ей уже пользуются. Как они смогут приобрести устройство и безболезненно пользоваться им. Тогда со временем у них появится желание перейти на полноценную работу и изучить «правильное» программирование.


      1. vvzvlad Автор
        24.12.2021 23:22
        +3

        Как показывает практика, если человек не хочет осваивать описанное в статье (а оно уж реально несложное, я не понимаю, как описать проще), и хочет писать как и дальше в Arduino IDE, то в 90% случаев желания перейти дальше у него так и не появится.

        Плюс, надо понимать что от того, что этот код будет написан в привычном редакторе и при этом разработчик не будет видеть страшных папок с чужим кодом вокруг, сложность самого кода уменьшится несильно: любая серьезная работа с железом потребует вникания в схемотехнику и специфичный для флиппера HAL, попытки нарисовать что-то больше двух кнопок — потребуют работы с вызовами интерфейса. Интерфейс и HAL требуют динамического выделения памяти, а оно требует как минимум понимания, что ты делаешь. А совместимости с ардуиновскими либами и вызовами, как у esp не будет, слишком разная база.

        Так что я не очень понимаю, для чего Arduino IDE?
        Чтобы не напугать пользователя? Так если его пугает взять готовый каркас приложения и писать в нем, то вряд ли он напишет что-то толковое.
        Упростить задачу? А она не упростится, библиотеки переиспользовать нельзя, готовые программы работать не будут, интерфейс надо будет писать с нуля.
        Поиграться, написав hello world и поморгав светодиодом? Ну можно, тоже важная штука, но в списке приоритетов далеко не на первом месте.


        1. dernuss
          24.12.2021 23:27

          Так что я не очень понимаю, для чего Arduino IDE?

          Действительно, зачем? если под ардуино полно всяких либ....

          Упростить задачу? А она не упростится, библиотеки переиспользовать нельзя, готовые программы работать не будут, интерфейс надо будет писать с нуля.

          Так и смысл в том, чтобы библиотеки можно было бы переиспользовать....


          1. Dr_Zlo13
            24.12.2021 23:40
            +1

            Большая часть библиотек ардуино не портируется под другой камень даже при наличии интерфейсов, из-за регистров и PROGMEM. Еще большая часть - не портируется в RTOS вообще, из-за подхода к памяти как к единоличному ресурсу.


            1. dernuss
              24.12.2021 23:43
              -1

              даже не знаю как работают одинаково либы под ардуино нано на atmega328 и например на esp32 ;)


              1. Dr_Zlo13
                24.12.2021 23:49

                Пишутся одновременно под все камни. Например https://github.com/FastLED/FastLED/blob/26baf51559a7f09c580b1c0f38ea6c7675663ea9/src/fastspi.h

                Или пишутся отдельно под каждый камень, например https://github.com/jkb-git/ESP32Servo


                1. dernuss
                  24.12.2021 23:52

                  и? под flipper сложно?

                  дигитал врайт\реад, wire и прочее конечно сложно под flipper


                  1. Dr_Zlo13
                    24.12.2021 23:59

                    Все возможные существующие библиотеки для Ардуино переписать под Флиппер? Да, сложновато, судя по тому что даже под ESP не все еще переписаны, только самые распространенные.


                    1. dernuss
                      25.12.2021 00:03
                      -1

                      ну так то дигитал врайт\реад, wire конечно сложно реализовать, я понимаю

                      С учётом того, что и продавать уже целый год динамят и устройство устарело


                      1. kAIST
                        25.12.2021 00:39
                        +1

                        Скажу как ленивый человек, которому приходилось вылезать из теплого уютного arduino ide.

                        Ломка проходит очень быстро. И без всех этих digitalWrite и пр., живётся вполне себе нормально. И нет большой разницы, пишешь ли ты код с использованием новой для тебя библиотеки в arduino ide или любой другой си библиотеки.

                        Если ты не знаешь самых основ, да, становится менее уютно.


                      1. dernuss
                        25.12.2021 10:23
                        +2

                        И без всех этих digitalWrite и пр., живётся вполне себе нормально.

                        Конечно, и пилить заново дравера каждый раз под новую патформу вообще люкс


                      1. vvzvlad Автор
                        25.12.2021 03:34
                        -1

                        ну так то дигитал врайт\реад, wire конечно сложно реализовать, я понимаю


                        Вам что, надо #define DigitalWrite hal_gpio_write? Так это и сейчас можно сделать.


                      1. dernuss
                        25.12.2021 10:22
                        +1

                        а spi и i2c так заработают?


                      1. Dr_Zlo13
                        25.12.2021 13:20

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


                      1. dernuss
                        25.12.2021 15:06

                        так как в каждой библиотеке свой собственный слой совместимости с железом

                        нет в каждой библиотеке такого слоя, полным полно либ без этого слоя.


                      1. AotD
                        25.12.2021 20:27

                        А можно пару-тройку примеров?


                      1. dernuss
                        25.12.2021 23:47

                        Пара попроще

                        https://github.com/claws/BH1750

                        https://github.com/BlueDot-Arduino/BlueDot_BME280

                        Один посложнее

                        https://www.arduino.cc/reference/en/libraries/lvgl/

                        Ну и тот же https://github.com/FastLED на который ссылался @Dr_Zlo13 вполне работает через стандартный spi


        1. Dr_Zlo13
          24.12.2021 23:28

          Слой совместимости, к слову, не очень сложно делается. Только на одних analog read и gpio write далеко не уехать, большая часть сложностей в GUI и работе с файлами, и в ардуино для этого нет подходящих абстракций. А если нужно что-то изучать в любом случае, то почему не изучить сразу голый апи Флиппера?


          1. predator86
            24.12.2021 23:46

            То есть без шансов, сазу в бой, без обучения!


            1. Dr_Zlo13
              24.12.2021 23:52

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


              1. predator86
                24.12.2021 23:59
                +2

                слоя для базовой совместимости
                Этого вполне достаточно.


  1. Haoose
    24.12.2021 20:22

    Классная статья, все по порядку, читать интересно, понравилось, жду продолжения.

    Например, сразу при старте, даже без запуска других приложений, уже становятся активны 19 тасков

    Подскажите, сколько места они занимают изначально, из тех 256 кб оперативки?


    1. utsu
      24.12.2021 20:24
      +2

      256 это на самом деле 192 + 32 + 32.

      Где 32 + 32 отдается второму ядру и IPC.

      Из оставшихся 192 мы используем 70.

      Остается 120к.


  1. dernuss
    24.12.2021 22:42
    +2

    Так а как купить у кого есть купон то?

    Further instructions will be provided in the coming two weeks, on the day we launch priority sales. We will send instructions by email which will also include your personal pre-order coupon in case you've lost it.

    Но как то глухо....


    1. zhovner
      24.12.2021 23:10
      +1

      Чтобы не нарваться на страйк платежных систем рассылаем постепенно. Использовать свой купон на предзаказ вы можете по этой инструкции https://t.me/zhovner_hub/1516


      1. dernuss
        24.12.2021 23:17
        -5

        это ваше постепенно уже год наверное

        устройство уже устарело


        1. koteeq
          24.12.2021 23:20
          +2

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


        1. kAIST
          25.12.2021 00:41
          +3

          Не подскажите более современные аналоги, которые не устарели?


          1. dernuss
            25.12.2021 10:25
            -1

            proxmark, хотябы продаётся и уже давно


            1. Xobotun
              25.12.2021 12:09

              Proxmark всё же больше по части NFC и 125/128 кГц. И ноутбук с линуксом и знанием специфичных команд для него требует, емнип. По крайней мере, я себе 125 кГц чипы запорол, не указав пароль и с тех пор не могу перезаписать. Только NFC остался.

              А у флипперва можно хранить коды от домофонов и шлагбаумов во дворе, удобно. И батарейка встроенная сразу, и интерфейс с меню есть. Он, имхо, больше прикладной девайс, чем исследовательский proxmark.


              1. dernuss
                26.12.2021 00:11

                А у флипперва можно хранить коды от домофонов и шлагбаумов во дворе, удобно.

                Вот думаю, вы реально будете этим заниматься?

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

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

                а в третьих, флиппер как то великоват для таких дел


  1. notastorm
    25.12.2021 00:45
    +1

    Шикарно. Никак не дождусь, когда уже можно будет попробовать это все в деле.


  1. QtRoS
    25.12.2021 01:53
    +1

    Еще эта реализация не потокобезопасна

    Хм, заставило задуматься. На x86 не происходит ничего страшного, если один тред инкрементит значение поля структуры, а второй его по тому же адресу читает для отрисовки. Отрисованное значение всегда будет корректным, актуальным на момент чтения. На ARM'е хуже обстоят дела? Не понимаю за счет чего. Слабая модель памяти вроде не должна повлиять...

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


    1. utsu
      25.12.2021 02:32
      +2

      Концептуально нету разницы между армом и x86 для того что мы написали. Есть общие проблемы дизайна lock-free алгоритмов и структур данных. Самый просто пример это когда модель не помещается в машинное слово и её частичное обновление приводит к крашу, в этом случае явно нужно ее гардить каким-либо примитивом синхронизации ОС.

      Чтобы не заставлять разработчиков ломать голову над доступом к памяти у нас есть несколько типов моделей представления: без блокировок и с блокировками доступа.

      В общем описанная ситуация не возможна: приложение включается в дерево отрисовки только когда оно сказало что готово.


      1. yawny
        25.12.2021 15:56
        +1

        Так в итоге в вашем примере может упасть или нет? Не очень понял.

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


        1. vvzvlad Автор
          25.12.2021 16:46
          +1

          Так в итоге в вашем примере может упасть или нет? Не очень понял.

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


  1. MarSoft
    25.12.2021 09:23
    +2

    Возможно, я что-то не понимаю, но в момент выделения counter_app_alloc и counter_app_free в alloc выделяется память под структуру CounterApp с помощью furi_alloc. А в counter_app_free эта память не освобождается. То есть утекает...


    1. vvzvlad Автор
      25.12.2021 16:43

      Да, free(app) потерялся, mea culpa. Поправил.


  1. Lesterrry
    25.12.2021 14:52
    +3

    У ребят на сайте сказано, мол

    Flipper's main components are written in C, C++, and Rust

    Интересно, на двух последних языках процесс разработки приложений что примерно из себя представляет?


    1. utsu
      25.12.2021 14:54

      Rust нету, его дропнули давно. Всё ядро на С, а С++ можно найти во фрэймворке гуйовых приложений и приложениях RFID, IR, iButton.


      1. Lesterrry
        25.12.2021 18:06
        +2

        Почему, интересно, дропнули


        1. Dr_Zlo13
          25.12.2021 21:03

          Обскурно и прилично добавляет сложностей в экспорте api.


        1. dernuss
          26.12.2021 00:15

          Вероятно потому что программистов раст не очень много;)