Skip to content

Commit c00e2f2

Browse files
committed
Fix event loop one-event delay from ISR and add Honeywell 5834-4 SubGhz (fixes #4336)
Event loop fix: - Use xTimerPendFunctionCallFromISR so event group bits are set in the timer daemon before notifying the event loop (fixes callbacks seeing flag one event late when set from interrupt context). - Document event flag and event loop ISR behavior in API headers. - Add furi_hal_interrupt_trigger_pending for software-pending interrupt tests. - Add regression test test_furi_event_loop_event_flag_from_isr. - Add Issue 4336 review doc, README unit tests section, UnitTests.md example, FuriHalInterrupt.md, and js_event_loop note. SubGhz Honeywell 5834-4: - Add Honeywell 5834-4 protocol (decoder/encoder, 345 MHz, 48-bit). - Register protocol; add Honeywell5834_345 preset to Set Type menu. - Add unit tests for decoder and encoder. - Add SubGhz_Honeywell_5834_Optimizations.md documenting resource optimizations.
1 parent c9ab2b6 commit c00e2f2

22 files changed

+780
-7
lines changed

ReadMe.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ Make sure your Flipper is on, and your firmware is functioning. Connect your Fli
9090
./fbt flash_usb
9191
```
9292

93+
## Running unit tests
94+
95+
Unit tests run on the device. Build firmware with tests enabled, flash it (and copy resources from `build/latest/resources` to the SD card), then in a CLI session run:
96+
97+
```shell
98+
unit_tests
99+
```
100+
101+
To run only the Furi tests (e.g. after changing event loop or event flag code):
102+
103+
```shell
104+
unit_tests test_furi
105+
```
106+
107+
See [Unit Tests](/documentation/UnitTests.md) for details and how to add tests.
108+
93109
## Documentation
94110

95111
- [Flipper Build Tool](/documentation/fbt.md) - building, flashing, and debugging Flipper software

applications/debug/unit_tests/tests/furi/furi_event_loop_test.c

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,82 @@ void test_furi_event_loop_self_unsubscribe(void) {
495495
furi_event_loop_free(event_loop);
496496
}
497497

498+
/*
499+
* Regression test for issue #4336: event loop must receive the correct flag
500+
* value when furi_event_flag_set() is called from ISR (no one-event delay).
501+
* Uses a software-pending LPTIM2 interrupt to run the ISR path.
502+
*/
503+
#define EVENT_FLAG_ISR_TEST_BITS 3U
504+
#define EVENT_FLAG_ISR_TEST_MASK ((1U << EVENT_FLAG_ISR_TEST_BITS) - 1U)
505+
506+
typedef struct {
507+
FuriEventFlag* flag;
508+
uint32_t bits_to_set;
509+
} EventFlagIsrTestIsrContext;
510+
511+
typedef struct {
512+
uint32_t bits_received;
513+
FuriSemaphore* sync;
514+
} EventFlagIsrTestCallbackContext;
515+
516+
static void event_flag_isr_test_isr(void* context) {
517+
EventFlagIsrTestIsrContext* ctx = context;
518+
furi_event_flag_set(ctx->flag, ctx->bits_to_set);
519+
}
520+
521+
static void event_flag_isr_test_callback(FuriEventLoopObject* object, void* context) {
522+
EventFlagIsrTestCallbackContext* ctx = context;
523+
ctx->bits_received =
524+
furi_event_flag_wait((FuriEventFlag*)object, EVENT_FLAG_ISR_TEST_MASK, FuriFlagWaitAny, 0);
525+
furi_semaphore_release(ctx->sync);
526+
}
527+
528+
static int32_t event_flag_isr_test_loop_thread(void* arg) {
529+
FuriEventLoop* loop = arg;
530+
furi_event_loop_run(loop);
531+
return 0;
532+
}
533+
534+
void test_furi_event_loop_event_flag_from_isr(void) {
535+
FuriEventFlag* flag = furi_event_flag_alloc();
536+
FuriEventLoop* loop = furi_event_loop_alloc();
537+
FuriSemaphore* sync = furi_semaphore_alloc(1, 0);
538+
539+
EventFlagIsrTestIsrContext isr_ctx = {.flag = flag, .bits_to_set = 0};
540+
EventFlagIsrTestCallbackContext cb_ctx = {.bits_received = 0, .sync = sync};
541+
542+
furi_event_loop_subscribe_event_flag(
543+
loop, flag, FuriEventLoopEventIn, event_flag_isr_test_callback, &cb_ctx);
544+
545+
FuriThread* loop_thread =
546+
furi_thread_alloc_ex("event_flag_isr_loop", 1024, event_flag_isr_test_loop_thread, loop);
547+
furi_thread_start(loop_thread);
548+
549+
furi_delay_ms(10);
550+
551+
furi_hal_interrupt_set_isr(FuriHalInterruptIdLpTim2, event_flag_isr_test_isr, &isr_ctx);
552+
553+
const uint32_t test_bits[] = {1U, 2U, 4U};
554+
for(size_t i = 0; i < sizeof(test_bits) / sizeof(test_bits[0]); i++) {
555+
isr_ctx.bits_to_set = test_bits[i];
556+
furi_hal_interrupt_trigger_pending(FuriHalInterruptIdLpTim2);
557+
mu_assert(
558+
furi_semaphore_acquire(sync, furi_ms_to_ticks(500)) == FuriStatusOk,
559+
"timeout waiting for event flag callback");
560+
mu_assert_int_eq(cb_ctx.bits_received, test_bits[i]);
561+
}
562+
563+
furi_hal_interrupt_set_isr(FuriHalInterruptIdLpTim2, NULL, NULL);
564+
furi_event_loop_stop(loop);
565+
furi_thread_join(loop_thread);
566+
567+
furi_thread_free(loop_thread);
568+
furi_event_loop_unsubscribe(loop, flag);
569+
furi_semaphore_free(sync);
570+
furi_event_loop_free(loop);
571+
furi_event_flag_free(flag);
572+
}
573+
498574
void test_furi_event_loop(void) {
499575
TestFuriEventLoopData data = {};
500576

applications/debug/unit_tests/tests/furi/furi_test.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ void test_furi_pubsub(void);
99
void test_furi_memmgr(void);
1010
void test_furi_event_loop(void);
1111
void test_furi_event_loop_self_unsubscribe(void);
12+
void test_furi_event_loop_event_flag_from_isr(void);
1213
void test_errno_saving(void);
1314
void test_furi_primitives(void);
1415
void test_stdin(void);
@@ -51,6 +52,10 @@ MU_TEST(mu_test_furi_event_loop_self_unsubscribe) {
5152
test_furi_event_loop_self_unsubscribe();
5253
}
5354

55+
MU_TEST(mu_test_furi_event_loop_event_flag_from_isr) {
56+
test_furi_event_loop_event_flag_from_isr();
57+
}
58+
5459
MU_TEST(mu_test_errno_saving) {
5560
test_errno_saving();
5661
}
@@ -74,6 +79,7 @@ MU_TEST_SUITE(test_suite) {
7479
MU_RUN_TEST(mu_test_furi_memmgr);
7580
MU_RUN_TEST(mu_test_furi_event_loop);
7681
MU_RUN_TEST(mu_test_furi_event_loop_self_unsubscribe);
82+
MU_RUN_TEST(mu_test_furi_event_loop_event_flag_from_isr);
7783
MU_RUN_TEST(mu_test_stdio);
7884
MU_RUN_TEST(mu_test_errno_saving);
7985
MU_RUN_TEST(mu_test_furi_primitives);

applications/debug/unit_tests/tests/subghz/subghz_test.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,14 @@ MU_TEST(subghz_decoder_honeywell_wdb_test) {
585585
"Test decoder " SUBGHZ_PROTOCOL_HONEYWELL_WDB_NAME " error\r\n");
586586
}
587587

588+
MU_TEST(subghz_decoder_honeywell_5834_test) {
589+
mu_assert(
590+
subghz_decoder_test(
591+
EXT_PATH("unit_tests/subghz/honeywell_5834_raw.sub"),
592+
SUBGHZ_PROTOCOL_HONEYWELL_5834_NAME),
593+
"Test decoder " SUBGHZ_PROTOCOL_HONEYWELL_5834_NAME " error\r\n");
594+
}
595+
588596
MU_TEST(subghz_decoder_magellan_test) {
589597
mu_assert(
590598
subghz_decoder_test(
@@ -837,6 +845,12 @@ MU_TEST(subghz_encoder_honeywell_wdb_test) {
837845
"Test encoder " SUBGHZ_PROTOCOL_HONEYWELL_WDB_NAME " error\r\n");
838846
}
839847

848+
MU_TEST(subghz_encoder_honeywell_5834_test) {
849+
mu_assert(
850+
subghz_encoder_test(EXT_PATH("unit_tests/subghz/honeywell_5834.sub")),
851+
"Test encoder " SUBGHZ_PROTOCOL_HONEYWELL_5834_NAME " error\r\n");
852+
}
853+
840854
MU_TEST(subghz_encoder_magellan_test) {
841855
mu_assert(
842856
subghz_encoder_test(EXT_PATH("unit_tests/subghz/magellan.sub")),
@@ -973,6 +987,7 @@ MU_TEST_SUITE(subghz) {
973987
MU_RUN_TEST(subghz_decoder_doitrand_test);
974988
MU_RUN_TEST(subghz_decoder_phoenix_v2_test);
975989
MU_RUN_TEST(subghz_decoder_honeywell_wdb_test);
990+
MU_RUN_TEST(subghz_decoder_honeywell_5834_test);
976991
MU_RUN_TEST(subghz_decoder_magellan_test);
977992
MU_RUN_TEST(subghz_decoder_intertechno_v3_test);
978993
MU_RUN_TEST(subghz_decoder_clemsa_test);
@@ -1012,6 +1027,7 @@ MU_TEST_SUITE(subghz) {
10121027
MU_RUN_TEST(subghz_encoder_doitrand_test);
10131028
MU_RUN_TEST(subghz_encoder_phoenix_v2_test);
10141029
MU_RUN_TEST(subghz_encoder_honeywell_wdb_test);
1030+
MU_RUN_TEST(subghz_encoder_honeywell_5834_test);
10151031
MU_RUN_TEST(subghz_encoder_magellan_test);
10161032
MU_RUN_TEST(subghz_encoder_intertechno_v3_test);
10171033
MU_RUN_TEST(subghz_encoder_clemsa_test);

applications/main/subghz/helpers/subghz_custom_event.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ typedef enum {
2222
SubmenuIndexSecPlus_v2_310_00,
2323
SubmenuIndexSecPlus_v2_315_00,
2424
SubmenuIndexSecPlus_v2_390_00,
25+
SubmenuIndexHoneywell5834_345,
2526

2627
//SubGhzCustomEvent
2728
SubGhzCustomEventSceneDeleteSuccess = 100,

applications/main/subghz/scenes/subghz_scene_set_type.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ void subghz_scene_set_type_on_enter(void* context) {
109109
SubmenuIndexSecPlus_v2_390_00,
110110
subghz_scene_set_type_submenu_callback,
111111
subghz);
112+
submenu_add_item(
113+
subghz->submenu,
114+
"Honeywell5834_345",
115+
SubmenuIndexHoneywell5834_345,
116+
subghz_scene_set_type_submenu_callback,
117+
subghz);
112118

113119
submenu_set_selected_item(
114120
subghz->submenu, scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType));
@@ -212,6 +218,14 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
212218
generated_protocol = subghz_txrx_gen_secplus_v2_protocol(
213219
subghz->txrx, "AM650", 390000000, key, 0x68, 0xE500000);
214220
break;
221+
case SubmenuIndexHoneywell5834_345: {
222+
uint64_t serial_20 = (uint64_t)(key & 0xFFFFF) << 28;
223+
uint64_t state_bits = (uint64_t)0x40 << 4; /* Arm Away */
224+
uint64_t frame = serial_20 | state_bits;
225+
frame |= subghz_protocol_blocks_get_parity(frame >> 1, 47);
226+
generated_protocol = subghz_txrx_gen_data_protocol(
227+
subghz->txrx, "AM650", 345000000, SUBGHZ_PROTOCOL_HONEYWELL_5834_NAME, frame, 48);
228+
} break;
215229
default:
216230
return false;
217231
break;

documentation/FuriHalInterrupt.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Furi HAL Interrupt API {#furi_hal_interrupt}
2+
3+
The interrupt HAL provides a unified way to register and control interrupt service routines (ISRs) for various on-chip peripherals. It is used by the system and by apps that need timer, DMA, UART, or other hardware interrupts.
4+
5+
**Header:** `furi_hal_interrupt.h` (target-specific, e.g. `targets/f7/furi_hal/furi_hal_interrupt.h`)
6+
7+
## Initialization
8+
9+
The interrupt subsystem must be initialized before use. The system does this at startup. User code typically only calls `furi_hal_interrupt_set_isr()` or `furi_hal_interrupt_set_isr_ex()` to register a handler.
10+
11+
## Registering an ISR
12+
13+
- **`furi_hal_interrupt_set_isr(index, isr, context)`** — Set the ISR for the given interrupt ID and enable the interrupt with default priority. Pass `NULL` as `isr` to clear and disable.
14+
- **`furi_hal_interrupt_set_isr_ex(index, priority, isr, context)`** — Same, but with an explicit priority.
15+
16+
**Warning:** Interrupt flags are not cleared automatically. Ensure your peripheral status flags are cleared in the ISR or before enabling, as required by the hardware.
17+
18+
## Interrupt IDs and priorities
19+
20+
Interrupt IDs are defined in **`FuriHalInterruptId`** (e.g. `FuriHalInterruptIdTIM2`, `FuriHalInterruptIdLpTim2`, `FuriHalInterruptIdDma1Ch1`, `FuriHalInterruptIdUart1`). See the enum in the header for the full list.
21+
22+
Priorities are in **`FuriHalInterruptPriority`**. Use one of the levels that allow ISR-safe OS primitives (e.g. `FuriHalInterruptPriorityNormal`) unless you have a reason to use a higher or lower level. The special level `FuriHalInterruptPriorityKamiSama` must not use any OS primitives; see the header and FreeRTOS `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY` before using it.
23+
24+
## Software-triggered pending interrupt (for tests)
25+
26+
**`furi_hal_interrupt_trigger_pending(index)`** sets the given interrupt as pending so that the CPU will run the currently registered ISR without hardware firing. This is intended for **unit tests** that need to run code in interrupt context (e.g. to test `furi_event_flag_set()` from ISR). Not for use in production application logic.
27+
28+
Example: the Furi event loop regression test uses `FuriHalInterruptIdLpTim2` and `furi_hal_interrupt_trigger_pending()` to trigger the ISR from software and assert that the event loop callback receives the correct flag values.
29+
30+
## Other functions
31+
32+
- **`furi_hal_interrupt_get_name(exception_number)`** — Return the interrupt name for a given exception number (e.g. from IPSR). Useful for debugging.
33+
- **`furi_hal_interrupt_get_time_in_isr_total()`** — Return total time (CPU clocks) spent in ISRs. Useful for profiling.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Bug Review: Issue #4336 — EventLoop receives callbacks one event late when setting FuriEventFlag from interrupts
2+
3+
**Issue:** [flipperdevices/flipperzero-firmware#4336](https://github.com/flipperdevices/flipperzero-firmware/issues/4336)
4+
**Reporter:** @sorgloomer
5+
**Demo:** https://github.com/sorgloomer/flipper_eventflag_demo
6+
**Status:** Fixed in firmware. The sections below describe the bug, root cause (previous behavior), and the fix.
7+
8+
---
9+
10+
## Summary
11+
12+
When `furi_event_flag_set()` is called from interrupt context (e.g. GPIO interrupt) and the app uses `furi_event_loop_subscribe_event_flag()`, the event loop callback is woken **one event late**: the first interrupt does not trigger the callback, and each later callback sees the **previous** flag value. This matches a **level-triggered vs deferred-set** race caused by FreeRTOS’s design of `xEventGroupSetBitsFromISR()`.
13+
14+
---
15+
16+
## Root cause
17+
18+
### 1. FreeRTOS defers the actual bit set from ISR
19+
20+
From `lib/FreeRTOS-Kernel/include/event_groups.h`:
21+
22+
> Setting bits in an event group is not a deterministic operation … FreeRTOS does not allow nondeterministic operations to be performed in interrupts … **Therefore xEventGroupSetBitsFromISR() sends a message to the timer task** to have the set operation performed in the context of the timer task.
23+
24+
So when you call `xEventGroupSetBitsFromISR()` in an ISR:
25+
26+
- A command is **queued** to the timer daemon.
27+
- The **bits are not set in the ISR**; they are set when the timer task later processes that command.
28+
29+
### 2. Furi notifies the event loop immediately
30+
31+
Previously in `furi/core/event_flag.c`, `furi_event_flag_set()` used the following ISR path (now fixed):
32+
33+
```c
34+
uint32_t furi_event_flag_set(FuriEventFlag* instance, uint32_t flags) {
35+
// ...
36+
if(FURI_IS_IRQ_MODE()) {
37+
yield = pdFALSE;
38+
if(xEventGroupSetBitsFromISR(hEventGroup, (EventBits_t)flags, &yield) == pdFAIL) {
39+
// ...
40+
} else {
41+
rflags = flags;
42+
portYIELD_FROM_ISR(yield);
43+
}
44+
} else {
45+
rflags = xEventGroupSetBits(hEventGroup, (EventBits_t)flags);
46+
}
47+
48+
if(rflags & flags) {
49+
furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventIn);
50+
}
51+
// ...
52+
}
53+
```
54+
55+
So in ISR we:
56+
57+
1. Call `xEventGroupSetBitsFromISR()` → only **schedules** the set in the timer task.
58+
2. Then call `furi_event_loop_link_notify()` → **wakes the event loop task right away**.
59+
60+
The event loop therefore wakes **before** the timer task has applied the new bits.
61+
62+
### 3. Event loop reads the flag level too early
63+
64+
When the loop runs, it processes the waiting item and (for level-triggered) calls `furi_event_flag_event_loop_get_level()` which uses `furi_event_flag_get()` → `xEventGroupGetBits()`. At that moment the bits from the **current** ISR have not been set yet, so:
65+
66+
- **First interrupt:** Loop wakes, sees no (or old) bits, does not call the callback (or sees previous value). The “current” set is still pending in the timer queue.
67+
- **Second interrupt:** Another set is queued, loop is woken again. When it runs, the **first** set may have already been applied by the timer task, so the callback sees the **previous** event’s bits. The second set may still be pending.
68+
69+
Result: callbacks are effectively **one event late** and the first interrupt can appear to be “lost.”
70+
71+
---
72+
73+
## Why the first interrupt often doesn’t wake the loop (or shows no bits)
74+
75+
- Loop is idle, waiting in `xTaskNotifyWaitIndexed(..., ticks_to_sleep)`.
76+
- ISR runs: `xEventGroupSetBitsFromISR()` queues the set; `furi_event_loop_link_notify()` wakes the loop.
77+
- Loop runs, processes the event-flag item, calls `get_level()` → `furi_event_flag_get()`. The timer task may not have run yet, so bits are still 0 (or old).
78+
- For level-triggered, `furi_event_loop_process_level_event()` only invokes the callback when `get_level()` is true, so the callback may not run at all for that wakeup.
79+
- So the first interrupt can be “consumed” (loop woke, processed, found no bits) and the user sees no callback until the **next** interrupt, at which point they see the first interrupt’s bits.
80+
81+
---
82+
83+
## Conclusion
84+
85+
The behavior is a **firmware-level design limitation**: the event loop is notified in the same critical section as the (deferred) `xEventGroupSetBitsFromISR()`, so the loop consistently sees the flag state **before** the current ISR’s set has been applied. This matches the “one event late” and “first interrupt missing” symptoms.
86+
87+
---
88+
89+
## Workarounds (as in the issue thread)
90+
91+
1. **Use `FuriMessageQueue` for ISR → task**
92+
Post to a message queue from the ISR and subscribe with `furi_event_loop_subscribe_message_queue()`. Queues have ISR-safe enqueue and don’t rely on the timer daemon for the data to be visible.
93+
94+
2. **Use a counting semaphore**
95+
If you only need a wake-up count (no bit pattern), use a semaphore released from ISR and subscribe with `furi_event_loop_subscribe_semaphore()`.
96+
97+
3. **Avoid clearing flags in the callback in a way that assumes “current” set**
98+
Doesn’t fix the lag; at best it only avoids losing bits. The fundamental lag remains because the set is deferred.
99+
100+
---
101+
102+
## Firmware fix (implemented)
103+
104+
**Approach:** Notify the event loop only **after** the bits are set. We use `xTimerPendFunctionCallFromISR()` with a custom callback that runs in the timer daemon: it calls `xEventGroupSetBits()` then `furi_event_loop_link_notify()`. So when the loop runs, the bits are already set. Same timer queue as before; failure when queue is full still returns `FuriFlagErrorResource`. See `furi/core/event_flag.c` (ISR path and `furi_event_flag_set_bits_and_notify_callback`).
105+
106+
**Other options (not chosen):** Documentation-only; or a separate message queue / buffer (would change API or add a parallel path).
107+
108+
**Regression test:** `test_furi_event_loop_event_flag_from_isr()` in `applications/debug/unit_tests/tests/furi/furi_event_loop_test.c` runs `furi_event_flag_set()` from a software-pending LPTIM2 ISR and asserts that the event loop callback receives the correct flag value for each of three triggers (bits 1, 2, 4). If the one-event-late bug returns, the second or third assertion would fail (callback would see the previous bit). Run with `unit_tests test_furi` on device.
109+
110+
---
111+
112+
## Possible firmware fixes (for maintainers) — superseded by fix above
113+
114+
- **Document** that `furi_event_flag_set()` from ISR is deferred by FreeRTOS and can cause one-event lag when used with the event loop; recommend message queue or semaphore for ISR-driven events.
115+
- **Optionally:** Provide an ISR-safe path that doesn’t rely on the event group for **notification** (e.g. a separate “pending count” or queue of flag values) and have the event loop read from that instead of from the event group immediately after wake. That would require a different contract/link type for “event flag driven from ISR.”
116+
- **Alternative:** Use a software timer or a task that runs after the timer daemon to set the flag and then notify the event loop, so the set is always visible when the loop runs (adds latency and complexity).
117+
118+
---
119+
120+
## Post-merge: notify the reporter
121+
122+
After the fix is merged, post a short comment on [the issue](https://github.com/flipperdevices/flipperzero-firmware/issues/4336) so the reporter (@sorgloomer) can retest. Suggested text:
123+
124+
> Fixed in [PR #xxxx](link-to-pr). The event loop now receives the correct flag value when `furi_event_flag_set()` is called from interrupt context (no one-event delay). Please retest with the latest firmware; the regression test `unit_tests test_furi` includes `test_furi_event_loop_event_flag_from_isr` which covers this path.
125+
126+
Replace `#xxxx` and `link-to-pr` with the actual PR number and URL once the fix is merged.
127+
128+
---
129+
130+
## References
131+
132+
- Issue: https://github.com/flipperdevices/flipperzero-firmware/issues/4336
133+
- FreeRTOS `event_groups.h` (doc for `xEventGroupSetBitsFromISR`)
134+
- `furi/core/event_flag.c` — `furi_event_flag_set()`, `furi_event_loop_link_notify()`
135+
- `furi/core/event_loop.c` — `furi_event_loop_process_waiting_list()`, `furi_event_loop_process_level_event()`
136+
- `furi/core/event_loop_i.h` — level vs edge handling

0 commit comments

Comments
 (0)