Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
38 changes: 37 additions & 1 deletion src/modm/architecture/interface/fiber.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/processing/fiber/semaphore.hpp>

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()`
Expand All @@ -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.
4 changes: 3 additions & 1 deletion src/modm/architecture/module.lb
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,27 @@

#pragma once

%% if core.startswith("cortex-m")
#include <modm/platform/device.hpp>
%% endif

/// @cond
namespace modm::this_fiber
{

void inline
yield()
{
// do nothing and return
__asm__ volatile ("" ::: "memory");
}

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;
}

Expand Down
12 changes: 10 additions & 2 deletions src/modm/processing/module.lb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions test/config/hosted-no-fiber.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='UTF-8'?>
<library>
<options>
<option name="modm:build:build.path">../../build/generated-unittest/hosted-no-fiber/</option>
<option name="modm:build:unittest.source">../../build/generated-unittest/hosted-no-fiber/modm-test</option>
</options>
<modules>
<module>modm:platform:core</module>
<module>modm:driver:terminal</module>
<!-- Tests the fiber API without the scheduler of modm:processing:fiber -->
<module>modm-test:test:architecture:fiber</module>
</modules>
</library>
109 changes: 109 additions & 0 deletions test/modm/architecture/fiber/fiber_sync_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <modm/processing/fiber.hpp>
#include <modm/processing/fiber/mutex.hpp>
#include <modm/processing/fiber/shared_mutex.hpp>
#include <modm/processing/fiber/semaphore.hpp>
#include <modm/processing/fiber/latch.hpp>
#include <modm/processing/fiber/condition_variable.hpp>

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();
}
34 changes: 34 additions & 0 deletions test/modm/architecture/fiber/fiber_sync_test.hpp
Original file line number Diff line number Diff line change
@@ -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 <unittest/testsuite.hpp>

/// 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();
};
17 changes: 16 additions & 1 deletion test/modm/architecture/module.lb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/*"))

Loading