Rust 有很多优势,内存安全、并发安全、生态系统、包管理与构建管理,同时也有与 C/C++ 相同等级的性能。Rust 通过强化所有权和借用的概念,尽力消除了开发过程中可能出现的内存问题。同时作为一门现代语言,有着许多方便的特性与丰富的生态资源,如统一的包管理,高可读代码等等。
但 Rust 也有美中不足,如缺乏对底层的完全控制,学习难度高,编译时间长等。由于 Rust 的安全与高抽象能力,许多非安全操作被禁止,许多在 C 中能够通过指针进行的简单操作在 Rust 中需要十分复杂的操作,这也导致 Rust 的学习难度更高。
本文会对在 NXP MCX 平台上使用 Rust 进行简单介绍。在本文中使用 FRDM-MCXN947 为例,所有的例子均运行在 Core0 上。
安装Rust工具链Rust 工具链的安装十分简单,参考 Rustup 即可。默认状态下,Rustup 工具只会安装本机的 TARGET ,为了能够在我们的 MCU 上运行编译产物, 需要安装对应的 TARGET 。可以通过运行如下命令来添加 armv8m hard-float 支持。
rustup target add thumbv8m.main-none-eabihf
同样,如果我们想为其他平台编译,像 cortex-m3 ,则需要运行:
rustup target add thumbv8m.main-none-eabihf
如果要在没有 FPU 的 Core1 上运行,则需要使用命令rustup target add thumbv8m.main-none-eabi 添加 thumbv8m.main-none-eabi target ,注意无 hf 尾缀, 代表不使用硬件浮点数 ABI
创建项目在添加支持后,在我们想要保存项目的文件夹运行命令 cargo new mcx-example 创建一个名为 mcx-example 的工程, 同时创建一个配置文件 .cargo/config.toml 来指定编译参数和默认 target。
创建的 .cargo/config.toml 文件内容如下:
[build]
target = "thumbv8m.main-none-eabihf"
[target.thumbv8m.main-none-eabihf]
rustflags = ["-C", "link-arg=-Tlink.x"]
接下来添加必要的依赖, Cargo.toml 的内容应该与下面的内容类似:
[package]
name = "mcx-example"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.3"
mcxn947-pac = "0.0.3"
panic-halt = "0.2.0"
让我简单介绍各个依赖的作用:
panic-halt 实现默认的 panic handler
说起具体介绍,当然举个例子-点灯:
// src/main.rs
#![no_std] // 无标准库
#![no_main] // 无入口
// 提供一个 panic 实现
extern crate panic_halt;
use cortex_m_rt::entry;
use mcxn947_pac as pac;
// entry 标志在 Reset 中跳转到此
#[entry]
fn main() -> ! {
let dp = pac::take().unwrap();
let cp = pac::take().unwrap();
// 启用 PORT0 和 GPIO0 的时钟
dp.SYSCON0
.ahbclkctrl0()
.modify(|_r, w| w.port0().enable().gpio0().enable());
// 设置 PIO0_10 为推挽输出
dp.PORT0.pcr(10).modify(|_r, w| w.mux().mux00());
dp.GPIO0.pdor().modify(|_r, w| w.pdo10().clear_bit());
dp.GPIO0.pddr().modify(|_r, w| w.pdd10().set_bit());
// cortex-m 库提供的方便的抽象,使用 SysTick timer 来进行延时
// 在默认情况下 SysTick 的频率与主频相同,在这段代码中我们没有对时钟进行配置,所以默认为48MHz
let mut delay = cortex_m::new(cp.SYST, 48_000_000u32);
loop {
delay.delay_ms(1000u32);
dp.GPIO0.ptor().write(|w| w.ptto10().set_bit());
}
}
#![no_main] 指定不向外暴露符号 main, 所以即使我们的代码中有 main 函数,它也不会被当作真正的 “main” 函数看待。同时 #[entry] 标志该函数被链接到 cortex-m-rt 库中内置链接脚本中的 “main” 函数。
添加一份 memory.x ,这是一份 linker script ,在 cortex-m-rt 库中包含的默认 linker script 中有 include memory.x 的定义。所以我们需要添加一份,顾名思义,这份文件包含内存定义,同时如果我们想把特定数据或函数放在某个段也是可以在这定义。
MEMORY {
FLASH : ORIGIN = 0x00000000, LENGTH = 2M
RAM : ORIGIN = 0x20000000, LENGTH = 320K
}
运行命令 cargo build 进行构建,产物位于 target/thumbv8m.main-none-eabihf/debug/mcx-example 格式为 elf 。
使用任意工具把产物加载到 MCU 上, 就以 jlink 为例,首先把产物转成 hex 格式, arm-none-eabi-objcopy -O ihex target/thumbv8m.main-none-eabihf/debug/mcx-example mcx-example.hex, 然后使用 jflashLite 加载。
功能正常实现。
如果需要 Debug, 请参考使用VSCode调试嵌入式程序:配置与使用多样化的gdb server:
#![no_std]
#![no_main]
extern crate panic_halt;
use core::cell::{Cell, RefCell};
use cortex_m::{asm::wfi, interrupt::Mutex};
use cortex_m_rt::entry;
use mcxn947_pac as pac;
use pac::interrupt;
// Rust 的安全特性要求
static FLAG_BTN_PRESSED: Mutex> = Mutex::new(false)); |
static GPIO0: Mutex>> = Mutex::new(None));
#[entry]
fn main() -> ! {
let dp = pac::take().unwrap();
let cp = pac::take().unwrap();
// 启用 PORT0 和 GPIO0 的时钟
dp.SYSCON0
.ahbclkctrl0()
.modify(|_r, w| w.port0().enable().gpio0().enable());
// 设置 PIO0_10 为推挽输出
dp.PORT0.pcr(10).modify(|_r, w| w.mux().mux00());
dp.GPIO0.pdor().modify(|_r, w| w.pdo10().clear_bit());
dp.GPIO0.pddr().modify(|_r, w| w.pdd10().set_bit());
// 设置 PIO0_6
dp.PORT0.pcr(6).modify(|_r, w| w.mux().mux00());
dp.GPIO0.pddr().modify(|_r, w| w.pdd6().clear_bit());
dp.GPIO0
.icr(6)
.write(|w| w.isf().clear_bit_by_one().irqs().irqs0().irqc().irqc10());
// 启用 GPIO00 中断
unsafe { pac::GPIO00) }
// 在关闭中断的情况下向全局变量写入数据
// why: GPIO0 可能在 main 与 GPIO00 中共享
cortex_m::free(|cs| {
GPIO0.borrow(cs).replace(dp.GPIO0.into());
});
loop {
wfi();
cortex_m::free(|cs| {
if FLAG_BTN_PRESSED.borrow(cs).get() {
GPIO0
.borrow(cs)
.borrow_mut()
.as_mut()
.unwrap()
.ptor()
.write(|w| w.ptto10().set_bit());
}
})
}
}
// GPIO00 中断
#[interrupt]
fn GPIO00() {
cortex_m::free(|cs| {
let mut gpio = GPIO0.borrow(cs).borrow_mut();
gpio.as_mut()
.unwrap()
.icr(6)
.modify(|_r, w| w.isf().clear_bit_by_one());
FLAG_BTN_PRESSED.borrow(cs).set(true);
})
}
在上面这段代码中我们当然可以不使用 Mutex ,而是直接使用一个 static mut FLAG_BTN_PRESSED: bool = false ,但有关于该变量的所有操作都需要使用 unsafe 标签,这在正常的开发过程中应该是极力避免的,因为这种 unsafe 操作会导致 data race 。Mutex 是一个简单包装,使用一个 CriticalSection 标志来实现,cortex_m::free 提供一个简单的临界区实现,即关闭所有中断。或者可以使用原子操作 core::AtomicBool, static FLAG_BTN_PRESSED: AtomicBool = AtomicBool::new(false);
cortex_m::free 使用一个 lambda 函数来在其操作前后添加关闭、打开中断的操作。
虽然看起来写起来很麻烦,但实际上编译结果并没有多余的操作,所有看上去繁琐的操作实际上只是指导编译器如何编译,并不会生成实际的代码,例如中断中的 cs 变量,它并没有实际的大小。
同样烧录进 MCU ,按下 ISP 按键即可控制灯的开关。
使用HAL
上述两个例子均直接使用寄存器进行操作,方法过于原始,可以使用 HAL 库来简化操作。HAL 库正在积极开发中,所以使用 GitHub 上的最新版本。添加依赖。
[package]
name = "mcx-example"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.3"
mcxn947-pac = "0.0.3"
panic-halt = "0.2.0"
mcx-hal = { git = "https://github.com/mcx-rs/mcx-hal.git" }
#![no_std]
#![no_main]
use embedded_hal::digital::StatefulOutputPin;
use panic_halt as _;
use core::cell::{Cell, RefCell};
use cortex_m::asm::wfi;
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use mcx_hal::{self as hal, pac, pac::interrupt};
type BtnType = hal::PIO0_6>;
static FLAG_BTN_PRESSED: Mutex> = Mutex::new(false)); |
static BTN: Mutex>> = Mutex::new(None));
#[entry]
fn main() -> ! {
let dp = pac::take().unwrap();
// 设置 pin 的状态更方便了
let gpio0 = hal::split(dp.GPIO0, dp.PORT0);
let mut btn = gpio0.pio0_6.into_floating_input();
let mut led_r = gpio0.pio0_10.into_push_pull_output();
btn.enable_irq(
hal::FallingEdge,
hal::IRQ0,
);
cortex_m::free(|cs| {
BTN.borrow(cs).replace(Some(btn));
});
// enable GPIO0 irq
unsafe {
pac::GPIO00);
}
loop {
wfi();
cortex_m::free(|cs| {
if FLAG_BTN_PRESSED.borrow(cs).get() {
FLAG_BTN_PRESSED.borrow(cs).set(false);
led_r.toggle().unwrap();
}
});
}
}
#[interrupt]
fn GPIO00() {
cortex_m::free(|cs| {
let mut btn = BTN.borrow(cs).borrow_mut();
btn.as_mut().unwrap().clear_irq_flag();
FLAG_BTN_PRESSED.borrow(cs).set(true);
});
}
如果我们想要使用其他中断,该怎么知道它的名字呢?十分简单,所有的中断定义都在 mcxn947-pac::Interrupt 中。
这个例子同样是使用 ISP 按键来控制红灯的开关。
Linker script
将特定的数据放置在特定的位置,也是嵌入式开发中常见的操作,那么怎么在 Rust 上实现呢?
修改 memory.x 即可:
MEMORY {
FLASH : ORIGIN = 0x00000000, LENGTH = 1M
RAM : ORIGIN = 0x20000000, LENGTH = 128K
MY_RAM : ORIGIN = 0x04000000, LENGTH = 16K
}
SECTIONS {
.my_custom_data_in_my_ram (NOLOAD) : ALIGN(4) {
(.my_custom_data_in_my_ram.my_custom_data_in_my_ram.);
. = ALIGN(4);
} > MY_RAM
}
#[link_section = ".my_custom_data_in_my_ram.my_custom_name"]
#[no_mangle]
fn GPIO00() {
cortex_m::free(|cs| {
let mut btn = BTN.borrow(cs).borrow_mut();
btn.as_mut().unwrap().clear_irq_flag();
FLAG_BTN_PRESSED.borrow(cs).set(true);
});
}
使用命令 arm-none-eabi-size -Ax target/thumbv8m.main-none-eabihf/debug/mcx-example 或者使用命令 cargo install cargo-binutils && rustup component add llvm-tools ,之后就可以 cargo size -- -Ax.
查看编译结果,确认中断被我们放进了 MY_RAM 里:在 Debug 下,也可以看到中断时 PC 的地址:
希望以上内容可以对想使用 Rust 进行嵌入式开发的伙伴们提供指引与助力。
接下来,我们还将深入探索Rust 中的RTOS与实时工具,会为大家揭开更多技术奥秘,敬请持续关注,精彩不容错过!
全部0条评论
快来发表一下你的评论吧 !