Программирование ARM Rust ESP: как написать глобальную функцию, управляющую выводом GPIO? Fri, August 29 2025  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Rust ESP: как написать глобальную функцию, управляющую выводом GPIO? Печать
Добавил(а) microsin   

На языке Rust можно создать глобальную функцию для управления портом вывода GPIO, которая может быть вызвана из нескольких контекстов (задач). Сделать это можно несколькими способами. Далее будут рассмотрены эти способы на примере управления ножкой порта GPIO8 микроконтроллера ESP32-C3 платы SuperMini [1], к этому выводу на этой плате подключен голубой светодиод.

[Способ 1: передача сообщения (рекомендуемый вариант)]

use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

#[derive(Clone, Copy)]
pub enum GpioCommand {
SetHigh,
SetLow,
Toggle,
SetState(bool), }

static GPIO8_CHANNEL: Channel< CriticalSectionRawMutex, GpioCommand, 8> = Channel::new();

// Глобальная функция которая управляет ножкой GPIO8:
pub async fn set_gpio8(command: GpioCommand) {
let _ = GPIO8_CHANNEL.send(command).await; }

// Функции для удобства:
pub async fn gpio8_high() {
set_gpio8(GpioCommand::SetHigh).await; }

pub async fn gpio8_low() {
set_gpio8(GpioCommand::SetLow).await; }

pub async fn gpio8_toggle() {
set_gpio8(GpioCommand::Toggle).await; }

pub async fn gpio8_set_state(state: bool) {
set_gpio8(GpioCommand::SetState(state)).await; }

// Выделенная задача менеджера, управляющая выводом GPIO
#[task]
async fn gpio8_manager_task(mut gpio8: Output< 'static>) {
let rx = GPIO8_CHANNEL.receiver();

loop {
if let cmd = rx.receive().await {
match cmd {
GpioCommand::SetHigh => gpio8.set_high(),
GpioCommand::SetLow => gpio8.set_low(),
GpioCommand::Toggle => gpio8.toggle(),
GpioCommand::SetState(state) => {
if state {
gpio8.set_high();
} else {
gpio8.set_low();
}
}
}
}
} }

#[esp_hal_embassy::main]
async fn main(spawner: Spawner) {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
info!("Embassy инициализирован");
// TODO: здесь можно инициализировать другие задачи вызовом spawner.spawn
let _ = spawner;
...
// Инициализация на выход ножки порта 8:
let gpio8 = Output::new(
peripherals.GPIO8,
Level::Low, // начальное состояние вывода порта лог. 0
OutputConfig::default()
);
// Порождение задачи менеджера - инициализирует систему управления
// ножкой порта 8:
spawner.spawn(gpio8_manager_task(gpio8)).unwrap();
// Теперь можно в любом месте программы (в том числе и в любых
// потоках) вызывать функцию set_gpio8, передавая в неё параметр типа
// GpioCommand, а также функции gpio8_high, gpio8_low, gpio8_toggle,
// например:
// set_gpio8(GpioCommand::Toggle).await;
// gpio8_low().await;

let mut tick = Ticker::every(Duration::from_millis(1000));
info!("инициализация завершена, вход в бесконечный цикл main");
loop {
gpio8_toggle().await;
tick.next().await;
} }

При таком способе физический доступ к аппаратному ресурсу вывода GPIO8 предоставлен исключительно только для задачи gpio8_manager_task, все другие контексты могут переключить состояние вывода только путем отправки сообщения в канал GPIO8_CHANNEL. Эти сообщения посылаются с помощью вызова функции set_gpio8, принимающей необходимое состояние для вывода GPIO8.

[Способ 2: мьютекс с глобальным экземпляром]

use embassy_sync::mutex::Mutex;

// Глобальный мьютекс GPIO8, используемый между задачами
static GPIO8: Mutex< CriticalSectionRawMutex, RefCell< Option< Output< 'static>>>> =
Mutex::new(RefCell::new(None));

// Инициализация глобального мьютекса GPIO8
pub async fn init_gpio8(gpio8: Output< 'static>) {
*GPIO8.lock().await.borrow_mut() = Some(gpio8); }

// Глобальные функции управления:
pub async fn set_gpio8_high() {
let guard = GPIO8.lock().await;
let mut borrow = guard.borrow_mut();
if let Some(gpio) = borrow.as_mut() {
gpio.set_high();
} }

pub async fn set_gpio8_low() {
let guard = GPIO8.lock().await;
let mut borrow = guard.borrow_mut();
if let Some(gpio) = borrow.as_mut() {
gpio.set_low();
} }

pub async fn toggle_gpio8() {
let guard = GPIO8.lock().await;
let mut borrow = guard.borrow_mut();
if let Some(gpio) = borrow.as_mut() {
gpio.toggle();
} }

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

Пример использования:

#[esp_hal_embassy::main]
async fn main(spawner: Spawner) {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
info!("Embassy инициализирован");
// В этом месте можно создавать другие задачи вызовами spawner.spawn
let _ = spawner;
...

let mut tick = Ticker::every(Duration::from_millis(1000));

// Инициализация порта 8:
let gpio8 = Output::new(
peripherals.GPIO8,
Level::Low, // начальное состояние вывода порта 8
OutputConfig::default()
);
init_gpio8(gpio8).await;
// Теперь в любом месте программы (в том числе и в других задачах)
// можно вызывать функции set_gpio8_high().await, set_gpio8_low().await,
// toggle_gpio8().await.

info!("Инициализация завершена, вход в бесконечный цикл main");
loop {
toggle_gpio8().await;
tick.next().await;
} }

[Способ 3: примитив атомарного доступа к данным]

В этом способе применяется модуль atomic [2].

use core::sync::atomic::{AtomicBool, Ordering};

static GPIO8_STATE: AtomicBool = AtomicBool::new(false);

pub fn set_gpio8_state(state: bool) {
GPIO8_STATE.store(state, Ordering::SeqCst);
// Замечание: вам все еще необходимо обновить аппаратуру GPIO,
// см. функцию main далее. }

pub fn gpio8_high() {
set_gpio8_state(true); }

pub fn gpio8_low() {
set_gpio8_state(false); }

// Периодическая задача для применения состояния аппаратуры
#[task]
async fn gpio8_hardware_task(mut gpio8: Output< 'static>) {
let mut current_state = false;

loop {
let new_state = GPIO8_STATE.load(Ordering::SeqCst);

if new_state != current_state {
if new_state {
gpio8.set_high();
} else {
gpio8.set_low();
} current_state = new_state;
}

Timer::after_millis(10).await;
} }

#[esp_hal_embassy::main]
async fn main(spawner: Spawner) {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
info!("Embassy инициализирован");
let _ = spawner;
// Здесь можно запустить другие задачи вызовом spawner.spawn
...

let mut tick = Ticker::every(Duration::from_millis(1000));
let gpio8 = Output::new(
peripherals.GPIO8,
Level::Low,
OutputConfig::default()
);

// Запуск задачи менеджера, она инициализирует систему управления
// ножкой порта 8:
spawner.spawn(gpio8_hardware_task(gpio8)).unwrap();
// Теперь в любом месте кода (в том числе и в разных задачах)
// можно управлять состоянием выхода порта 8, вызывая
// функции gpio8_high() и gpio8_low().

info!("инициализация завершена, вход в бесконечный цикл main");
loop {
gpio8_low();
tick.next().await;
gpio8_high();
} }

[Рекомендации]

Лучше использовать передачу сообщений (способ 1), потому что:

1. Thread-safe: не допускается прямое управление аппаратурой для задач.
2. Развязка операций: логика управления отделена от доступа к аппаратуре.
3. Гибкость: легко добавить новые команды для изменения поведения.
4. Простота поддержки: централизованная логика управления GPIO (которую можно перенести в отдельный модуль).
5. Async-friendly: хорошо работает с асинхронной экосистемой задач Embassy [3].
6. Глобальные функции предоставляют понятный и безопасный API, который можно вызывать из любой задачи и/или обработчиков прерывания, не беспокоясь о блокировках мьютекса или аппаратных конфликтах.

[Ссылки]

1. ESP32-C3 SuperMini site:dl.artronshop.co.th.
2. Module atomic site:doc.rust-lang.org.
3. Embassy on ESP: Getting Started site:dev.to.
4. rustup + espup: тулчейн для Rust на платформе ESP32.

 

Добавить комментарий


Защитный код
Обновить

Top of Page