20 releases (5 stable)
1.3.0 | Sep 19, 2024 |
---|---|
1.2.0 | May 5, 2024 |
1.1.0 | Apr 29, 2024 |
1.0.0-pre.2 | Mar 6, 2024 |
0.1.2 | Apr 29, 2021 |
#86 in Embedded development
199 downloads per month
Used in 4 crates
200KB
2K
SLoC
lilos
This is a wee operating system written to support the async
style of
programming in Rust on microcontrollers. It fits in about 2 kiB of Flash and
uses about 40 bytes of RAM (before your tasks are added). In that space, you get
a full async
runtime with multiple tasks, support for complex concurrency via
join
and select
, and a lot of convenient but simple APIs. (If you want to
see what a lilos
program looks like, look in the examples
directory, or read
the intro guide.)
lilos
has been deployed in real embedded systems since 2019, running
continuously. I've built about a dozen systems around it of varying complexity,
on half a dozen varieties of microcontroller. It works pretty okay! Perhaps you
will find it useful too.
See the repository for example code and getting started instructions, or the
Rustdoc on the top level lilos
module for API details and descriptions of
modules.
lib.rs
:
A simple but powerful async
RTOS based around Rust Future
s.
This provides a lightweight operating environment for running async Rust code on ARM Cortex-M microprocessors, plus some useful doodads and gizmos.
lilos
is deliberately designed to be compact, to avoid the use of proc
macros, to be highly portable to different microcontrollers, and to be as
statically predictable as possible with no dynamic resource allocation.
These are the API docs for the OS. If you'd like a higher-level introduction with worked examples, please have a look at the intro guide!
About the OS
lilos
is designed around the notion of a fixed set of concurrent tasks
that run forever. To use the OS, your application startup routine calls
exec::run_tasks
, giving it an array of tasks you've defined; run_tasks
never returns.
The OS provides cooperative multitasking: while tasks are concurrent, they are not preemptive, and are not "threads" in the traditional sense. Tasks don't even have their own stacks -- they return completely whenever they yield the CPU.
This would be incredibly frustrating to program, were it not for Future
and async
.
Each task co-routine must be a Future
that can be polled but will never
complete (because, remember, tasks run forever). The OS provides an
executor that manages polling of a set of Future
s.
Rust's async
keyword provides a convenient way to have the compiler
rewrite a normal function into a co-routine-style Future
. This means that
writing co-routines to run on this OS looks very much like programming
with threads.
For detailed discussion and some cookbook examples, see either the intro
guide or the examples
directory in the repo.
Feature flags
lilos
currently exposes the following Cargo features for
enabling/disabling portions of the system:
-
systick
(on by default). Enables reliance on the ARM M-profile SysTick timer for portable timekeeping. Disabling makes the executor smaller at the cost of losing alltime
API. On platforms where the SysTick timer stops during sleep, such as Nordic nRF52, you may want to disable this feature and use a different timekeeping mechanism. -
mutex
(on by default). Enables access to themutex
module for blocking access to shared data. Leaving this feature enabled has no cost if you're not actually usingmutex
. -
spsc
(on by default). Enables access to thespsc
module for single-producer single-consumer inter-task queues. Leaving this feature enabled has no cost if you're not actually usingspsc
.
Composition and dynamic behavior
The notion of a fixed set of tasks might seem limiting, but it's more
flexible than you might expect. Because Future
s can be composed, the
fixed set of OS tasks can drive a dynamic set of program Future
s.
For instance, a task can fork into several concurrent routines using macros
like select_biased!
or join!
from the futures
crate.
Concurrency and interrupts
The OS supports the use of interrupt handlers to wake tasks through the
Notify
mechanism, but most OS facilities are not available
in interrupt context.
By default, interrupts are masked when task code is running, so tasks can be
confident that they will preempted if, and only if, they await
.
Each time through the task polling loop, the OS unmasks interrupts to let any pending interrupts run. Because the Cortex-M collects pending interrupts while interrupts are masked, we don't run the risk of missing events.
Interrupts are also unmasked whenever the idle processor is woken from sleep, in order to handle the event that woke it up.
If your application requires tighter interrupt response time, you can
configure the OS at startup to permit preemption of various kinds --
including allowing preemption by only a subset of your interrupts. See the
exec
module for more details and some customization options.
Cancellation
Co-routine tasks in this OS are just Future
s, which means they can be
dropped. Future
s are typically dropped just after they resolve (often just
after an await
keyword in the calling code), but it's also possible to
drop a Future
while it is pending. This can happen explicitly (by calling
drop
), or as a side effect of other operations; for example, the macro
select_biased!
waits for one future to resolve, and then drops the
others, whether they're done or not.
This means it's useful to consider what cancellation means for any particular task, and to ensure that its results are what you intend. There's more about this in The Intro Guide and the technical note on Cancellation.
lilos
itself tries to make it easier for you to handle cancellation in
your programs, by providing APIs that have reasonable behavior on cancel.
All of the core APIs aim for strict cancel-safety, where dropping a future
and retrying the operation that produced it is equivalent to not dropping
the future, in terms of visible side effects. (Obviously doing more work
will take more CPU cycles; that's not what we mean by side effects.)
If some code is useful, but can't achieve strict cancel safety, it should go
in a separate crate. For example, the lilos-handoff
crate used to be
part of the core OS, but was evicted because it can only achieve a weaker
notion of cancel safety.
Any deviations from this principle are considered bugs, and should be reported if you notice them!
Dependencies
~2.5MB
~41K SLoC