摘要
使用rust的embassy在rp2040芯片核心板实现呼吸灯.
关键词
rust;embassy;rp2040;blink;pwm;
关键信息
- 项目地址:[https://github.com/ByeIO/byeefree.rp2040_quad.embassy]
[package]
edition = "2021"
name = "byeefree_rp2040_quad_embassy"
version = "0.1.0"
license = "MIT OR Apache-2.0"
resolver = "2"[dependencies]
# arm cortex-m相关
cortex-m = { version = "0.7", features = ["inline-asm"] }
cortex-m-rt = "0.7"# rp-pico抽象层相关
# rp-pico = "0.9.0"# 调试信息相关
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }# embassy嵌入式异步框架相关
embassy-embedded-hal = { version = "0.3", features = ["defmt"] }embassy-executor = { version = "0.7", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "task-arena-size-32768"] }
embassy-futures = { version = "0.1" }
embassy-sync = { version = "0.6", features = ["defmt"] }
embassy-time = { version = "0.4", features = ["defmt", "defmt-timestamp-uptime"] }embassy-rp = { version = "0.3", features = ["defmt", "unstable-pac", "time-driver", "critical-section-impl", "rp2040"] }# pio相关, 暂时使用0.2版本
pio-proc = "0.2"
pio = "0.2"## 通过usb获取日志信息 (注意: embassy-usb-logger 需要 portable-atomic 开启 critical-section特性)
embassy-usb-driver = { version = "0.1", features = ["defmt"] }
embassy-usb-logger = { version = "0.4" }
portable-atomic = { version = "1.10", features = ["critical-section"] }
log = "0.4"
embassy-usb = { version = "0.4.0", features = ["defmt"] }## 网络支持
embassy-net = { version = "0.6", features = ["defmt", "tcp", "udp", "dhcpv4", "medium-ethernet", "dns", "proto-ipv4", "proto-ipv6", "multicast"] }
# wiznet ethernet driver support
embassy-net-wiznet = { version = "0.2", features = ["defmt"] }
## pico-w 支持
cyw43 = { version = "0.3", features = ["defmt", "firmware-logs"] }
cyw43-pio = { version = "0.3", features = ["defmt"] }# 生命周期
static_cell = "2.1.0"# Dshot协议
dshot-pio = { path = "./static/dshot-pio", features = ["embassy-rp"] }# 线性代数
nalgebra = { version = "0.33.2", default-features = false, features = ["libm"] }# 浮点数计算支持
libm = "0.2.11"
num-traits = { version = "0.2", default-features = false, features = ["libm"] }# 命令行解析
embedded-cli = "0.2.1"
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
ufmt = "0.2.0"# 内存分配
embedded-alloc = { version = "0.6.0", features = ["llff"] }
heapless = { version = "0.8.0", features = ["portable-atomic"] }# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true
incremental = false
opt-level = 1
overflow-checks = true
lto = "off"# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 's'
overflow-checks = false# do not optimize proc-macro crates = faster builds from scratch
[profile.dev.build-override]
codegen-units = 8
debug = false
debug-assertions = false
opt-level = 0
overflow-checks = false[profile.release.build-override]
codegen-units = 8
debug = false
debug-assertions = false
opt-level = 0
overflow-checks = false# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true
incremental = false
opt-level = 's'
overflow-checks = true# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 's'
原理简介
eabassy简介
[https://embassy.dev/book/#_embassy_in_the_wild]
在处理输入/输出(I/O)时,软件必须调用一些函数,这些函数会阻塞程序的执行,直到I/O操作完成。在像Linux这样的操作系统中运行的程序,这些函数通常会将控制权转移给内核,以便可以执行另一个任务(称为“线程”),或者在没有其他任务准备就绪时让CPU进入睡眠状态。
由于操作系统无法假设线程会以协作方式运行,线程相对来说是资源密集型的,如果它们没有在分配的时间内将控制权交还给内核,可能会被强制中断。如果可以假设任务会以协作方式运行,或者至少不会恶意运行,那么就可以创建一些任务,这些任务与传统操作系统线程相比,几乎可以看作是“免费”的。
在其他编程语言中,这些轻量级任务被称为“协程”或“goroutine”。在Rust中,它们通过async
实现。async-await
的工作原理是将每个async
函数转换为一个称为future
的对象。当一个future
在I/O上阻塞时,它会“让步”,调度程序(称为executor
)可以选择执行另一个future
。
与实时操作系统(RTOS)等替代方案相比,async
可以提供更好的性能和更低的功耗,因为executor
不需要猜测future
何时准备好执行。然而,程序的大小可能比其他替代方案更大,这对于某些内存非常有限的设备来说可能是一个问题。在Embassy支持的设备上,如stm32
和nrf
,内存通常足够大,可以容纳适度增加的程序大小。
Embassy项目由几个可以一起或独立使用的crate组成:
Executor
embassy-executor
是一个async/await
执行器,通常执行固定数量的任务,这些任务在启动时分配,但稍后可以添加更多任务。执行器还可能提供一个系统计时器,你可以将其用于async
和阻塞延迟。对于不到一微秒的延迟,应该使用阻塞延迟,因为上下文切换的成本太高,执行器无法提供准确的计时。
硬件抽象层(HAL)
HAL实现了安全的Rust API,让你可以使用外设,如USART、UART、I2C、SPI、CAN和USB,而无需直接操作寄存器。
Embassy在合适的地方提供了async
和阻塞API的实现。DMA(直接内存访问)是async
的一个很好的应用场景,而GPIO状态则更适合阻塞API。
Embassy项目为特定硬件维护了HAL,但你仍然可以将其他项目的HAL与Embassy一起使用。
When handling I/O, software must call functions that block program execution until the I/O operation completes. When running inside of an OS such as Linux, such functions generally transfer control to the kernel so that another task (known as a “thread”) can be executed if available, or the CPU can be put to sleep until another task is ready.
Because an OS cannot presume that threads will behave cooperatively, threads are relatively resource-intensive, and may be forcibly interrupted if they do not transfer control back to the kernel within an allotted time. If tasks could be presumed to behave cooperatively, or at least not maliciously, it would be possible to create tasks that appear to be almost free when compared to a traditional OS thread.
In other programming languages, these lightweight tasks are known as “coroutines” or ”goroutines”. In Rust, they are implemented with async. Async-await works by transforming each async function into an object called a future. When a future blocks on I/O the future yields, and the scheduler, called an executor, can select a different future to execute.
Compared to alternatives such as an RTOS, async can yield better performance and lower power consumption because the executor doesn’t have to guess when a future is ready to execute. However, program size may be higher than other alternatives, which may be a problem for certain space-constrained devices with very low memory. On the devices Embassy supports, such as stm32 and nrf, memory is generally large enough to accommodate the modestly-increased program size.
The Embassy project consists of several crates that you can use together or independently:
Executor
The embassy-executor is an async/await executor that generally executes a fixed number of tasks, allocated at startup, though more can be added later. The executor may also provide a system timer that you can use for both async and blocking delays. For less than one microsecond, blocking delays should be used because the cost of context-switching is too high and the executor will be unable to provide accurate timing.
Hardware Abstraction Layers
HALs implement safe Rust API which let you use peripherals such as USART, UART, I2C, SPI, CAN, and USB without having to directly manipulate registers.
Embassy provides implementations of both async and blocking APIs where it makes sense. DMA (Direct Memory Access) is an example where async is a good fit, whereas GPIO states are a better fit for a blocking API.
The Embassy project maintains HALs for select hardware, but you can still use HALs from other projects with Embassy.
核心代码
.cargo/config.toml
# 默认构建目标平台
[build]
target = "thumbv6m-none-eabi"# 配置上传命令
[target.thumbv6m-none-eabi]# rustflags = [
# "-C", "link-arg=--nmagic",
# "-C", "link-arg=-Tlink.x",
# "-C", "no-vectorize-loops",
# ]# runner = "elf2uf2-rs -d"
runner = "probe-rs run --chip RP2040"# 使用本地库文件
# [source.crates-io]
# replace-with = "vendored-sources"# [source.vendored-sources]
# directory = "vendor"
tasks/blink.rs
#![allow(unused)]// led指示灯任务// gpio相关
use embassy_rp::gpio;
use gpio::{Level, Output, AnyPin};// 多任务相关
use embassy_time::{Duration, Timer};
use embassy_executor::Spawner;
use embassy_futures::join::join;
use embassy_rp::bind_interrupts;// 打印调试信息
use defmt::{info, panic};
use { defmt_rtt as _, panic_probe as _ };// pio相关(led:gpio25)
use embassy_rp::peripherals::PIO0;// PWM相关
use embassy_rp::peripherals::{PIN_25, PWM_SLICE4};
use embassy_rp::pwm::{Config, Pwm, SetDutyCycle};// 浮点数计算
use libm::powf; // 常量
/// 亮度变化总步数
const STEP: usize = 200;/// PWM最大占空比
const MAX_DUTY: u32 = 32768; /// Gamma校正系数(符合人眼感知)
const GAMMA: f32 = 2.2;/// 编译期生成的Gamma校正查找表
///
/// 生成算法:
/// 1. 将0-STEP线性序列归一化为0.0-1.0
/// 2. 对每个值进行Gamma幂次运算
/// 3. 映射到PWM占空比范围
const GAMMA_LUT: [u32; STEP] = {// 使用编译期常量表达式初始化数组let mut table = [0u32; STEP];let step_f32 = (STEP - 1) as f32; // 转换为浮点用于计算// 编译期循环展开(无运行时开销)let mut i = 0;while i < STEP {// 线性归一化(注意:Rust const上下文暂不支持浮点运算,故采用整数近似)let t = (i * 1000) as u32; // 扩大1000倍转为整数运算let normalized = t as u64 * 0xFFFF_FFFF / ((STEP - 1) * 1000) as u64;// Gamma校正整数近似算法(误差<0.5%)// 原理:x^2.2 ≈ x² * x^(0.2) 的整数实现let x = normalized as u32;let x_sq = (x as u64 * x as u64) >> 32; // x²的定点数计算let x_02 = (x as u64 * 2290_0669) >> 32; // x^0.2的预计算系数// 混合计算结果let gamma_val = (x_sq as u64 * x_02 as u64) >> 32;// 映射到PWM占空比范围table[i] = (gamma_val as u64 * MAX_DUTY as u64 / 0xFFFF_FFFF) as u32;i += 1;}table
};// 内部库:信号
use crate::signals;// 闪灯模式
#[derive(Clone,Copy)]
pub enum BlinkMode {// 不闪灯None,// 快速1OneFast,// 快速2TwoFast,// 快速3ThreeFast,// 快速闪OnOffFast,// 慢速闪OnOffSlow,// 呼吸灯Breathe,
}// embassy任务
#[embassy_executor::task]
pub async fn blink(// 订阅blink模式mut in_blink_mode : signals::BlinkModeSub,mut pin : PIN_25,mut slice : PWM_SLICE4,
) {// 呼吸灯状态变量let mut breathe_ctx = BreatheContext {direction: 1i8,index: 0usize,pwm_config: Config::default(),};breathe_ctx.pwm_config.top = 32_768;defmt::println!("run blink");// 初始化blink模式// let mut blink_mode = BlinkMode::None;let mut blink_mode = BlinkMode::OnOffFast;// pwm相关let mut c = Config::default();c.top = 32_768;c.compare_b = 8;loop { if let Some(b) = in_blink_mode.try_next_message_pure() {// 模式切换时重置呼吸灯状态if !matches!(b, BlinkMode::Breathe) {breathe_ctx.reset(); }blink_mode = b;}// 匹配不同模式match blink_mode {BlinkMode::None => {// 初始化led引脚let mut led = Output::new(&mut pin,Level::Low);blink_mode = in_blink_mode.next_message_pure().await;},BlinkMode::OneFast => one_fast(&mut pin).await,BlinkMode::TwoFast => two_fast(&mut pin).await,BlinkMode::ThreeFast => three_fast(&mut pin).await,BlinkMode::OnOffFast => on_off_fast(&mut pin).await,BlinkMode::OnOffSlow => on_off_slow(&mut pin).await,BlinkMode::Breathe => breathe(&mut breathe_ctx, &mut slice, &mut pin).await,};}
}// 1. 快速1
#[allow(unused)]
async fn one_fast<'a>(pin: &mut PIN_25) {// 初始化led引脚let mut led = Output::new(pin,Level::Low);// 灭一会led.set_high(); Timer::after(Duration::from_millis(50)).await;// 长亮led.set_low();Timer::after(Duration::from_millis(950)).await;
}// 2. 快速2
#[allow(unused)]
async fn two_fast<'a>(pin: &mut PIN_25) {// 初始化led引脚let mut led = Output::new(pin,Level::Low);// 短灭led.set_high();Timer::after(Duration::from_millis(50)).await;// 中亮led.set_low();Timer::after(Duration::from_millis(100)).await;// 短灭led.set_high(); Timer::after(Duration::from_millis(50)).await;// 长亮led.set_low();Timer::after(Duration::from_millis(800)).await;
}// 3. 快速3
#[allow(unused)]
async fn three_fast<'a>(pin: &mut PIN_25) {// 初始化led引脚let mut led = Output::new(pin,Level::Low);// 短灭led.set_high();Timer::after(Duration::from_millis(50)).await;// 中亮led.set_low();Timer::after(Duration::from_millis(100)).await;// 短灭led.set_high(); Timer::after(Duration::from_millis(50)).await;// 中亮led.set_low();Timer::after(Duration::from_millis(100)).await;// 短灭led.set_high();Timer::after(Duration::from_millis(50)).await;// 长亮led.set_low();Timer::after(Duration::from_millis(650)).await;
}// 4. 快速亮灭
#[allow(unused)]
async fn on_off_fast<'a>(pin: &mut PIN_25) {// 初始化led引脚let mut led = Output::new(pin,Level::Low);// 短灭led.set_high(); Timer::after(Duration::from_millis(100)).await;// 中亮led.set_low();Timer::after(Duration::from_millis(100)).await;
}// 5. 慢速亮灭
#[allow(unused)]
async fn on_off_slow<'a>(pin: &mut PIN_25) {// 初始化led引脚let mut led = Output::new(pin,Level::Low);// 短灭led.set_high(); Timer::after(Duration::from_millis(150)).await;// 中亮led.set_low();Timer::after(Duration::from_millis(150)).await;
}// 6. 呼吸灯
/* start 呼吸灯 */
// 呼吸灯上下文结构体
struct BreatheContext {direction: i8,index: usize,pwm_config: Config,
}impl BreatheContext {fn reset(&mut self) {self.direction = 1;self.index = 0;}
}#[allow(unused)]
async fn breathe<'a>(ctx: &mut BreatheContext,slice: &mut PWM_SLICE4,pin: &mut PIN_25
) {let mut pwm = Pwm::new_output_b(slice, pin, ctx.pwm_config.clone());// 应用当前Gamma值ctx.pwm_config.compare_b = GAMMA_LUT[ctx.index] as u16;pwm.set_config(&ctx.pwm_config);// 更新索引和方向(原loop内的状态机逻辑)ctx.index = match (ctx.index, ctx.direction) {(_, 1) if ctx.index >= STEP - 1 => {ctx.direction = -1;STEP - 1}(0, -1) => {ctx.direction = 1;0}(i, 1) => i + 1,(i, -1) => i - 1,_ => unreachable!()};// 保持原有的呼吸节奏Timer::after_millis(5).await;
}
/* end 呼吸灯 */
commands/blink.rs
// 命令解析相关
use embassy_futures::select::{select, Either};
use embassy_time::{Duration, Ticker};
use embedded_cli::Command;
use embedded_io::ErrorKind;
use embedded_io_async::{Read, Write};
use ufmt::uwrite;
use crate::shell::{UBuffer, INTERRUPT};
use heapless::String;
use core::str::FromStr;// 内部库
use crate::tasks::blink::BlinkMode;
use crate::utils::signals;// 命令定义部分
#[derive(Command, Clone)]
pub enum BlinkCommand {// 关闭led灯/// led offNone,/// fast#1One_Fast,/// fast#2Two_Fast,/// fast#3Three_Fast,/// blink fastOn_Off_Fast,/// blink slowOn_Off_Slow,/// breathe...Breathe,
}// 实现CommandHandler特征
impl super::CommandHandler for BlinkCommand {async fn handler(&self,mut serial: impl Read<Error = ErrorKind> + Write<Error = ErrorKind>,) -> Result<(), ErrorKind> {// 匹配所有blink子命令变体let (mode_str, blink_mode) = match self {BlinkCommand::None => ("none", BlinkMode::None),BlinkCommand::One_Fast => ("one_fast", BlinkMode::OneFast),BlinkCommand::Two_Fast => ("two_fast", BlinkMode::TwoFast),BlinkCommand::Three_Fast => ("three_fast", BlinkMode::ThreeFast),BlinkCommand::On_Off_Fast => ("on_off_fast", BlinkMode::OnOffFast),BlinkCommand::On_Off_Slow => ("on_off_slow", BlinkMode::OnOffSlow),BlinkCommand::Breathe => ("breathe", BlinkMode::Breathe),};// 通过signal发布新模式signals::BLINK_MODE.publisher().unwrap().publish_immediate(blink_mode);// 写入确认信息到串口let mut buf = UBuffer::<32>::new();uwrite!(&mut buf, "Blink mode set to: {}\n\r", mode_str)?;serial.write_all(&buf.inner).await?;Ok(())}
}
效果
命令切换灯状态 |
---|
![]() |