Flipper Zero · Volume 7

Flipper Zero Volume 7 — GPIO, FAP Development, JS Runtime

ufbt, view_dispatcher, the Momentum JS modules, SWD debugging

Contents

SectionTopic
1About this Volume
2The 18-pin GPIO Pinout (canonical)
3The Furi Runtime in 5 Minutes
4The Application UI Stack
5ufbt — the build tool
6FAP Anatomy
7The Hardware HAL — using each subsystem from C
· 7.1GPIO
· 7.2SPI
· 7.3I²C
· 7.4UART (USART1)
· 7.5Sub-GHz
8The JS Runtime (Momentum first-class)
· 8.1Module surface (Momentum mainline)
· 8.2A minimal JS app
· 8.3The VGM JS module
9SWD Debugging via Black Magic Probe
· 9.1Cabling
· 9.2GDB session
· 9.3VS Code + Cortex-Debug
10Common Pitfalls
11Useful Repos and Pointers
12What’s next

1. About this Volume

This is the volume you read when you want to write code for the Flipper. It covers the canonical 18-pin GPIO header pinout (the table the rest of the series links back to), the FAP build pipeline with ufbt, the Furi runtime patterns the firmware is built around, the Momentum JS runtime modules, and SWD debugging via the official Wi-Fi Devboard running Black Magic Probe.

2. The 18-pin GPIO Pinout (canonical)

This is the table other volumes link back to. From the Flipper Devices docs.1

PinNetSTM32 pinDefaultAlt functions
15V(load switch)Switched +5 V out, 1.2 A max, OFF on battery by default
2PA7PA7GPIOSPI1_MOSI, TIM17_CH1
3PA6PA6GPIOSPI1_MISO, TIM16_CH1
4PA4PA4GPIOSPI1_NSS, DAC1_OUT1
5PB3PB3GPIOSPI1_SCK, USART1_RTS
6PB2PB2GPIOLPUART1_RX
7PC3PC3GPIOADC1_IN4, LPUART1_RTS
8GNDGround
93V3+3.3 V out, 1.2 A max, ON by default (sags during SD ops/FW update)
10SWCPA14SWCLK
11GNDGround
12SIOPA13SWDIO
13TXPB6USART1_TXI²C1_SCL (= internal i2c if reconfigured)
14RXPB7USART1_RXI²C1_SDA
15PC1PC1GPIOI²C3_SDA, ADC1_IN2
16PC0PC0GPIOI²C3_SCL, ADC1_IN1
171WPB14iButton 1-Wire (shared with iButton pad)
18GNDGround

Electrical:

  • All I/Os are 3.3 V tolerant only.
  • Each pin has a 51 Ω series resistor (ESD).
  • Each pin sources up to 20 mA.
  • Aggregate I/O current ≤ 5 W or PMIC trips.
  • 3V3 rail community-rated ~150 mA continuous for external loads.
  • 5 V rail up to 1.2 A on USB; ~600 mA on battery boost.

3. The Furi Runtime in 5 Minutes

The Flipper firmware is built on FreeRTOS (the kernel) plus Furi (the runtime / HAL). Furi gives you:

  • Threadsfuri_thread_* for FreeRTOS task wrapper
  • Synchronizationfuri_mutex_*, furi_semaphore_*, furi_event_flag_*, furi_message_queue_*
  • HALfuri_hal_gpio_*, furi_hal_spi_*, furi_hal_i2c_*, furi_hal_subghz_*, furi_hal_nfc_*, furi_hal_ibutton_*
  • Records (service registry)furi_record_open("gui") returns a service handle (the GUI service); furi_record_close releases it. Common records: gui, notification, storage, dialogs, cli.
  • LoggingFURI_LOG_I("MyApp", "starting") etc.

The pattern is acquire-via-record, do work, release-via-record. RAII in spirit; manual in execution.

4. The Application UI Stack

The GUI service exposes a ViewDispatcher that owns a stack of Views. Each View is a screen with input handler, drawing callback, and state. Common pre-built views:

ViewPurposeWhen to use
SubmenuVertical scrollable menuMain menus
PopupModal popup with auto-dismissErrors, “press button to continue”
Dialog / DialogExYes/No/Cancel modalConfirmations
TextInputOn-screen keyboardFilename entry, search
ByteInputHex byte editorNFC key entry, raw subghz
VariableItemListSettings-style list with < / >App settings
WidgetCompose primitives — text, buttons, framesCustom layouts
LoadingView”Working…” spinnerLong ops
EmptyScreenBlack screen + draw callbackCustom render

A typical app:

typedef struct {
    Gui* gui;
    ViewDispatcher* view_dispatcher;
    Submenu* submenu;
    Popup* popup;
    // app-specific state
} MyApp;

int32_t my_app_main(void* p) {
    MyApp* app = malloc(sizeof(MyApp));
    app->gui = furi_record_open(RECORD_GUI);
    app->view_dispatcher = view_dispatcher_alloc();
    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui,
        ViewDispatcherTypeFullscreen);

    app->submenu = submenu_alloc();
    submenu_add_item(app->submenu, "Hello", 0, callback_hello, app);
    submenu_add_item(app->submenu, "Quit",  1, callback_quit,  app);
    view_dispatcher_add_view(app->view_dispatcher, MainViewId,
        submenu_get_view(app->submenu));
    view_dispatcher_switch_to_view(app->view_dispatcher, MainViewId);

    view_dispatcher_run(app->view_dispatcher);   // blocks until quit

    view_dispatcher_remove_view(app->view_dispatcher, MainViewId);
    submenu_free(app->submenu);
    view_dispatcher_free(app->view_dispatcher);
    furi_record_close(RECORD_GUI);
    free(app);
    return 0;
}

Free what you alloc; the heap is shared with the OS, leaks accumulate across FAPs.

5. ufbt — the build tool

ufbt = “micro Flipper Build Tool”. It’s a Python wrapper around the Flipper SDK that builds FAPs without you having to clone the full firmware tree.2

Setup once:

pip install --upgrade ufbt
ufbt update            # downloads the SDK for the current target firmware
ufbt update --branch=dev   # or for a specific firmware branch

For Momentum-specific SDK:

ufbt update --index-url https://up.momentum-fw.dev/firmware/directory.json

In your FAP source directory:

ufbt                   # build the FAP
ufbt launch            # build, upload via qFlipper-style protobuf, run
ufbt cli               # open serial console
ufbt vscode_dist       # generate VS Code workspace

6. FAP Anatomy

A FAP is one directory:

my_app/
├── application.fam       # manifest (Python)
├── my_app.c              # source
├── my_app_icon.png       # 10x10 1bpp icon
└── (other .c, .h, headers as needed)

application.fam:

App(
    appid="my_app",
    name="My App",
    apptype=FlipperAppType.EXTERNAL,
    entry_point="my_app_main",
    requires=["gui"],
    stack_size=2 * 1024,
    fap_category="Tools",
    fap_icon="my_app_icon.png",
    fap_icon_assets="assets",
    fap_version=(0, 1),
    fap_description="My app",
    fap_author="tjscientist",
    fap_weburl="https://example.invalid/",
    targets=["f7"],
)

Build:

ufbt
# Output: dist/f7-C/my_app.fap

Upload:

ufbt launch
# Pushes to /ext/apps/Tools/my_app.fap and runs

7. The Hardware HAL — using each subsystem from C

7.1 GPIO

#include <furi_hal_gpio.h>
#include <furi_hal_resources.h>

// Set pin mode + initial state
furi_hal_gpio_init(&gpio_ext_pa7, GpioModeOutputPushPull,
                   GpioPullNo, GpioSpeedLow);
furi_hal_gpio_write(&gpio_ext_pa7, true);

// Read
bool v = furi_hal_gpio_read(&gpio_ext_pa6);

gpio_ext_pa4 / pa6 / pa7 / pb2 / pb3 / pc0 / pc1 / pc3 / pb6 / pb7 are the header pins.

7.2 SPI

#include <furi_hal_spi.h>

furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
furi_hal_spi_bus_trx(&furi_hal_spi_bus_handle_external,
                     tx_buf, rx_buf, len, FuriHalSpiTimeout);
furi_hal_spi_release(&furi_hal_spi_bus_handle_external);

The “external” bus uses pins 2/3/4/5 with PA4 as CS. Mutually exclusive with the internal CC1101 — furi_hal_subghz_* calls fight with this for the bus. Pick one mode per app.

7.3 I²C

#include <furi_hal_i2c.h>

furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
furi_hal_i2c_tx(&furi_hal_i2c_handle_external, addr,
                buf, len, FuriHalI2cTimeout);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);

External I²C is on pins 15/16 (PC1/PC0). Pull-ups on the device side are ~10 kΩ to 3V3 — you don’t need to add your own unless your slave needs stronger pull-ups.

7.4 UART (USART1)

#include <furi_hal_uart.h>

furi_hal_uart_init(FuriHalUartIdUSART1, 115200);
furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, on_byte_rx, NULL);
furi_hal_uart_tx(FuriHalUartIdUSART1, buf, len);

USART1 on pins 13/14 (PB6/PB7). This is the bus all the ESP32-based Wi-Fi modules talk over.

7.5 Sub-GHz

#include <furi_hal_subghz.h>

furi_hal_subghz_reset();
furi_hal_subghz_load_preset(FuriHalSubGhzPresetOok650Async);
furi_hal_subghz_set_frequency(433920000);
furi_hal_subghz_rx();   // enter RX
// ... read packets via callbacks ...
furi_hal_subghz_sleep();

The high-level subghz_devices API in lib/subghz/ is what most apps use; direct furi_hal_subghz_* is for specialized cases.

8. The JS Runtime (Momentum first-class)

Momentum’s JS engine is mJS — a subset of JavaScript optimized for embedded. It’s not full ES2020; closures, generators, and Promises behave in restricted ways. Treat it as ES5 with a few selected ES6 features.

JS apps live as .js files at /ext/apps/Scripts/. Run via Apps → Scripts → pick → run. They have access to a curated set of modules.

8.1 Module surface (Momentum mainline)

ModulePurposeExample
event_loopCooperative schedulereventLoop.tick(handler)
guiScreen + inputgui.viewDispatcher.add(...)
gui/submenuStandard submenulet m = submenu.create(); m.addItem('Hello', 0)
gui/dialog, gui/popupModalsdialog.show('Title', 'Message')
gui/text_input, byte_inputInputs
gui/widgetComposite widget
notificationLED + vibration + speakernotification.success(); notification.error()
storageFile IOstorage.read('/ext/test.txt')
serialUART (USART1)serial.setup(115200); serial.write('AT\r')
i2cExternal I²C busi2c.write(0x50, [...])
spiExternal SPI bus
gpioRaw GPIOgpio.setMode('PA7', 'output'); gpio.write('PA7', 1)
subghzSub-GHzsubghz.setFrequency(433920000); subghz.rx()
usbdiskMount SD as USB-MSC
keyboardUSB HID keyboardkeyboard.string('Hello'); keyboard.press('ENTER')
usbGeneric USB
ble / btBluetoothbt.beacon.start({...})
vgmVideo Game Module IMUlet imu = vgm.imu(); print(imu.yaw)
dialog (top-level)Quick dialogs

8.2 A minimal JS app

let event_loop = require("event_loop");
let gui        = require("gui");
let submenu    = require("gui/submenu");

let m = submenu.create({
    header: "Hello, JS",
    items: ["First", "Second", "Quit"],
});

m.subscribe("chosen", function (item) {
    if (item === 2) event_loop.stop();
    else print("Chose:", item);
});

gui.viewDispatcher.switchTo(m);
event_loop.run();

Save as /ext/apps/Scripts/hello.js; run from Apps → Scripts → hello.

8.3 The VGM JS module

If a VGM is attached, require("vgm") exposes:

let vgm = require("vgm");
let imu = vgm.imu();
// imu.yaw, imu.pitch, imu.roll - in degrees, refreshed every call
// imu.acc.x, imu.acc.y, imu.acc.z - accelerometer in g
// imu.gyro.x, imu.gyro.y, imu.gyro.z - gyro in deg/s

Useful for motion-controlled apps; also feeds the official “Air Mouse” implementation.

9. SWD Debugging via Black Magic Probe

The official Wi-Fi Devboard ships with Black Magic Probe firmware. That makes it a cross-platform GDB server over USB-CDC.

9.1 Cabling

Wire the Devboard’s SWD output to the Flipper’s SWD pins (10, 12) plus GND. The Devboard has labeled SWD output headers; on the Flipper, attach to GPIO header pins SWC (10) and SIO (12), GND on 8 or 11. Double-check voltages — both should be on 3V3.

9.2 GDB session

arm-none-eabi-gdb path/to/your_fap.elf
(gdb) target extended-remote /dev/cu.usbmodem71B19E101    # macOS
(gdb) monitor swdp_scan
(gdb) attach 1
(gdb) load
(gdb) break my_app_main
(gdb) continue

monitor swdp_scan enumerates the target. The STM32WB55 should appear. Standard GDB commands work — bt, info reg, display *(uint8_t*)0x..., watch, etc.

9.3 VS Code + Cortex-Debug

Cortex-Debug extension. Configure .vscode/launch.json:

{
    "configurations": [{
        "name": "Flipper FAP Debug",
        "type": "cortex-debug",
        "servertype": "external",
        "gdbTarget": "/dev/cu.usbmodem71B19E101",
        "request": "attach",
        "executable": "${workspaceFolder}/dist/f7-C/my_app.fap.elf",
        "device": "STM32WB55RG"
    }]
}

Click Run → Debug. Variables view, breakpoints, step-through all work.

10. Common Pitfalls

PitfallSymptomFix
Forgetting to furi_record_closeApp leaks; later apps failPair every _open with _close
Calling subghz and spi external from the same appOne fails or returns garbageThey share the SPI bus; serialize access
Stack size too smallRandom hard fault when calling deep into firmwareBump stack_size in .fam to 4 KB+
Running a JS app that blocks the event loopUI hangs, hardware watchdog rebootUse event_loop.tick() for periodic work
Using a removed API after firmware updateLoader refuses (“API mismatch”)ufbt update and rebuild against new SDK
Crashing in interrupt contextHard fault, sometimes silentAvoid mallocs in IRQ; minimize work in IRQ
Forgetting that UART pins are 3.3 VModule silently dies after first 5 V signalLevel-shift external UART signals

11. Useful Repos and Pointers

  • flipperdevices/flipperzero-firmware — main firmware
  • flipperdevices/flipperzero-ufbt — ufbt itself
  • Next-Flip/Momentum-Firmware — Momentum
  • RogueMaster/RogueMaster_Flipper_IRDB — RogueMaster IR DB
  • jamisonderek/flipper-zero-tutorials — worked examples
  • derskythe/flipperzero-firmware-derskythe — DeepWiki for FAP creation
  • UberGuidoZ/Flipper — the largest community resources collection

API symbol table: firmware/targets/f7/api_symbols.csv — defines what’s exported to FAPs.

12. What’s next

Vol 8 — Official Modules. Full chapter on the Video Game Module (which tjscientist owns), plus WiFi Devboard, NRF24, and the official external CC1101 amp. Then Vol 9 covers Game Over, AWOK V3, and the rest of the third-party catalog.

Footnotes

  1. Flipper Devices, “GPIO & Modules”, https://docs.flipper.net/zero/gpio-and-modules.

  2. https://pypi.org/project/ufbt/; repo flipperdevices/flipperzero-ufbt.