Note about rust support in the project
- Rust samples: avrtos-examples
AVRTOS is a real-time operating system (RTOS) crafted for 8-bit AVR microcontrollers. It aims to provide an efficient and highly configurable RTOS solution for AVR-based systems. Fully C/C++ compliant, AVRTOS is compatible with the AVR-GCC toolchain, Arduino and PlatformIO frameworks, it can even be emulated using QEMU.
AVRTOS has been successfully tested on the following AVR architectures:
- AVR5, particularly ATmega328p
- AVR6, especially ATmega2560
Please bear in mind that as a personal project, it doesn't come with any guarantees. I hope you find AVRTOS as exciting to use as I found it to develop !
Please refer to developer.md for troubleshooting and development notes.
AVRTOS offers an extensive list of features, including:
- Cooperative and preemptive threads
- Naive scheduler without priority support
- Configurable system clock with support for all hardware timers (e.g. 0-2 for ATmega328p and 0-5 for ATmega2560)
- Synchronization objects like mutexes, semaphores, workqueues (+delayables), FIFOs, message queues, memory slabs, flags, signals
- Drivers for UART, timers, GPIO, SPI, I2C and external interrupts
- Devices drivers for TCN75, MCP2515
- Thread sleep with up to 65-second duration in simple mode (extendable using high-precision time objects)
- Scheduler lock/unlock to temporarily prevent preemption for preemptive threads
- Thread switching from interrupts
- Runtime creation for many kernel objects (threads, mutexes, semaphores, workqueues, fifos, memory slabs, ...)
- Diagnostics: Thread canaries and sentinel stack protection
- Events and timers
- Atomic API for 8-bit variables
- Macro-based logging subsystem
- Uptime API
- Data structures: singly and doubly linked lists, queues, ring buffers, timeout queue
Additional Features:
- Thread naming with symbols (e.g., 'M' for the main thread, 'I' for the idle thread)
- Pseudorandom number generator (LFSR)
- Debugging and utility functions (RAM_DUMP, CORE_DUMP, z_read_ra)
- Kernel assertions (__ASSERT)
- Custom error codes (e.g., EAGAIN, EINVAL, EIO, ENOMEM, ...)
- Thread safe termination (excluding main thread)
stdout
redirection to USART0- Various wait variants (e.g., k_sleep, k_wait with modes IDLE, ACTIVE, BLOCK and z_cpu_block_us)
- Reset reason detection
- Dockerfile and Jenkinsfile templates for CI/CD
- Full compatibility with QEMU emulation
Planned Features (TODOs):
- Comprehensive tests
- Enhanced documentation based on
mkdocs
(anddoxygen
??) - Ultra-low duration sleep (e.g., 18us) using a dedicated timer counter with API:
uscounter_init()
,uscounter_get()
,uscounter_set
- Functions like sys_le32_read/write
- Utilizing sysclock for thread wake-up granularity (k_sleep) instead of timeslice
- Option to select SYSCLOCK or TIMESLICE as the scheduling point (
KERNEL_SCHEDULING_EVENT
) - Note on using
k_busy_wait(K_USEC(20u))
for precise short-duration waits
- Option to select SYSCLOCK or TIMESLICE as the scheduling point (
- Thread priority implementation
- Per-thread CPU usage statistics
- Polling mechanisms
- ADC drivers
- Memory heap management
- Runtime detection of available space for the main thread stack (init)
- Consideration of available heap space
- Refactoring: Move or remove unused
AVRTOS_VERSION_MAJOR
variables - Organize MCU specific fixups and board-specific items in a dedicated "board" directory
- Relocation of assembly files to "arch/avr"
- Consolidation of private defines in _private.h headers
- Investigation of high metric readings
- Tickless kernel + timeslicing features
- Renaming "static inline" functions to "__always_inline"
- Implementation of builtin_ctz for 8-bit variables
- Removal of outdated samples
- Tutorial
- Kconfig to configure the kernel and generate the configuration file
- make the kernel ISR aware with a dedicated ISR stack (can IDLE thread be reused?)
- Sample for discovering the I2C bus
- Doubly linked list implementation for tqueue for optimized removal
- Set
CONFIG_STDIO_USART=0
by default
Example minimal-example configures an usart, and blink a led at a frequency of 1Hz. Morover, typing a character on the serial console will wake up a thread which will print the received character.
Configuration:
CONFIG_KERNEL_COOPERATIVE_THREADS=1
CONFIG_KERNEL_TIME_SLICE_US=1000
CONFIG_INTERRUPT_POLICY=1
CONFIG_KERNEL_THREAD_TERMINATION_TYPE=-1
CONFIG_THREAD_MAIN_COOPERATIVE=1
CONFIG_STDIO_USART=0
CONFIG_KERNEL_UPTIME=1
Code:
/*
* Copyright (c) 2022 Lucas Dietrich <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <avrtos/avrtos.h>
#include <avrtos/debug.h>
#include <avrtos/drivers/gpio.h>
#include <avrtos/drivers/usart.h>
#include <avrtos/logging.h>
#include <avrtos/misc/led.h>
#define LOG_LEVEL LOG_LEVEL_DBG
K_MSGQ_DEFINE(usart_msgq, 1u, 16u);
static void thread_usart(void *arg);
static void thread_led(void *arg);
K_THREAD_DEFINE(th_usart, thread_usart, 164u, K_COOPERATIVE, NULL, 'X');
K_THREAD_DEFINE(th_led, thread_led, 164u, K_COOPERATIVE, NULL, 'L');
ISR(USART0_RX_vect)
{
const char c = USART0_DEVICE->UDRn;
k_msgq_put(&usart_msgq, &c, K_NO_WAIT);
}
int main(void)
{
const struct usart_config usart_config = {
.baudrate = USART_BAUD_115200,
.receiver = 1u,
.transmitter = 1u,
.mode = USART_MODE_ASYNCHRONOUS,
.parity = USART_PARITY_NONE,
.stopbits = USART_STOP_BITS_1,
.databits = USART_DATA_BITS_8,
.speed_mode = USART_SPEED_MODE_NORMAL,
};
ll_usart_init(USART0_DEVICE, &usart_config);
ll_usart_enable_rx_isr(USART0_DEVICE);
led_init();
LOG_INF("Application started");
k_thread_dump_all();
k_abort();
}
static void thread_usart(void *arg)
{
char c;
for (;;) {
if (k_msgq_get(&usart_msgq, &c, K_FOREVER) >= 0) {
k_show_uptime();
LOG_INF("<inf> Received: %c", c);
}
}
}
static void thread_led(void *arg)
{
for (;;) {
k_show_uptime();
led_toggle();
LOG_DBG("<dbg> toggled LED");
k_sleep(K_MSEC(500u));
}
}
Build and flash using PlatformIO. Expected output on the serial console:
AVRTOS is highly configurable, file src/avrtos/avrtos_conf.h lists all the default configuration options.
For arduino framework, a dedicated configuration file is provided in src/avrtos/avrtos_conf_arduino.h.
Several build systems are supported:
- Cmake
- Arduino IDE
- PlatformIO (with and without Arduino Framework)
Install the Arduino IDE (1 or 2) from arduino.cc.
Copy complete AVRTOS
folder to your Arduino libraries folder.
Open the Arduino IDE and select your board/port.
Select the sample :
Build and upload the sample.
PlatformIO extension for VSCode is recommended (platformio.platformio-ide
).
Simply select your sample and build it.
You can clone this repository in your project's lib
folder, a typical platformio.ini
file would look like this:
[env]
platform = atmelavr
board = pro16MHzatmega328
; board = megaatmega2560
upload_port = COM3
monitor_port = COM3
monitor_speed = 115200
build_src_filter =
+<AVRTOS/src/>
build_flags =
-Wl,-T./avrtos-avr5.xn
; -Wl,-T./avrtos-avr6.xn
-DCONFIG_THREAD_MAIN_STACK_SIZE=256
-DCONFIG_THREAD_EXPLICIT_MAIN_STACK=0
-DCONFIG_THREAD_MAIN_COOPERATIVE=1
-DCONFIG_KERNEL_SYSCLOCK_PERIOD_US=1000
-DCONFIG_KERNEL_TIME_SLICE_US=1000
; other options ...
An example project which uses AVRTOS as a dependency can be found here: github.com/lucasdietrich/caniot-device
To build the samples with cmake, you'll need to have avr-gcc
, avr-gcc-c++
,
avr-libc
, avr-binutils
, cmake
, make
, ninja-build
installed,
avr-gdb
, avrdude
and qemu-system-avr
are optional
To configure your environnement for an Arduino Mega 2560, run the following commands:
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE="cmake/avr6-atmega2560.cmake"
You can also specify the generator :
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE="cmake/avr6-atmega2560.cmake" \
-DCMAKE_GENERATOR="Unix Makefiles"
You can also provide the device where the program will be flashed, this enables
make commands like make upload_sample_program
:
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE="cmake/avr6-atmega2560.cmake" \
-DPROG_DEV=/dev/ttyACM0
To build the minimal_example
program, run the following command (Replace ninja
with make
if you specified the Unix Makefiles
generator) :
make -C build sample_minimal_example
To flash the binary to your device, run the following command:
make -C build upload_sample_minimal_example
Monitor the serial console with miniterm
:
BAUDRATE=115200 make monitor
In case you have a custom target (e.g. a board not explicitly supported by AVRTOS),
you can create a custom avr<version>-<target>.cmake
file and specify it with the CMAKE_TOOLCHAIN_FILE
option.
A toolchain file has the following structure:
set(F_CPU 16000000UL)
set(MCU atmega328p)
set(LINKER_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/../architecture/avr/avrtos-avr5-atmega328p.xn)
set(QEMU_MCU uno)
set(PROG_TYPE wiring) # arduino
set(PROG_PARTNO m328p)
include(${CMAKE_CURRENT_LIST_DIR}/avr.cmake)
Option | Description |
---|---|
F_CPU |
Defines the CPU clock frequency. |
MCU |
Specifies the target microcontroller unit (MCU) as ATmega328P. See list https://www.nongnu.org/avr-libc/user-manual/using_tools.html |
LINKER_SCRIPT |
Sets the linker script path for the project. Linker scripts location: architecture/avr |
QEMU_MCU (QEMU only) |
Specifies the target microcontroller unit (MCU) for QEMU simulation as "uno." Can be listed with command qemu-system-avr -machine help |
PROG_TYPE (real board only) |
Sets the programming type to "wiring." Can be listed with command avrdude -c help |
PROG_PARTNO m328p |
Specifies the target microcontroller part number as "m328p." Can be listed with command avrdude -c ${PROG_TYPE} -p help |
FEATURE_TIMER_COUNT |
Sets the number of 16-bit timers available on the target. |
FEATURE_UART_COUNT |
Sets the number of UARTs available on the target. |
include(...) |
Includes the AVR architecture generic cmake file |
In order to describe a custom target with PlatformIO, please refer to the
example platformio.ini
file from section Use AVRTOS as a library dependency in your project.
Note about AVR support in QEMU: https://qemu-project.gitlab.io/qemu/system/target-avr.html You will have limited support for peripherals: all UART are supported, however only 16-bit timers are supported.
Moreover you'll need to apply the following patch to QEMU (<= 8.0.2): scripts/patches/0001-Fix-handling-of-AVR-interrupts-above-33-by-switching.patch to have all 16-bit timers working.
In case you want to emulate you program with QEMU:
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE="cmake/avr6-atmega2560.cmake" \
-DCMAKE_BUILD_TYPE=Debug \
-DQEMU=ON
This enables targets like run_sample_*
and qemu_sample_*
.
For example, to run the minimal-example
program in QEMU, run the following command:
ninja -C build run_sample_minimal_example
To exit QEMU, press Ctrl+A
and then x
.
If you want to debug your program with QEMU, run the following command:
ninja -C build qemu_sample_minimal_example
With VS Code, select QEMU (avr)
configuration, then press F5
to start debugging (launch.json
configuration is automatically generated during build).
The Makefile
at the root of the project provides shortcut commands to build
and flash samples.
In order to build the sample shell
for QEMU run:
QEMU=ON SAMPLE=shell make
Run it with:
make run_qemu
Debug it with:
make qemu
In order to build the sample drv-timer
for a custom target run:
TOOLCHAIN_FILE="cmake/avr5-board-caniot-tiny-pb.cmake" SAMPLE="drv-timer" make single
Upload it with:
make upload
Monitor it with:
make monitor
Two Dockerfile
are provided to build the project in a container (based on Fedora)
- scripts/Dockerfile-base is used as a base image for Jenkins (Devops)
scripts/Dockerfile-run is used to build the project directly(deprecated)
Build the container with:
docker build -t fedora-avr-toolchain -f scripts/Dockerfile-base .
Run the container with (Z
flag is required with SELinux)
docker run -it --rm -v $(pwd):/avrtos:Z fedora-avr-toolchain
Build the project within it:
cd /avrtos
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE="cmake/avr6-atmega2560.cmake" \
-DCMAKE_BUILD_TYPE=Debug \
-DQEMU=ON \
-G="Ninja"
ninja -C build sample_minimal_example
Run the sample in QEMU:
ninja -C build run_sample_minimal_example
A Jenkinsfile is provided to build the project in a Jenkins (multibranch) pipeline.
It is based on the previous Docker container: devops/fedora-avr-toolchain
Rust support is currently in development, the goal is to provide a Rust API to compile rust programs using AVRTOS.
Major steps are:
- Generate rust bindings for the C API using
bindgen
. - Compile the kernel sources as a static library to link with the rust program, this will take place in the
avrtos-sys
crate. - Provide an rust idiomatic API to use AVRTOS, this will take place in the
avrtos-core
crate. - Provide examples to demonstrate the usage of AVRTOS in Rust using a real board and QEMU, this will take place in the
avrtos-examples
crate. - Extend the support to other boards (e.g. ATmega328p).
External (resources):
Note about current development:
- Prefer
--release
build over debug. - Only
atmega2560
is supported - src/avrtos/rust_helpers.c servers development purposes but should behave as a bridge between C and avrtos-core in the future.
- For rust support a very recent version of the AVR compiler is required (14.2.0), crosstoold-ng has been used to build the AVR toolchain.
A list of examples is provided in the avrtos-examples crate.
Build current examples with:
cargo run --example hello_world --release
cargo run --example serial --release
cargo run --example sleep --release
Feel free to test the binary by configuring the runner
in the .cargo/config.toml file:
- The
atmega2560
withrunner = "ravedude -cb 115200 mega2560"
- QEMU with
runner = "./scripts/qemu-runner.sh"
- QEMU debug with
runner = "./scripts/qemu-debug.sh"
, also start debugging withF5
, see .vscode/launch.json
#![no_std]
#![no_main]
use avrtos::kernel::{Kernel, KernelParams};
use avrtos::{arduino_hal, println};
#[arduino_hal::entry]
fn main() -> ! {
let _kernel = Kernel::init_with_params(KernelParams::default()).unwrap();
println!("Hello, world from rust!");
loop {}
}
use avrtos::duration::Duration;
use avrtos::kernel::{sleep, yeet, Kernel};
use avrtos::thread::Priority;
use avrtos::{arduino_hal, println, thread};
#[arduino_hal::entry]
fn main() -> ! {
let kernel = Kernel::init().unwrap();
let mut counter = 5;
let handle = thread::spawn(0x300, Priority::Cooperative, b'1', move || loop {
println!("loop: {}", counter);
sleep(Duration::from_secs(1));
if counter == 0 {
break;
}
counter -= 1;
})
.unwrap();
handle.join(Duration::FOREVER).unwrap();
println!("done");
loop {
yeet();
}
}
Rust bindings are automatically generated during the build process, see build.rs.
Install bindgen with:
- `cargo install bindgen-cli`
Script rust-avrtos-sys/scripts/bindgen.sh generates the Rust bindings for the C API. It takes care only avrtos
API is generated (mainly structures and functions).
The bindgen targets the atmega2560
cpu variant only, bingen relies on clang which
has a experimental support for AVR. Default configuration constants are provided,
most of the constants are not used by the headers, only constants which enable/disable
a feature are used.
Bindings will end up in rust-avrtos-sys/src/bindings.rs.
TODOs:
- The path to the
avr
headers must be updated to find the system headers automatically. Maybe the script can be transformed into a CMakelist to generate the bindings during the build process. - Move default configuration constants to a dedicated header file: .
The build script rust-avrtos-sys/build.rs compiles the kernel sources as a static library. It uses the cc
crate to compile the sources.
Configuration constants must be correct at this stage, as the sources are compiled.
Linker script is provided in architecture/avr/avrtos-avr6.xn.
The avrtos-core
crate will provide an idiomatic API to use AVRTOS in Rust.
The goal is to be to able most of the avrtos features with a fully safe rust API.
Below are the current issues I'm facing with the Rust support:
I'm not sure, but float calculations seems to be an issue, any use of the
macro K_MSEC
or even calling directly k_sleep
generates issues.
I'm unable to find a correct version for the compiler (see rust-toolchain.toml):
nightly
seems to work but cannot be reproduced.nightly-2024-03-22
which was tested with avr-hal, doesn't work withcc
orbindgen
.- Error is exactly described in this issue (avr-hal: 537)
Following example generates invalid assembly:
#![no_std]
#![no_main]
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
use core::ffi::c_char;
use avrtos_sys::{
serial_init_baud, serial_print
};
use panic_halt as _;
const boot: &'static str = "avrtos starting\n\x00";
#[arduino_hal::entry]
fn main() -> ! {
unsafe {
serial_init_baud(115200);
serial_print(boot.as_ptr() as *const c_char);
}
loop {
}
}
The generated assembly of the main
function is invalid:
000001b2 <main>:
#[arduino_hal::entry]
1b2: 0e 94 db 00 call 0x1b6 ; 0x1b6 <_ZN21loop_invalid_asm20__avr_device_rt_main17h75ecf0a9a15b5945E>
000001b6 <_ZN21loop_invalid_asm20__avr_device_rt_main17h75ecf0a9a15b5945E>:
fn main() -> ! {
1b6: 60 e0 ldi r22, 0x00 ; 0
1b8: 72 ec ldi r23, 0xC2 ; 194
1ba: 81 e0 ldi r24, 0x01 ; 1
1bc: 90 e0 ldi r25, 0x00 ; 0
unsafe {
serial_init_baud(115200);
1be: 0e 94 95 00 call 0x12a ; 0x12a <serial_init_baud>
serial_print(boot.as_ptr() as *const c_char);
1c2: 80 e0 ldi r24, 0x00 ; 0
1c4: 92 e0 ldi r25, 0x02 ; 2
1c6: 0e 94 c6 00 call 0x18c ; 0x18c <serial_print>
}
loop {
1ca: 00 c0 rjmp .+0 ; 0x1cc <__udivmodsi4>
000001cc <__udivmodsi4>:
1cc: a1 e2 ldi r26, 0x21 ; 33
1ce: 1a 2e mov r1, r26
1d0: aa 1b sub r26, r26
1d2: bb 1b sub r27, r27
1d4: fd 01 movw r30, r26
1d6: 0d c0 rjmp .+26 ; 0x1f2 <__udivmodsi4_ep>
Loop consists of a rjmp .+0
instruction, which actually refers to the instruction
at the address following the rjmp
instruction which is the start of the __udivmodsi4
function.
This issue appears in most of infinite loop
I've experienced.
This seems to be solved in LLVM 17: https://github.com/llvm/llvm-project/commit/697a162fa63df328ec9ca334636c5e85390b2bf0
Rust nightly-2025-02-13
does not longer generate invalid assembly.