环境配置
使用 Rust 进行嵌入式开发需要配置相应的开发环境。由于 Windows 系统在嵌入式开发中可能会遇到一些兼容性问题,建议使用 Linux 或 macOS 系统进行开发。如果你使用的是 Windows 系统,可以考虑使用 WSL(Windows Subsystem for Linux)来创建一个 Linux 开发环境。
本文档采取 WSL 作为示例,介绍如何在 Windows 系统上配置 Rust 嵌入式开发环境。
配置 cargo
参照 stm32-template 项目的 README 文档,配置 cargo 的相关设置。
首先,安装 cargo-generate 工具:
cargo install cargo-generate
然后安装烧录工具与解决方案管理器:
cargo install probe-rs-tools --locked
其中 --locked 参数用于确保安装的版本与 probe-rs 的版本一致,避免出现版本不兼容的问题。
最后为 Cortex-M 处理器安装工具链:
rustup target add thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf
部署空白项目
运行如下命令,使用 cargo-generate 创建一个新的项目:
cargo generate --git https://github.com/burrbull/stm32-template/
接下来会问几个问题:
- Project Name: 输入你的项目名称,例如
first-stm。 - What microcontroller name: 输入你使用的微控制器型号,例如
stm32g431cbu6。 - What HAL version to use: 选择 HAL 源,此处直接使用
last-release。 - Is it RTIC-based application: 选择
true,表示使用 RTIC 框架。
什么是 RTIC 框架?
RTIC(Real-Time Interrupt-driven Concurrency)是一个专为嵌入式系统设计的、基于 Rust 语言的实时并发框架。它通过硬件加速的方式,将并发任务直接映射到微控制器的中断系统上,从而取代了传统的软件实时操作系统(RTOS)内核。
RTIC 常被视作RTOS 的轻量级替代方案,其定位介于裸机编程和完整 RTOS 之间。
特性 RTIC 传统 RTOS (如 FreeRTOS) 内核 无软件内核,依赖硬件中断控制器 包含软件调度器内核 内存占用 极小,任务共享栈,无动态分配 相对较大,通常需要为每个任务分配独立栈 安全性 编译期保证无竞态、无死锁 通常依赖运行时机制,保证有限 调度开销 极低,硬件完成大部分调度 存在任务切换等软件调度开销 适用场景 安全关键、RAM 受限的系统,追求极致效率和确定性 需要丰富服务(如文件系统、网络协议栈)的复杂系统 此处选择使用 RTIC 框架,主要是因为它在嵌入式开发中提供了更高的安全性和效率,尤其适合资源受限的微控制器环境。
- Will this program use defmt logger: 选择
true,表示使用defmt日志系统。
什么是 defmt 日志系统?
defmt是一个专为资源受限设备(如微控制器)设计的高效日志框架。它的名字是“deferred formatting”(延迟格式化)的缩写,这直接点明了其核心设计思想。
极低的运行时开销:由于格式化在主机端完成,设备端的日志调用开销远低于传统方式,非常适合在中断服务程序(ISR)或对时序要求严格的代码中使用。
更小的二进制体积:编译时生成的字符串索引表,避免了将大量格式字符串编译进固件,从而显著减小了二进制文件的大小。
编译期过滤:支持在编译时按模块和日志级别(error, warn, info, debug, trace)过滤日志,未启用的日志代码会被直接移除,不产生任何开销。
清晰的日志级别:提供了与 log crate 类似的error!、warn!、info!、debug!、trace!宏。
defmt常被拿来与 Rust 标准的logcrate 比较,两者定位不同:
特性 defmtlog(标准库)设计目标 资源受限的嵌入式系统 通用 Rust 应用程序(服务器、CLI 等) 格式化位置 主机端(延迟格式化) 设备端(运行时格式化) 运行时开销 极低,仅发送二进制数据和索引 相对较高,需在设备端进行字符串格式化 二进制体积 更小,字符串表在主机端 相对较大,格式字符串嵌入固件 灵活性 较低,对日志格式和类型有约束 高,与 std::fmt完全兼容适用平台 主要针对 no_std嵌入式环境支持所有 Rust 目标平台
- Do you want to load SVD and add it to VSCode task: 选择
true,表示加载 SVD 文件并将其添加到 VSCode 任务中。
什么是 SVD 文件?
SVD(System View Description)是 ARM 定义的一种基于 XML 的标准文件格式,用于描述 Cortex-M 微控制器中外设寄存器的详细信息。一个 SVD 文件通常包含:
外设信息:名称、基地址、中断号等。
寄存器详情:名称、地址偏移、位域定义、访问权限等。
芯片厂商(如 ST、Nordic)会随芯片发布对应的 SVD 文件。
到此,你已经成功创建了一个基于 RTIC 框架的 Rust 嵌入式项目,并配置了相关的开发环境。
创建项目的目录结构
drwxr-xr-x 6 shizuku shizuku 4096 Sep 29 18:19 ./
drwxr-xr-x 3 shizuku shizuku 4096 Sep 29 18:02 ../
drwxr-xr-x 2 shizuku shizuku 4096 Sep 29 18:19 .cargo/
drwxr-xr-x 6 shizuku shizuku 4096 Sep 29 18:20 .git/
-rw-r--r-- 1 shizuku shizuku 8 Sep 29 18:19 .gitignore
drwxr-xr-x 2 shizuku shizuku 4096 Sep 29 18:19 .vscode/
-rw-r--r-- 1 shizuku shizuku 612 Sep 29 18:19 Cargo.toml
-rw-r--r-- 1 shizuku shizuku 3218 Sep 29 18:19 Embed.toml
-rw-r--r-- 1 shizuku shizuku 10846 Sep 29 18:19 LICENSE-APACHE
-rw-r--r-- 1 shizuku shizuku 1022 Sep 29 18:19 LICENSE-MIT
-rw-r--r-- 1 shizuku shizuku 2083 Sep 29 18:19 README.md
-rw-r--r-- 1 shizuku shizuku 1324 Sep 29 18:19 build.rs
-rw-r--r-- 1 shizuku shizuku 367 Sep 29 18:19 memory.x
drwxr-xr-x 2 shizuku shizuku 4096 Sep 29 18:19 src/
上述为项目的目录结构,其中:
Embed.toml 配置
[default.probe] 调试器与 Probe 配置
probe表示调试器的配置,主要用于指定使用的调试器类型和相关参数。常见的调试器类型包括:
jlink:J-Link 调试器,由 Segger 提供,广泛用于 ARM Cortex-M 系> 列微控制器的调试。stlink:ST-Link 调试器,由 STMicroelectronics 提供,专为 > STM32 系列微控制器设计。cmsis-dap:CMSIS-DAP 调试器,符合 ARM 的 CMSIS-DAP 标准,支持> 多种微控制器。daplink:DAPLink 调试器,由 ARM 提供,支持多种微控制器和开发板。
usb_vid:指定调试器的 USB 供应商 ID(Vendor ID)。这个值用于识别连接到计算机的 USB 设备。不同的调试器可能有不同的供应商 ID。0483:STMicroelectronics 的 USB VID。046d:Logitech 的 USB VID。05e3:ARM 的 USB VID。1337:leet speak(通常用于自定义或实验性设备)。
使用
lsusb命令可以查看连接到计算机的 USB 设备及其 VID 和 PID。例如:lsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 001 Device 002: ID 0483:374b STMicroelectronics ST-LINK/V2.1 Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub如上,
0483:374b表示 ST-LINK/V2.1 调试器的 VID 为0483,PID 为374b。
usb_pid:指定调试器的 USB 产品 ID(Product ID)。这个值用于进一步识别连接到计算机的特定设备型号。不同的调试器型号可能有不同的产品 ID。374b:ST-LINK/V2.1 的 USB PID。3748:ST-LINK/V2 的 USB PID。374a:ST-LINK/V2-1 的 USB PID。374c:ST-LINK/V3 的 USB PID。
serial:指定调试器的序列号,用于唯一标识连接的调试器设备。这个参数在同时连接多个相同型号的调试器时非常有用,可以确保选择正确的设备进行调试。protocol:指定调试器使用的通信协议。常见的协议包括:swd:Serial Wire Debug,适用于大多数 ARM Cortex-M 微控制器。jtag:Joint Test Action Group,适用于支持 JTAG 的微控制器。
speed:指定调试器与目标设备之间的通信速度,单位为 kHz。常见的速度值包括:1000:表示 1 MHz 的通信速度。4000:表示 4 MHz 的通信速度。8000:表示 8 MHz 的通信速度。
[default.flashing] 烧录配置
enable:布尔值,表示是否启用烧录功能。如果设置为true,则在构建项目后会自动进行烧录操作;如果设置为false,则不会进行烧录。halt_afterwards:已废弃 布尔值,表示在烧录完成后是否暂停目标设备的执行。如果设置为true,则烧录完成后目标设备会进入暂停状态,等待调试器的进一步操作;如果设置为false,则烧录完成后目标设备会继续运行。restore_unwritten_bytes:布尔值,表示在烧录过程中是否恢复未写入的字节。如果设置为true,则在烧录过程中会尝试恢复那些未被写入的字节,以确保目标设备的内存状态尽可能接近预期;如果设置为false,则不会进行恢复操作。
对
restore_unwritten_bytes的解释假设 Flash 中:
Flash
0x08000000 程序区
0x08010000 数据区
0x08020000 数据区ELF 只覆盖了 0x08000000 ~ 0x08010000 的程序区,而数据区 0x08010000 ~ 0x08020000 没有被覆盖。如果
restore_unwritten_bytes设置为true,则烧录工具会尝试恢复数据区的内容,以确保 Flash 中的数据区保持原有状态;如果设置为false,则数据区的内容可能会被擦除或变为未定义状态。这在把特定的 Flash 区域用于存储配置数据或其他重要信息时尤其重要,因为这些区域的数据不应被意外覆盖或丢失。
flash_layout_output_path:输出一个 SVG 布局图,参数为 SVG 文件的路径。该图形化布局图显示了 Flash 内存的分区情况,包括程序区、数据区和其他自定义分区。通过查看该图,可以直观地了解 Flash 的使用情况和各个分区的大小及位置。do_chip_erase:布尔值,表示在烧录前是否执行芯片擦除操作。如果设置为true,则在烧录之前会擦除整个芯片的内容,以确保目标设备的内存处于干净状态;如果设置为false,则不会进行芯片擦除操作,可能会保留原有数据。
[default.reset] 复位配置
enable:布尔值,表示是否启用复位功能。如果设置为true,则在烧录完成后会自动对目标设备进行复位操作;如果设置为false,则不会进行复位。halt_afterwards:布尔值,表示在复位完成后是否暂停目标设备的执行。如果设置为true,则复位完成后目标设备会进入暂停状态,等待调试器的进一步操作;如果设置为false,则复位完成后目标设备会继续运行。
[default.general] 通用配置
chip:指定目标微控制器的型号,例如STM32G431CBUx。这个参数用于告诉烧录工具和调试器目标设备的具体型号,以便正确地进行烧录和调试操作。chip_description:指定目标微控制器的描述信息,例如STM32G431CBUx。这个参数用于提供关于目标设备的更多信息,可能包括其特性、功能和其他相关细节。此选项一般留空。log_level:指定日志输出的详细程度。常见的日志级别包括:ERROR:仅输出错误信息。WARN:输出警告和错误信息。INFO:输出一般信息、警告和错误信息。DEBUG:输出调试信息、一般信息、警告和错误信息。TRACE:输出最详细的跟踪信息,包括调试、一般信息、警告和错误信息。
connect_under_reset:布尔值,表示是否在复位状态下连接目标设备。如果设置为true,则调试器会在目标设备复位时尝试建立连接,这对于某些需要在复位期间进行调试的场景非常有用;如果设置为false,则调试器会在目标设备正常运行时建立连接。
[default.rtt] RTT 配置
enable:布尔值,表示是否启用 RTT(Real-Time Transfer)功能。如果设置为true,则在调试过程中会启用 RTT,以便进行实时数据传输和调试;如果设置为false,则不会启用 RTT。up_mode:指定 RTT 上行通道的模式。常见的模式包括:BlockIfFull:如果上行缓冲区已满,则阻塞写入操作,直到有空间可用。NoBlockSkip:不阻塞写入操作,如果缓冲区已满,则跳过写入。NoBlockTrim:不阻塞写入操作,如果缓冲区已满,则截断写入数据,只保留部分数据。
channels:指定 RTT 通道的配置,包括通道号、名称和缓冲区大小。每个通道可以用于不同类型的数据传输,例如日志输出、调试信息或其他自定义数据。
channels = [
{ up = 0, down = 0, name = "log", format = "Defmt" },
]
timeout:毫秒,指定 RTT 操作的超时时间,单位为毫秒。如果在指定时间内未能完成 RTT 操作,则会触发超时处理。show_timestamp:布尔值,表示是否在 RTT 输出中显示时间戳。如果设置为true,则每条 RTT 输出都会附带时间戳信息,以便进行时间相关的调试和分析;如果设置为false,则不会显示时间戳。log_enable:布尔值,表示是否启用 RTT 日志功能。如果设置为true,则会将 RTT 输出作为日志记录;如果设置为false,则不会记录 RTT 日志。
[default.gdb] GDB 配置
enable:布尔值,表示是否启用 GDB(GNU Debugger)功能。如果设置为true,则在调试过程中会启用 GDB,以便进行源代码级调试;如果设置为false,则不会启用 GDB。gdb_connect_string:指定 GDB 连接的字符串,用于告诉 GDB 如何连接到目标设备。"127.0.0.1:1337"表示 GDB 将监听本地 IP 地址的 1337 端口,以便与调试器进行通信。
build.rs 配置
use std::fs;
use std::io::Write;
use std::path::Path;
fn main() {
// Retrieve the target chip series from the environment variable
let target = "stm32g431.svd";
let file_name = format!("{target}.svd");
let output_path = Path::new(&file_name);
let url = format!("https://stm32-rs.github.io/stm32-rs/{target}.patched");
// Check if the file already exists
if output_path.exists() {
println!(
"SVD file already exists at {:?}, skipping download.",
output_path
);
} else {
// Ensure the output directory exists
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).expect("Failed to create directory for SVD file");
}
// Download the file
println!("Downloading SVD file from {}...", url);
let response = reqwest::blocking::get(&url).expect("Failed to fetch SVD file");
let content = response.text().expect("Failed to read response body");
// Write the downloaded content to the file
let mut file = fs::File::create(output_path).expect("Failed to create SVD file");
file.write_all(content.as_bytes())
.expect("Failed to write SVD file");
println!("SVD file saved to {:?}", output_path);
}
}
这个 build.rs 文件的主要作用是自动下载并保存指定的 SVD 文件,以便在构建项目时使用。它首先检查目标 SVD 文件是否已经存在,如果存在则跳过下载;如果不存在,则从指定的 URL 下载该文件,并将其保存到项目目录中。
memory.x 配置
MEMORY
{
/* NOTE K = KiBi = 1024 bytes */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
初始的 memory.x 文件定义了微控制器的内存布局,包括 Flash 和 RAM 的起始地址和长度。它还定义了 _stack_start,用于指定调用栈的起始位置。
现在使用 STM32G431CBUx 微控制器为例,Flash 的起始地址为 0x08000000,长度为 128K,RAM 的起始地址为 0x20000000,长度为 32K。这些信息对于链接器和运行时环境非常重要,因为它们决定了程序的存储位置和可用内存。
src/main.rs
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Print panic message to probe console
use panic_probe as _;
#[rtic::app(device = stm32g4xx_hal::pac, peripherals = true)]
mod app {
use stm32g4xx_hal::{
prelude::*,
};
#[shared]
struct Shared {}
// Local resources go here
#[local]
struct Local {}
#[init]
fn init(mut ctx: init::Context) -> (Shared, Local, init::Monotonics) {
(
Shared {
// Initialization of shared resources go here
},
Local {
// Initialization of local resources go here
},
init::Monotonics()
)
}
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
continue;
}
}
}
头上:
#![deny(unsafe_code)]:禁止使用不安全代码,确保代码的安全性。#![no_main]:表示不使用标准的main函数入口点,而是使用 RTIC 框架提供的#[init]和#[idle]等函数作为入口点。#![no_std]:表示不使用 Rust 标准库,而是使用core库,适用于嵌入式系统。
引入了一个未使用的 stm32g4xx_hal 预导入模块 prelude::*,它提供了对 HAL(硬件抽象层)功能的访问。
还有两个结构体:
Shared:用于定义在任务之间共享的资源。Local:用于定义仅在特定任务中使用的本地资源。
现在引入 app 模块,它是 RTIC 框架的核心模块,包含了应用程序的主要逻辑。
#[init] 函数
#[init] 函数是 RTIC 框架的初始化函数,用于在系统启动时进行资源的初始化。它接收一个 init::Context 参数,返回一个元组,包含共享资源、局部资源和单调计时器。
GPIO、时钟、UART、SPI、CAN、定时器等初始化通常都会放这里。
mut ctx: init::Context 参数
init::Context 提供了对系统资源的访问权限,包括外设、单调计时器和其他初始化所需的上下文信息。通过 mut ctx,可以在初始化过程中修改这些资源的状态。
#[idle] 函数
#[idle] 函数是 RTIC 框架的空闲任务函数,当系统没有其他任务需要执行时,会进入该函数。它接收一个 idle::Context 参数,并且是一个无限循环,确保系统在空闲时不会退出。
通常把 continue 换做 cortex_m::asm::wfi(),让 CPU 进入低功耗模式,等待中断唤醒。
cortex_m::asm::wfi()
wfi(Wait For Interrupt)是 ARM Cortex-M 处理器中的一条汇编指令,用于让处理器进入低功耗模式,直到下一个中断发生。使用cortex_m::asm::wfi()可以有效降低系统的功耗,特别是在嵌入式系统中,当没有任务需要执行时,可以让 CPU 进入休眠状态,等待外部事件或中断唤醒。
警告:wfi()指令是 ARM Cortex-M 系列处理器的一个指令,用于让处理器进入低功耗模式,等待中断唤醒。但是这样会导致defmt调试信息完全被阻断,所以在调试阶段不使用wfi(),正式编译发布版本时再使用。其余如
Cargo.toml等配置与普通 Rust 项目类似,主要区别在于添加了嵌入式相关的依赖和配置。在此不过多赘述。
尝试编译一下
cargo build
编译出现一些警告是正常的,因为现在没有使用任何外设或功能。只要没有错误,说明环境配置成功。
烧录与调试
只烧录:
cargo flash
烧录与运行:
cargo run
多功能烧录、运行、调试:
cargo embed
以上工具都可以 --release 进行 release 模式编译。
尝试烧录,可能看到出现了:
WARN probe_rs::probe::stlink: Target voltage (VAPP) is 0.47 V. Is your target device powered?
这个警告表示目标设备的电压过低,可能是因为目标设备没有正确供电。请确保目标设备已连接到电源,并且电压在正常范围内(通常为 3.3V 或 5V,具体取决于微控制器型号)。
此处发现 G431 无法正常工作,切换 F103C8T6 进行测试。
但是:
Running `probe-rs run --chip STM32F103C8Tx target/thumbv7m-none-eabi/debug/first-stm-f`
WARN probe_rs::probe::stlink: Target voltage (VAPP) is 0.47 V. Is your target device powered?
Erasing ✔ 100% [####################] 2.00 KiB @ 3.47 KiB/s (took 1s)
Programming ✔ 100% [####################] 2.00 KiB @ 3.07 KiB/s (took 1s)
Finished in 1.36s
同样出现了电压过低的警告,但烧录成功了。原因可能在于复刻 STLink 缺少这一功能。
到此,已经成功配置了 Rust 嵌入式开发环境,并创建了一个基于 RTIC 框架的项目。可以继续在此基础上进行开发,添加外设驱动、实现功能逻辑,并使用 cargo flash 或 cargo embed 进行烧录和调试。
