Embedded HandbookПриложения
Все разделы › X. Проектирование, сети и IoT
STM32WB55 · уровень продвинутый · 15 мин чтения
Открыть в интерактивном справочнике

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.

Flipper Zero изнутри: в центре STM32WB55 с ядрами Cortex-M4 на 64 МГц под FreeRTOS и Cortex-M0+ со стеком BLE; слева по SPI1 радио CC1101 на 300–928 МГц и считыватель NFC ST25R3916; справа по SPI2 экран ST7567 128 на 64 и карта microSD; снизу RFID 125 кГц, ИК-порт, iButton и Bluetooth LE; приём 433 МГц, ИК и RFID меряются одним таймером TIM2 в микросекундах

Три тракта из пяти — 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). Его части:

Программа описывается файлом 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 Грабли


94б.8 Итоги

Дальше: раздел 95 «Как подключать датчики: питание, подтяжки, уровни, провода».

Открыть в интерактивном справочнике