Skip to content

Fiber Interface

lbuild module: modm:architecture:fiber

This module provides an interface to yield control back to a scheduler. The basic functionality is provided by the yield() function which transparently gives control back to the scheduler and returns afterwards. It is particularly important to yield in long running loops to prevent the system from locking up by preventing other fibers from making progress:

while(true)
{
    // run your code here
    // but always yield to other fibers whenever possible
    modm::this_fiber::yield();
}

For convenience a poll() function is provided that can be used to yield until a condition is met:

modm::this_fiber::poll([&]{ return condition; });

An extension of this concept is provided by the poll_for() and poll_until() functions, which yield until the condition is met or until a timeout occurs:

bool condition_met = modm::this_fiber::poll_for(1s, [&]{ return condition; });
// if (not condition_met) condition did not return true for 1s.

If microseconds are passed for the duration, the functions use the modm::chrono::micro_clock (=modm::PreciseClock), otherwise they use modm::chrono::milli_clock (=modm::Clock). This requires that these clocks are already initialized and running.

These basic building blocks are then used to implement the sleep_for() and sleep_until() convenience functions:

modm::this_fiber::sleep_for(1s);

Implementation

The yield() function is implemented by the modm:processing:fiber module which provides a cooperative multitasking scheduler that is able to switch between multiple fiber contexts.

If yield() is called outside of a fiber context, for example, in the main() function when the scheduler is not yet running, yield() will return in-place. This mechanism allows for a graceful fallback to a blocking API without changes to the code using yield().

modm::Fiber fiber_nonblocking([]
{
    modm::Timeout timeout(100ms);
    timeout.wait(); // non-blocking call!
});
int main()
{
    modm::Timeout timeout(100ms);
    timeout.wait(); // blocking call!

    modm::fiber::Scheduler::run()
    return 0;
}

This mechanism also supports running modm on devices with very small memories where a stackful scheduler may be to resource intensive: The modm:processing:fiber module is strictly opt-in and if not selected the scheduler is not included and the yield() function is implemented as an empty stub while still allowing for the whole API to be used without changes:

modm::Lis3mdl<I2cMaster1> sensor{};
int main()
{
    sensor.configure(); // blocking but works just fine
    while(true)
    {
        modm::Vector3f magVector;
        sensor.readMagnetometer(magVector); // another blocking call
        modm::this_fiber::sleep_for(10ms); // this will wait in place
    }
}

Therefore, if you use these functions in your code, only depend on modm:architecture:fiber, include <modm/architecture/interface/fiber.hpp> and let the user decide on the implementation by including modm:processing:fiber or not. This compromise allows for a seamless transition between different devices and scheduling strategies.

Synchronization without Scheduler

This module also provides synchronization primitives in the <modm/architecture/interface/fiber/*.hpp> headers that work with and without the scheduler. They can then be used to synchronize the main context with interrupts:

#include <modm/architecture/interface/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
    }
}

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() function:

auto id = modm::this_fiber::get_id();
// if (id == 0) called outside a fiber
// else called from inside a fiber

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 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.

Dependencies

modm:architecture:fiber modm_architecture_fiber modm: architecture: fiber modm_architecture_atomic modm: architecture: atomic modm_architecture_fiber->modm_architecture_atomic modm_architecture_clock modm: architecture: clock modm_architecture_fiber->modm_architecture_clock modm_cmsis_device modm: cmsis: device modm_architecture_fiber->modm_cmsis_device modm_stdc++ modm: stdc++ modm_architecture_fiber->modm_stdc++