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.

Ссылки

1

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.

2

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.

Лицензия

Все исходные тексты (включая примеры кода) лицензированы либо под:

на Ваше усмотрение.

Текст книги лицензирован по условиям лицензий 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

#![no_main]
#![no_std]

use panic_semihosting as _; // panic handler
use rtfm::app;

#[app(device = lm3s6965)]
const APP: () = {};

#}

Все программы на 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]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965, peripherals = true)]
const APP: () = {
    #[init]
    fn init(cx: init::Context) {
        static mut X: u32 = 0;

        // Cortex-M peripherals
        let _core: cortex_m::Peripherals = cx.core;

        // Device specific peripherals
        let _device: lm3s6965::Peripherals = cx.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]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;

#[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() {
{{#include ../../../../examples/interrupt.rs}}
#}
$ cargo run --example interrupt
{{#include ../../../../ci/expected/interrupt.run}}```

До сих пор программы 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]

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        // A resource
        #[init(0)]
        shared: u32,
    }

    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::UART0);
        rtfm::pend(Interrupt::UART1);
    }

    // `shared` cannot be accessed from this context
    #[idle]
    fn idle(_cx: idle::Context) -> ! {
        debug::exit(debug::EXIT_SUCCESS);

        // error: no `resources` field in `idle::Context`
        // _cx.resources.shared += 1;

        loop {}
    }

    // `shared` can be accessed from this context
    #[task(binds = UART0, resources = [shared])]
    fn uart0(cx: uart0::Context) {
        let shared: &mut u32 = cx.resources.shared;
        *shared += 1;

        hprintln!("UART0: shared = {}", shared).unwrap();
    }

    // `shared` can be accessed from this context
    #[task(binds = UART1, resources = [shared])]
    fn uart1(cx: uart1::Context) {
        *cx.resources.shared += 1;

        hprintln!("UART1: shared = {}", cx.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]

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        #[init(0)]
        shared: u32,
    }

    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::GPIOA);
    }

    // when omitted priority is assumed to be `1`
    #[task(binds = GPIOA, 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);
    }

    #[task(binds = GPIOB, priority = 2, resources = [shared])]
    fn gpiob(c: gpiob::Context) {
        // the higher priority task does *not* need a critical section
        *c.resources.shared += 1;

        hprintln!("D - shared = {}", *c.resources.shared).unwrap();
    }

    #[task(binds = GPIOC, 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]

use cortex_m_semihosting::{debug, hprintln};
use heapless::{
    consts::*,
    i,
    spsc::{Consumer, Producer, Queue},
};
use lm3s6965::Interrupt;
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    // Late resources
    struct Resources {
        p: Producer<'static, u32, U4>,
        c: Consumer<'static, u32, U4>,
    }

    #[init]
    fn init(_: init::Context) -> init::LateResources {
        static mut Q: Queue<u32, U4> = Queue(i::Queue::new());

        let (p, c) = Q.split();

        // Initialization of late resources
        init::LateResources { p, 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);
            }
        }
    }

    #[task(binds = UART0, 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() {
{{#include ../../../../examples/static.rs}}
#}
$ cargo run --example static
{{#include ../../../../ci/expected/static.run}}```

Программные задачи

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]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;

#[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 - start").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();

        hprintln!("foo - middle").unwrap();

        // spawns `baz` onto the task scheduler
        // `baz` has higher priority than `foo` so it immediately preempts `foo`
        c.spawn.baz().unwrap();

        hprintln!("foo - end").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 - start
foo - middle
baz
foo - end
bar

Передача сообщений

Другое преимущество программных задач - возможность передавать сообщения задачам во время их вызова. Тип полезной нагрузки сообщения должен быть определен в сигнатуре обработчика задачи.

Пример ниже демонстрирует три задачи, две из которых ожидают сообщения.


# #![allow(unused_variables)]
#fn main() {
//! examples/message.rs

#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;

#[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]

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::UART0);
    }

    #[task(binds = UART0, 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]

use cortex_m::peripheral::DWT;
use cortex_m_semihosting::hprintln;
use panic_halt as _;
use rtfm::cyccnt::{Instant, U32Ext as _};

// NOTE: does NOT work on QEMU!
#[rtfm::app(device = lm3s6965, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    #[init(schedule = [foo, bar])]
    fn init(mut cx: init::Context) {
        // Initialize (enable) the monotonic timer (CYCCNT)
        cx.core.DCB.enable_trace();
        // required on Cortex-M7 devices that software lock the DWT (e.g. STM32F7)
        DWT::unlock();
        cx.core.DWT.enable_cycle_counter();

        // semantically, the monotonic timer is frozen at time "zero" during `init`
        // NOTE do *not* call `Instant::now` in this context; it will return a nonsense value
        let now = cx.start; // the start time of the system

        hprintln!("init @ {:?}", now).unwrap();

        // Schedule `foo` to run 8e6 cycles (clock cycles) in the future
        cx.schedule.foo(now + 8_000_000.cycles()).unwrap();

        // Schedule `bar` to run 4e6 cycles in the future
        cx.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]

use cortex_m_semihosting::hprintln;
use panic_semihosting as _;
use rtfm::cyccnt::{Instant, U32Ext};

const PERIOD: u32 = 8_000_000;

// NOTE: does NOT work on QEMU!
#[rtfm::app(device = lm3s6965, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    #[init(schedule = [foo])]
    fn init(cx: init::Context) {
        // omitted: initialization of `CYCCNT`

        cx.schedule.foo(Instant::now() + PERIOD.cycles()).unwrap();
    }

    #[task(schedule = [foo])]
    fn foo(cx: foo::Context) {
        let now = Instant::now();
        hprintln!("foo(scheduled = {:?}, now = {:?})", cx.scheduled, now).unwrap();

        cx.schedule.foo(cx.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]

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;

// NOTE: does NOT properly work on QEMU
#[rtfm::app(device = lm3s6965, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    #[init(spawn = [foo])]
    fn init(cx: init::Context) {
        // omitted: initialization of `CYCCNT`

        hprintln!("init(baseline = {:?})", cx.start).unwrap();

        // `foo` inherits the baseline of `init`: `Instant(0)`
        cx.spawn.foo().unwrap();
    }

    #[task(schedule = [foo])]
    fn foo(cx: foo::Context) {
        static mut ONCE: bool = true;

        hprintln!("foo(baseline = {:?})", cx.scheduled).unwrap();

        if *ONCE {
            *ONCE = false;

            rtfm::pend(Interrupt::UART0);
        } else {
            debug::exit(debug::EXIT_SUCCESS);
        }
    }

    #[task(binds = UART0, spawn = [foo])]
    fn uart0(cx: uart0::Context) {
        hprintln!("UART0(baseline = {:?})", cx.start).unwrap();

        // `foo` inherits the baseline of `UART0`: its `start` time
        cx.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]

use cortex_m_semihosting::debug;
use panic_semihosting as _;
use rtfm::cyccnt;

#[rtfm::app(device = lm3s6965, peripherals = true, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    struct Resources {
        #[init(0)]
        shared: u32,
    }

    #[init(schedule = [foo], spawn = [foo])]
    fn init(cx: init::Context) {
        let _: cyccnt::Instant = cx.start;
        let _: rtfm::Peripherals = cx.core;
        let _: lm3s6965::Peripherals = cx.device;
        let _: init::Schedule = cx.schedule;
        let _: init::Spawn = cx.spawn;

        debug::exit(debug::EXIT_SUCCESS);
    }

    #[idle(schedule = [foo], spawn = [foo])]
    fn idle(cx: idle::Context) -> ! {
        let _: idle::Schedule = cx.schedule;
        let _: idle::Spawn = cx.spawn;

        loop {}
    }

    #[task(binds = UART0, resources = [shared], schedule = [foo], spawn = [foo])]
    fn uart0(cx: uart0::Context) {
        let _: cyccnt::Instant = cx.start;
        let _: resources::shared = cx.resources.shared;
        let _: uart0::Schedule = cx.schedule;
        let _: uart0::Spawn = cx.spawn;
    }

    #[task(priority = 2, resources = [shared], schedule = [foo], spawn = [foo])]
    fn foo(cx: foo::Context) {
        let _: cyccnt::Instant = cx.scheduled;
        let _: &mut u32 = cx.resources.shared;
        let _: foo::Resources = cx.resources;
        let _: foo::Schedule = cx.schedule;
        let _: foo::Spawn = cx.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]

use core::marker::PhantomData;

use cortex_m_semihosting::debug;
use panic_halt as _;
use rtfm::app;

pub struct NotSend {
    _0: PhantomData<*const ()>,
}

#[app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        #[init(None)]
        shared: Option<NotSend>,
    }

    #[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(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(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]

use core::marker::PhantomData;

use cortex_m_semihosting::debug;
use panic_halt as _;

pub struct NotSync {
    _0: PhantomData<*const ()>,
}

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        #[init(NotSync { _0: PhantomData })]
        shared: NotSync,
    }

    #[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, Вы можете попробовать его использовать на Вашем оборудовании следуя этим инструкциям.

  1. Создайте экземпляр из шаблона cortex-m-quickstart.
$ # например используя `cargo-generate`
$ cargo generate \
    --git https://github.com/rust-embedded/cortex-m-quickstart \
    --name app

$ # следуйте остальным инструкциям
  1. Добавьте крейт устройства, сгенерированный с помощью svd2rust v0.14.x, или библиотеку отладочной платы, у которой в зависимостях одно из устройств. Убедитесь, что опция rt крейта включена.

В этом примере я покажу использование крейта устройства lm3s6965. Эта библиотека не имеет Cargo-опции rt; эта опция всегда включена.

Этот крейт устройства предоставляет линковочный скрипт с макетом памяти целевого устройства, поэтому memory.x и build.rs не нужно удалять.

$ cargo add lm3s6965 --vers 0.1.3

$ rm memory.x build.rs
  1. Добавьте библиотеку cortex-m-rtfm как зависимость, и если необходимо, включите опцию timer-queue.
$ cargo add cortex-m-rtfm --allow-prerelease --upgrade=none
  1. Напишите программу 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
  1. Соберите его, загрузите в микроконтроллер и запустите.
$ # ПРИМЕЧАНИЕ: Я раскомментировал опцию `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]

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;
use rtfm::{Exclusive, Mutex};

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        #[init(0)]
        shared: u32,
    }

    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::UART0);
        rtfm::pend(Interrupt::UART1);
    }

    #[task(binds = UART0, resources = [shared])]
    fn uart0(c: uart0::Context) {
        static mut STATE: u32 = 0;

        hprintln!("UART0(STATE = {})", *STATE).unwrap();

        // second argument has type `resources::shared`
        advance(STATE, c.resources.shared);

        rtfm::pend(Interrupt::UART1);

        debug::exit(debug::EXIT_SUCCESS);
    }

    #[task(binds = UART1, priority = 2, resources = [shared])]
    fn uart1(c: uart1::Context) {
        static mut STATE: u32 = 0;

        hprintln!("UART1(STATE = {})", *STATE).unwrap();

        // just to show that `shared` can be accessed directly
        *c.resources.shared += 0;

        // second argument has type `Exclusive<u32>`
        advance(STATE, Exclusive(c.resources.shared));
    }
};

// the second parameter is generic: it can be any type that implements the `Mutex` trait
fn advance(state: &mut u32, mut shared: impl Mutex<T = u32>) {
    *state += 1;

    let (old, new) = shared.lock(|shared: &mut u32| {
        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]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;

#[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::'
00000162 t ramfunc::foo::h30e7789b08c08e19```

``` console
$ cargo nm --example ramfunc --release | grep ' bar::'
20000000 t ramfunc::bar::h9d6714fe5a3b0c89```

## `binds`

**ПРИМЕЧАНИЕ**: Требуется RTFM не ниже 0.4.2

Вы можете давать аппаратным задачам имена похожие на имена обычных задач.
Для этого нужно использовать аргумент `binds`: Вы называете функцию
по своему желанию и назначаете ей прерывание / исключение
через аргумент `binds`. `Spawn` и другие служебные типы будут размещены в модуле,
названном в соответствии с названием функции, а не прерывания / исключения.
Давайте посмотрим пример:

``` rust
//! examples/binds.rs

#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;

// `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 {}
    }

    #[task(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