Flipper your way into hardware
Read more in the "Flipper into hardware" series:
- Flipper your way into hardware
This series is incomplete! Stay tuned for more :)
Let me read it for you!
The Flipper Zero is a cool little multitool. You can clone RFID cards or… do lots of other things. From what I can tell, most people just clone a few cards then have a fancy paperweight. It’s cool, but the actual utility is kind of hard to spot.
I’m interested in the Flipper Zero, but for a different reason: it has GPIO pins! I really love messing with microcontrollers. This seems like a neat tool for that. You can do anything a microcontroller does, plus you have a lot of stuff built in (like a screen for GUI).
So, here I’ll explore what I did in order to make a small application to run a stepper motor. This isn’t for any practical purpose, so the app itself is pretty minimal. It’s just fun to see what it can do, then maybe later I’ll use that knowledge to create something actually useful.
If you do want to see the code, though, you can find it on my Github.
This isn’t a tutorial. In fact, some of this code probably isn’t worth copying. It’s just an exploration. A lot of the Flipper Zero content that I’ve seen assumes a lot of knowledge or doesn’t explain it very well. This is my journey to sift through all of that.
But, it will be multiple parts.
Making the app
Throughout this process, we’ll use micro Flipper Build Tool.
The first step is easy: make the project!
python3 -m ufbt create APPID=stepper
Now we have a stepper app with a few different files. We need to make sure this runs as is, so we’ll launch it:
python3 -m ufbt launch
This should load the app on the Flipper Zero. That app is in the apps menu, Examples
directory.
… and it does nothing. Well, the source code provided does log some stuff:
stepper.c
#include <furi.h>
/* generated by fbt from .png files in images folder */
#include <stepper_icons.h>
int32_t stepper_app(void* p) {
UNUSED(p);
FURI_LOG_I("TEST", "Hello world");
FURI_LOG_I("TEST", "I'm stepper!");
return 0;
}
So how do we see that? Through the CLI! Find the way that fits you best via the documentation’s CLI section. For me, I do everything in the terminal, so I used screen
with the device directly. To do that, I just found the device:
$ ls /dev/serial/by-id/
usb-Flipper_Devices_Inc._Flipper_Idolisle_flip_Idolisle-if00
Then I ran screen
on that device:
$ screen /dev/serial/by-id/usb-Flipper_Devices_Inc._Flipper_Idolisle_flip_Idolisle-if00
_.-------.._ -,
.-"```"--..,,_/ /`-, -, \
.:" /:/ /'\ \ ,_...,
\ _\~`_-"` _;
' / /`"""'\ \ \.~`_-' ,-"'/
| | | 0 | | .-' ,/` /
| ,..\ \ ,.-"` ,/` /
; : `/`""\` ,/--==,/-----,
| `-...| -.___-Z:_______J...---;
: ` _-'
_L_ _ ___ ___ ___ ___ ____--"`___ _ ___
| __|| | |_ _|| _ \| _ \| __|| _ \ / __|| | |_ _|
| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |
|_| |____||___||_| |_| |___||_|_\ \___||____||___|
Welcome to Flipper Zero Command Line Interface!
Read the manual: https://docs.flipper.net/development/cli
Run or to list available commands
Firmware version: 1.0.1 1.0.1 (fe424061 built on 10-09-2024)
>:
Great, we’re in. Now it’s pretty simple. I just ran log
in the CLI prompt and I see:
>: log
Current log level: info
Use <log ?> to list available log levels
Press CTRL+C to stop...
1431052 [I][AnimationManager] Unload animation 'L1_Sad_song_128x64'
1431060 [I][Loader] Loading /ext/apps/Examples/stepper.fap
1431091 [I][Elf] Total size of loaded sections: 74
1431094 [I][Loader] Loaded in 34ms
1431097 [I][TEST] Hello world
1431099 [I][TEST] I'm stepper!
1431123 [I][Loader] App returned: 0
1431125 [I][Loader] Application stopped. Free heap: 124880
1432254 [I][AnimationManager] Select 'L1_Sad_song_128x64' animation
1432258 [I][AnimationManager] Load animation 'L1_Sad_song_128x64'
You can see our two log commands: “Hello world” and “I’m stepper!” Please ignore the fact that my flipper is playing the sad song animation, I promise it’s fine.
So, now back to the code. How did we even get those logging macros (FURI_LOG_I
)? Well, the developer docs link to our included furi.h
file here - but that is just a header that includes a bunch of other headers. You can find the source code in that developer documentation or in the firmware repo. For now I’ll look in the developer documentation. Just searching for the FURI_LOG_I
in question finds a macro definition with the following value:
log.h
furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
So, we now know where the development library is and how to search for what we need - great!
My goal here is to use one single pin from the Flipper to control a stepper motor. If we can get a pin working to, say, control an LED - then we can modify that until we control a stepper motor.
That means I need to find how to control a pin. As a first course of action, I searched for GPIO and… found exactly what I wanted. But, that’s Javascript, so I searched a little more and found the HAL (hardware abstraction layer) file in C.
Unfortunately, for C, we don’t get a nice and easy example to copy-paste to do this for us, so we’ll have to figure it out ourselves. :(
GPIO is all handled in some furi_hal_gpio.h
header. We can find a couple functions that seem useful, like:
furi_hal_gpio.h
void furi_hal_gpio_init_simple(const GpioPin* gpio, const GpioMode mode);
and:
furi_hal_gpio.h
static inline void furi_hal_gpio_write(const GpioPin* gpio, const bool state) {
We do need the GpioPin
struct, which is:
furi_hal_gpio.h
typedef struct {
GPIO_TypeDef* port;
uint16_t pin;
} GpioPin;
So there are a couple ways to do this. Manually doing this would mean taking 2 seconds to look at the data sheet and finding the port and pin used for the pin we want (something I explored a lot in my Arduino Demystified post). But, they also conveniently provide a furi_hal_resources.h
file with this promising function:
furi_hal_resources.h
const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number);
Okay, so now we have a solid understanding of how to get what we need for GPIO. Let’s put that into action.
Making my app blink
So first, let’s just make sure we know what these functions do. For this, I just want to grab a random pin number (say pin 7) then see if the name we grab matches what is printed on my Flipper. To do this, I needed to switch to the firmware dev
branch, since those functions were just added (I’m on the cutting edge, you know). You can do this, too:
$ python3 -m ufbt update --branch=dev
$ python3 -m ufbt flash_usb
Then with that, I changed my stepper.c
file:
stepper.c
#include <furi.h>
#include <furi_hal_resources.h>
int32_t stepper_app(void* p) {
UNUSED(p);
FURI_LOG_I("PIN", furi_hal_resources_pin_by_number(7)->name);
return 0;
}
Loaded it on my flipper, then checked the logs again:
38695 [I][PIN] PC3
That’s what pin 7 is labelled with! Awesome, so now we have access to a pin. It’s all through this GpioPinRecord
struct:
furi_hal_resources.h
typedef struct {
const GpioPin* pin;
const char* name;
const FuriHalAdcChannel channel;
const uint8_t number;
const bool debug;
} GpioPinRecord;
All we really care about is the pin
member - that’s what the furi_hal_gpio_write
function expects.
The GUI nonsense
At this point, we want an application that looks like something. Not only will that make everything else easier, but it’s actually necessary. The GUI’s viewport is what deals with inputs, we can even see view_port_input_callback_set
which we provide a function that gets called on input. While we’re at it, we’ll just also add a nice draw callback, too. We could do this without the buttons, but I want to use the buttons.
The goal for a UI is simple right now: just show the current state of the pin (0 or 1). We can double check that aligns with what the LED shows. This is pretty explanatory with the code. It’s also not very useful right now. Here’s the callback I made:
stepper.c
#define TEXT_STORE_SIZE 64U
static void stepper_draw_callback(Canvas* canvas, void* ctx) {
StepperContext* context = ctx;
char text_store[TEXT_STORE_SIZE];
snprintf(text_store, TEXT_STORE_SIZE, "State: %d", context->state);
const size_t middle_x = canvas_width(canvas) / 2U;
canvas_draw_str_aligned(canvas, middle_x, 58, AlignCenter, AlignBottom, text_store);
}
These callbacks use a void*
parameter to pass in the “context” used - we’ll see how that’s set up in a second. The other callback is for inputs (say, button presses). That’s even easier:
stepper.c
static void stepper_input_callback(InputEvent* event, void* ctx) {
StepperContext* context = ctx;
furi_message_queue_put(context->event_queue, event, FuriWaitForever);
}
We just shove the event
we get into an event_queue
. That’s in a StepperContext
struct that we put at the top:
stepper.c
typedef struct {
Gui* gui;
ViewPort* view_port;
FuriMessageQueue* event_queue;
const GpioPin *pin;
bool state;
} StepperContext;
Cool, but we have no idea how these are set up. This set up is in a function stepper_context_alloc
:
stepper.c
static StepperContext* stepper_context_alloc(uint8_t pin) {
StepperContext* context = malloc(sizeof(StepperContext));
context->view_port = view_port_alloc();
view_port_draw_callback_set(context->view_port, stepper_draw_callback, context);
view_port_input_callback_set(context->view_port, stepper_input_callback, context);
context->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
context->pin = furi_hal_resources_pin_by_number(pin)->pin;
context->gui = furi_record_open(RECORD_GUI);
gui_add_view_port(context->gui, context->view_port, GuiLayerFullscreen);
context->state = false;
return context;
}
Okay that’s a lot. I’ll go line by line:
stepper.c
StepperContext* context = malloc(sizeof(StepperContext));
Allocates the memory for context
- easy enough, that’s just C.
stepper.c
context->view_port = view_port_alloc();
Same as before (allocates memory for a ViewPort
) but for some reason we have a helper function.
stepper.c
view_port_draw_callback_set(context->view_port, stepper_draw_callback, context);
view_port_input_callback_set(context->view_port, stepper_input_callback, context);
This sets up the two callbacks we defined before. You can see this it where we pass in the context
pointer that gets passed into the callback on call.
stepper.c
context->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
Allocates a message queue which can hold 8
messages, but it’s still basically just a malloc
call.
stepper.c
context->pin = furi_hal_resources_pin_by_number(pin)->pin;
Grabs the pin associated with the one passed into the function. This is where the stepper motor gets wired in.
stepper.c
context->gui = furi_record_open(RECORD_GUI);
Sets up the GUI. This is just a “record” in the Flipper code, where RECORD_GUI
is a string constant "gui"
stepper.c
gui_add_view_port(context->gui, context->view_port, GuiLayerFullscreen);
Adds a viewport which we created earlier.
stepper.c
context->state = false;
return context;
Sets the state and returns the context. We’re done!
All of that is basic boilerplate for a lot of applications. We also free that all later, but I won’t go line by line. Here it is:
stepper.c
static void stepper_context_free(StepperContext* context) {
view_port_enabled_set(context->view_port, false);
gui_remove_view_port(context->gui, context->view_port);
furi_message_queue_free(context->event_queue);
view_port_free(context->view_port);
furi_record_close(RECORD_GUI);
}
Since I got this from the onewire example, I’m… pretty sure that it’s the right order. :)
Back to GPIO
Okay, so now we have a setup and free function. Here we use that setup function and free before/after some main loop. So, first setup code:
stepper.c
int32_t stepper_app(void* p) {
UNUSED(p);
StepperContext *context = stepper_context_alloc(7);
furi_hal_gpio_write(context->pin, context->state);
furi_hal_gpio_init(context->pin, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow);
That just calls the function we made with a hardcoded pin 7, then sets up the GPIO pin. The furi_hal_gpio_init
function is like calling pinMode
for an Arduino - we’re just calling that pin an output pin.
Here’s that main loop:
stepper.c
for (bool is_running = true; is_running; ) {
InputEvent event;
const FuriStatus status = furi_message_queue_get(context->event_queue, &event, FuriWaitForever);
if ((status != FuriStatusOk) || (event.type != InputTypeShort)) {
continue;
}
switch (event.key) {
case InputKeyBack: is_running = false; break;
case InputKeyOk:
context->state = !context->state;
view_port_update(context->view_port);
break;
default: break;
}
furi_hal_gpio_write(context->pin, context->state);
}
While the app is running, we get events and loop if it’s an invalid event (like long button presses). Then we stop running if we hit back, or change the state if we hit Ok. We’ll update the viewport if we see Ok in order to get it to change the status
. Then we write the state to the pin. Great.
Note, don’t remove the InputKeyBack
case - while setting this up and minimizing this, I had to reset my Flipper many times to get it right with the GUI. Oops.
Then some teardown:
stepper.c
furi_hal_gpio_init(context->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
stepper_context_free(context);
return 0;
The final furi_hal_gpio_init
call is correct from what I can tell, it’s just trying to get it back into “floating” voltage rather than forcing it 0 or 1. I got that from a separate tutorial on GPIO. At the very least, it seems to stop sending a high voltage if we exit, so I call that a success.
So now, finally, let’s run this and press Ok with an LED wired up on the correct pin (which I have hardcoded to pin 7
):
spooky
Okay, now we have fundamentals:
1) A GUI to see what’s going on
2) Interacting with the Flipper’s buttons
3) Interacting with some external GPIO stuff
That seems like all we need to do most of what we want! Sure, this is just a reskinned (and worse) version of apps that already come with the Flipper. But, it’s fun to learn about it. Now, we’ll make this interact with a stepper motor. In part 2.