STM32F4 Встроенный Rust на уровне HAL: прерывания таймера


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

В качестве примечания, эта статья в значительной степени зависит от предыдущей, STM32F4 Embedded Rust at the HAL: GPIO Interrupts. Поэтому я рекомендую читателю обратиться к предыдущему посту, прежде чем читать этот.

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

Введение

В предыдущем посте, STM32F4 Embedded Rust at the HAL: GPIO Interrupts, я реализовал приложение для управления мигающим светодиодом с помощью прерываний. В том коде только событие кнопки было настроено на прерывания. Однако было и второе событие, которое также можно было закодировать, — это задержка таймера. Когда задержка таймера истекает, мы получаем прерывание, на которое нам нужно отреагировать. Для тех, кто знаком с прошлыми постами и с тем, как мы делали задержку, использовался блокирующий метод delay_ms. Использование delay_ms по сути останавливает все операции в контроллере до истечения задержки. Можно представить, насколько неэффективной может быть такая операция. Вместо этого, используя прерывания, можно запустить таймер на отсчет времени, пока контроллер занимается другими делами. Контроллер будет реагировать только по истечении таймера.

Этот пост будет в основном посвящен реализации кода, так как многие другие разделы идентичны разделам в STM32F4 Embedded Rust at the HAL: GPIO Interrupts.

📚 Предварительные требования к знаниям

Чтобы понять содержание этого поста, вам необходимо следующее:

  • Базовые знания о кодировании на языке Rust.
  • Знакомство с базовым шаблоном для создания встраиваемых приложений на Rust.
  • Знакомство с прерываниями в процессорах Cortex-M.

💾 Установка программного обеспечения

Весь код, представленный в этом посте, а также инструкции по настройке среды и инструментария доступны в git-репо apollolabsdev Nucleo-F401RE. Обратите внимание, что если код в git-репозитории немного отличается, это означает, что он был изменен для улучшения качества кода или с учетом обновлений HAL/Rust.

🛠 Аппаратная установка

Материалы

  • Плата Nucleo-F401RE

🔌 Подключения

Внешние соединения не понадобятся. Будут использоваться встроенные соединения, которые включают в себя следующее:

  • Светодиод подключен к выводу PA5 микроконтроллера. Этот вывод будет использоваться как выход.
  • Кнопка пользователя подключена к выводу PC13 микроконтроллера. Контакт будет использоваться как вход.

👨🎨 Разработка программного обеспечения

Здесь я хочу быстро обратиться к машине состояний, которую я представил в прошлом посте. Вспомните, что событие/переход нажатия кнопки было сконфигурировано с прерываниями в прошлом посте. Идея здесь заключается в том, чтобы интегрировать прерывания и для событий задержки таймера, таким образом, чтобы приложение работало полностью на основе прерываний. Это означает, что вместо этого потребуется два ISR: один для GPIO и один для таймера, в сочетании с главным циклом, который ничего не делает.

📝 Примечание

Приложения, которые полностью управляются прерываниями, являются приложениями, которые могут обеспечить базовую аппаратную среду планирования. Как правило, такая архитектура выгодна приложениям с низким энергопотреблением. По сути, когда приложение простаивает, оно переходит в спящий режим с низким энергопотреблением, используя инструкции wfi или wfe, и просыпается для обработки только при возникновении событий.

Теперь перейдем к реализации этого алгоритма.

👨💻 Реализация кода

📥 Импорт крейта

В этой реализации импорт крейтов более или менее такой же, как и раньше, за исключением того, что я добавил типы таймеров, необходимые из stm32f4xx_hal.

use core::cell::{Cell, RefCell};
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    gpio::{self, Edge, Input, Output, PushPull},
    pac::{self, interrupt, TIM2},
    prelude::*,
    timer::{CounterMs, Event},
};
Вход в полноэкранный режим Выход из полноэкранного режима

🌍 Глобальные переменные

Помните, что до этого я создал две глобальные переменные, обернутые в безопасные абстракции, одну для кнопки, чтобы передавать вокруг хэндла GPIO button, а другую для значения задержки. Я сохраню предыдущие глобальные переменные, но добавлю еще две для работы с выходом LED GPIO и периферийным таймером. Это связано с тем, что мне нужно управлять светодиодом из ISR таймера и дополнительно очищать флаг прерывания таймера также в ISR таймера.

Опять же, для удобства, я создаю еще один псевдоним для вывода PA5, который подключен к светодиоду, следующим образом:

type LedPin = gpio::PA5<Output<PushPull>>;
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее, как и раньше, я создаю две дополнительные глобальные переменные static с именами G_LED и G_TIMER, обернув вывод светодиода LedPin и миллисекундный таймер (я выбрал TIM2) CounterMs<TIM2> в безопасные абстракции следующим образом:

static G_TIM: Mutex<RefCell<Option<CounterMs<TIM2>>>> = Mutex::new(RefCell::new(None));

static G_LED: Mutex<RefCell<Option<LedPin>>> = Mutex::new(RefCell::new(None));
Войти в полноэкранный режим Выход из полноэкранного режима

🎛 Код конфигурации периферийного устройства

Получите хэндл для периферийных устройств: Это первый шаг, который всегда необходим перед настройкой чего-либо, принимая периферийные устройства на уровне PAC:

let mut dp = pac::Peripherals::take().unwrap();
Вход в полноэкранный режим Выход из полноэкранного режима

🕹 Конфигурация GPIO:

📝 Примечание

Конфигурация пинов GPIO и прерываний в этом посте точно такая же, как и в предыдущем. Я сохраняю их, чтобы сделать часть кода конфигурации в этом посте как можно более самодостаточной.

1️⃣ Продвигаем структуру GPIO на уровне PAC: Нам нужно настроить вывод LED как push-pull выход и получить обработчик для вывода, чтобы мы могли им управлять. Нам также нужно получить обработчик для входного вывода кнопки. Согласно деталям подключения, подключение светодиодного вывода является частью GPIOA, а подключение кнопки — частью GPIOC. Прежде чем мы сможем получить хэндлы для светодиода и кнопки, нам нужно продвинуть структуры pac-уровня GPIOA и GPIOC, чтобы иметь возможность создавать хэндлы для отдельных пинов. Для этого мы используем метод split() следующим образом:

let gpioa = dp.GPIOA.split();
let gpioc = dp.GPIOC.split();
Вход в полноэкранный режим Выйти из полноэкранного режима

2️⃣ Получите хэндл для светодиода и настройте его на вывод: Как было сказано ранее, встроенный светодиод на Nucleo-F401RE подключен к выводу PA5 (Pin 5 Port A). Поэтому нам нужно создать хэндл для вывода светодиода, в котором PA5 будет настроен на push-pull выход с помощью метода into_push_pull_output(). Мы назовем хэндл led и настроим его следующим образом:

let mut led = gpioa.pa5.into_push_pull_output();
Вход в полноэкранный режим Выход из полноэкранного режима

Для тех, кому интересно, на этой странице документации HAL приведен полный список методов, которые поддерживает тип Pin. Также, если метод split() показался вам непонятным, пожалуйста, обратитесь к моей записи в блоге здесь для более подробной информации.

3️⃣ Получите хэндл и настройте кнопку ввода: Встроенная пользовательская кнопка на Nucleo-F401RE подключена к пину PC13 (Pin 13 Port C), как было сказано ранее. По умолчанию пины настроены на вход, поэтому при создании хэндла для кнопки мы не вызываем никаких специальных методов.

let mut button = gpioc.pc13;
Вход в полноэкранный режим Выход из полноэкранного режима

⏱ ⏸ Конфигурация таймера:

1️⃣ Настройте системные часы: Системные часы должны быть настроены, так как они необходимы для настройки периферийных таймеров. Для настройки системных часов нам нужно сначала продвинуть структуру RCC из PAC и ограничить ее с помощью метода constrain() (подробнее о методе constrain здесь), чтобы дать доступ к структуре cfgr. После этого мы создаем хэндл clocks, который предоставляет доступ к настроенным (и замороженным) системным часам. Часы настроены на использование частоты HSE 8 МГц путем применения метода use_hse() к структуре cfgr. Частота HSE определена в справочном руководстве к плате разработки Nucleo-F401RE. Наконец, метод freeze() применяется к структуре cfgr для замораживания конфигурации часов. Обратите внимание, что замораживание часов является защитным механизмом HAL, чтобы избежать изменения конфигурации часов во время выполнения. Из этого следует, что периферийные устройства, которым требуется информация о часах, будут принимать только замороженную конфигурационную структуру Clocks.

let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
Вход в полноэкранный режим Выход из полноэкранного режима

2️⃣ Получение хэндла для таймера: я буду использовать TIM2 для создания хэндла таймера, я также использую реализацию counter_ms для удобства, так как это позволит мне предоставить значение миллисекунды напрямую.

let mut timer = dp.TIM2.counter_ms(&clocks);
Вход в полноэкранный режим Выход из полноэкранного режима

GPIO и таймер теперь настроены, но еще не настроены на прерывания, что является следующим шагом.

⏸ Конфигурация прерываний:

Последнее, что осталось сделать в конфигурации, это настроить и включить прерывания для периферийных устройств GPIO кнопки и таймера. Это необходимо для того, чтобы при нажатии кнопки или истечении таймера выполнение переключилось на соответствующую процедуру обслуживания прерывания. Как уже упоминалось, нам необходимо настроить аппаратное обеспечение в три этапа:

1️⃣ Включить глобальные прерывания на уровне процессора Cortex-M: Процессоры Cortex-M имеют архитектурный регистр PRIMASK, который содержит бит для глобального включения/выключения прерываний. Обратите внимание, что прерывания глобально включены по умолчанию в регистре Cortex-M PRIMASK. С точки зрения кода здесь ничего делать не нужно, однако я предпочитаю включать этот шаг для осведомленности.

2️⃣ Настройка и включение прерываний на уровне периферии (например, GPIO и таймер): Для настройки прерываний для GPIO нам нужен доступ к структуре SYSCFG. Чтобы иметь возможность использовать структуру SYSCFG, нам сначала нужно перевести ее на уровень HAL, ограничив ее с помощью метода constrain(). Как я узнал, что мне нужно constrain SYSCFG? Я рекомендую вам обратиться к этому посту для получения более подробной информации. SYSCFG содержит информацию для набора регистров в STM32, которые необходимы для конфигурирования прерываний для периферийных устройств, и продвигается следующим образом:

    let mut syscfg = dp.SYSCFG.constrain();
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь кнопку gpio необходимо сконфигурировать как источник прерывания в устройстве STM32. В STM32 прерывания GPIO считаются одним из типов внешних прерываний. В документации stm32f4xx-hal по GPIO я нашел кучу признаков внешнего пина ExtiPin, которые позволят мне настроить прерывания пина GPIO. По существу, пин GPIO сначала настраивается с помощью метода make_interrupt_source, чтобы сделать его источником прерывания в устройстве STM32. Метод make_interrupt_source имеет следующую сигнатуру, из которой все, что ему нужно — это изменяемая ссылка на SysCfg.

fn make_interrupt_source(&mut self, syscfg: &mut SysCfg)
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее нам нужно определить, как срабатывает прерывание (например, по нарастающему, спадающему или обоим фронтам). В том же списке признаков ExtiPin есть метод trigger_on_edge со следующей сигнатурой:

fn trigger_on_edge(&mut self, exti: &mut EXTI, level: Edge)
Войти в полноэкранный режим Выйти из полноэкранного режима

Метод требует два параметра, изменяемую ссылку на внешний контроллер прерываний/событий pac EXTI, и уровень срабатывания прерывания. Edge представляет собой перечисление с различными типами триггеров прерываний для GPIO и выглядит следующим образом:

pub enum Edge {
    Rising,
    Falling,
    RisingFalling,
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, остается еще один шаг — включение прерывания на периферийном уровне. Это делается с помощью метода enable_interrupt:

fn enable_interrupt(&mut self, exti: &mut EXTI)
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь метод требует только мутабельной ссылки на внешний контроллер прерываний/событий pac EXTI.

Используя описанные выше методы, пин кнопки GPIO теперь можно настроить на прерывание следующим образом:

    button.make_interrupt_source(&mut syscfg);
    button.trigger_on_edge(&mut dp.EXTI, Edge::Rising);
    button.enable_interrupt(&mut dp.EXTI);
Вход в полноэкранный режим Выход из полноэкранного режима

Что касается таймера, то сначала нам нужно инициализировать таймер со значением таймаута и начать отсчет. Это делается с помощью метода start, я выбрал таймаут 2 секунды:

timer.start(2000.millis()).unwrap();
Войти в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что выше я использую метод millis, который я использовал в прошлом посте «Мигание кнопки, управляемое опросом таймера». Напомню, что метод millis позволяет преобразовать u32 в Duration, поскольку именно этого требует метод start.

Во-вторых, как и GPIO, прерывания должны быть включены на периферийном уровне. Метод, который делает это для таймеров, называется listen. Метод принимает один параметр, который является перечислением Event, определяющим тип события, для которого таймер генерирует прерывание. Эти события определяются техническим описанием контроллера. Здесь я выбрал событие Update:

 timer.listen(Event::Update);
Вход в полноэкранный режим Выход из полноэкранного режима

3️⃣ Включите источник прерывания в NVIC: Теперь, когда прерывания от кнопок и таймера настроены, соответствующие номера прерываний в NVIC должны быть размаскированы. Это делается с помощью метода NVIC unmask в криейте cotrex_m::peripheral. Напомним, что метод unmask ожидает, что мы передадим ему номер прерывания, которое хотим размаскировать. Это было сделано путем применения общего метода Pin interrupt на хэндле button. Однако для таймера я не смог найти в документации метод, возвращающий номер прерывания для таймера. Хотя в прошлом сообщении я упоминал, что существует альтернативный подход, использующий перечисление interrupt, которое перечисляет все источники прерываний. Для таймера источником прерывания является TIM2, и размаскировка NVIC может быть выполнена следующим образом:

    unsafe {
        cortex_m::peripheral::NVIC::unmask(interrupt::TIM2);
        cortex_m::peripheral::NVIC::unmask(button.interrupt());
    }
Войдите в полноэкранный режим Выйти из полноэкранного режима

4️⃣ Перемещение переменной в глобальный контекст: Я инициализировал три глобальные переменные в начале с None, ожидая инициализации. Это были G_BUTTON, G_TIM, и G_LED. Теперь, когда все связанные периферийные устройства инициализированы, их нужно переместить в глобальный контекст. Это можно сделать в одной критической секции следующим образом:

    cortex_m::interrupt::free(|cs| {
        G_TIM.borrow(cs).replace(Some(timer));
        G_BUTTON.borrow(cs).replace(Some(button));
        G_LED.borrow(cs).replace(Some(led));
    });
Вход в полноэкранный режим Выход из полноэкранного режима

Мы закончили с конфигурацией! Переходим к коду приложения!

📱 Код приложения

🔁 Прикладной цикл

В соответствии с ранее описанным дизайном, код приложения отсутствует! По сути, все, что нам нужно сделать, это установить пустой цикл, который будет выполняться вечно, пока его не прервет одно из наших прерываний. Однако я поступаю несколько иначе:

    loop {
        cortex_m::asm::wfi();
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Я вызываю инструкцию wfi из библиотеки cortex_m. wfi означает «ждать прерывания», и ее действие заключается в том, что она отправляет процессор в спящий режим, пока он простаивает. Затем процессор проснется, когда произойдет любое из наших прерываний. Это механизм энергосбережения, в качестве альтернативы я мог бы оставить loop пустым.

⏸ Маршрут(ы) обслуживания прерываний

Здесь мне нужно установить ISR, которые будут включать код, выполняемый при обнаружении любого из прерываний. Здесь у нас будет два ISR, один для таймера и один для кнопки. Как и раньше, мы должны использовать атрибут #[interrupt], за которым следует имя функции, соответствующее имени прерывания. Опять же, имя прерывания берется из документации hal, и в нашем случае для пина PC13 это EXTI15_10, а для таймера TIM2.

1️⃣ ISR нажатия кнопки
Внутри ISR нажатия кнопки первое, что нужно сделать, это изменить задержку G_DELAYMS. Как и в предыдущем приложении, я решил начать с задержки в 2 секунды и уменьшать ее с шагом в 0,5 секунды. Если значение задержки достигает нуля, то я снова сбрасываю его до 2 секунд. Хотя, как и раньше, для доступа к G_DELAYMS необходима критическая секция. Кроме того, я использую новый метод set, который позволяет мне изменять содержимое G_DELAYMS.

    cortex_m::interrupt::free(|cs| {
        G_DELAYMS
            .borrow(cs)
            .set(G_DELAYMS.borrow(cs).get() - 500_u32);
        if G_DELAYMS.borrow(cs).get() < 500_u32 {
            G_DELAYMS.borrow(cs).set(2000_u32);
        });
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, когда задержка стала другой, это изменение нужно передать таймеру, чтобы он начал отсчет с новой задержкой (в этом смысл нажатия кнопки, в конце концов). Нам снова придется использовать метод start, но помните, что таймер также находится в глобальном контексте, обернутом в переменную G_TIM.

        let mut timer = G_TIM.borrow(cs).borrow_mut();

        timer
            .as_mut()
            .unwrap()
            .start(G_DELAYMS.borrow(cs).get().millis())
            .unwrap();
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, поскольку флаг ожидания прерывания для нажатия кнопки в аппаратном обеспечении все еще установлен. Если его не сбросить, то мы не сможем обнаружить последующие прерывания. Для этого мне понадобится ссылка на периферийное устройство GPIO кнопки, которую может предоставить глобальная переменная G_BUTTON, созданная ранее. В результате в ту же самую критическую секцию, начатую ранее, можно добавить следующие строки:

        let mut button = G_BUTTON.borrow(cs).borrow_mut();
        button.as_mut().unwrap().clear_interrupt_pending_bit();
Вход в полноэкранный режим Выйти из полноэкранного режима

Первая строка получает изменяемую ссылку на Option в G_BUTTON с помощью метода borrow_mut. В следующей строке мутабельная ссылка разворачивается, предоставляя доступ к хэндлу button. Наконец, метод clear_interrupt_pending_bit из трейта ExtiPin применяется для очистки флага/бита ожидания прерывания.

2️⃣ ISR таймера
В ISR таймера нужно сделать только две вещи. Первое — переключение светодиода по истечении таймера и второе — очистка флага ожидания прерывания таймера. Это также должно быть сделано в критической секции, поскольку нам понадобится доступ к глобальным переменным G_LED и G_TIM. Для таймера флаг прерывания очищается с помощью метода clear_interrupt, который также требует указать тип Event, который мы очищаем.

    cortex_m::interrupt::free(|cs| {
        let mut led = G_LED.borrow(cs).borrow_mut();
        led.as_mut().unwrap().toggle();

        let mut timer = G_TIM.borrow(cs).borrow_mut();
        timer.as_mut().unwrap().clear_interrupt(Event::Update);
    });
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

#![no_std]
#![no_main]

// Imports
use core::cell::{Cell, RefCell};
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    gpio::{self, Edge, Input, Output, PushPull},
    pac::{self, interrupt, TIM2},
    prelude::*,
    timer::{CounterMs, Event},
};

// Create aliases for pins PC13 and PA5
type ButtonPin = gpio::PC13<Input>;
type LedPin = gpio::PA5<Output<PushPull>>;

// Global Variable Definitions
// Global variables are wrapped in safe abstractions.
// If you notice peripherals are wrapped in a different manner than regular global mutable data.
// In the case of peripherals we must be sure only one refrence exists at a time.
// Refer to Chapter 6 of the Embedded Rust Book for more detail.

// Create a Global Variable for the Button GPIO Peripheral that I'm going to pass around.
static G_BUTTON: Mutex<RefCell<Option<ButtonPin>>> = Mutex::new(RefCell::new(None));
// Create a Global Variable for the Timer Peripheral that I'm going to pass around.
static G_TIM: Mutex<RefCell<Option<CounterMs<TIM2>>>> = Mutex::new(RefCell::new(None));
// Create a Global Variable for the LED GPIO Peripheral that I'm going to pass around.
static G_LED: Mutex<RefCell<Option<LedPin>>> = Mutex::new(RefCell::new(None));
// Create a Global Variable for the delay value that I'm going to use to manage the delay.
static G_DELAYMS: Mutex<Cell<u32>> = Mutex::new(Cell::new(2000));

#[entry]
fn main() -> ! {
    // Setup handler for device peripherals
    let mut dp = pac::Peripherals::take().unwrap();

    // 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);

    // Enable the external interrupt in the NVIC for all peripherals by passing the interrupt numbers
    unsafe {
        cortex_m::peripheral::NVIC::unmask(interrupt::TIM2);
        cortex_m::peripheral::NVIC::unmask(button.interrupt());
    }

    // Now that all peripherals are configured, move them into global context
    cortex_m::interrupt::free(|cs| {
        G_TIM.borrow(cs).replace(Some(timer));
        G_BUTTON.borrow(cs).replace(Some(button));
        G_LED.borrow(cs).replace(Some(led));
    });

    // Application Loop
    loop {
        // Go to sleep
        cortex_m::asm::wfi();
    }
}

// Button Interrupt
#[interrupt]
fn EXTI15_10() {
    // When Button 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

    // Start a Critical Section
    cortex_m::interrupt::free(|cs| {
        // Obtain Access to Delay Global Data and Adjust Delay
        G_DELAYMS
            .borrow(cs)
            .set(G_DELAYMS.borrow(cs).get() - 500_u32);

        // Reset delay value if it drops below 500 milliseconds
        if G_DELAYMS.borrow(cs).get() < 500_u32 {
            G_DELAYMS.borrow(cs).set(2000_u32);
        }

        // Obtain access to global timer
        let mut timer = G_TIM.borrow(cs).borrow_mut();

        // Adjust and start timer with updated delay value
        timer
            .as_mut()
            .unwrap()
            .start(G_DELAYMS.borrow(cs).get().millis())
            .unwrap();

        // Obtain access to Global Button Peripheral and Clear Interrupt Pending Flag
        let mut button = G_BUTTON.borrow(cs).borrow_mut();
        button.as_mut().unwrap().clear_interrupt_pending_bit();
    });
}

// Timer Interrupt
#[interrupt]
fn TIM2() {
    // When Timer Interrupt Happens Two Things Need to be Done
    // 1) Toggle the LED
    // 2) Clear Timer Pending Interrupt

    // Start a Critical Section
    cortex_m::interrupt::free(|cs| {
        // Obtain Access to Delay Global Data and Adjust Delay
        let mut led = G_LED.borrow(cs).borrow_mut();
        led.as_mut().unwrap().toggle();

        // Obtain access to Global Timer Peripheral and Clear Interrupt Pending Flag
        let mut timer = G_TIM.borrow(cs).borrow_mut();
        timer.as_mut().unwrap().clear_interrupt(Event::Update);
    });
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

  • Если у вас есть дополнительные кнопки, попробуйте реализовать дополнительные прерывания от других входных пинов, где каждое нажатие кнопки применяет разную задержку. Это вместо того, чтобы одна кнопка применяла одну задержку.
  • Классный мини-проект — засечь время реакции человека. Используя светодиод и кнопку, посмотрите, сколько времени вам потребуется, чтобы нажать кнопку после того, как загорится светодиод. Вы можете использовать периферийный счетчик/таймер для регистрации длительности и UART для передачи результата. Для работы со счетчиком и UART обратитесь к прошлым постам.

Заключение

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

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