Эта запись в блоге является третьей из серии статей, состоящей из трех частей, в которых я исследую прерывания для микроконтроллера 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. Есть вопросы/замечания? Поделитесь своими мыслями в комментариях ниже 👇. Если вы нашли это полезным, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.