diff --git a/core/embed/io/button/button_fsm.c b/core/embed/io/button/button_fsm.c new file mode 100644 index 0000000000..f28ae43970 --- /dev/null +++ b/core/embed/io/button/button_fsm.c @@ -0,0 +1,86 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef KERNEL_MODE + +#include + +#include + +#include "button_fsm.h" + +void button_fsm_init(button_fsm_t* fsm) { + memset(fsm, 0, sizeof(button_fsm_t)); +} + +bool button_fsm_event_ready(button_fsm_t* fsm, uint32_t new_state) { + // Remember state changes + fsm->pressed |= new_state & ~fsm->state; + fsm->released |= ~new_state & fsm->state; + fsm->time = systick_us(); + // Return true if there are any state changes + return (fsm->pressed | fsm->released) != 0; +} + +bool button_fsm_get_event(button_fsm_t* fsm, uint32_t new_state, + button_event_t* event) { + uint64_t now = systick_us(); + + if ((now - fsm->time) > 100000) { + // Reset the history if the button was not read for 100ms + fsm->pressed = 0; + fsm->released = 0; + } + + // Remember state changes and the time of the last read + fsm->pressed |= new_state & ~fsm->state; + fsm->released |= ~new_state & fsm->state; + + // Bring the automaton out of invalid states, + // in case it somehow ends up in one. + fsm->released &= fsm->pressed | fsm->state; + fsm->pressed &= fsm->released | ~fsm->state; + + uint8_t button_idx = 0; + while (fsm->pressed | fsm->released) { + uint32_t mask = 1 << button_idx; + + if ((fsm->pressed & mask) != 0 && (fsm->state & mask) == 0) { + // Button press was not signalled yet + fsm->pressed &= ~mask; + fsm->state |= mask; + event->button = (button_t)button_idx; + event->event_type = BTN_EVENT_DOWN; + return true; + } else if ((fsm->released & mask) != 0 && (fsm->state & mask) != 0) { + // Button release was not signalled yet + fsm->released &= ~mask; + fsm->state &= ~mask; + event->button = (button_t)button_idx; + event->event_type = BTN_EVENT_UP; + return true; + } + + ++button_idx; + } + + return false; +} + +#endif // KERNEL_MODE diff --git a/core/embed/io/button/button_fsm.h b/core/embed/io/button/button_fsm.h new file mode 100644 index 0000000000..bfb66ffe50 --- /dev/null +++ b/core/embed/io/button/button_fsm.h @@ -0,0 +1,59 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +// This module is a simple finite state machine for buttons. +// +// It is designed to be used in a polling loop, where the state of the buttons +// is read periodically. The module keeps track of the state changes and +// provides a simple interface to get the events that happened since the last +// call to button_fsm_get_event(). +// +// The structure is designed to be used in a multi-threaded environment, where +// each thread has its own state machine. The state machines are stored in an +// array indexed by the task ID. + +typedef struct { + // Time of last update of pressed/released data + uint64_t time; + // Button presses that were detected since last get_event call + uint32_t pressed; + // Button releases that were detected since last get_event call + uint32_t released; + // State of buttons signalled to the poller + uint32_t state; +} button_fsm_t; + +// Initializes button finite state machine +void button_fsm_init(button_fsm_t* fsm); + +// Checks if button_fsm_get_event() would return `true` on the next call +bool button_fsm_event_ready(button_fsm_t* fsm, uint32_t new_state); + +// Processes the new_state of the button and fills the event structure. +// +// `new_state` is the current state of the buttons - each bit represents +// the state of one button (up to 32 buttons can be handled simultaneously). +// +// Returns `true` if the event structure was filled. +bool button_fsm_get_event(button_fsm_t* fsm, uint32_t new_state, + button_event_t* event); diff --git a/core/embed/io/button/inc/io/button.h b/core/embed/io/button/inc/io/button.h index 7db72ff656..4335bb278b 100644 --- a/core/embed/io/button/inc/io/button.h +++ b/core/embed/io/button/inc/io/button.h @@ -68,8 +68,4 @@ void button_deinit(void); bool button_get_event(button_event_t* event); // Checks if the specified button is currently pressed -// -// The current implementation returns the state of the button at the time -// `button_get_event()` was called. In the future, we may fix this limitation. -// For now, `button_get_event()` must be called before `button_is_down()`. bool button_is_down(button_t button); diff --git a/core/embed/io/button/stm32/button.c b/core/embed/io/button/stm32/button.c index a426ae1b9c..4437505af8 100644 --- a/core/embed/io/button/stm32/button.c +++ b/core/embed/io/button/stm32/button.c @@ -23,6 +23,9 @@ #include #include #include +#include + +#include "../button_fsm.h" #ifdef USE_POWERCTL #include @@ -34,15 +37,8 @@ typedef struct { bool initialized; -#ifdef BTN_LEFT_PIN - bool left_down; -#endif -#ifdef BTN_RIGHT_PIN - bool right_down; -#endif -#ifdef BTN_POWER_PIN - bool power_down; -#endif + // Each task has its own state machine + button_fsm_t tls[SYSTASK_MAX_TASKS]; } button_driver_t; @@ -51,7 +47,10 @@ static button_driver_t g_button_driver = { .initialized = false, }; -static void button_setup_pin(GPIO_TypeDef *port, uint16_t pin) { +// Forward declarations +static const syshandle_vmt_t g_button_handle_vmt; + +static void button_setup_pin(GPIO_TypeDef* port, uint16_t pin) { GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIO_InitStructure.Mode = GPIO_MODE_INPUT; @@ -62,7 +61,7 @@ static void button_setup_pin(GPIO_TypeDef *port, uint16_t pin) { } bool button_init(void) { - button_driver_t *drv = &g_button_driver; + button_driver_t* drv = &g_button_driver; if (drv->initialized) { return true; @@ -99,106 +98,76 @@ bool button_init(void) { NVIC_EnableIRQ(BTN_EXTI_INTERRUPT_NUM); #endif // BTN_EXTI_INTERRUPT_HANDLER - drv->initialized = true; + if (!syshandle_register(SYSHANDLE_BUTTON, &g_button_handle_vmt, drv)) { + goto cleanup; + } + drv->initialized = true; return true; + +cleanup: + button_deinit(); + return false; } void button_deinit(void) { + button_driver_t* drv = &g_button_driver; + + syshandle_unregister(SYSHANDLE_BUTTON); + #ifdef BTN_EXIT_INTERRUPT_HANDLER NVIC_DisableIRQ(BTN_EXTI_INTERRUPT_NUM); #endif + + memset(drv, 0, sizeof(button_driver_t)); } -bool button_get_event(button_event_t *event) { - button_driver_t *drv = &g_button_driver; +static uint32_t button_read_state(button_driver_t* drv) { + UNUSED(drv); + uint32_t state = 0; +#ifdef BTN_LEFT_PIN + if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(BTN_LEFT_PORT, BTN_LEFT_PIN)) { + state |= (1U << BTN_LEFT); + } +#endif + +#ifdef BTN_RIGHT_PIN + if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(BTN_RIGHT_PORT, BTN_RIGHT_PIN)) { + state |= (1U << BTN_RIGHT); + } +#endif + +#ifdef BTN_POWER_PIN + if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(BTN_POWER_PORT, BTN_POWER_PIN)) { + state |= (1U << BTN_POWER); + } +#endif + return state; +} + +bool button_get_event(button_event_t* event) { + button_driver_t* drv = &g_button_driver; memset(event, 0, sizeof(*event)); if (!drv->initialized) { return false; } -#ifdef BTN_LEFT_PIN - bool left_down = - (GPIO_PIN_RESET == HAL_GPIO_ReadPin(BTN_LEFT_PORT, BTN_LEFT_PIN)); + uint32_t new_state = button_read_state(drv); - if (drv->left_down != left_down) { - drv->left_down = left_down; - if (left_down) { - event->button = BTN_LEFT; - event->event_type = BTN_EVENT_DOWN; - return true; - } else { - event->button = BTN_LEFT; - event->event_type = BTN_EVENT_UP; - return true; - } - } -#endif - -#ifdef BTN_RIGHT_PIN - bool right_down = - (GPIO_PIN_RESET == HAL_GPIO_ReadPin(BTN_RIGHT_PORT, BTN_RIGHT_PIN)); - - if (drv->right_down != right_down) { - drv->right_down = right_down; - if (right_down) { - event->button = BTN_RIGHT; - event->event_type = BTN_EVENT_DOWN; - return true; - } else { - event->button = BTN_RIGHT; - event->event_type = BTN_EVENT_UP; - return true; - } - } -#endif - -#ifdef BTN_POWER_PIN - bool power_down = - (GPIO_PIN_RESET == HAL_GPIO_ReadPin(BTN_POWER_PORT, BTN_POWER_PIN)); - - if (drv->power_down != power_down) { - drv->power_down = power_down; - if (power_down) { - event->button = BTN_POWER; - event->event_type = BTN_EVENT_DOWN; - return true; - } else { - event->button = BTN_POWER; - event->event_type = BTN_EVENT_UP; - return true; - } - } -#endif - - return 0; + button_fsm_t* fsm = &drv->tls[systask_id(systask_active())]; + return button_fsm_get_event(fsm, new_state, event); } bool button_is_down(button_t button) { - button_driver_t *drv = &g_button_driver; + button_driver_t* drv = &g_button_driver; if (!drv->initialized) { return false; } - switch (button) { -#ifdef BTN_LEFT_PIN - case BTN_LEFT: - return drv->left_down; -#endif -#ifdef BTN_RIGHT_PIN - case BTN_RIGHT: - return drv->right_down; -#endif -#ifdef BTN_POWER_PIN - case BTN_POWER: - return drv->power_down; -#endif - default: - return false; - } + return (button_read_state(drv) & (1 << button)) != 0; } #ifdef BTN_EXTI_INTERRUPT_HANDLER @@ -221,4 +190,40 @@ void BTN_EXTI_INTERRUPT_HANDLER(void) { } #endif +static void on_task_created(void* context, systask_id_t task_id) { + button_driver_t* drv = (button_driver_t*)context; + button_fsm_t* fsm = &drv->tls[task_id]; + button_fsm_init(fsm); +} + +static void on_event_poll(void* context, bool read_awaited, + bool write_awaited) { + button_driver_t* drv = (button_driver_t*)context; + + UNUSED(write_awaited); + + if (read_awaited) { + uint32_t state = button_read_state(drv); + syshandle_signal_read_ready(SYSHANDLE_BUTTON, &state); + } +} + +static bool on_check_read_ready(void* context, systask_id_t task_id, + void* param) { + button_driver_t* drv = (button_driver_t*)context; + button_fsm_t* fsm = &drv->tls[task_id]; + + uint32_t new_state = *(uint32_t*)param; + + return button_fsm_event_ready(fsm, new_state); +} + +static const syshandle_vmt_t g_button_handle_vmt = { + .task_created = on_task_created, + .task_killed = NULL, + .check_read_ready = on_check_read_ready, + .check_write_ready = NULL, + .poll = on_event_poll, +}; + #endif // KERNEL_MODE diff --git a/core/embed/io/button/unix/button.c b/core/embed/io/button/unix/button.c index e66c1cfd0e..f4647b7f49 100644 --- a/core/embed/io/button/unix/button.c +++ b/core/embed/io/button/unix/button.c @@ -20,23 +20,19 @@ #include #include -#include - #include +#include +#include + +#include "../button_fsm.h" // Button driver state typedef struct { bool initialized; - -#ifdef BTN_LEFT_KEY - bool left_down; -#endif -#ifdef BTN_RIGHT_KEY - bool right_down; -#endif -#ifdef BTN_POWER_KEY - bool power_down; -#endif + // Global state of buttons + uint32_t state; + // Each task has its own state machine + button_fsm_t tls[SYSTASK_MAX_TASKS]; } button_driver_t; // Button driver instance @@ -44,8 +40,12 @@ static button_driver_t g_button_driver = { .initialized = false, }; +// Forward declarations +static const syshandle_vmt_t g_button_handle_vmt; +static void button_sdl_event_filter(void* context, SDL_Event* sdl_event); + bool button_init(void) { - button_driver_t *drv = &g_button_driver; + button_driver_t* drv = &g_button_driver; if (drv->initialized) { return true; @@ -53,79 +53,128 @@ bool button_init(void) { memset(drv, 0, sizeof(button_driver_t)); + if (!syshandle_register(SYSHANDLE_BUTTON, &g_button_handle_vmt, drv)) { + goto cleanup; + } + + if (!sdl_events_register(button_sdl_event_filter, drv)) { + goto cleanup; + } + drv->initialized = true; - return true; -} - -bool button_get_event(button_event_t *event) { - button_driver_t *drv = &g_button_driver; - - memset(event, 0, sizeof(button_event_t)); - - if (!drv->initialized) { - return 0; - } - - SDL_Event sdl_event; - - if (SDL_PollEvent(&sdl_event) > 0 && - (sdl_event.type == SDL_KEYDOWN || sdl_event.type == SDL_KEYUP) && - !sdl_event.key.repeat) { - bool down = (sdl_event.type == SDL_KEYDOWN); - uint32_t evt_type = down ? BTN_EVENT_DOWN : BTN_EVENT_UP; - - switch (sdl_event.key.keysym.sym) { -#ifdef BTN_LEFT_KEY - case BTN_LEFT_KEY: - drv->left_down = down; - event->event_type = evt_type; - event->button = BTN_LEFT; - return true; -#endif -#ifdef BTN_RIGHT_KEY - case BTN_RIGHT_KEY: - drv->right_down = down; - event->event_type = evt_type; - event->button = BTN_RIGHT; - return true; -#endif -#ifdef BTN_POWER_KEY - case BTN_POWER_KEY: - drv->power_down = down; - event->event_type = evt_type; - event->button = BTN_POWER; - return true; -#endif - default: - break; - } - } +cleanup: + button_deinit(); return false; } -bool button_is_down(button_t button) { - button_driver_t *drv = &g_button_driver; +void button_deinit(void) { + button_driver_t* drv = &g_button_driver; + + syshandle_unregister(SYSHANDLE_BUTTON); + + sdl_events_unregister(button_sdl_event_filter, drv); + + memset(drv, 0, sizeof(button_driver_t)); +} + +// Called from global event loop to filter and process SDL events +static void button_sdl_event_filter(void* context, SDL_Event* sdl_event) { + button_driver_t* drv = &g_button_driver; + + if (sdl_event->type != SDL_KEYDOWN && sdl_event->type != SDL_KEYUP) { + return; + } + + if (sdl_event->key.repeat) { + return; + } + + button_t button; + + switch (sdl_event->key.keysym.sym) { +#ifdef BTN_LEFT_KEY + case BTN_LEFT_KEY: + button = BTN_LEFT; + break; +#endif +#ifdef BTN_RIGHT_KEY + case BTN_RIGHT_KEY: + button = BTN_RIGHT; + break; +#endif +#ifdef BTN_POWER_KEY + case BTN_POWER_KEY: + button = BTN_POWER; + break; +#endif + default: + return; + } + + if (sdl_event->type == SDL_KEYDOWN) { + drv->state |= (1 << button); + } else { + drv->state &= ~(1 << button); + } +} + +static uint32_t button_read_state(button_driver_t* drv) { + sdl_events_poll(); + return drv->state; +} + +bool button_get_event(button_event_t* event) { + button_driver_t* drv = &g_button_driver; + memset(event, 0, sizeof(*event)); if (!drv->initialized) { return false; } - switch (button) { -#ifdef BTN_LEFT_KEY - case BTN_LEFT: - return drv->left_down; -#endif -#ifdef BTN_RIGHT_KEY - case BTN_RIGHT: - return drv->right_down; -#endif -#ifdef BTN_POWER_KEY - case BTN_POWER: - return drv->power_down; -#endif - default: - return false; + uint32_t new_state = button_read_state(drv); + + button_fsm_t* fsm = &drv->tls[systask_id(systask_active())]; + return button_fsm_get_event(fsm, new_state, event); +} + +bool button_is_down(button_t button) { + button_driver_t* drv = &g_button_driver; + + if (!drv->initialized) { + return false; + } + + return (button_read_state(drv) & (1 << button)) != 0; +} + +static void on_event_poll(void* context, bool read_awaited, + bool write_awaited) { + button_driver_t* drv = (button_driver_t*)context; + + UNUSED(write_awaited); + + if (read_awaited) { + uint32_t state = button_read_state(drv); + syshandle_signal_read_ready(SYSHANDLE_BUTTON, &state); } } + +static bool on_check_read_ready(void* context, systask_id_t task_id, + void* param) { + button_driver_t* drv = (button_driver_t*)context; + button_fsm_t* fsm = &drv->tls[task_id]; + + uint32_t new_state = *(uint32_t*)param; + + return button_fsm_event_ready(fsm, new_state); +} + +static const syshandle_vmt_t g_button_handle_vmt = { + .task_created = NULL, + .task_killed = NULL, + .check_read_ready = on_check_read_ready, + .check_write_ready = NULL, + .poll = on_event_poll, +}; diff --git a/core/embed/projects/bootloader/bootui.c b/core/embed/projects/bootloader/bootui.c index 800161ba8d..eb4d94f51b 100644 --- a/core/embed/projects/bootloader/bootui.c +++ b/core/embed/projects/bootloader/bootui.c @@ -70,15 +70,11 @@ void ui_click(void) { void ui_click(void) { for (;;) { - button_event_t event = {0}; - button_get_event(&event); if (button_is_down(BTN_LEFT) && button_is_down(BTN_RIGHT)) { break; } } for (;;) { - button_event_t event = {0}; - button_get_event(&event); if (!button_is_down(BTN_LEFT) && !button_is_down(BTN_RIGHT)) { break; } diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c index 5b6f9f6b5b..65a4934146 100644 --- a/core/embed/projects/bootloader/main.c +++ b/core/embed/projects/bootloader/main.c @@ -362,8 +362,6 @@ int bootloader_main(void) { } } #elif defined USE_BUTTON - button_event_t btn_evt = {0}; - button_get_event(&btn_evt); if (button_is_down(BTN_LEFT)) { touched = 1; } diff --git a/core/embed/projects/prodtest/cmd/prodtest_button.c b/core/embed/projects/prodtest/cmd/prodtest_button.c index d899119741..c8ac570022 100644 --- a/core/embed/projects/prodtest/cmd/prodtest_button.c +++ b/core/embed/projects/prodtest/cmd/prodtest_button.c @@ -67,10 +67,6 @@ static void test_button_combination(cli_t* cli, uint32_t timeout, button_t btn1, cli_trace(cli, "Waiting for button combination to be pressed..."); while (true) { - // Event must be read before calling `button_is_down()` - button_event_t e = {0}; - button_get_event(&e); - if (button_is_down(btn1) && button_is_down(btn2)) { break; } else if (ticks_expired(expire_time)) { @@ -84,10 +80,6 @@ static void test_button_combination(cli_t* cli, uint32_t timeout, button_t btn1, cli_trace(cli, "Waiting for buttons to be released..."); while (true) { - // Event must be read before calling `button_is_down()` - button_event_t e = {0}; - button_get_event(&e); - if (!button_is_down(btn1) && !button_is_down(btn2)) { break; } else if (ticks_expired(expire_time)) { diff --git a/core/site_scons/models/T2B1/emulator.py b/core/site_scons/models/T2B1/emulator.py index 6f20161db2..172fd65ddf 100644 --- a/core/site_scons/models/T2B1/emulator.py +++ b/core/site_scons/models/T2B1/emulator.py @@ -50,6 +50,7 @@ def configure( if "input" in features_wanted: sources += ["embed/io/button/unix/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [("USE_BUTTON", "1")] diff --git a/core/site_scons/models/T2B1/trezor_r_v10.py b/core/site_scons/models/T2B1/trezor_r_v10.py index 98eb84a501..3781ff0985 100644 --- a/core/site_scons/models/T2B1/trezor_r_v10.py +++ b/core/site_scons/models/T2B1/trezor_r_v10.py @@ -50,6 +50,7 @@ def configure( if "input" in features_wanted: sources += ["embed/io/button/stm32/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [("USE_BUTTON", "1")] diff --git a/core/site_scons/models/T3B1/emulator.py b/core/site_scons/models/T3B1/emulator.py index 4a4654b516..a24f57c2df 100644 --- a/core/site_scons/models/T3B1/emulator.py +++ b/core/site_scons/models/T3B1/emulator.py @@ -50,6 +50,7 @@ def configure( if "input" in features_wanted: sources += ["embed/io/button/unix/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [("USE_BUTTON", "1")] diff --git a/core/site_scons/models/T3B1/trezor_t3b1_revB.py b/core/site_scons/models/T3B1/trezor_t3b1_revB.py index fd8bc66bb8..b448448fa0 100644 --- a/core/site_scons/models/T3B1/trezor_t3b1_revB.py +++ b/core/site_scons/models/T3B1/trezor_t3b1_revB.py @@ -49,6 +49,7 @@ def configure( if "input" in features_wanted: sources += ["embed/io/button/stm32/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [("USE_BUTTON", "1")] diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revA.py b/core/site_scons/models/T3W1/trezor_t3w1_revA.py index 05bfcc8194..04b7e47fa7 100644 --- a/core/site_scons/models/T3W1/trezor_t3w1_revA.py +++ b/core/site_scons/models/T3W1/trezor_t3w1_revA.py @@ -63,6 +63,7 @@ def configure( paths += ["embed/io/touch/inc"] features_available.append("touch") sources += ["embed/io/button/stm32/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [ diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revB.py b/core/site_scons/models/T3W1/trezor_t3w1_revB.py index 79f1d98a03..e36da80dab 100644 --- a/core/site_scons/models/T3W1/trezor_t3w1_revB.py +++ b/core/site_scons/models/T3W1/trezor_t3w1_revB.py @@ -63,6 +63,7 @@ def configure( paths += ["embed/io/touch/inc"] features_available.append("touch") sources += ["embed/io/button/stm32/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [ diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revC.py b/core/site_scons/models/T3W1/trezor_t3w1_revC.py index b737b8e59e..611e1d5eb0 100644 --- a/core/site_scons/models/T3W1/trezor_t3w1_revC.py +++ b/core/site_scons/models/T3W1/trezor_t3w1_revC.py @@ -63,6 +63,7 @@ def configure( paths += ["embed/io/touch/inc"] features_available.append("touch") sources += ["embed/io/button/stm32/button.c"] + sources += ["embed/io/button/button_fsm.c"] paths += ["embed/io/button/inc"] features_available.append("button") defines += [