STM32F4 Встроенный Rust на уровне HAL: фреймворк RTIC


Эта запись в блоге является третьей из серии статей, состоящей из трех частей, в которых я исследую прерывания для микроконтроллера STM32F401RE с помощью встроенного языка Rust на уровне HAL.

Если вам понравилась эта статья, пожалуйста, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.

Введение

В предыдущих двух постах я рассмотрел создание приложения, полностью основанного на прерываниях, которое изменяет скорость мигания светодиода в зависимости от нажатия кнопки. Имелось две подпрограммы обслуживания прерываний, одна из которых реагировала на событие нажатия кнопки, а другая — на истечение задержки таймера. Было показано, как для создания безопасных операций используемые абстракции делают код довольно многословным. К счастью, существует альтернативное решение, которое можно использовать — это фреймворк Real-Time Interrupt-driven Concurrency (RTIC). Фреймворк RTIC обеспечивает те же гарантии безопасности, что и предыдущий код, но является менее многословным и более элегантным решением. В этом посте я перенесу код из последнего поста о прерываниях таймера, чтобы продемонстрировать простоту перехода на фреймворк RTIC. Я буду создавать приложение RTIC шаг за шагом, объясняя, как каждая часть связана с приложением прерывания таймера.

Полный код также доступен для ознакомления в git-репо apollolabsdev Nucleo-F401RE.

📝 Примечания:

1️⃣ Код переноса прерываний GPIO также доступен в git-репо apollolabsdev Nucleo-F401RE.

2️⃣ RTIC предоставляет гораздо больше возможностей, чем просто обработка прерываний (например, планирование, передача сообщений… и т.д.), но они не будут рассматриваться в этом посте. Например, RTIC может обеспечить удобную настройку для программиста, который хочет создать ОС. Я бы рекомендовал заинтересованному читателю обратиться к документации по фреймворку Real-Time Interrupt-driven Concurrency (RTIC) за подробностями.

📱 Приложение RTIC

Структуру фреймворка RTIC можно рассматривать как комбинацию специализированных атрибутов. Каждый атрибут может определять задачу, функцию инициализации, таймер для планирования, локальные и глобальные ресурсы или даже само приложение. Некоторые из атрибутов могут также принимать аргументы. В следующих разделах я буду разбивать структуру на отдельные атрибуты и описывать содержимое каждого из них, импортируя в него наше приложение.

Атрибут #[app]

Для начала, все приложения RTIC требуют атрибут #[app]. Атрибут #[app] также требует, чтобы мы передали обязательный аргумент device, который содержит путь, указывающий на используемый PAC. В нашем случае это будет stm32f4xx_hal::pac, а атрибут определяется следующим образом:

#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true)]
Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что здесь peripherals = true гарантирует, что хэндл/поле device будет доступно для использования позже в нашем коде. По умолчанию peripherals установлено в true, но может быть установлено в false для уменьшения размера приложения, если не требуется доступ к периферии устройства. Непосредственно под атрибутом #[app] мы включаем модуль app, mod app, который будет инкапсулировать остальные атрибуты и импорты, необходимые для нашего приложения. Поэтому, прежде чем вводить какие-либо новые атрибуты, в самом начале mod app body/scope, мы можем просто скопировать импорты из поста прерывания таймера. Код под #[rtic::app(device = stm32f4xx_hal::pac, peripherals = true)] таким образом расширяется до следующего:

#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true)]
mod app {

    use stm32f4xx_hal::{
        gpio::{self, Edge, Input, Output, PushPull},
        pac::TIM2,
        prelude::*,
        timer::{self, Event},
    };
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы должны быть готовы определить остальную часть нашего приложения (код реализации).

📚 Ресурсы приложения RTIC

В приложении RTIC есть два типа ресурсов: общие и локальные. Общие ресурсы — это ресурсы/переменные, которые будут совместно использоваться/доступаться несколькими задачами. С другой стороны, локальные ресурсы — это ресурсы, доступ к которым может получить только одна задача. Обратите внимание, что все эти ресурсы относятся к тем, которые мы инициализируем в начале нашего приложения (под атрибутом #[init], показанным позже), чтобы использовать их позже в различных задачах. Это означает, что мне не нужно назначать ресурс для локальной переменной, если я хочу создать переменную для локального использования в любой из задач.

Другими словами, я пытаюсь сказать, что и общие, и локальные ресурсы в RTIC являются глобальными переменными согласно нашему предыдущему определению. Ключевое различие заключается в том, что общие ресурсы предназначены для глобальных переменных, которые используются несколькими задачами. Однако локальные ресурсы предназначены для глобальных переменных, используемых одной задачей (технически двумя, задачей #[init] и еще одной).

🗄 Общие ресурсы

Общие ресурсы определяются в атрибуте #[shared]. Внутри #[shared] находится структура Shared, в которую мы включаем дескрипторы для типов, которые будут использоваться совместно.

    #[shared]
    struct Shared {     
        timer: timer::CounterMs<TIM2>,
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что timer — это периферийный таймер, который я буду разделять между прерыванием нажатия кнопки и прерыванием истечения таймера. Напомним, что в сервисной процедуре прерывания нажатия кнопки (ISR) таймер был перезапущен с новым значением задержки. С другой стороны, в ISR истечения срока действия таймера, дескриптор таймера был необходим для очистки ожидающего прерывания в периферийном устройстве.

📁 Локальные ресурсы

Как и общие ресурсы, локальные ресурсы определяются в атрибуте #[local]. Внутри #[local] также есть структура Local, в которую мы включаем дескрипторы для типов, которые будут локальными.

    #[local]
    struct Local {
        delayval: u32,
        button: gpio::PC13<Input>,
        led: gpio::PA5<Output<PushPull>>,
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Где delayval — переменная задержки, которая будет изменяться при каждом нажатии кнопки. button — это хэндл для входного GPIO кнопки, который необходим для сброса флага ожидания прерывания нажатия кнопки. Наконец, led — это хэндл для выходного светодиода GPIO, который необходим для переключения выходного светодиода. Обратите внимание, что все эти ресурсы являются локальными, поскольку они будут использоваться только в ISR нажатия кнопки.

🤹 Задачи приложения RTIC

Задача #[init]

Задача #[init] включает в себя код настройки системы (наш код конфигурации) и выполняется после перезагрузки системы. Для задачи #[init] за атрибутом #[init] сразу следует функция init, которая должна иметь сигнатуру fn(ctx: init::Context) -> (Shared, Local, init::Monotonics). В задаче init мы можем получить доступ к периферии устройства через поля device и core в init::Context, который привязан к хэндлу ctx. Это как бы заменяет то, что мы делали раньше, используя хэндл take. Поэтому, чтобы иметь возможность скопировать код конфигурации из предыдущего сообщения как есть, я включаю следующую строку:

let mut dp = ctx.device;
Войти в полноэкранный режим Выйти из полноэкранного режима

Это позволит мне использовать ручку dp, которую я использовал раньше, заменяя то, что мы делали с помощью метода take. Прежде чем показать полный код для init, есть еще один момент. В конце задачи init мы должны вернуть инициализированные значения для общесистемных ресурсов #[shared] и #[local], а также набор инициализированных таймеров, используемых приложением. Это выглядит следующим образом:

        (
            Shared { timer },
            Local { button, led, delayval: 2000_u32 },
            init::Monotonics(),
        )
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь необходимо отметить несколько моментов. Во-первых, хотя мы не используем Monotonics для планирования, их все равно нужно вернуть. Во-вторых, я инициализирую delayval с его начальным значением непосредственно в возвращаемой структуре.

Вот код для задачи #[init]:

#[init]
    fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
        let mut dp = ctx.device;

        // Configure the LED pin as a push pull ouput and obtain handle
        // On the Nucleo FR401 theres an on-board LED connected to pin PA5
        // 1) Promote the GPIOA PAC struct
        let gpioa = dp.GPIOA.split();
        // 2) Configure Pin and Obtain Handle
        let led = gpioa.pa5.into_push_pull_output();

        // Configure the button pin as input and obtain handle
        // On the Nucleo FR401 there is a button connected to pin PC13
        // 1) Promote the GPIOC PAC struct
        let gpioc = dp.GPIOC.split();
        // 2) Configure Pin and Obtain Handle
        let mut button = gpioc.pc13;

        // Configure Button Pin for Interrupts
        // 1) Promote SYSCFG structure to HAL to be able to configure interrupts
        let mut syscfg = dp.SYSCFG.constrain();
        // 2) Make button an interrupt source
        button.make_interrupt_source(&mut syscfg);
        // 3) Make button an interrupt source
        button.trigger_on_edge(&mut dp.EXTI, Edge::Rising);
        // 4) Enable gpio interrupt for button
        button.enable_interrupt(&mut dp.EXTI);

        // Configure and obtain handle for delay abstraction
        // 1) Promote RCC structure to HAL to be able to configure clocks
        let rcc = dp.RCC.constrain();
        // 2) Configure the system clocks
        // 8 MHz must be used for HSE on the Nucleo-F401RE board according to manual
        let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
        // 3) Create delay handle
        //let mut delay = dp.TIM1.delay_ms(&clocks);
        let mut timer = dp.TIM2.counter_ms(&clocks);

        // Kick off the timer with 2 seconds timeout first
        // It probably would be better to use the global variable here but I did not to avoid the clutter of having to create a crtical section
        timer.start(2000.millis()).unwrap();

        // Set up to generate interrupt when timer expires
        timer.listen(Event::Update);

        (
            // Initialization of shared resources
            Shared { timer },
            // Initialization of task local resources
            Local {
                button,
                led,
                delayval: 2000_u32,
            },
            // Move the monotonic timer to the RTIC run-time, this enables
            // scheduling
            init::Monotonics(),
        )
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что наш предыдущий код здесь практически не изменился, круто, да?!

Задача #[idle]

Задача idle также известна как фоновая задача, которая выполняется, когда нет никаких прерываний, и является необязательной. Как и раньше, за атрибутом #[idle] сразу следует функция idle, которая должна иметь сигнатуру fn(idle::Context) -> !. Вспомните, что все, что мы делали в состоянии бездействия, это переходили в спящий режим. Таким образом, код задачи idle выглядит следующим образом:

    #[idle]
    fn idle(_: idle::Context) -> ! {
        loop {
            cortex_m::asm::wfi();
        }
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь device и core доступны через контекст на случай, если потребуется обратиться к любому из них. Однако, поскольку я не обращаюсь ни к одному из них, я использую идентификатор с подчеркиванием _, поскольку мне не нужно привязывать idle::Context к чему-либо.

Задачи для аппаратного обеспечения

В нашем коде теперь остается только определить аппаратные задачи, привязанные к нашим прерываниям. Для привязки прерывания нам нужно использовать атрибут #[task] с аргументом binds = InterruptName. Таким образом, задача становится обработчиком прерывания для связанного вектора аппаратного прерывания. В нашем случае есть две аппаратные задачи, одна из которых привязывается к имени прерывания EXTI15_10, а другая — к имени прерывания TIM2. Кроме того, атрибут принимает в качестве аргументов ресурсы, которые будет использовать задача. За атрибутом также сразу следует функция, которую вы можете назвать следующим образом fn func_name (func_name::Context), где func_name — имя по вашему выбору. Таким образом, здесь приведен код для ISR нажатия кнопки:

#[task(binds = EXTI15_10, local = [delayval, button], shared=[timer])]
    fn button_pressed(mut ctx: button_pressed::Context) {

        // Obtain a copy of the delay value from the global context
        let mut delay = *ctx.local.delayval;

        // Adjust the amount of delay
        delay = delay - 500_u32;
        if delay < 500_u32 {
            delay = 2000_u32;
        }

        // Update the timeout value in the timer peripheral
        ctx.shared
            .timer
            .lock(|tim| tim.start(delay.millis()).unwrap());

        // Obtain access to Button Peripheral and Clear Interrupt Pending Flag
        ctx.local.button.clear_interrupt_pending_bit();
    }
Войти в полноэкранный режим Выход из полноэкранного режима

Как видно из строки #[task(binds = EXTI15_10, local =

, shared=[delayval, timer])], я привязываю источник прерывания EXTI15_10 к этой задаче, которая содержит функцию, которую я называю button_pressed. Кроме того, я передаю в качестве аргументов ранее определенные локальные и общие ресурсы, которые будет использовать ISR. Общие ресурсы здесь — это те, которые будут использоваться ISR истечения таймера, а это только периферийное устройство таймера timer.

Далее, обратите внимание на строки, в которых я обращаюсь к local ресурсам. Сюда входят ctx.local.button.clear_interrupt_pending_bit(); и let mut delay = *ctx.local.delayval;. Доступ к локальным ресурсам осуществляется через local, которое является полем контекста задачи ctx. Это то же самое, что мы имели в задаче init, мы могли получить доступ к периферии устройства через поля device и core в init::Context, который привязан к хэндлу ctx. Еще один момент: ссылка на delayval происходит потому, что ctx.local.delayval сам по себе является &mut u32, но нам нужно значение u32.

В случае с shared ресурсами все не так просто. Поскольку ресурс является общим, нам нужно получить блокировку, в которой токен предоставляется в закрытии для написания кода. Это похоже на наш прошлый код, хотя и менее многословный, в котором мы получили блокировку с помощью interrupt::free. Опять же, как и в случае с local, мы можем получить доступ к timer через поле shared и ctx. Однако дополнительно нам необходимо использовать метод lock, который предоставляет доступ к «заблокированному» ресурсу через токен в закрытии. Это делается в строке ctx.shared.timer.lock(|tim| tim.start(delay.millis()).unwrap()));, где в общий ресурс таймера загружается новое значение задержки.

Наконец, осталось только определить ISR истечения таймера. Ниже приведен ISR истечения таймера:

    #[task(binds = TIM2, local=[led], shared=[timer])]
    fn timer_expired(mut ctx: timer_expired::Context) {
        ctx.local.led.toggle();
        ctx.shared
            .timer
            .lock(|tim| tim.clear_interrupt(Event::Update));
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Идея точно такая же, как и у предыдущего ISR button_pressed. Все, что здесь делается, это переключение led, которое является локальным для ISR истечения таймера. Кроме того, обратите внимание, что ресурс таймера shared timer здесь также блокируется, чтобы снять прерывание.

📱 Полный код приложения

Вот полный код реализации, описанной в этом посте. Вы также можете найти полный проект и другие доступные на git-репо apollolabsdev Nucleo-F401RE.

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

use panic_halt as _;

#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true)]
mod app {

    use stm32f4xx_hal::{
        gpio::{self, Edge, Input, Output, PushPull},
        pac::TIM2,
        prelude::*,
        timer::{self, Event},
    };

    // Resources shared between tasks
    #[shared]
    struct Shared {
        timer: timer::CounterMs<TIM2>,
    }

    // Local resources to specific tasks (cannot be shared)
    #[local]
    struct Local {
        button: gpio::PC13<Input>,
        led: gpio::PA5<Output<PushPull>>,
        delayval: u32,
    }

    #[init]
    fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
        let mut dp = ctx.device;

        // Configure the LED pin as a push pull ouput and obtain handle
        // On the Nucleo FR401 theres an on-board LED connected to pin PA5
        // 1) Promote the GPIOA PAC struct
        let gpioa = dp.GPIOA.split();
        // 2) Configure Pin and Obtain Handle
        let led = gpioa.pa5.into_push_pull_output();

        // Configure the button pin as input and obtain handle
        // On the Nucleo FR401 there is a button connected to pin PC13
        // 1) Promote the GPIOC PAC struct
        let gpioc = dp.GPIOC.split();
        // 2) Configure Pin and Obtain Handle
        let mut button = gpioc.pc13;

        // Configure Button Pin for Interrupts
        // 1) Promote SYSCFG structure to HAL to be able to configure interrupts
        let mut syscfg = dp.SYSCFG.constrain();
        // 2) Make button an interrupt source
        button.make_interrupt_source(&mut syscfg);
        // 3) Make button an interrupt source
        button.trigger_on_edge(&mut dp.EXTI, Edge::Rising);
        // 4) Enable gpio interrupt for button
        button.enable_interrupt(&mut dp.EXTI);

        // Configure and obtain handle for delay abstraction
        // 1) Promote RCC structure to HAL to be able to configure clocks
        let rcc = dp.RCC.constrain();
        // 2) Configure the system clocks
        // 8 MHz must be used for HSE on the Nucleo-F401RE board according to manual
        let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
        // 3) Create delay handle
        //let mut delay = dp.TIM1.delay_ms(&clocks);
        let mut timer = dp.TIM2.counter_ms(&clocks);

        // Kick off the timer with 2 seconds timeout first
        // It probably would be better to use the global variable here but I did not to avoid the clutter of having to create a crtical section
        timer.start(2000.millis()).unwrap();

        // Set up to generate interrupt when timer expires
        timer.listen(Event::Update);

        (
            // Initialization of shared resources
            Shared { timer },
            // Initialization of task local resources
            Local {
                button,
                led,
                delayval: 2000_u32,
            },
            // Move the monotonic timer to the RTIC run-time, this enables
            // scheduling
            init::Monotonics(),
        )
    }

    // Background task, runs whenever no other tasks are running
    #[idle]
    fn idle(_: idle::Context) -> ! {
        loop {
            // Go to sleep
            cortex_m::asm::wfi();
        }
    }

    #[task(binds = EXTI15_10, local = [delayval, button], shared=[timer])]
    fn button_pressed(mut ctx: button_pressed::Context) {
        // When Button press interrupt happens three things need to be done
        // 1) Adjust Global Delay Variable
        // 2) Update Timer with new Global Delay value
        // 3) Clear Button Pending Interrupt

        // Obtain a copy of the delay value from the global context
        let mut delay = *ctx.local.delayval;

        // Adjust the amount of delay
        delay = delay - 500_u32;
        if delay < 500_u32 {
            delay = 2000_u32;
        }

        // Update the timeout value in the timer peripheral
        ctx.shared
            .timer
            .lock(|tim| tim.start(delay.millis()).unwrap());

        // Obtain access to Button Peripheral and Clear Interrupt Pending Flag
        ctx.local.button.clear_interrupt_pending_bit();
    }

    #[task(binds = TIM2, local=[led], shared=[timer])]
    fn timer_expired(mut ctx: timer_expired::Context) {
        // When Timer Interrupt Happens Two Things Need to be Done
        // 1) Toggle the LED
        // 2) Clear Timer Pending Interrupt

        ctx.local.led.toggle();
        ctx.shared
            .timer
            .lock(|tim| tim.clear_interrupt(Event::Update));
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

## 🔬 Дальнейшие эксперименты/идеи

  • Рефакторинг этого кода для имитации поведения приложения, основанного на прерываниях GPIO. Пример доработанного кода доступен в git-репозитории. Вы можете сравнить свой отрефакторенный код с тем, который доступен в репозитории.
  • Если у вас есть дополнительные кнопки, с помощью RTIC попробуйте реализовать дополнительные прерывания от других входных пинов, где каждое нажатие кнопки применяет разную задержку и имеет разный источник. Это означает, что вам придется создавать дополнительные задачи прерывания.
  • Преобразуйте приложение для аналогового измерения температуры в приложение на основе прерываний с помощью RTIC.

Заключение

В предыдущих сообщениях были показаны примеры приложений на Rust, которые полностью управляются прерываниями. Также было показано, что работа с прерываниями в Rust может быть немного многословной из-за всех добавляемых безопасных абстракций. В этой заметке RTIC был использован для демонстрации процесса переноса кода существующего приложения, основанного на прерываниях, созданного в Rust. Пост также показывает мощь и простоту использования RTIC, где гарантии безопасности предоставляются без излишней многословности кода. Приложение было создано с использованием микроконтроллера STM32F401RE на плате разработки Nucleo-F401RE. Весь код был создан на уровне HAL с использованием stm32f4xx Rust HAL. Есть вопросы/замечания? Поделитесь своими мыслями в комментариях ниже 👇. Если вы нашли это полезным, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.

Оцените статью
devanswers.ru
Добавить комментарий