基础示例:SSD1306 OLED显示

这里用一个很简单的SSD1306 OLED显示例子带大家感受一下Embassy工程的创建和编写。

使用cargo-embassy创建工程

embassy提供了cargo-embassy,我们只需要选择对应的MCU即可。

cargo install cargo-embassy
cargo embassy init ssd_demo --chip stm32g031g8
cd ssd_demo

可以看到直接生成了工程

├──.cargo
│  └──config.toml
├──src
│  ├──fmt.rs
│  └──main.rs
├──build.rs
├──Cargo.lock
├──Cargo.toml
├──Embed.toml
└──rust-toolchain.toml

对于main文件中核心就是打印Hello, World!,然后led闪烁

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let mut led = Output::new(p.PB7, Level::High, Speed::Low);

    loop {
        info!("Hello, World!");
        led.set_high();
        Timer::after(Duration::from_millis(500)).await;
        led.set_low();
        Timer::after(Duration::from_millis(500)).await;
    }
}

在目录中执行

cargo run

就能自动编译并运行示例,可以看到命令行中打印对应示例的输出

    Finished `dev` profile [optimized + debuginfo] target(s) in 23.37s
     Running `probe-rs run --chip STM32G031G8Ux target\thumbv6m-none-eabi\debug\ssd_demo`
      Erasing ✔ [00:00:00] [###############################################################################################################################################] 14.00 KiB/14.00 KiB @ 54.25 KiB/s (eta 0s )  Programming ✔ [00:00:00] [###############################################################################################################################################] 14.00 KiB/14.00 KiB @ 36.94 KiB/s (eta 0s )    Finished in 0.662s
TRACE BDCR configured: 00008200
└─ embassy_stm32::rcc::bd::{impl#3}::init::{closure#4} @ C:\Users\User\.cargo\registry\src\index.crates.io-6f17d22bba15001f\embassy-stm32-0.2.0\src\rcc\bd.rs:280
DEBUG rcc: Clocks { hclk1: MaybeHertz(16000000), hsi: MaybeHertz(16000000), lse: MaybeHertz(0), lsi: MaybeHertz(0), pclk1: MaybeHertz(16000000), pclk1_tim: MaybeHertz(16000000), pll1_p: MaybeHertz(0), pll1_q: MaybeHertz(0), rtc: MaybeHertz(32000), sys: MaybeHertz(16000000) }
└─ embassy_stm32::rcc::set_freqs @ C:\Users\User\.cargo\registry\src\index.crates.io-6f17d22bba15001f\embassy-stm32-0.2.0\src\rcc\mod.rs:71
INFO  Hello, World!
└─ ssd_demo::____embassy_main_task::{async_fn#0} @ src\fmt.rs:131
INFO  Hello, World!
└─ ssd_demo::____embassy_main_task::{async_fn#0} @ src\fmt.rs:131
INFO  Hello, World!
└─ ssd_demo::____embassy_main_task::{async_fn#0} @ src\fmt.rs:131

从头开始创建工程

根据embassy-book中的指导我们也能一步一步开始我们的示例 首先我们创建一个Rust工程

cargo new ssd1306_demo
cd ssd1306_demo

我们生成了一个空的工程

├──src
│  └──main.rs
└──Cargo.toml

我们使用的处理器为stm32g031,打开Embassy中对应示例 可以看到有个.cargo/config.toml文件,我们将配置文件复制过来 示例中使用的是

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# replace STM32G0B1RETx with your chip as listed in `probe-rs chip list`
runner = "probe-rs run --chip STM32G0B1RETx"
[build]
target = "thumbv6m-none-eabi"
[env]
DEFMT_LOG = "trace"

STM32G0B1RETx这个不是我们当前的型号。 查看支持的型号可以使用

probe-rs chip list

使用的是STM32G031G8Ux这里替换上面runner中的STM32G0B1RETx 现在编译器和调试器已经知道我们使用的指令集和具体芯片类型。

下面添加embassy及其支持库的依赖

[dependencies]
embassy-executor = { version = "0.7.0", features = ["arch-cortex-m", "executor-thread"] }
embassy-futures = "0.1.1"
embassy-stm32 = { version = "0.2.0", features = ["memory-x", "stm32g031g8", "time-driver-any", "exti", "unstable-pac"] }
embassy-sync = "0.6.2"
embassy-time = { version = "0.4.0", features = ["tick-hz-32_768"] }

以及其他构建依赖

defmt = "0.3.5"
defmt-rtt = "0.4.0"
cortex-m = {version = "0.7.7", features = ["critical-section-single-core"]}
cortex-m-rt = "0.7.3"
panic-probe = "0.3.1"

最后,将[profile.release]示例中的部分复制Cargo.toml到我们的部分中。

[profile.release]
debug = 2

embassy处于不断更新中,推荐尝试从git仓库安装,这里通过命令找到最新的

git ls-remote https://github.com/embassy-rs/embassy.git HEAD
fb5ce05b26ae0c90a872a8e0787c9419178d475a        HEAD

在Cargo.toml中添加

[patch.crates-io]
embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "fb5ce05b26ae0c90a872a8e0787c9419178d475a" }
embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "fb5ce05b26ae0c90a872a8e0787c9419178d475a" }
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "fb5ce05b26ae0c90a872a8e0787c9419178d475a" }

最后从示例中复制构建脚本build.rs到项目中

fn main() {
    println!("cargo:rustc-link-arg-bins=--nmagic");
    println!("cargo:rustc-link-arg-bins=-Tlink.x");
    println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}

这个构建脚本的作用是为嵌入式Rust项目配置链接器参数,这是嵌入式Rust项目的标准配置之一。

目录结构为

├──.cargo
│  └──config.toml
├──src
│  └──main.rs
├──build.rs
└──Cargo.toml

在example中复制一个我们的示例到main.rs中并执行

cargo run

完成构建烧录和调试

添加屏幕显示

SSD1306是一款OLED屏幕,嵌入式中经常使用它来显示信息。 Rust提供了包管理器,配合embedded-hal的跨平台功能我们可以找到SSD1306 Rust驱动并添加到Cargo.toml中

[dependencies]
ssd1306 = "0.10.0"

由于我们在embassy中使用的是async,所以需要添加features

[dependencies]
ssd1306 = { version = "0.10.0", features = ["async"] }

下面在main.rs中添加i2c初始化

Embassy为每个具体MCU型号生成特定的硬件抽象层代码。在这个过程中,只有该MCU实际拥有的引脚和功能才会被包含在Peripherals结构体中。由于Rust的强类型系统,当你尝试访问不存在的外设或引脚时,编译器能立即检测到问题并报错。在这里如果我们初始化错误的引脚将会报错

#![allow(unused)]
fn main() {
let p = embassy_stm32::init(Default::default());
let mut i2c = I2c::new(p.I2C1, p.PB9, p.PB7, Irqs, p.DMA1_CH1, p.DMA1_CH2, Hertz(100_000), Default::default());
}

报错如下

error[E0609]: no field `PB9` on type `Peripherals`
  --> src\main.rs:28:11
   |
28 |         p.PB9,
   |           ^^^ unknown field
   |

这种将硬件功能的约束直接体现在类型系统中的设计对比传统C语言外设库有很大的优势。

正确初始化I2C

#![allow(unused)]
fn main() {
    let i2c = embassy_stm32::i2c::I2c::new(
        p.I2C1,
        p.PB6,
        p.PB7,
        Irqs,
        p.DMA1_CH1,
        p.DMA1_CH2,
        Hertz::khz(400),
        Default::default(),
    );
}

创建SSD1306显示屏的I2C接口

#![allow(unused)]
fn main() {
    let interface = I2CDisplayInterface::new(i2c);
}

创建SSD1306显示屏实例 指定显示屏尺寸为128x64像素 设置显示方向为不旋转(Rotate0) 转换为终端模式,便于文本显示

#![allow(unused)]
fn main() {
    let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
        .into_terminal_mode();
}

初始化显示屏

#![allow(unused)]
fn main() {
display.init().await.unwrap();
let _ = display.clear().await;
}

至此完成了初始化,添加显示

#![allow(unused)]
fn main() {
let _ = display.write_str("Hello Rust!").await;
}

下面就能在屏幕中看到Hello Rust!

最终用很简单的代码就能实现屏幕显示,总体代码如下

//src/main.rs
#![no_std]
#![no_main]
use defmt_rtt as _;
use embassy_executor::Spawner;
use embassy_stm32::{bind_interrupts, i2c, peripherals, time::Hertz};
use panic_probe as _;
use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306Async};
bind_interrupts!(struct Irqs {
    I2C1 => i2c::EventInterruptHandler<peripherals::I2C1>, i2c::ErrorInterruptHandler<peripherals::I2C1>;
});
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let i2c = embassy_stm32::i2c::I2c::new(p.I2C1,p.PB6,p.PB7,Irqs,p.DMA1_CH1,p.DMA1_CH2,Hertz::khz(400),Default::default(),);
    let interface = I2CDisplayInterface::new(i2c);
    let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
        .into_terminal_mode();
    display.init().await.unwrap();
    let _ = display.clear().await;
    //显示Hello Rust
    let _ = display.write_str("Hello Rust!").await;
    loop {

    }
}

Irqs 结构体通过 bind_interrupts! 宏将硬件中断与处理程序连接起来。
在 I2C 初始化过程中,这些中断处理程序被注册到系统中。
当 I2C 通信事件发生(如数据传输完成或发生错误)时,相应的中断被触发。
中断处理程序作为 waker 的实现,负责唤醒等待这些事件的异步任务。 这样的设计使得代码可以使用 async/await 风格编程,而不需要阻塞式等待或手动轮询。当 I2C 操作(如读/写)正在进行时,CPU 可以执行其他任务,只有当操作完成(触发中断)时才会返回继续执行异步函数中未完成的部分。
这正是 Embassy 框架的核心优势之一:将硬件中断机制与 Rust 的异步编程模型无缝集成。

小结

通过这个基础示例,我们了解了Embassy的创建工程、初始化、显示等基本操作。 Embassy提供了大量不同平台示例可以直接到Embassy示例中查看。