Back to Blog
App DevelopmentPublished on July 5, 2026

Demystifying Flipper Zero Firmware: Architecting Custom Applications (FAPs) with the Flipper SDK

Learn how to develop custom applications (FAPs) for the Flipper Zero using its C-based SDK. This deep dive covers the event loop, GUI design patterns, and memory management for resource-constrained embedded systems.

Introduction to the Flipper Zero Ecosystem and FAPs

The Flipper Zero has evolved from a viral multi-tool gadget into a robust, open-source hardware prototyping platform. At the heart of this portability is its firmware, built on top of FreeRTOS, which manages multi-tasking, hardware abstraction layers (HAL), and a modular application system. While the device comes packed with default tools for Sub-GHz, RFID, NFC, Infrared, and BadUSB, the real power of the Flipper Zero lies in its extensibility through Flipper Application Packages (FAPs).

FAPs are dynamic applications stored on the microSD card and loaded into the device's RAM at runtime. This architecture bypasses the need to reflash the entire firmware flash memory every time you want to add a new tool. To write these applications, developers must interface with the Furi API, the Flipper Zero's proprietary operating system abstraction layer, alongside its custom GUI library. This guide will walk you through setting up the toolchain, understanding the core architecture of a FAP, and writing a functional, event-driven application that interacts with both the screen and the GPIO pins.

Understanding the Flipper Zero Hardware Architecture

Before writing code, it is critical to understand the hardware limits of the Flipper Zero. The device is powered by an STM32WB55 microcontroller, a dual-core ARM Cortex-M4 (64 MHz) and Cortex-M0+ (32 MHz) system-on-chip.

  • RAM Constraints: The Cortex-M4 core, which runs the main application firmware, has access to roughly 256 KB of SRAM. Because the base operating system occupies a significant portion of this memory, your custom FAP typically has less than 20-30 KB of free heap memory to work with.
  • Flash vs. RAM Execution: The core firmware is flashed directly to the internal flash memory. FAPs, however, are compiled as position-independent ELF binaries (.fap) and are loaded dynamically into SRAM by the operating system's loader. This means memory leaks or inefficient heap allocations will quickly result in a system panic or out-of-memory crash.

Setting Up the Toolchain: fbt (Flipper Build Tool)

The official method for building Flipper Zero firmware and applications is fbt (Flipper Build Tool), an automated wrapper built on top of SCons. To start developing, you need to clone the official firmware repository (or popular community forks like RogueMaster or Unleashed) which contain the full SDK and build environment.

Step 1: Clone the Repository

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

Step 2: Create Your App Directory

Navigate to the applications_user directory. This directory is ignored by git updates, making it the perfect place to house your custom code.

mkdir applications_user/gpio_controller

Anatomy of a Flipper Application (.fap)

Every FAP requires at least two files: an application manifest (application.fam) and the main C source file. The manifest informs the build system and the firmware loader about the application's metadata, entry point, and system requirements.

The Manifest: application.fam

Create a file named application.fam inside your app folder with the following configuration:

App(
    appid="gpio_controller",
    name="GPIO Controller",
    apptype=FlipperAppType.EXTERNAL,
    entry_point="gpio_controller_app",
    stack_size=2 * 1024,
    requires=["gui"],
    fap_category="GPIO",
    fap_icon="icon.png",
)
  • appid: A unique identifier for your application.
  • entry_point: The C function that acts as the starting point of execution.
  • stack_size: The amount of stack memory allocated to your app thread (2 KB is standard for simple UI apps).
  • requires: Shared system services (records) your app needs access to.

Writing the App: Event Loops and GUI Rendering

Flipper Zero applications are highly event-driven. You must not block the main execution thread with infinite loops; instead, you use an event queue to process user inputs, timers, and hardware interrupts asynchronously.

Let's write a complete gpio_controller.c application. This application will draw a basic UI, listen for physical keypresses, and toggle the state of GPIO Pin C0 (Pin 15 on the physical header).

#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <furi_hal_gpio.h>

// Define our GPIO pin
#define TARGET_GPIO_PIN &gpio_ext_pc0

// Custom event structure to pass messages to our event queue
typedef enum {
    EventTypeInput,
    EventTypeTick,
} EventType;

typedef struct {
    EventType type;
    InputEvent input;
} AppEvent;

// App state tracking
typedef struct {
    bool pin_state;
    FuriMutex* mutex;
} AppState;

// Callback for screen rendering
static void render_callback(Canvas* canvas, void* ctx) {
    AppState* app_state = ctx;
    furi_mutex_acquire(app_state->mutex, FuriWaitForever);

    canvas_clear(canvas);
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 10, 20, "GPIO Controller");

    canvas_set_font(canvas, FontSecondary);
    if (app_state->pin_state) {
        canvas_draw_str(canvas, 10, 40, "Pin PC0 (15): HIGH [ON]");
    } else {
        canvas_draw_str(canvas, 10, 40, "Pin PC0 (15): LOW [OFF]");
    }
    
    canvas_draw_str(canvas, 10, 55, "Press OK to toggle, BACK to exit");

    furi_mutex_release(app_state->mutex);
}

// Callback for input processing
static void input_callback(InputEvent* input_event, void* ctx) {
    FuriMessageQueue* event_queue = ctx;
    AppEvent event = {.type = EventTypeInput, .input = *input_event};
    furi_message_queue_put(event_queue, &event, FuriWaitForever);
}

// Application Entry Point
int32_t gpio_controller_app(void* p) {
    UNUSED(p);
    
    // 1. Allocate application state
    AppState app_state = {
        .pin_state = false,
        .mutex = furi_mutex_alloc(FuriMutexTypeNormal)
    };
    
    // 2. Initialize GPIO
    furi_hal_gpio_init(TARGET_GPIO_PIN, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
    furi_hal_gpio_write(TARGET_GPIO_PIN, false);

    // 3. Allocate event queue
    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(AppEvent));

    // 4. Register ViewPort and GUI
    ViewPort* view_port = view_port_alloc();
    view_port_draw_callback_set(view_port, render_callback, &app_state);
    view_port_input_callback_set(view_port, input_callback, event_queue);

    Gui* gui = furi_record_open(RECORD_GUI);
    gui_add_view_port(gui, view_port, GuiLayerFullscreen);

    // 5. Main Event Loop
    AppEvent event;
    bool running = true;
    while (running) {
        if (furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) {
            if (event.type == EventTypeInput) {
                // Only trigger action on short press or repeat
                if (event.input.type == InputTypeShort) {
                    switch (event.input.key) {
                        case KeyBack:
                            running = false; // Exit app
                            break;
                        case KeyOk:
                            // Toggle pin state securely
                            furi_mutex_acquire(app_state.mutex, FuriWaitForever);
                            app_state.pin_state = !app_state.pin_state;
                            furi_hal_gpio_write(TARGET_GPIO_PIN, app_state.pin_state);
                            furi_mutex_release(app_state.mutex);
                            
                            // Request screen redraw
                            view_port_update(view_port);
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }

    // 6. Cleanup memory and hardware state
    furi_hal_gpio_write(TARGET_GPIO_PIN, false); // Reset pin to safe state
    
    view_port_enabled_set(view_port, false);
    gui_remove_view_port(gui, view_port);
    view_port_free(view_port);
    furi_message_queue_free(event_queue);
    furi_mutex_free(app_state.mutex);
    furi_record_close(RECORD_GUI);

    return 0;
}

Compiling and Running Your Application

Once your code is written and saved to gpio_controller.c inside the applications_user/gpio_controller/ directory, you can compile and launch it directly onto your plugged-in Flipper Zero using fbt.

  1. Connect your Flipper Zero via USB-C.
  2. Run the compilation and launch command:
./fbt launch APPSRC=applications_user/gpio_controller

This command will compile the application, generate the .fap binary, upload it over the serial connection to the /ext/apps/GPIO/ directory on your SD card, and immediately launch it on the Flipper's screen.

Memory Management and Resource Constraints

When writing complex applications (e.g., protocol decoders or custom games), memory management is paramount. Here are critical best practices to prevent memory leaks and crashes on the FreeRTOS platform:

  • Avoid Raw malloc: Use Furi's memory allocation wrappers like malloc and free which are mapped to FreeRTOS's thread-safe heap management. Always verify allocations against NULL to prevent null pointer dereferences.
  • Use Mutexes for Shared State: The draw callback (render_callback) is executed in the context of the GUI thread, not your application thread. When accessing app state variables from inside the draw callback, you must synchronize access using a FuriMutex to prevent race conditions and screen artifacts.
  • Reduce Stack Usage: Do not declare large arrays (e.g., uint8_t buffer[1024]) inside your functions. Doing so will blow past the stack_size defined in your application.fam and corrupt adjacent memory. Instead, allocate large buffers on the heap and free them upon application exit.

Conclusion

The Flipper Zero's flexible SDK and modular architecture provide a powerful playground for embedded software developers. By understanding the event queue paradigm, leveraging the asynchronous Furi API, and respecting the hard hardware limits of the STM32WB55 microcontroller, you can transform this portable device into a bespoke tool tailored to your specific hardware analysis, automation, and prototyping needs.

#Flipper Zero#Embedded Systems#C Programming#Firmware Development