94б. Flipper Zero изнутри: STM32WB55, FreeRTOS и радио 433 МГц, NFC, RFID и ИК в одной прошивке
Разделы 93 и 94а разбирали по одному радио: модуль LoRa на SPI и NFC-метку на I²C. Flipper Zero — карманный прибор, где таких трактов пять: приёмопередатчик 300–928 МГц, считыватель NFC, считыватель меток 125 кГц, ИК-порт и контакт iButton. Всё это обслуживает одна STM32WB55, и прошивка у прибора открытая. Это редкий случай, когда можно прочитать от начала до конца, как устроен серийный прибор на STM32 с RTOS, внешними программами и несколькими радио сразу.
Здесь — что к чему подключено, как устроена ОС прибора поверх FreeRTOS и как из длительностей импульсов получаются коды пульта, метки и брелока. Три декодера прошивки собраны на компьютере без изменений и прогнаны на сгенерированных посылках с дрожанием: видно, где каждый перестаёт узнавать сигнал.
Раздел учебный и про защиту. Принимать и разбирать эфир и свои метки — нормально. Передавать чужие коды, копировать чужие брелоки и пропуска без разрешения владельца нельзя; как это сделать, здесь не описано. Прошивка и сама не даёт передавать вне разрешённых полос (94б.4).
Опоры: исходники прошивки — github.com/flipperdevices/flipperzero-firmware
(коммит 7f0b6e1) — ниже только короткие фрагменты с путями.
Микроконтроллер — описание пака Keil STM32WBxx_DFP (частоты ядер),
даташит DS11929 и справочное руководство RM0434 из того же пака, база
STM32CubeMX (объём памяти STM32WB55RG). Декодеры собраны gcc в образе
stm-sys:1 с заглушками furi, размеры — arm-none-eabi-gcc в stm-arm:1; каталог _verify/flip94b.
Прошивка целиком не собрана: 10 подмодулей (FreeRTOS, HAL, стек BLE
и другие) для раздела не выкачивались, а сборщик fbt скачивает свой набор
компиляторов из сети. Регистры STM32WB — приложение A17.
Лицензия: GPL-3.0.
Как это называется в документации
Оригинал Перевод Sub-GHz радио ниже 1 ГГц: 315, 433, 868 МГц te, te_short, te_long, te_delta шаг посылки, короткий и длинный импульс, допуск Static, Dynamic (тип протокола) постоянный код и плавающий код preamble, guard time вступление посылки и пауза между посылками mark, space вспышка ИК и пауза Manchester манчестерский код: бит — переход в середине такта record, pubsub запись (именованный общий объект) и рассылка по подписке FAP, .fapвнешняя программа с карты памяти CPU2, coprocessor (copro) второе ядро Cortex-M0+ с радиостеком region provisioning таблица разрешённых полос передачи для страны
94б.1 Что внутри
Сердце прибора — STM32WB55RG (так он указан в
.vscode/example/launch.json): корпус VFQFPN68, флеш 1 МБ, ОЗУ 256 КБ
(база STM32CubeMX). У WB55 два ядра: Cortex-M4 на 64 МГц для программ
и Cortex-M0+ на 32 МГц для радио (описание пака STM32WBxx_DFP). Радио
2,4 ГГц для Bluetooth LE — внутри самого кристалла.
Остальное подключено снаружи. Две шины SPI: SPI1 («шина R» в
furi_hal_spi_config.c) — к радиомикросхемам, SPI2 («шина D») — к
экрану и карте памяти:
| Что | Микросхема | Как подключено | Файл |
|---|---|---|---|
| радио 300–928 МГц | CC1101 | SPI1, CS на PD0, выход GDO0 на PA1 → TIM2 | lib/drivers/cc1101.c |
| NFC 13,56 МГц | ST25R3916 | SPI1, CS на PE4, прерывание на PA2 | lib/drivers/st25r3916.c |
| экран 128×64 | ST7567 | SPI2, CS на PC11 | lib/u8g2/u8g2_glue.c |
| карта microSD | — | SPI2, CS на PC12, FatFS | targets/f7/fatfs |
| RFID 125 кГц | — | несущая TIM1 на PB13, сигнал через COMP1 → TIM2 | furi_hal_rfid.c |
| ИК-порт | — | приём PA0 → TIM2, передача PB9 | furi_hal_infrared.c |
| iButton | — | 1-Wire на PB14 | lib/one_wire |
Выводы — из targets/f7/furi_hal/furi_hal_resources.h.
Три тракта из пяти — 433 МГц, ИК и RFID — сводятся к одному и тому же:
таймер TIM2 в режиме захвата меряет длительности импульсов в
микросекундах. Делитель везде 64 - 1 при такте 64 МГц
(CPU_CLOCK_PLL_HZ в furi_hal_clock.c). Декодеры протоколов видят
только пары «уровень, длительность» — поэтому их можно собрать и
проверить на компьютере без прибора (94б.4–94б.6).
94б.2 Два ядра, память и FreeRTOS
Второе ядро. Стек Bluetooth выполняет Cortex-M0+; его прошивка —
двоичный файл от ST. В fbt_options.py выбран облегчённый стек:
COPRO_STACK_BIN = "stm32wb5x_BLE_Stack_light_fw.bin"
Стек лежит в защищённой области флеша второго ядра. По RM0434 поле
SFSA регистра FLASH_SFR — номер первой 4-килобайтной страницы этой
области; записать этот регистр может только CPU2, а запись ядра M4 в
защищённую область игнорируется с ошибкой шины. Прошивка так и
считает, где кончается её место: SFSA × 4096 + начало флеша
(furi_hal_flash_get_free_end_address в furi_hal_flash.c).
Память ядра M4. Скрипт компоновки targets/f7/stm32wb55xx_flash.ld
берёт флеш с 0x08000000 длиной 1024K, SRAM1 с 0x20000008 длиной
0x2FFF8 (192 КБ без 8 байт) и по 10K в SRAM2a (0x20030000) и
SRAM2b (0x20038000). Остальную часть SRAM2 скрипт не трогает.
FreeRTOS. Настройки — targets/f7/inc/FreeRTOSConfig.h: вытеснение
включено, тик 1000 Гц, 32 уровня приоритета, имя задачи до 32 символов.
Куча — вся свободная ОЗУ между __heap_start__ и __heap_end__;
секции программ с карты загрузчик тоже берёт из кучи (aligned_malloc
в elf_file.c). Как устроено ядро FreeRTOS —
раздел 24.
94б.3 furi: ОС поверх FreeRTOS и программы с карты
Программы не зовут FreeRTOS напрямую — между ними слой furi
(furi/core). Его части:
- потоки
furi_thread_*— задачи FreeRTOS с именем, стеком и кодом возврата; - очереди
furi_message_queue_*— очередь сообщений фиксированного размера; - записи
furi_record_*— общие объекты по имени: экран (gui), питание, хранилище.furi_record_openждёт, пока запись создадут: внутри — ожидание флагаFURI_RECORD_FLAG_READYбез тайм-аута; - рассылка
furi_pubsub_*— подписка на события: сервис публикует, подписчики получают; - цикл событий
furi_event_loop_*— один поток ждёт сразу очередь, семафор, мьютекс, поток байтов и таймер и вызывает обработчик готового.
Программа описывается файлом application.fam. Пример из
applications/examples/example_thermo (градусник DS18B20 на 1-Wire):
App(
appid="example_thermo",
name="Example: Thermometer",
apptype=FlipperAppType.EXTERNAL,
entry_point="example_thermo_main",
requires=["gui"],
stack_size=1 * 1024,
fap_icon="example_thermo_10px.png",
fap_category="Examples",
)
EXTERNAL значит: программа собирается отдельным файлом .fap и
грузится с карты памяти. Это ELF с перемещениями
(targets/f7/application_ext.ld кладёт код с адреса 0) и манифестом в
секции .fapmeta: версия API, код платы, имя, размер стека. Загрузчик
(lib/flipper_application) отказывает, если API программы старше или
новее, чем у прошивки, или плата другая. Что программе можно звать,
перечислено в targets/f7/api_symbols.csv: версия API 88.2, 3537
функций, из них 2451 открыты (статус +), 1086 закрыты (-).
Сам градусник — типичная программа: поток чтения датчика, очередь
событий кнопок на 8 сообщений, записи gui и power.
94б.4 433 МГц: CC1101 и декодер Princeton
Частоты. Прошивка принимает частоты в трёх окнах —
299 999 755…348 000 335, 386 999 938…464 000 000 и
778 999 847…928 000 000 Гц (furi_hal_subghz_is_frequency_valid).
Кварц CC1101 — 26 МГц (CC1101_QUARTZ): частота пишется в регистры
как значение · 65 536 / 26 000 000.
Передача только в своей полосе. При смене частоты прошивка ищет её в
таблице региона (furi_hal_region.c). Для региона «EU» (Европа и
Россия) там две полосы: 433,05–434,79 и 868,15–868,55 МГц. Вне таблицы
включается режим «только приём», и передача отказывает:
bool furi_hal_subghz_tx(void) {
if(furi_hal_subghz.regulation != SubGhzRegulationTxRx) return false;
Приём. Выход демодулятора CC1101 (GDO0) заведён на TIM2. Спад
записывает длительность высокого уровня, фронт — низкого, и обе уходят
в обработчик парой «уровень, длительность» (furi_hal_subghz_capture_ISR).
Входной фильтр захвата отбрасывает всплески короче 16 мкс — это
написано прямо в комментарии к настройке TIM2.
Как декодер узнаёт пульт. В прошивке 53 протокола
(lib/subghz/protocols/protocol_items.c); приёмник (lib/subghz/receiver.c)
отдаёт каждую длительность декодерам всех протоколов, прошедших фильтр, и
каждый сам решает, похожа ли она на его посылку. Самый простой —
Princeton, постоянный 24-битный код дешёвых пультов. Его константы
(lib/subghz/protocols/princeton.c):
static const SubGhzBlockConst subghz_protocol_princeton_const = {
.te_short = 390,
.te_long = 1170,
.te_delta = 300,
.min_count_bit_for_found = 24,
};
Бит — пара «высокий, низкий»: 1 — длинный и короткий, 0 — короткий
и длинный. Решение по каждой паре:
if((DURATION_DIFF(instance->decoder.te_last, subghz_protocol_princeton_const.te_short) <
subghz_protocol_princeton_const.te_delta) &&
(DURATION_DIFF(duration, subghz_protocol_princeton_const.te_long) <
subghz_protocol_princeton_const.te_delta * 3)) {
subghz_protocol_blocks_add_bit(&instance->decoder, 0);
Короткий импульс допускается в пределах ±300 мкс, длинный — ±900. Начало ищется по паузе 390 · 36 = 14 040 мкс с допуском 300 · 36; низкий уровень не короче 2 · 1170 мкс заканчивает посылку. Пауза между посылками по умолчанию — 30 шагов. И главное: декодер сообщает код, только если две посылки подряд совпали.
Опыт. Функция feed вырезана из princeton.c без изменений и
собрана на компьютере. Посылки — код 0xA1B2C3, шаг 390 мкс:
| Условие | Результат |
|---|---|
| 4 посылки подряд | 3 срабатывания: данные 0xA1B2C3, серийный 0xA1B2C, кнопка 0x3, шаг 390, пауза 30 шагов |
| 1 посылка | 0 срабатываний |
| 2 посылки | 1 срабатывание |
| шаг без дрожания | узнаётся от 110 до 680 мкс |
| дрожание ±299 мкс | 1000 из 1000 |
| дрожание ±300 мкс | 983 из 1000 |
| дрожание ±350 мкс | 1 из 1000 |
| дрожание ±400 мкс | 0 из 1000 |
Серийный номер и кнопка — просто старшие 20 и младшие 4 бита. Шаг пульта декодер не знает заранее: он считает его по сумме длительностей посылки. Допуски широкие: короткий импульс может отличаться от 390 мкс на 77 % (300 мкс). Случайное совпадение с шумом отсекает правило двух одинаковых посылок.
Плавающий код. Постоянный код одинаков при каждом нажатии: кто его
услышал, тот знает его навсегда. В плавающем коде, например KeeLoq
(keeloq.c, 64 бита в посылке, тип Dynamic), есть шифрованная часть
со счётчиком (Cnt в выводе). Декодер показывает открытую часть —
серийный номер и кнопку, а шифрованную расшифровывает только ключом
производителя из отдельного хранилища ключей. Для своего устройства
вывод простой: постоянный код годится для звонка, но не для замка.
94б.5 ИК-порт: декодер NEC
Протокол NEC разобран в разделе 198о: там шаг 562,5 мкс и свой
декодер с допуском 25 %. Во Flipper — округлённые времена и
постоянные допуски (lib/infrared/encoder_decoder/nec/infrared_protocol_nec_i.h):
| Что | Вспышка | Пауза | Допуск |
|---|---|---|---|
| вступление | 9000 мкс | 4500 мкс | ±200 мкс |
| бит 1 | 560 мкс | 1690 мкс | ±120 мкс |
| бит 0 | 560 мкс | 560 мкс | ±120 мкс |
| повтор | 9000 мкс | 2250 мкс | ±200 мкс |
Несущая передатчика — 38 000 Гц, заполнение 0,33
(INFRARED_COMMON_CARRIER_FREQUENCY, INFRARED_COMMON_DUTY_CYCLE).
Общий декодер infrared_common_decoder.c один на все протоколы: он
ищет вступление и режет посылку на биты по таблице времён, а файл
протокола задаёт времена и разбор битов. Если во второй половине
посылки не инверсии адреса и команды, она считается NECext с 16-битными
адресом и командой. Семейств протоколов в прошивке восемь: NEC, Samsung, RC5, RC6,
SIRC, Kaseikyo, RCA, Pioneer.
Опыт. Кодер и декодер собраны из файлов прошивки без изменений.
| Посылка | Результат |
|---|---|
| кодер прошивки: адрес 0x04, команда 0x08 | 68 длительностей; декодер: NEC, 0x04, 0x08 |
| времена 562/1688 мкс (как в 198о) | NEC, 0x04, 0x08 |
| байты адреса 0x34 и 0x12 | NECext, адрес 0x1234, команда 0xF708 |
| посылка и код повтора | 1 посылка, 1 повтор |
| вспышки длиннее на 119 мкс, паузы короче | узнаётся |
| вспышки длиннее на 120 мкс | не узнаётся |
| дрожание каждой длительности ±119 мкс | 1000 из 1000 |
| дрожание ±120 мкс | 606 из 1000 |
| дрожание ±150 мкс | 0 из 1000 |
Край ровно на допуске: сравнение строгое, MATCH_TIMING пропускает
только отличие меньше 120 мкс. В разделе 198о разобрано, как приёмник
растягивает вспышки на 100 мкс; у Flipper такой приёмник ещё проходит,
но запас — 20 мкс.
94б.6 RFID 125 кГц и iButton
Как читается метка. TIM1 выдаёт на антенну несущую 125 кГц
(furi_hal_rfid_tim_read_start(125000, 0.5) в lfrfid_worker_modes.c).
Сигнал с антенны через аналоговую цепь платы (её в исходниках нет)
приходит на компаратор COMP1: порог — половина VREFINT, гистерезис
большой, выход компаратора внутри кристалла заведён на вход захвата
TIM2. Как в 433 МГц, дальше только длительности: декодеры
получают сначала длительность вспышки, потом паузы. Протоколов меток
в прошивке 24 (lfrfid_protocols.c).
EM4100 — самая простая метка, только чтение. Кадр 64 бита:
9 единиц заголовка, 10 строк по 4 бита данных и биту чётности, 4 бита
чётности столбцов и стоповый 0. Данные — 5 байт. Код — манчестерский;
бит длится 64 периода несущей, полубит — 32 периода, 256 мкс. Окна
декодера (protocol_em4100.c):
#define EM_READ_SHORT_TIME_BASE (256)
#define EM_READ_LONG_TIME_BASE (512)
#define EM_READ_JITTER_TIME_BASE (100)
Короткий импульс — строго между 156 и 356 мкс, длинный — между 412 и 612 мкс. Кадр принимается, только если сходятся заголовок, стоповый бит, все 10 строчных и 4 столбцовых чётности, а следующие 9 бит снова заголовок.
Опыт. protocol_em4100.c и manchester_decoder.c собраны без
изменений. Кодер прошивки для данных 12 34 56 78 9A выдал кадр
0xFF8CA64A98F8CA96 и полубит 32 периода; своя сборка кадра по формату
выше дала то же. Дальше кадры по 256 мкс на полубит — в декодер:
| Условие | Результат |
|---|---|
| 6 кадров без дрожания | 4 верных чтения, первое — после 191 бита |
| дрожание ±99 мкс | 4 чтения из 6 кадров в 1000 из 1000 |
| дрожание ±100 мкс | в 12 из 1000 |
| дрожание ±120 мкс | в 0 из 1000 |
| один бит данных инвертирован | ни одного чтения |
Первое чтение приходит почти через три кадра: декодер ждёт заголовок
следующего кадра. Прибору мало одного чтения: lfrfid_worker_modes.c
принимает метку, когда после первого чтения ещё столько же совпало,
сколько велит validate_count протокола — у EM4100 это 3. Итого четыре
одинаковых чтения — ровно столько дали 6 кадров.
iButton — таблетка на 1-Wire. Времена шины — в
lib/one_wire/one_wire_host.c: для обычной скорости импульс сброса
480 мкс, через 70 мкс проверка ответа, потом ещё 410 мкс. Та же
библиотека читает и датчик DS18B20 в примере example_thermo из 94б.3;
DS18B20 и его шина — раздел 96.
Размер декодеров. Под Cortex-M4 с -Os (arm-none-eabi-gcc):
| Файл | Код, байт |
|---|---|
infrared_common_decoder.c + infrared_decoder_nec.c |
1066 + 346 |
feed из princeton.c + decoder.c |
376 + 94 |
protocol_em4100.c (с кодером) + manchester_decoder.c |
1102 + 64 |
Декодер — сотни байт: NEC вместе с общим декодером — 1412 байт, 0,13 % флеша в 1 МБ.
94б.7 Грабли
- Два радио одновременно не работают. TIM2 — один: им меряют 433 МГц, ИК и RFID, и им же RFID излучает при эмуляции. TIM1 делят несущая RFID, передача ИК и iButton. Отсюда и устройство программ: одна радиофункция за раз.
- Прерывание NFC на выводе RFID. В
furi_hal_resources.hNFC_IRQ_Pin— этоRFID_PULL_Pin, PA2. Один вывод на два тракта — ещё одна причина работать с ними по очереди. - Одна посылка Princeton — ноль кодов. Декодер ждёт двух одинаковых посылок подряд. Если свой передатчик шлёт посылку один раз, Flipper его не покажет, а на приёме кажется, что «не ловит».
- Шаг пульта вне 110–680 мкс. При шаге 100 мкс пауза в 30 шагов — 3000 мкс, а окно вступления начинается с 14 040 − 10 800 = 3240 мкс. При шаге 690 мкс короткий импульс отличается от 390 на 300 мкс — это уже вне допуска. Декодер молчит, хотя сигнал сильный.
- Строгий край допуска. ±119 мкс — всё принято, ±120 — 606 посылок из 1000 у NEC; у EM4100 ±99 и ±100 — 1000 и 12. Приёмник, который стабильно растягивает вспышку ИК на 120 мкс, не даст ни одной посылки.
- Передача «пропала». На частоте вне таблицы региона прошивка
переходит в «только приём», и
furi_hal_subghz_txвозвращаетfalse. Это не поломка, а таблица полос. - Программа с карты не запускается. Манифест
.fapнесёт версию API: при несовпадении старшего номера с прошивкой загрузчик отказывает. После обновления прошивки программу пересобирают. - Прошивку не собрать без сети. Подмодули (FreeRTOS, HAL, стек BLE)
в скачанной копии пустые, а
fbtскачивает свой набор компиляторов.
94б.8 Итоги
- STM32WB55RG: Cortex-M4 64 МГц для программ и Cortex-M0+ 32 МГц со стеком BLE; флеш 1 МБ, ОЗУ 256 КБ, приложение берёт 192 КБ SRAM1 и по 10 КБ SRAM2a и SRAM2b.
- Снаружи: CC1101 и ST25R3916 на SPI1, экран ST7567 128×64 и microSD на SPI2; RFID, ИК и 433 МГц меряет TIM2 с шагом 1 мкс.
- furi поверх FreeRTOS (тик 1000 Гц, 32 приоритета): потоки, очереди,
записи, рассылка, цикл событий; программы
.fapс карты, API 88.2 — 2451 открытая функция. - Princeton: шаг 390 мкс, допуск ±300/±900, две одинаковые посылки; узнаёт шаг 110–680 мкс и дрожание до ±299 мкс.
- NEC во Flipper: 560/1690 мкс, допуск ±120 мкс строго — ±119 всё, ±120 — 606 из 1000.
- EM4100: кадр 64 бита, полубит 256 мкс, окно ±100 мкс; метка принята после 4 одинаковых чтений.
- Передача — только в полосах региона; для «EU» это 433,05–434,79 и 868,15–868,55 МГц.
Дальше: раздел 95 «Как подключать датчики: питание, подтяжки, уровни, провода».