Real Time For the Masses
Конкурентный фреймворк для создания систем реального времени
Введение
Эта книга содержит документацию уровня пользователя фреймворком Real Time For the Masses (RTFM). Описание API можно найти здесь.
Возможности
-
Задачи - единица конкуренции 1. Задачи могут запускаться по событию (в ответ на асинхронный стимул) или вызываться программно по желанию.
-
Передача сообщений между задачами. А именно, сообщения можно передавать программным задачам в момент вызова.
-
Очередь таймера 2. Программные задачи можно планировать на запуск в определенный момент в будущем. Это свойство можно использовать, чтобы реализовывать периодические задачи.
-
Поддержка приоритетов задач, и таким образом, вытесняющей многозадачности.
-
Эффективное, свободное от гонок данных разделение памяти через хорошо разграниченные критические секции на основе приоритетов 1.
-
Выполнение без взаимной блокировки задач, гарантированное на этапе компиляции. Это более сильная гарантия, чем предоставляемая стандартной абстракцией
Mutex
.
-
Минимальные затраты на диспетчеризацию. Диспетчер задач имеет минимальный след; основная часть работы по диспетчеризации делается аппаратно.
-
Высокоэффективное использование памяти: Все задачи используют общий стек вызовов и нет сильной зависимости от динамического распределителя памяти.
-
Все устройства Cortex-M полностью поддерживаются.
-
Эта модель задач поддается известному анализу методом WCET (наихудшего времени исполнения) и техникам анализа диспетчеризации. (Хотя мы еще не разработали для дружественных инструментов для этого).
Требования
-
Rust 1.31.0+
-
Программы нужно писать используя 2018 edition.
Благодарности
Эта библиотека основана на языке RTFM, созданном Embedded Systems group в Техническом Университете Luleå, под рук. Prof. Per Lindgren.
Ссылки
Eriksson, J., Häggström, F., Aittamaa, S., Kruglyak, A., & Lindgren, P. (2013, June). Real-time for the masses, step 1: Programming API and static priority SRP kernel primitives. In Industrial Embedded Systems (SIES), 2013 8th IEEE International Symposium on (pp. 110-113). IEEE.
Lindgren, P., Fresk, E., Lindner, M., Lindner, A., Pereira, D., & Pinho, L. M. (2016). Abstract timers and their implementation onto the arm cortex-m family of mcus. ACM SIGBED Review, 13(1), 48-53.
Лицензия
Все исходные тексты (включая примеры кода) лицензированы либо под:
- Apache License, Version 2.0 (LICENSE-APACHE или https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
на Ваше усмотрение.
Текст книги лицензирован по условиям лицензий Creative Commons CC-BY-SA v4.0 (LICENSE-CC-BY-SA или https://creativecommons.org/licenses/by-sa/4.0/legalcode).
Contribution
Если вы явно не заявляете иначе, любой взнос, преднамеренно представленный для включения в эту работу, как определено в лицензии Apache-2.0, лицензируется, как указано выше, без каких-либо дополнительных условий.
RTFM в примерах
Эта часть книги представляет фреймворк Real Time For the Masses (RTFM) новым пользователям через примеры с растущей сложностью.
Все примеры в этой книге можно найти в репозитории проекта на GitHub, и большинство примеров можно запустить на эмуляторе QEMU, поэтому никакого специального оборудования не требуется их выполнять.
Чтобы запустить примеры на Вашем ноутбуке / ПК, Вам нужна программа
qemu-system-arm
. Инструкции по настройке окружения для разработки
встраиваемых устройств, в том числе QEMU, Вы можете найти в the embedded Rust book.
The app
attribute
Это наименьшая возможная программа на RTFM:
# #![allow(unused_variables)] #fn main() { //! examples/smallest.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] // panic-handler crate extern crate panic_semihosting; use rtfm::app; #[app(device = lm3s6965)] const APP: () = { #[init] fn init(_: init::Context) {} }; #}
Все программы на RTFM используют атрибут app
(#[app(..)]
). Этот атрибут
нужно применять к const
-элементам, содержащим элементы. Атрибут app
имеет
обязательный аргумент device
, в качестве значения которому передается путь.
Этот путь должен указывать на библиотеку устройства, сгенерированную с помощью
svd2rust
v0.14.x. Атрибут app
развернется в удобную точку входа,
поэтому нет необходимости использовать атрибут cortex_m_rt::entry
.
ОТСТУПЛЕНИЕ: Некоторые из вас удивятся, почему мы используем ключевое слово
const
как модуль, а не правильноеmod
. Причина в том, что использование атрибутов на модулях требует feature gate, который требует ночную сборку. Чтобы заставить RTFM работать на стабильной сборке, мы используем вместо него словоconst
. Когда большая часть макросов 1.2 стабилизируются, мы прейдем отconst
кmod
и в конце концов в атрибуту уровне приложения (#![app]
).
init
Внутри псевдо-модуля атрибут app
ожидает найти функцию инициализации, обозначенную
атрибутом init
. Эта функция должна иметь сигнатуру [unsafe] fn()
.
Эта функция инициализации будет первой частью запускаемого приложения.
Функция init
запустится с отключенными прерываниями и будет иметь эксклюзивный
доступ к периферии Cortex-M и специфичной для устройства периферии через переменные
core
and device
, которые внедряются в область видимости init
атрибутом app
.
Не вся периферия Cortex-M доступна в core
, потому что рантайм RTFM принимает владение
частью из неё -- более подробно см. структуру rtfm::Peripherals
.
Переменные static mut
, определённые в начале init
будут преобразованы
в ссылки &'static mut
с безопасным доступом.
Пример ниже показывает типы переменных core
и device
и
демонстрирует безопасный доступ к переменной static mut
.
# #![allow(unused_variables)] #fn main() { //! examples/init.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init] fn init(c: init::Context) { static mut X: u32 = 0; // Cortex-M peripherals let _core: rtfm::Peripherals = c.core; // Device specific peripherals let _device: lm3s6965::Peripherals = c.device; // Safe access to local `static mut` variable let _x: &'static mut u32 = X; hprintln!("init").unwrap(); debug::exit(debug::EXIT_SUCCESS); } }; #}
Запуск примера напечатает init
в консоли и завершит процесс QEMU.
$ cargo run --example init
init
idle
Функция, помеченная атрибутом idle
может присутствовать в псевдо-модуле
опционально. Эта функция используется как специальная задача ожидания и должна иметь
сигнатуру [unsafe] fn() - > !
.
Когда она присутствует, рантайм запустит задачу idle
после init
. В отличие от
init
, idle
запустится с включенными прерываниями и не может завершиться,
поэтому будет работать бесконечно.
Когда функция idle
определена, рантайм устанавливает бит SLEEPONEXIT, после чего
отправляет микроконтроллер в состояние сна после выполнения init
.
Как и в init
, переменные static mut
будут преобразованы в ссылки &'static mut
с безопасным доступом.
В примере ниже показан запуск idle
после init
.
# #![allow(unused_variables)] #fn main() { //! examples/idle.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init] fn init(_: init::Context) { hprintln!("init").unwrap(); } #[idle] fn idle(_: idle::Context) -> ! { static mut X: u32 = 0; // Safe access to local `static mut` variable let _x: &'static mut u32 = X; hprintln!("idle").unwrap(); debug::exit(debug::EXIT_SUCCESS); loop {} } }; #}
$ cargo run --example idle
init
idle
interrupt
/ exception
Как Вы бы сделали с помощью библиотеки cortex-m-rt
, Вы можете использовать атрибуты
interrupt
и exception
внутри псевдо-модуля app
, чтобы определить обработчики
прерываний и исключений. В RTFM, мы называем обработчики прерываний и исключений
аппаратными задачами.
# #![allow(unused_variables)] #fn main() { //! examples/interrupt.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init] fn init(_: init::Context) { // Pends the UART0 interrupt but its handler won't run until *after* // `init` returns because interrupts are disabled rtfm::pend(Interrupt::UART0); hprintln!("init").unwrap(); } #[idle] fn idle(_: idle::Context) -> ! { // interrupts are enabled again; the `UART0` handler runs at this point hprintln!("idle").unwrap(); rtfm::pend(Interrupt::UART0); debug::exit(debug::EXIT_SUCCESS); loop {} } #[interrupt] fn UART0(_: UART0::Context) { static mut TIMES: u32 = 0; // Safe access to local `static mut` variable *TIMES += 1; hprintln!( "UART0 called {} time{}", *TIMES, if *TIMES > 1 { "s" } else { "" } ) .unwrap(); } }; #}
$ cargo run --example interrupt
init
UART0 called 1 time
idle
UART0 called 2 times
До сих пор программы RTFM, которые мы видели не отличались от программ, которые
можно написать, используя только библиотеку cortex-m-rt
. В следующем разделе
мы начнем знакомиться с функционалом, присущим только RTFM.
Ресурсы
Одно из ограничений атрибутов, предоставляемых библиотекой cortex-m-rt
является
то, что совместное использование данных (или периферии) между прерываниями,
или прерыванием и функцией init
, требуют cortex_m::interrupt::Mutex
, который
всегда требует отключения всех прерываний для доступа к данным. Отключение всех
прерываний не всегда необходимо для безопасности памяти, но компилятор не имеет
достаточно информации, чтобы оптимизировать доступ к разделяемым данным.
Атрибут app
имеет полную картину приложения, поэтому может оптимизировать доступ к
static
-переменным. В RTFM мы обращаемся к static
-переменным, объявленным внутри
псевдо-модуля app
как к ресурсам. Чтобы получить доступ к ресурсу, контекст
(init
, idle
, interrupt
или exception
) должен сначала определить
аргумент resources
в соответствующем атрибуте.
В примере ниже два обработчика прерываний имеют доступ к одному и тому же ресурсу.
Никакого Mutex
в этом случае не требуется, потому что оба обработчика запускаются
с одним приоритетом и никакого вытеснения быть не может.
К ресурсу SHARED
можно получить доступ только из этих двух прерываний.
# #![allow(unused_variables)] #fn main() { //! examples/resource.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { // A resource static mut SHARED: u32 = 0; #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); } #[idle] fn idle(_: idle::Context) -> ! { debug::exit(debug::EXIT_SUCCESS); // error: `SHARED` can't be accessed from this context // SHARED += 1; loop {} } // `SHARED` can be access from this context #[interrupt(resources = [SHARED])] fn UART0(mut c: UART0::Context) { *c.resources.SHARED += 1; hprintln!("UART0: SHARED = {}", c.resources.SHARED).unwrap(); } // `SHARED` can be access from this context #[interrupt(resources = [SHARED])] fn UART1(mut c: UART1::Context) { *c.resources.SHARED += 1; hprintln!("UART1: SHARED = {}", c.resources.SHARED).unwrap(); } }; #}
$ cargo run --example resource
UART0: SHARED = 1
UART1: SHARED = 2
Приоритеты
Приоритет каждого прерывания можно определить в атрибутах interrupt
и exception
.
Невозможно установить приоритет любым другим способом, потому что рантайм
забирает владение прерыванием NVIC
; также невозможно изменить приоритет
обработчика / задачи в рантайме. Благодаря этому ограничению у фреймворка
есть знание о статических приоритетах всех обработчиков прерываний и исключений.
Прерывания и исключения могут иметь приоритеты в интервале 1..=(1 << NVIC_PRIO_BITS)
,
где NVIC_PRIO_BITS
- константа, определённая в библиотеке device
.
Задача idle
имеет приоритет 0
, наименьший.
Ресурсы, совместно используемые обработчиками, работающими на разных приоритетах, требуют критических секций для безопасности памяти. Фреймворк проверяет, что критические секции используются, но только где необходимы: например, критические секции не нужны для обработчика с наивысшим приоритетом, имеющим доступ к ресурсу.
API критической секции, предоставляемое фреймворком RTFM (см. Mutex
),
основано на динамических приоритетах вместо отключения прерываний. Из этого следует,
что критические секции не будут допускать запуск некоторых обработчиков,
включая все соперничающие за ресурс, но будут позволять запуск обработчиков с
большим приоритетом не соперничащих за ресурс.
В примере ниже у нас есть 3 обработчика прерываний с приоритетами от одного
до трех. Два обработчика с низким приоритетом соперничают за ресурс SHARED
.
Обработчик с низшим приоритетом должен заблокировать (lock
) ресурс
SHARED
, чтобы получить доступ к его данным, в то время как обработчик со
средним приоритетом может напрямую получать доступ к его данным. Обработчик
с наивысшим приоритетом может свободно вытеснять критическую секцию,
созданную обработчиком с низшим приоритетом.
# #![allow(unused_variables)] #fn main() { //! examples/lock.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { static mut SHARED: u32 = 0; #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::GPIOA); } // when omitted priority is assumed to be `1` #[interrupt(resources = [SHARED])] fn GPIOA(mut c: GPIOA::Context) { hprintln!("A").unwrap(); // the lower priority task requires a critical section to access the data c.resources.SHARED.lock(|shared| { // data can only be modified within this critical section (closure) *shared += 1; // GPIOB will *not* run right now due to the critical section rtfm::pend(Interrupt::GPIOB); hprintln!("B - SHARED = {}", *shared).unwrap(); // GPIOC does not contend for `SHARED` so it's allowed to run now rtfm::pend(Interrupt::GPIOC); }); // critical section is over: GPIOB can now start hprintln!("E").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [SHARED])] fn GPIOB(mut c: GPIOB::Context) { // the higher priority task does *not* need a critical section *c.resources.SHARED += 1; hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap(); } #[interrupt(priority = 3)] fn GPIOC(_: GPIOC::Context) { hprintln!("C").unwrap(); } }; #}
$ cargo run --example lock
A
B - SHARED = 1
C
D - SHARED = 2
E
Поздние ресурсы
В отличие от обычных static
-переменных, к которым должно быть присвоено
начальное значение, ресурсы можно инициализировать в рантайме.
Мы называем ресурсы, инициализируемые в рантайме поздними. Поздние ресурсы
полезны для переноса (как при передаче владения) периферии из init
в
обработчики прерываний и исключений.
Поздние ресурсы определяются как обычные ресурсы, но им присваивается начальное
значение ()
(the unit value). init
должен вернуть начальные значения для
всех поздних ресурсов, упакованные в структуру типа init::LateResources
.
В примере ниже использованы поздние ресурсы, чтобы установить неблокированный,
односторонний канал между обработчиком прерывания UART0
и функцией idle
.
Очередь типа один производитель-один потребитель Queue
использована как канал.
Очередь разделена на элементы потребителя и поизводителя в init
и каждый элемент
расположен в отдельном ресурсе; UART0
владеет ресурсом произодителя, а idle
владеет ресурсом потребителя.
# #![allow(unused_variables)] #fn main() { //! examples/late.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use heapless::{ consts::*, spsc::{Consumer, Producer, Queue}, }; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { // Late resources static mut P: Producer<'static, u32, U4> = (); static mut C: Consumer<'static, u32, U4> = (); #[init] fn init(_: init::Context) -> init::LateResources { // NOTE: we use `Option` here to work around the lack of // a stable `const` constructor static mut Q: Option<Queue<u32, U4>> = None; *Q = Some(Queue::new()); let (p, c) = Q.as_mut().unwrap().split(); // Initialization of late resources init::LateResources { P: p, C: c } } #[idle(resources = [C])] fn idle(c: idle::Context) -> ! { loop { if let Some(byte) = c.resources.C.dequeue() { hprintln!("received message: {}", byte).unwrap(); debug::exit(debug::EXIT_SUCCESS); } else { rtfm::pend(Interrupt::UART0); } } } #[interrupt(resources = [P])] fn UART0(c: UART0::Context) { c.resources.P.enqueue(42).unwrap(); } }; #}
$ cargo run --example late
received message: 42
static
-ресурсы
Переменные типа static
также можно использовать в качестве ресурсов. Задачи
могут получать только (разделяемые) &
ссылки на ресурсы, но блокировки не
нужны для доступа к данным. Вы можете думать о static
-ресурсах как о простых
static
-переменных, которые можно инициализировать в рантайме и иметь лучшие
правила видимости: Вы можете контролировать, какие задачи получают доступ к
переменной, чтобы переменная не была видна всем фунциям в область видимости,
где она была объявлена.
В примере ниже ключ загружен (или создан) в рантайме, а затем использован в двух задачах, запущенных на разных приоритетах.
# #![allow(unused_variables)] #fn main() { //! examples/static.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { static KEY: u32 = (); #[init] fn init(_: init::Context) -> init::LateResources { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); init::LateResources { KEY: 0xdeadbeef } } #[interrupt(resources = [KEY])] fn UART0(c: UART0::Context) { hprintln!("UART0(KEY = {:#x})", c.resources.KEY).unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [KEY])] fn UART1(c: UART1::Context) { hprintln!("UART1(KEY = {:#x})", c.resources.KEY).unwrap(); } }; #}
$ cargo run --example static
UART1(KEY = 0xdeadbeef)
UART0(KEY = 0xdeadbeef)
Программные задачи
RTFM обрабатывает прерывания и исключения как аппаратные задачи. Аппаратные задачи могут вызываться устройством в ответ на события, такие как нажатие кнопки. RTFM также поддерживает программные задачи, порождаемые программой из любого контекста выполнения.
Программным задачам также можно назначать приоритет и диспетчеризовать из
обработчиков прерываний. RTFM требует определения свободных прерываний в блоке
extern
, когда используются программные задачи; эти свободные прерывания будут использованы, чтобы диспетчеризовать программные задачи. Преимущество программных
задач перед аппаратными в том, что на один обработчик прерывания можно назначить
множество задач.
Программные задачи определяются заданием функциям атрибута task
. Чтобы было
возможно вызывать программные задачи, имя задачи нужно передать в аргументе
spawn
контекста атрибута (init
, idle
, interrupt
, etc.).
В примере ниже продемонстрированы три программных задачи, запускаемые на 2-х разных приоритетах. Трем задачам назначены 2 обработчика прерываний.
# #![allow(unused_variables)] #fn main() { //! examples/task.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init(c: init::Context) { c.spawn.foo().unwrap(); } #[task(spawn = [bar, baz])] fn foo(c: foo::Context) { hprintln!("foo").unwrap(); // spawns `bar` onto the task scheduler // `foo` and `bar` have the same priority so `bar` will not run until // after `foo` terminates c.spawn.bar().unwrap(); // spawns `baz` onto the task scheduler // `baz` has higher priority than `foo` so it immediately preempts `foo` c.spawn.baz().unwrap(); } #[task] fn bar(_: bar::Context) { hprintln!("bar").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[task(priority = 2)] fn baz(_: baz::Context) { hprintln!("baz").unwrap(); } // Interrupt handlers used to dispatch software tasks extern "C" { fn UART0(); fn UART1(); } }; #}
$ cargo run --example task
foo
baz
bar
Передача сообщений
Другое преимущество программных задач - возможность передавать сообщения задачам во время их вызова. Тип полезной нагрузки сообщения должен быть определен в сигнатуре обработчика задачи.
Пример ниже демонстрирует три задачи, две из которых ожидают сообщения.
# #![allow(unused_variables)] #fn main() { //! examples/message.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init(c: init::Context) { c.spawn.foo(/* no message */).unwrap(); } #[task(spawn = [bar])] fn foo(c: foo::Context) { static mut COUNT: u32 = 0; hprintln!("foo").unwrap(); c.spawn.bar(*COUNT).unwrap(); *COUNT += 1; } #[task(spawn = [baz])] fn bar(c: bar::Context, x: u32) { hprintln!("bar({})", x).unwrap(); c.spawn.baz(x + 1, x + 2).unwrap(); } #[task(spawn = [foo])] fn baz(c: baz::Context, x: u32, y: u32) { hprintln!("baz({}, {})", x, y).unwrap(); if x + y > 4 { debug::exit(debug::EXIT_SUCCESS); } c.spawn.foo().unwrap(); } extern "C" { fn UART0(); } }; #}
$ cargo run --example message
foo
bar(0)
baz(1, 2)
foo
bar(1)
baz(2, 3)
Ёмкость
Диспетчеры задач не используют динамическое выделение памяти. Память
необходимая для размещения сообщений, резервируется статически. Фреймворк
зарезервирует достаточно памяти для каждого контекста, чтобы можно было вызвать
каждую задачу как минимум единожды. Это разумно по умолчанию, но
"внутреннюю" ёмкость каждой задачи можно контролировать используя аргумент
capacity
атрибута task
.
В примере ниже установлена ёмкость программной задачи foo
на 4. Если ёмкость
не определена, тогда второй вызов spawn.foo
в UART0
вызовет ошибку.
# #![allow(unused_variables)] #fn main() { //! examples/capacity.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::UART0); } #[interrupt(spawn = [foo, bar])] fn UART0(c: UART0::Context) { c.spawn.foo(0).unwrap(); c.spawn.foo(1).unwrap(); c.spawn.foo(2).unwrap(); c.spawn.foo(3).unwrap(); c.spawn.bar().unwrap(); } #[task(capacity = 4)] fn foo(_: foo::Context, x: u32) { hprintln!("foo({})", x).unwrap(); } #[task] fn bar(_: bar::Context) { hprintln!("bar").unwrap(); debug::exit(debug::EXIT_SUCCESS); } // Interrupt handlers used to dispatch software tasks extern "C" { fn UART1(); } }; #}
$ cargo run --example capacity
foo(0)
foo(1)
foo(2)
foo(3)
bar
Очередь таймера
Когда включена опция timer-queue
, фреймворк RTFM включает
глобальную очередь таймера, которую приложения могут использовать, чтобы
планировать программные задачи на запуск через некоторое время в будущем.
Чтобы была возможность планировать программную задачу, имя задачи должно
присутствовать в аргументе schedule
контекста атрибута. Когда задача
планируется, момент (Instant
), в который задачу нужно запустить, нужно передать
как первый аргумент вызова schedule
.
Рантайм RTFM включает монотонный, растущий только вверх, 32-битный таймер,
значение которого можно запросить конструктором Instant::now
. Время (Duration
)
можно передать в Instant::now()
, чтобы получить Instant
в будущем. Монотонный
таймер отключен пока запущен init
, поэтому Instant::now()
всегда возвращает
значение Instant(0 /* циклов тактовой частоты */)
; таймер включается сразу перед
включением прерываний и запуском idle
.
В примере ниже две задачи планируются из init
: foo
и bar
. foo
-
запланирована на запуск через 8 миллионов тактов в будущем. Кроме того, bar
запланирован на запуск через 4 миллиона тактов в будущем. bar
запустится раньше
foo
, т.к. он запланирован на запуск первым.
ВАЖНО: Примеры, использующие API
schedule
или абстракциюInstant
не будут правильно работать на QEMU, потому что функциональность счетчика тактов Cortex-M не реализована вqemu-system-arm
.
# #![allow(unused_variables)] #fn main() { //! examples/schedule.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::hprintln; use rtfm::Instant; // NOTE: does NOT work on QEMU! #[rtfm::app(device = lm3s6965)] const APP: () = { #[init(schedule = [foo, bar])] fn init(c: init::Context) { let now = Instant::now(); hprintln!("init @ {:?}", now).unwrap(); // Schedule `foo` to run 8e6 cycles (clock cycles) in the future c.schedule.foo(now + 8_000_000.cycles()).unwrap(); // Schedule `bar` to run 4e6 cycles in the future c.schedule.bar(now + 4_000_000.cycles()).unwrap(); } #[task] fn foo(_: foo::Context) { hprintln!("foo @ {:?}", Instant::now()).unwrap(); } #[task] fn bar(_: bar::Context) { hprintln!("bar @ {:?}", Instant::now()).unwrap(); } extern "C" { fn UART0(); } }; #}
Запуск программы на реальном оборудовании производит следующий вывод в консоли:
init @ Instant(0)
bar @ Instant(4000236)
foo @ Instant(8000173)
Периодические задачи
Программные задачи имеют доступ к Instant
в момент, когда были запланированы
на запуск через переменную scheduled
. Эта информация и API schedule
могут
быть использованы для реализации периодических задач, как показано в примере ниже.
# #![allow(unused_variables)] #fn main() { //! examples/periodic.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::hprintln; use rtfm::Instant; const PERIOD: u32 = 8_000_000; // NOTE: does NOT work on QEMU! #[rtfm::app(device = lm3s6965)] const APP: () = { #[init(schedule = [foo])] fn init(c: init::Context) { c.schedule.foo(Instant::now() + PERIOD.cycles()).unwrap(); } #[task(schedule = [foo])] fn foo(c: foo::Context) { let now = Instant::now(); hprintln!("foo(scheduled = {:?}, now = {:?})", c.scheduled, now).unwrap(); c.schedule.foo(c.scheduled + PERIOD.cycles()).unwrap(); } extern "C" { fn UART0(); } }; #}
Это вывод, произведенный примером. Заметьте, что есть смещение / колебание нуля
даже если schedule.foo
была вызвана в конце foo
. Использование
Instant::now
вместо scheduled
имело бы влияние на смещение / колебание.
foo(scheduled = Instant(8000000), now = Instant(8000196))
foo(scheduled = Instant(16000000), now = Instant(16000196))
foo(scheduled = Instant(24000000), now = Instant(24000196))
Базовое время
Для задач, планируемых из init
мы имеем точную информацию о их планируемом
(scheduled
) времени. Для аппаратных задач нет scheduled
времени, потому
что эти задачи асинхронны по природе. Для аппаратных задач рантайм предоставляет
время старта (start
), которе отражает время, в которое обработчик прерывания
был запущен.
Заметьте, что start
не равен времени возникновения события, вызвавшего
задачу. В зависимости от приоритета задачи и загрузки системы время
start
может быть сильно отдалено от времени возникновения события.
Какое по Вашему мнению будет значение scheduled
для программных задач которые
вызываются, вместо того чтобы планироваться? Ответ в том, что вызываемые
задачи наследуют базовое время контекста, в котором вызваны. Бызовым для
аппаратных задач является start
, базовым для программных задач - scheduled
и базовым для init
- start = Instant(0)
. idle
на сомом деле не имеет
базового времени но задачи, вызванные из него будут использовать Instant::now()
как их базовое время.
Пример ниже демонстрирует разное значение базового времени.
# #![allow(unused_variables)] #fn main() { //! examples/baseline.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; // NOTE: does NOT properly work on QEMU #[rtfm::app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init(c: init::Context) { hprintln!("init(baseline = {:?})", c.start).unwrap(); // `foo` inherits the baseline of `init`: `Instant(0)` c.spawn.foo().unwrap(); } #[task(schedule = [foo])] fn foo(c: foo::Context) { static mut ONCE: bool = true; hprintln!("foo(baseline = {:?})", c.scheduled).unwrap(); if *ONCE { *ONCE = false; rtfm::pend(Interrupt::UART0); } else { debug::exit(debug::EXIT_SUCCESS); } } #[interrupt(spawn = [foo])] fn UART0(c: UART0::Context) { hprintln!("UART0(baseline = {:?})", c.start).unwrap(); // `foo` inherits the baseline of `UART0`: its `start` time c.spawn.foo().unwrap(); } extern "C" { fn UART1(); } }; #}
Запуск программы на реальном оборудовании произведет следующий вывод в консоли:
init(baseline = Instant(0))
foo(baseline = Instant(0))
UART0(baseline = Instant(904))
foo(baseline = Instant(904))
Одиночки
Атрибут app
знает о библиотеке owned-singleton
и её атрибуте Singleton
.
Когда этот атрибут применяется к одному из ресурсов, рантайм производит для Вас
unsafe
инициализацию одиночки, проверяя, что только один экземпляр одиночки
когда-либо создан.
Заметьте, что когда Вы используете атрибут Singleton
, Вым нужно иметь
owned_singleton
в зависимостях.
В примере ниже атрибутом Singleton
аннотирован массив памяти,
а экземпляр одиночки использован как фиксированный по размеру пул памяти
с помощью одной из абстракций alloc-singleton
.
# #![allow(unused_variables)] #fn main() { {{#include ../../../../examples/singleton.rs}} #}
$ cargo run --example singleton
bar(2)
foo(1)
Типы, Send и Sync
Атрибут app
вводит контекст, коллекцию переменных в каждую из функций.
Все эти переменные имеют предсказуемые, неанонимные типы, поэтому Вы можете
писать простые функции, получающие их как аргументы.
Описание API определяет как эти типы эти типы генерируются из входных данных.
Вы можете также сгенерировать документацию для Вашей бинарной библиотеки
(cargo doc --bin <name>
); в документации Вы найдете структуры Context
(например init::Context
и idle::Context
), чьи поля представляют переменные
включенные в каждую функцию.
В примере ниже сгенерированы разные типы с помощью атрибута app
.
# #![allow(unused_variables)] #fn main() { //! examples/types.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::debug; use rtfm::{Exclusive, Instant}; #[rtfm::app(device = lm3s6965)] const APP: () = { static mut SHARED: u32 = 0; #[init(schedule = [foo], spawn = [foo])] fn init(c: init::Context) { let _: Instant = c.start; let _: rtfm::Peripherals = c.core; let _: lm3s6965::Peripherals = c.device; let _: init::Schedule = c.schedule; let _: init::Spawn = c.spawn; debug::exit(debug::EXIT_SUCCESS); } #[exception(schedule = [foo], spawn = [foo])] fn SVCall(c: SVCall::Context) { let _: Instant = c.start; let _: SVCall::Schedule = c.schedule; let _: SVCall::Spawn = c.spawn; } #[interrupt(resources = [SHARED], schedule = [foo], spawn = [foo])] fn UART0(c: UART0::Context) { let _: Instant = c.start; let _: resources::SHARED = c.resources.SHARED; let _: UART0::Schedule = c.schedule; let _: UART0::Spawn = c.spawn; } #[task(priority = 2, resources = [SHARED], schedule = [foo], spawn = [foo])] fn foo(c: foo::Context) { let _: Instant = c.scheduled; let _: Exclusive<u32> = c.resources.SHARED; let _: foo::Resources = c.resources; let _: foo::Schedule = c.schedule; let _: foo::Spawn = c.spawn; } extern "C" { fn UART1(); } }; #}
Send
Send
- маркерный типаж (trait) для "типов, которые можно передавать через границы
потоков", как это определено в core
. В контексте RTFM типаж Send
необходим
только там, где возможна передача значения между задачами, запускаемыми на
разных приоритетах. Это возникает в нескольких случаях: при передаче сообщений,
в совместно используемых static mut
ресурсах и инициализации поздних ресурсов.
Атрибут app
проверит, что Send
реализован, где необходимо, поэтому Вам не
стоит волноваться об этом. Более важно знать, где Вам не нужен типаж Send
:
в типах, передаваемых между задачами с одинаковым приоритетом. Это возникает
в двух случаях: при передаче сообщений и в совместно используемых static mut
ресурсах.
В примере ниже показано, где можно использовать типы, не реализующие Send
.
# #![allow(unused_variables)] #fn main() { //! `examples/not-send.rs` #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_halt; use core::marker::PhantomData; use cortex_m_semihosting::debug; use rtfm::app; pub struct NotSend { _0: PhantomData<*const ()>, } #[app(device = lm3s6965)] const APP: () = { static mut SHARED: Option<NotSend> = None; #[init(spawn = [baz, quux])] fn init(c: init::Context) { c.spawn.baz().unwrap(); c.spawn.quux().unwrap(); } #[task(spawn = [bar])] fn foo(c: foo::Context) { // scenario 1: message passed to task that runs at the same priority c.spawn.bar(NotSend { _0: PhantomData }).ok(); } #[task] fn bar(_: bar::Context, _x: NotSend) { // scenario 1 } #[task(priority = 2, resources = [SHARED])] fn baz(mut c: baz::Context) { // scenario 2: resource shared between tasks that run at the same priority *c.resources.SHARED = Some(NotSend { _0: PhantomData }); } #[task(priority = 2, resources = [SHARED])] fn quux(mut c: quux::Context) { // scenario 2 let _not_send = c.resources.SHARED.take().unwrap(); debug::exit(debug::EXIT_SUCCESS); } extern "C" { fn UART0(); fn UART1(); } }; #}
Sync
Похожая ситуация, Sync
- маркерный типаж для "типов, на которых можно
ссылаться в разных потоках", как это определено в core
. В контексте RTFM
типаж Sync
необходим только там, где возможны две или более задачи,
запускаемые на разных приоритетах, чтобы захватить разделяемую ссылку на
ресурс. Это возникает только совместно используемых static
-ресурсах.
Атрибут app
проверит, что Sync
реализован, где необходимо, но важно знать,
где ограничение Sync
не требуется: в static
-ресурсах, разделяемых между
задачами с одинаковым приоритетом.
В примере ниже показано, где можно использовать типы, не реализующие Sync
.
# #![allow(unused_variables)] #fn main() { //! `examples/not-sync.rs` #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_halt; use core::marker::PhantomData; use cortex_m_semihosting::debug; pub struct NotSync { _0: PhantomData<*const ()>, } #[rtfm::app(device = lm3s6965)] const APP: () = { static SHARED: NotSync = NotSync { _0: PhantomData }; #[init] fn init(_: init::Context) { debug::exit(debug::EXIT_SUCCESS); } #[task(resources = [SHARED])] fn foo(c: foo::Context) { let _: &NotSync = c.resources.SHARED; } #[task(resources = [SHARED])] fn bar(c: bar::Context) { let _: &NotSync = c.resources.SHARED; } extern "C" { fn UART0(); } }; #}
Создание нового проекта
Теперь, когда Вы изучили основные возможности фреймворка RTFM, Вы можете попробовать его использовать на Вашем оборудовании следуя этим инструкциям.
- Создайте экземпляр из шаблона
cortex-m-quickstart
.
$ # например используя `cargo-generate`
$ cargo generate \
--git https://github.com/rust-embedded/cortex-m-quickstart \
--name app
$ # следуйте остальным инструкциям
- Добавьте крейт устройства, сгенерированный с помощью
svd2rust
v0.14.x, или библиотеку отладочной платы, у которой в зависимостях одно из устройств. Убедитесь, что опцияrt
крейта включена.
В этом примере я покажу использование крейта устройства lm3s6965
.
Эта библиотека не имеет Cargo-опции rt
; эта опция всегда включена.
Этот крейт устройства предоставляет линковочный скрипт с макетом памяти
целевого устройства, поэтому memory.x
и build.rs
не нужно удалять.
$ cargo add lm3s6965 --vers 0.1.3
$ rm memory.x build.rs
- Добавьте библиотеку
cortex-m-rtfm
как зависимость, и если необходимо, включите опциюtimer-queue
.
$ cargo add cortex-m-rtfm --allow-prerelease --upgrade=none
- Напишите программу RTFM.
Здесь я буду использовать пример init
из библиотеки cortex-m-rtfm
.
$ curl \
-L https://github.com/japaric/cortex-m-rtfm/raw/v0.4.0-beta.1/examples/init.rs \
> src/main.rs
Этот пример зависит от библиотеки panic-semihosting
:
$ cargo add panic-semihosting
- Соберите его, загрузите в микроконтроллер и запустите.
$ # ПРИМЕЧАНИЕ: Я раскомментировал опцию `runner` в `.cargo/config`
$ cargo run
init
Советы и хитрости
Обобщенное программирование (Generics)
Ресурсы, совместно используемые двумя или более задачами, реализуют трейт Mutex
во всех контекстах, даже в тех, где для доступа к данным не требуются
критические секции. Это позволяет легко писать обобщенный код оперирующий
ресурсами, который можно вызывать из различных задач. Вот такой пример:
# #![allow(unused_variables)] #fn main() { //! examples/generics.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; use rtfm::Mutex; #[rtfm::app(device = lm3s6965)] const APP: () = { static mut SHARED: u32 = 0; #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); } #[interrupt(resources = [SHARED])] fn UART0(c: UART0::Context) { static mut STATE: u32 = 0; hprintln!("UART0(STATE = {})", *STATE).unwrap(); advance(STATE, c.resources.SHARED); rtfm::pend(Interrupt::UART1); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [SHARED])] fn UART1(mut c: UART1::Context) { static mut STATE: u32 = 0; hprintln!("UART1(STATE = {})", *STATE).unwrap(); // just to show that `SHARED` can be accessed directly and .. *c.resources.SHARED += 0; // .. also through a (no-op) `lock` c.resources.SHARED.lock(|shared| *shared += 0); advance(STATE, c.resources.SHARED); } }; fn advance(state: &mut u32, mut shared: impl Mutex<T = u32>) { *state += 1; let (old, new) = shared.lock(|shared| { let old = *shared; *shared += *state; (old, *shared) }); hprintln!("SHARED: {} -> {}", old, new).unwrap(); } #}
$ cargo run --example generics
UART1(STATE = 0)
SHARED: 0 -> 1
UART0(STATE = 0)
SHARED: 1 -> 2
UART1(STATE = 1)
SHARED: 2 -> 4
Это также позволяет Вам изменять статические приоритеты задач без
переписывания кода. Если Вы единообразно используете lock
-и для доступа
к данным в разделяемых ресурсах, тогда Ваш код продолжит компилироваться,
когда Вы измените приоритет задач.
Запуск задач из ОЗУ
Главной целью переноса описания программы на RTFM в атрибуты в
RTFM v0.4.x была возможность взаимодействия с другими атрибутами.
Напримерe, атрибут link_section
можно применять к задачам, чтобы разместить
их в ОЗУ; это может улучшить производительность в некоторых случаях.
ВАЖНО: Обычно атрибуты
link_section
,export_name
иno_mangle
очень мощные, но их легко использовать неправильно. Неверное использование любого из этих атрибутов может вызвать неопределенное поведение; Вам следует всегда предпочитать использование безопасных, высокоуровневых атрибутов вокруг них, таких как атрибутыinterrupt
иexception
изcortex-m-rt
.В особых случаях функций RAM нет безопасной абстракции в
cortex-m-rt
v0.6.5 но создано RFC для добавления атрибутаramfunc
в будущем релизе.
В примере ниже показано как разместить высокоприоритетную задачу bar
в ОЗУ.
# #![allow(unused_variables)] #fn main() { //! examples/ramfunc.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; #[rtfm::app(device = lm3s6965)] const APP: () = { #[init(spawn = [bar])] fn init(c: init::Context) { c.spawn.bar().unwrap(); } #[inline(never)] #[task] fn foo(_: foo::Context) { hprintln!("foo").unwrap(); debug::exit(debug::EXIT_SUCCESS); } // run this task from RAM #[inline(never)] #[link_section = ".data.bar"] #[task(priority = 2, spawn = [foo])] fn bar(c: bar::Context) { c.spawn.foo().unwrap(); } extern "C" { fn UART0(); // run the task dispatcher from RAM #[link_section = ".data.UART1"] fn UART1(); } }; #}
Запуск этой программы произведет ожидаемый вывод.
$ cargo run --example ramfunc
foo
Можно посмотреть на вывод cargo-nm
, чтобы убедиться, что bar
расположен в ОЗУ
(0x2000_0000
), тогда как foo
расположен во Flash (0x0000_0000
).
$ cargo nm --example ramfunc --release | grep ' foo::'
20000100 B foo::FREE_QUEUE::ujkptet2nfdw5t20
200000dc B foo::INPUTS::thvubs85b91dg365
000002c6 T foo::sidaht420cg1mcm8
$ cargo nm --example ramfunc --release | grep ' bar::'
20000100 B bar::FREE_QUEUE::lk14244m263eivix
200000dc B bar::INPUTS::mi89534s44r1mnj1
20000000 T bar::ns9009yhw2dc2y25
binds
ПРИМЕЧАНИЕ: Требуется RTFM не ниже 0.4.2
Вы можете давать аппаратным задачам имена похожие на имена обычных задач.
Для этого нужно использовать аргумент binds
: Вы называете функцию
по своему желанию и назначаете ей прерывание / исключение
через аргумент binds
. Spawn
и другие служебные типы будут размещены в модуле,
названном в соответствии с названием функции, а не прерывания / исключения.
Давайте посмотрим пример:
# #![allow(unused_variables)] #fn main() { //! examples/binds.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; // `examples/interrupt.rs` rewritten to use `binds` #[rtfm::app(device = lm3s6965)] const APP: () = { #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::UART0); hprintln!("init").unwrap(); } #[idle] fn idle(_: idle::Context) -> ! { hprintln!("idle").unwrap(); rtfm::pend(Interrupt::UART0); debug::exit(debug::EXIT_SUCCESS); loop {} } #[interrupt(binds = UART0)] fn foo(_: foo::Context) { static mut TIMES: u32 = 0; *TIMES += 1; hprintln!( "foo called {} time{}", *TIMES, if *TIMES > 1 { "s" } else { "" } ) .unwrap(); } }; #}
$ cargo run --example binds
init
foo called 1 time
idle
foo called 2 times
Под капотом
В этом разделе описывабтся внутренности фркймворка на высоком уровне.
Низкоуровневые тонкости, такие как парсинг и кодогенерация производимые
процедурным макросом (#[app]
) здесь объясняться не будут. Мы сосредоточимся
на анализе пользовательской спецификации и структурах данных, используемых
рантаймом.
Ceiling analysis
TODO
Task dispatcher
TODO
Timer queue
TODO