未来嵌入式系统的黄金搭档 MCX N947遇上Rust

描述

 Rust 介绍Rust 是一门注重安全的语言,相比于 C/C++/ASM 有着更高级的抽象能力、编译器带来的安全特性与广泛友好的社区支持。Linux 与 Windows 内核也都基于 Rust 的安全性和性能引入了 Rust。

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"

让我简单介绍各个依赖的作用:

 

  • cortex-m 该库引入了 Cortex-M 架构的定义和一些抽象,如常见的汇编指令,中断等

     

  • cortex-m-rt 这个库是 Cortex-M 架构的通用运行时,提供一套内置的linker script 和 ResetHandler 的实现等等

     

  • mcxn947-pac 包含了 MCXN947 的寄存器定义,中断定义

     

  • 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:

 

嵌入式系统

 

 

 

示例:按键开灯

此示例主要是介绍中断用法及Rust 的线程安全用法:

#![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 = 128
    MY_RAM : ORIGIN = 0x04000000, LENGTH = 16
}


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 
}

如果要把中断函数放在 RAM 里,需要一点额外操作,首先需要去掉过程宏 #[interrupt] 。
#[interrupt] 可以看作是#[export_name = ...]和一段代码展开的缩写。
我们可以去掉它,手动加上一些宏来达到同样的效果。首先添加 
#[no_mangle] 防止编译器对它重新命名,或者使用 #[export_name = ...] 来让它的名字是中断名。
然后添加
#[link_section = ...] 来让它链接到 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与实时工具,会为大家揭开更多技术奥秘,敬请持续关注,精彩不容错过!


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分