- Rust嵌入式教程_哔哩哔哩_bilibili
- The Embedded Rust Book(中文版)
- https://github.com/knurling-rs/app-template
- https://github.com/stm32-rs/stm32f1xx-hal
STM32:STM32F103C8T6(Blue Pill)
1 配置开发环境
首先介绍一下博主使用的Rust嵌入式开发环境的配置。
1.1 VSCode Plugins
博主选择配置后的VSCode作为Rust嵌入式开发环境。首先创建一个新的配置文件(将各个插件之间彼此隔离),之后安装插件:rust
、Cortex-Debug
、Even Better TOML
、Dependi
。
1.2 probe-rs
probe-rs 是由Rust编写的嵌入式程序烧录工具,它的功能和OpenOCD比较类似,都用来烧录.elf
文件。很多Rust嵌入式的官方教程和官方模版工程都采用probe-rs作为例子,所以这里博主推荐使用probe-rs进行程序烧录。
博主使用下来,发现probe-rs在很多情况下比OpenOCD更好用。它相较于OpenOCD有以下优势:
- 开箱即用,基本无需配置,你甚至都不需要选择烧录器类型应该是st-link还是dap-link;
- 和Cargo有很好的兼容性,可以使用
cargo run
命令直接“无脑”烧录程序,开发体验统一; - C/C++工程也可以使用probe-rs进行烧录。
使用下面的命令安装 probe-rs(任选其一即可,有些方法可能安装速度较慢,此时考虑魔法上网或者换源):
# homebrew (for MacOS and some Linux)
brew install probe-rs/probe-rs/probe-rs
# windows
irm https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.ps1 | iex
# Cargo
cargo install cargo-binstall
cargo binstall probe-rs-tools
1.3 添加编译 target
根据使用的Cortex内核来添加编译target,如果使用STM32F103(Cortex-M3):
rustup target add thumbv7m-none-eabi
STM32H750为Cortex-M7F架构,拥有双精度浮点硬件运算单元(博主的H750芯片好像又有问题,创建工程后烧录提示找不到0x08000000
到0x08000298
的地址,非常奇怪):
rustup target add thumbv7em-none-eabihf
2 从 cargo new
创建工程
虽然可以使用 kunrling-rs 官方维护的模板库创建工程,但是博主认为这样的方法对于Rust嵌入式入门是不利的,因为很多细节对于入门的开发者是隐藏的,或者官方认为是“你应该提前知晓”的,这就导致官方的模板库对于初学者而言看起来非常臃肿。所以,一个简单而有效的方法是完全从零开始创建一个新的工程(尽量简洁而优雅),目的在于了解和学习工程中每个部件是如何运行的,以便于之后出问题了应该去哪里解决和追踪。
博主主要参考的资料来源于 stm32f1xx-hal ,在这里新建一个STM32F103点灯的工程。以供大家学习和参考:
2.1 工程创建
首先,使用cargo new led-f1
命令创建一个名为led-f1
的工程,并使用VSCode打开:
cargo new led-f1
cd led
code .
2.2 配置 Cargo 和 probe-rs
在工程根目录下创建memory.x
文件,并在其中填写如下内容:
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
这个文件为probe-rs提供可执行文件的烧录位置信息。注意,这里的FLASH
和RAM
的大小和起始地址和STM32F103C8T6
一致。如果你使用其他cortex微控制器,相应的参数需要改成对应的大小。
创建.cargo/config.toml
文件,并在文件中添加如下内容:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = 'probe-rs run --chip STM32F103C8'
rustflags = [
"-C", "link-arg=-Tlink.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
]
[build]
target = "thumbv7m-none-eabi"
简单解释一下上面的配置选项。首先,前面两行规定了cargo run
的行为:使用probe-rs将可执行文件烧录到STM32F103C8
芯片中。接下来,rustflags
定义了一系列编译选项,规定了编译烧录时使用link.x
脚本,当使用memory.x
定义FLASH
和RAM
的区域时这个脚本是必须的,而nmagic
参数在FLASH
和RAM
地址不在0x10000
时才需要添加。最后,[build]
模块指定了编译的目标(target),这里的值和最开始使用rustup add target
命令添加的target
是一致的。
2.3 添加 Crate
在Cargo.toml
的依赖中添加如下内容:
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "1.0"
stm32f1xx-hal = { version = "0.11.0", features = ["stm32f103", "rtic", "medium"] }
cortex-m
、cortex-m-rt
、panic-halt
三个crate对于大多数Cortex-M微控制器都是通用且必须的。前两个为Cortex-M控制器提供了环境支持和运行时支持,最后一个使得程序在panic
时自动停止运行。
最后一个crate stm32f1xx-hal
是STM32F1的HAL库,由官方维护。其中features
参数的写法也主要参考了官方的文档和示例,指定了芯片的具体型号,运行时环境等信息。
2.4 编写程序
博主尽量演示代码的逐步编写过程,以帮助读者更好地理解。修改main.rs
为如下内容:
#![no_std] // 非标准库工程,没有main函数
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*};
#[entry]
fn main() -> ! { // 注意这里的main函数返回一个!
loop {
}
}
接下来仅展示main
函数中的内容。首先对内核外设和设备外设进行获取:
#[entry]
fn main() -> ! {
let cp = cortex_m::Peripherals::take().unwrap(); // Core Peripherals
let dp = pac::Peripherals::take().unwrap(); // Device Peripherals
loop {
}
}
初始化时钟、GPIO和Delay函数:
#[entry]
fn main() -> ! {
let cp = cortex_m::Peripherals::take().unwrap(); // Core Peripherals
let dp = pac::Peripherals::take().unwrap(); // Device Peripherals
// Clock configration
let mut rcc = dp.RCC.constrain();
// GPIO
let mut gpioc = dp.GPIOC.split(&mut rcc);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
// Delay
let mut delay = cp.SYST.delay(&rcc.clocks);
loop {
}
}
之后就可以愉快地在loop
中点灯了:
loop {
led.toggle();
delay.delay_ms(100u16);
}
2.5 烧录和运行
将烧录器连接开发板后,接入电脑,直接运行cargo run
命令,probe-rs会自动识别烧录器类型(dap-link或者st-link)并烧录可执行程序。可执行程序的位置在target/thumbv7m-none-eabi/debug/led-f1
。博主使用MacOS,这里的led-f1
文件实际上就是.elf
文件,在这里没有显示后缀而已。
❯ cargo run
...
Compiling ...
...
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.56s
Running `probe-rs run --chip STM32F103C8 --log-format=oneline target/thumbv7m-none-eabi/debug/led-f1`
Erasing ✔ 100% [####################] 9.00 KiB @ 10.47 KiB/s (took 1s)
Programming ✔ 100% [####################] 9.00 KiB @ 6.85 KiB/s (took 1s)
Finished in 2.17s
Ctrl + C
。此时程序已经下载到STM32的FLASH中,重新上电就会自动执行烧录好的程序。通过建立一个“最小化”的工程,我们了解了Rust嵌入式工程的基本结构。一般在开发时都采用模版库初始化工程,这样可以减小无效的代码编辑量。之后通过对比app-template
创建的工程,可以看到很多都是类似的。模板库中还添加了其他便于嵌入式开发的功能,可以极大程度地提高开发体验。
3 从 app-template
初始化项目
3.1 安装 cargo-generate
和 flip-link
cargo-generate一般用来从github上克隆需要的工程模板,如果你需要使用官方模板库创建工程,那么建议先安装cargo-generate:
cargo install cargo-generate
flip-link 是一个用来解决在资源受限平台(例如微控制器)上栈溢出导致静态变量内存被覆盖的问题的工具。它的Github主页(README)详细介绍了这种情况出现的原因和解决方法。如果你理解不了,其实也不用完全理解,只需要知道因为它的存在可以让嵌入式程序的内存更安全,对程序员来说基本也感知不到其存在。但是都用Rust了,那就让可以更安全的东西更安全,总是没错的。
使用下面的命令安装flip-link:
cargo install flip-link
要使用flip-link,需要在.cargo/config.toml
文件中添加编译标志:
rustflags = [
"-C", "linker=flip-link",
]
3.2 安装 defmt
日志打印工具
defmt 是对于资源受限平台(例如微控制器)友好的日志打印框架。在Rust中将其添加为依赖需要命令:
cargo add defmt
要使用defmt,同样需要在.cargo/config.toml
文件中添加编译标志:
rustflags = [
"-C", "link-arg=-Tdefmt.x",
]
defmt.x
脚本。此时可以将.cargo/config.toml
文件中关于defmt的编译参数禁用(参考Rust STM32F103嵌入式开发教程之问题答疑 Q&A9)。它只是个日志打印工具,不用也无可厚非,当然,如果你没有遇到这个错误,那么博主还是推荐defmt,实际使用体验还是不错的。报错信息:
= note: rust-lld: error: cannot find linker script defmt.x
解决方法:
rustflags = [
# ...
# "-C", "link-arg=-Tdefmt.x", # 注释这一行
# ...
]
3.3 克隆工程模板
使用下面的命令克隆工程模板:
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
cortex-m-quickstart 已经寄了(太老旧了),点进他的Github仓库提示推荐使用 app-template 。
从另一个kunrling-rs
官方推荐的仓库 app-template 初始化工程(输入工程名后回车):
cargo generate --git https://github.com/knurling-rs/app-template
3.4 工程配置
其实 app-template 的README文档写的已经很清晰了,使用这个模板创建工程主要需要操作7-8步,跟着README文件操作即可。第一步【TODO 1】当然是使用cargo-generate克隆工程模板。
【TODO 2】在.cargo/config.toml
修改probe-rs烧录的芯片型号(可填写的型号使用probe-rs chip list
命令查看):
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# TODO(2) replace `$CHIP` with your chip's name (see `probe-rs chip list` output)
runner = ["probe-rs", "run", "--chip", "STM32F103C8", "--log-format=oneline"]
# If you have an nRF52, you might also want to add "--allow-erase-all" to the list
【TODO 3】在.cargo/config.toml
中修改编译目标:
[build]
# TODO(3) Adjust the compilation target.
# Select the correct target for your processor:
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
【TODO 4】在Cargo.toml
中添加需要的HAL层crate:
# TODO(4) enter your HAL here
# some-hal = "1.2.3"
stm32f1xx-hal = { version = "0.11.0", features = ["stm32f103", "rtic", "medium"] }
【TODO 5】在src/lib.rs
中引入hal库:
// TODO(5) adjust HAL import
// use some_hal as _; // memory layout
use stm32f1xx_hal as _;
【TODO 6】在项目根目录下添加memory.x
文件:
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
memory.x
的cortex-m工程,.cargo/config.toml -> [rustflags]
模块中的需要声明其需要link.x
脚本,不能省略。rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x", # 不能省略
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
]
memory.x
文件,并且在修改之前执行了编译,那么需要清除Rust的编译文件重新编译。使用cargo clean
命令清除之前的编译文件。这个问题我在app-template下提了一个issue(Force rebuild when memory.x changes),有遇到类似问题的小伙伴可以去看看。【TODO 7】app-template已经为我们编写好了一些例子,帮助我们快速了解这个模板的功能和一些特色,例如hello
、panic
等,存放在src/bin
目录下。我们可以直接通过cargo rb foo
或者cargo rrb bar
命令运行这些例子。当然,运行之前需要链接开发板。st-link或者dap-link都可以,probe-rs会自动识别你使用的是什么烧录器(很强的功能,我太喜欢了)。
❯ cargo rb hello
Compiling led-f1-app-template v0.1.0 (/Users/debussy/Rust-STM32/led-f1-app-template)
Finished `dev` profile [optimized + debuginfo] target(s) in 0.11s
Running `probe-rs run --chip STM32F103C8 --log-format=oneline target/thumbv7m-none-eabi/debug/hello`
Erasing ✔ 100% [####################] 8.00 KiB @ 10.16 KiB/s (took 1s)
Programming ✔ 100% [####################] 8.00 KiB @ 6.71 KiB/s (took 1s)
Finished in 1.98s
Hello, world!
Firmware exited successfully
对于其他的例子,大家也可以运行试试看,例子都很简单并且易于理解,博主在这里就不赘述了。
3.5 点灯
接下来修改一下src/bin/hello.rs
,完成点灯操作:
#![no_main]
#![no_std]
use led_f1_app_template as _; // global logger + panicking-behavior + memory layout
use stm32f1xx_hal::{pac, prelude::*};
#[cortex_m_rt::entry]
fn main() -> ! {
defmt::println!("Hello, world!");
let cp = cortex_m::Peripherals::take().unwrap(); // Core Peripherals
let dp = pac::Peripherals::take().unwrap(); // Device Peripherals
// Constrain and Freeze clock configration
let mut rcc = dp.RCC.constrain();
let mut gpioc = dp.GPIOC.split(&mut rcc);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
let mut delay = cp.SYST.delay(&rcc.clocks);
defmt::println!("F1 led start toggle!");
loop {
led.toggle();
delay.delay_ms(500u32);
}
// led_f1_app_template::exit()
}
运行一下,看灯是否按预期闪烁:
❯ cargo rb hello
Compiling led-f1-app-template v0.1.0 (/Users/debussy/Rust-STM32/led-f1-app-template)
Finished `dev` profile [optimized + debuginfo] target(s) in 0.18s
Running `probe-rs run --chip STM32F103C8 --log-format=oneline target/thumbv7m-none-eabi/debug/hello`
Erasing ✔ 100% [####################] 10.00 KiB @ 10.83 KiB/s (took 1s)
Programming ✔ 100% [####################] 10.00 KiB @ 6.92 KiB/s (took 1s)
Finished in 2.37s
Hello, world!
F1 led start toggle!
和之前的现象是一致的,说明工程的创建没有问题。