From 343daea122a1f4661799673d9ef1d0ae43edd956 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Fri, 9 Oct 2026 12:17:52 +0200 Subject: [PATCH 1/2] [fiber] Add memory barrier to empty yield stub Without the fiber scheduler, yield() is an empty inline function, so the compiler assumes that no memory changes while polling. It then hoists the load of non-volatile variables modified in interrupts out of the polling loop, which turns the loop into an endless loop or removes it entirely. The memory clobber emits no code, but forces all memory to be reloaded after yield(), just like for a real context switch. --- src/modm/processing/fiber/no_yield.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modm/processing/fiber/no_yield.hpp b/src/modm/processing/fiber/no_yield.hpp index e346f03c5e..eb6540e709 100644 --- a/src/modm/processing/fiber/no_yield.hpp +++ b/src/modm/processing/fiber/no_yield.hpp @@ -18,7 +18,7 @@ namespace modm::this_fiber void inline yield() { - // do nothing and return + __asm__ volatile ("" ::: "memory"); } modm::fiber::id inline From 69711449c32836565680b951a0bfe0d28afd0f60 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Fri, 9 Oct 2026 12:25:16 +0200 Subject: [PATCH 2/2] [fiber] Provide synchronization primitives without scheduler The mutexes, semaphores, latch, stop token and condition variable only depend on yield() and get_id(), so they also work with the empty yield stub to synchronize the main context with interrupts. The barrier is excluded, since it requires multiple fibers to make progress. The get_id() stub now returns the interrupt number inside interrupts on Cortex-M just like the scheduler does, otherwise the recursive mutex cannot tell the main context apart from an interrupt. The new hosted-no-fiber unit test configuration tests the fiber API without the scheduler. --- .github/workflows/linux.yml | 4 + src/modm/architecture/interface/fiber.md | 38 +++++- src/modm/architecture/module.lb | 4 +- .../fiber/{no_yield.hpp => no_yield.hpp.in} | 8 ++ src/modm/processing/module.lb | 12 +- test/Makefile | 2 + test/config/hosted-no-fiber.xml | 13 +++ .../architecture/fiber/fiber_sync_test.cpp | 109 ++++++++++++++++++ .../architecture/fiber/fiber_sync_test.hpp | 34 ++++++ test/modm/architecture/module.lb | 17 ++- 10 files changed, 236 insertions(+), 5 deletions(-) rename src/modm/processing/fiber/{no_yield.hpp => no_yield.hpp.in} (70%) create mode 100644 test/config/hosted-no-fiber.xml create mode 100644 test/modm/architecture/fiber/fiber_sync_test.cpp create mode 100644 test/modm/architecture/fiber/fiber_sync_test.hpp diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 59cc897445..d84b2dc425 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -85,6 +85,10 @@ jobs: if: always() run: | (cd test && make run-hosted-linux-clang) + - name: Hosted Unittests (without Fiber Scheduler) + if: always() + run: | + (cd test && make run-hosted-linux-no-fiber) - name: Compile STM32 Unittests if: always() run: | diff --git a/src/modm/architecture/interface/fiber.md b/src/modm/architecture/interface/fiber.md index 817d9a14a3..de7d94af4b 100644 --- a/src/modm/architecture/interface/fiber.md +++ b/src/modm/architecture/interface/fiber.md @@ -96,6 +96,40 @@ including `modm:processing:fiber` or not. This compromise allows for a seamless transition between different devices and scheduling strategies. +## Synchronization without Scheduler + +The synchronization primitives of the `modm:processing:fiber` module are also +available without the scheduler, with the exception of `modm::fiber::barrier`, +which requires multiple fibers. They can then be used to synchronize the main +context with interrupts: + +```cpp +#include + +modm::fiber::binary_semaphore data_ready{0}; +MODM_ISR(EXTI0) +{ + data_ready.release(); +} +int main() +{ + while(true) + { + data_ready.acquire(); // waits in place for the interrupt + // process data + } +} +``` + +!!! warning "Blocking calls wait in place" + Without the scheduler there is no other fiber that can make progress, so a + blocking call like `lock()`, `acquire()` or `wait()` only returns if an + interrupt unlocks, releases or notifies the primitive. Locking a + non-recursive mutex twice in the main context therefore waits forever. + Use the `try_*()` functions or the timed `try_*_for()` and `try_*_until()` + functions if you cannot guarantee this. + + ## Identifier You can check what fiber your code is executed in by calling the `get_id()` @@ -109,6 +143,8 @@ auto id = modm::this_fiber::get_id(); The returned ID is the address of the currently running fiber object. If called outside of a fiber, for example, in the main function before the scheduler is -running, the function returns `0`. The implementation ensures that all returned +running or if the scheduler is not included, the function returns `0`. On +Cortex-M devices the function returns the interrupt number if called from +inside an interrupt. The implementation ensures that all returned values are unique and thus allow the ID to be used for tracking ownership of various recursive locks, for example. diff --git a/src/modm/architecture/module.lb b/src/modm/architecture/module.lb index 0ff8e46708..dd6fb816e3 100644 --- a/src/modm/architecture/module.lb +++ b/src/modm/architecture/module.lb @@ -172,7 +172,9 @@ class Fiber(Module): module.description = FileReader("interface/fiber.md") def prepare(self, module, options): - module.depends(":stdc++", ":architecture:clock", ":processing") + module.depends(":stdc++", ":architecture:atomic", ":architecture:clock", ":processing") + core = options[":target"].get_driver("core")["type"] + if core.startswith("cortex-m"): module.depends(":cmsis:device") return True def build(self, env): diff --git a/src/modm/processing/fiber/no_yield.hpp b/src/modm/processing/fiber/no_yield.hpp.in similarity index 70% rename from src/modm/processing/fiber/no_yield.hpp rename to src/modm/processing/fiber/no_yield.hpp.in index eb6540e709..98d395848c 100644 --- a/src/modm/processing/fiber/no_yield.hpp +++ b/src/modm/processing/fiber/no_yield.hpp.in @@ -11,6 +11,10 @@ #pragma once +%% if core.startswith("cortex-m") +#include +%% endif + /// @cond namespace modm::this_fiber { @@ -24,6 +28,10 @@ yield() modm::fiber::id inline get_id() { +%% if core.startswith("cortex-m") + // Ensure that calling this in an interrupt gives a different ID + if (const auto irq = __get_IPSR(); irq >= 16) return irq; +%% endif return 0; } diff --git a/src/modm/processing/module.lb b/src/modm/processing/module.lb index 1fd866efd6..bc726c1179 100644 --- a/src/modm/processing/module.lb +++ b/src/modm/processing/module.lb @@ -31,8 +31,16 @@ def build(env): if env.has_module(":architecture:fiber") and not env.has_module(":processing:fiber"): env.outbasepath = "modm/src/modm/processing/" - env.substitutions = {"with_task": False} - env.copy("fiber/no_yield.hpp") + env.substitutions = { + "with_task": False, + "core": env[":target"].get_driver("core")["type"], + } + env.template("fiber/no_yield.hpp.in") + # Only the synchronization primitives work without the scheduler, + # except for the barrier, which requires multiple fibers + env.copy("fiber", ignore=env.ignore_files( + "barrier.hpp", "context*", "scheduler*", "stack*", "task*", + "*.md", "*.in")) env.template("fiber.hpp.in") env.outbasepath = "modm/src/modm" diff --git a/test/Makefile b/test/Makefile index c6596b0d8f..6fc34692ee 100644 --- a/test/Makefile +++ b/test/Makefile @@ -30,6 +30,8 @@ run-hosted-linux: $(call compile-test,hosted,run,-D":target=hosted-linux") run-hosted-linux-clang: $(call compile-test,hosted-clang,run,-D":target=hosted-linux") +run-hosted-linux-no-fiber: + $(call compile-test,hosted-no-fiber,run,-D":target=hosted-linux") run-hosted-linux-arm64: $(call compile-test,hosted,run,-D":target=hosted-linux-arm64") run-hosted-darwin: diff --git a/test/config/hosted-no-fiber.xml b/test/config/hosted-no-fiber.xml new file mode 100644 index 0000000000..907049d4ae --- /dev/null +++ b/test/config/hosted-no-fiber.xml @@ -0,0 +1,13 @@ + + + + + + + + modm:platform:core + modm:driver:terminal + + modm-test:test:architecture:fiber + + diff --git a/test/modm/architecture/fiber/fiber_sync_test.cpp b/test/modm/architecture/fiber/fiber_sync_test.cpp new file mode 100644 index 0000000000..d56c19c007 --- /dev/null +++ b/test/modm/architecture/fiber/fiber_sync_test.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, Niklas Hauser + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#include "fiber_sync_test.hpp" +#include +#include +#include +#include +#include +#include + +void +FiberSyncTest::testPolling() +{ + TEST_ASSERT_EQUALS(modm::this_fiber::get_id(), modm::fiber::id(0)); + + uint8_t count{0}; + modm::this_fiber::poll([&]{ return ++count >= 3; }); + TEST_ASSERT_EQUALS(count, 3); +} + +void +FiberSyncTest::testMutex() +{ + modm::fiber::mutex mtx; + mtx.lock(); + TEST_ASSERT_FALSE(mtx.try_lock()); + mtx.unlock(); + { + modm::fiber::lock_guard guard{mtx}; + TEST_ASSERT_FALSE(mtx.try_lock()); + } + TEST_ASSERT_TRUE(mtx.try_lock()); + mtx.unlock(); + + modm::fiber::recursive_mutex rc_mtx; + rc_mtx.lock(); + rc_mtx.lock(); + TEST_ASSERT_TRUE(rc_mtx.try_lock()); + rc_mtx.unlock(); + rc_mtx.unlock(); + rc_mtx.unlock(); + + modm::fiber::shared_mutex sh_mtx; + sh_mtx.lock_shared(); + TEST_ASSERT_TRUE(sh_mtx.try_lock_shared()); + TEST_ASSERT_FALSE(sh_mtx.try_lock()); + sh_mtx.unlock_shared(); + sh_mtx.lock(); + TEST_ASSERT_FALSE(sh_mtx.try_lock_shared()); + sh_mtx.unlock(); + + uint8_t calls{0}; + modm::fiber::once_flag flag; + modm::fiber::call_once(flag, [&]{ calls++; }); + modm::fiber::call_once(flag, [&]{ calls++; }); + TEST_ASSERT_EQUALS(calls, 1); +} + +void +FiberSyncTest::testSemaphore() +{ + modm::fiber::counting_semaphore sem{2}; + sem.acquire(); + sem.acquire(); + TEST_ASSERT_FALSE(sem.try_acquire()); + sem.release(); + sem.acquire(); + TEST_ASSERT_FALSE(sem.try_acquire()); +} + +void +FiberSyncTest::testLatch() +{ + modm::fiber::latch ltc{2}; + TEST_ASSERT_FALSE(ltc.try_wait()); + ltc.count_down(); + TEST_ASSERT_FALSE(ltc.try_wait()); + ltc.arrive_and_wait(); + TEST_ASSERT_TRUE(ltc.try_wait()); +} + +void +FiberSyncTest::testConditionVariable() +{ + // std::unique_lock pulls in the exception handling of libstdc++, which + // does not fit on small devices, so the mutex is used as lock directly. + modm::fiber::mutex mtx; + modm::fiber::condition_variable cv; + modm::fiber::stop_state stop; + mtx.lock(); + + // predicate is already true, so this must not wait + cv.wait(mtx, []{ return true; }); + TEST_ASSERT_FALSE(mtx.try_lock()); + + stop.request_stop(); + TEST_ASSERT_FALSE(cv.wait(mtx, stop.get_token(), []{ return false; })); + TEST_ASSERT_FALSE(mtx.try_lock()); + mtx.unlock(); +} diff --git a/test/modm/architecture/fiber/fiber_sync_test.hpp b/test/modm/architecture/fiber/fiber_sync_test.hpp new file mode 100644 index 0000000000..0c07c4f09f --- /dev/null +++ b/test/modm/architecture/fiber/fiber_sync_test.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Niklas Hauser + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#include + +/// Tests the fiber synchronization primitives from outside of a fiber, which +/// must work with and without the fiber scheduler. +/// @ingroup modm_test_test_architecture_fiber +class FiberSyncTest : public unittest::TestSuite +{ +public: + void + testPolling(); + + void + testMutex(); + + void + testSemaphore(); + + void + testLatch(); + + void + testConditionVariable(); +}; diff --git a/test/modm/architecture/module.lb b/test/modm/architecture/module.lb index 9375025d39..07227f50db 100644 --- a/test/modm/architecture/module.lb +++ b/test/modm/architecture/module.lb @@ -11,12 +11,27 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. +class Fiber(Module): + def init(self, module): + module.name = "fiber" + module.description = "Tests for Fiber Synchronization outside of Fibers" + + def prepare(self, module, options): + module.depends("modm:architecture:fiber") + return True + + def build(self, env): + env.outbasepath = "modm-test/src/modm-test/architecture" + env.copy("fiber") + + def init(module): module.name = ":test:architecture" module.description = "Tests for Architecture Interfaces" def prepare(module, options): + module.add_submodule(Fiber()) module.depends( ":mock:clock", "modm:architecture:accessor", @@ -32,5 +47,5 @@ def prepare(module, options): def build(env): env.outbasepath = "modm-test/src/modm-test/architecture" - env.copy('.') + env.copy('.', ignore=env.ignore_paths("*fiber/*"))