调试与 GPIO 初步
本次测试使用 STM32F103C8T6 进行调试。
开始调试
在使用 C 开发的时候,我们通常使用 SWD 通过 GDB 调试,或者通过串口打印调试信息。Rust 通过一些类似的方法也可以进行调试。
其中,使用 probe-rs 配合 Visual Studio Code 进行调试,方式类似于在 Keil 或 IAR 中使用 Watch 窗口,或通过 CMake 配合 GDB 进行调试;而 defmt 则更接近通过串口输出日志或使用 ESP32 调试信息的方式,适用于不同的调试场景。
probe-rs 调试
先看看连接是否正常:
probe-rs list
看到了设备:
The following debug probes were found:
[0]: STLink V2-1 -- 0483:374b:01210182524300143638414B (ST-LINK)
查看设备信息:
probe-rs info
由于 STLink 问题,其无法正常显示开发板信息,提示 VAPP 电压不足,使用 --verbose 详细展开仍然能够正常找到开发板。
使用 probe-rs read 可以读取指定寄存器。
比如,可以尝试读取 STM32F103C8T6 的 CPUID 地址 0xE000ED00:
probe-rs read b32 0xE000ED00 1 --chip STM32F103C8Tx
可以看到传了信息:
WARN probe_rs::probe::stlink: Target voltage (VAPP) is 0.47 V. Is your target device powered?
e000ed00: 411fc231
SFR(特殊功能寄存器)
此区域寄存器拥有特殊功能,通常用于控制芯片的工作状态。Cortex-M 的 SFR 寄存器地址范围为
0xE000E000~0xE00FFFFF。
0xE000E004Interrupt Control Type Registerr
0xE000E010SysTick Control and Status Registerr/w
0xE000E014SysTick Reload Value Registerr/w
0xE000E018SysTick Current Value Registerr/wc
0xE000E01CSysTick Calibration Value Registerr
0xE000E100Irq 0 to 31 Set Enable Registerr/w
0xE000E11CIrq 224 to 239 Set Enable Registerr/w
0xE000E180Irq 0 to 31 Clear Enable Registerr/w
0xE000E19CIrq 224 to 239 Clear Enable Registerr/w
0xE000E200Irq 0 to 31 Set Pending Registerr/w
0xE000E21CIrq 224 to 239 Set Pending Register
0xE000E280Irq 0 to 31 Clear Pending Register
0xE000E29CIrq 224 to 239 Clear Pending Register
0xE000E300Irq 0 to 31 Active Bit Register
0xE000E31CIrq 224 to 239 Active Bit Register
0xE000E400Irq 0 to 31 Priority Register
0xE000E4F0Irq 236 to 239 Priority Register
0xE000ED00CPUID Base Registerr
0xE000ED04Interrupt Control State Registerr/w
0xE000ED08Vector Table Offset Registerr/w
0xE000ED0CApplication Interrupt/Reset Control Registerr/w
0xE000ED10System Control Registerr/w
0xE000ED14Configuration Control Registerr/w
对于普通使用者来说,这个功能用处其实不大。
可以使用 probe-rs debug 进行调试。
probe-rs debug --chip STM32F103C8Tx
此时进入了一个 Console,输入 help 可以查看帮助:
probe-rs-debug: Log output for "probe_rs=warn" will be written to the Debug Console.
probe-rs-debug: Starting probe-rs as a DAP Protocol server
WARN probe_rs::probe::stlink:186: Target voltage (VAPP) is 0.47 V. Is your target device powered?
Core is running
Debug Console> help
Usage: - Use <Ctrl+Space> to get a list of available commands.
- Use <Up/DownArrows> to navigate through the command list.
- Use <Hab> to insert the currently selected command.
- Note: This implementation is a subset of gdb commands, and is intended to behave similarly.
Available commands:
break [*address | file:line[:column]]: Set a breakpoint at a location, or halt the target if unspecified.
clear *address | file:line[:column]: Clear a breakpoint.
rtt <subcommand>: Commands to work with Segger RTT
Subcommands:
- write channel_id data: Write data to a specific channel.
info <subcommand>: Information of specified program data.
Subcommands:
- frame [address]: Describe the current frame, or the frame at the specified (hex) address.
- locals: List local variables of the selected frame.
- reg [register name]: List registers in the selected frame.
- var: List all static variables.
- break: List all breakpoints.
dump [memory start address] [memory size in bytes] [path (default: ./coredump)]: Create a core dump at a target location. Specify memory ranges to dump, or leave blank to dump in-scope memory regions.
p [/f (f=format[n|v])] <local variable name>: Print known information about variable.
x [/Nuf (N=count, u=unit[b|h|w|g], f=format[t|x|i])] [address (hex)]: Examine Memory, using format specifications, at the specified address.
bt <subcommand>: Print the backtrace of the current thread.
Subcommands:
- yaml path (e.g. my_dir/backtrace.yaml): Print all information about the backtrace of the current thread to a local file in YAML format.
wreg register name value: Write a value to the named register in the selected core, e.g. `wreg pc 0x1000`.
help: Information about available commands and how to use them.
quit: Disconnect (and suspend) the target.
step: Step one instruction
reset: Reset the target
c: Continue running the program on the target.
读写寄存器
通过阅读上述帮助信息,可以看到 info reg 命令可以读取寄存器。
info reg r0
得到回应:
R0/a1/r1: 0x20004fee
如果要修改其值,可以使用 wreg 命令:
wreg r0 0x20000000
这样再读就发现 r0 已经被修改了:
Debug Console> info reg
R0/a1/r1: 0x20000000 R1/a2/r2: 0x00000004 R2/a3: 0x20004f86
R3/a4: 0x20000055 R4: 0x77bfeb3e R5: 0xe747fb2f
R6: 0xf3bfbfdc R7/FP: 0x20004fe0 R8: 0xefde337a
R9: 0xbd77dea6 R10: 0xbe7fdbdc R11: 0xfdffaefb
R12: 0x20000054 R13/SP: 0x20005000 R14/LR: 0xffffffff
R15/PC: 0x08000130 MSP: 0x20005000 PSP: 0xcffdbfcc
XPSR/PSR: 0x01000000 EXTRA: 0x00000000
读取内存
x 命令可以读取内存,指定地址和长度即可。
x /16xb 0x20000000
这样可以读 16 个字节的内存,输出为:
0x64 0x65 0x66 0x6d 0x74 0x00 0x00 0x00 0x53 0x45 0x47 0x47 0x45 0x52 0x20 0x52
在 Visual Studio Code 里面添加断点与修改变量
使用 break 命令可以设置断点,使用 clear 可以清除断点。
不过这个方法有点困难,因为需要知道具体的地址或者行号,且在 Rust 中,行号和地址的对应关系并不容易查找。
如果你使用 Visual Studio Code,可以使用 probe-rs 的插件进行调试,设置断点和查看变量都很方便。
这样一来,你就可以随意在代码上面打断点,然后使用 probe-rs 进行调试了。
由于上一节的模板里面包括了 Visual Studio Code 的配置文件,所以你可以直接使用 Visual Studio Code 的调试功能进行调试。
注意:实际调试时发现了问题,
defmt配置默认dataFormat为String,导致终端打印显示乱码,所以需要改变rttChannelFormats字段为以下内容。"rttChannelFormats": [ { "dataFormat": "Defmt", "channelNumber": 0, "showTimestamps": true, "showLocation": true } ],
你不仅能够看到变量值,还能够看到调用栈和寄存器的值,甚至可以双击修改寄存器和变量的值。
监视一栏里有像 IAR 一样的变量变化探测工具,此处还没有正式使用其编写代码,所以暂时没有使用到。在下一任务 GPIO 的时候会使用起来。
像往常写 Rust 一样跳断点,与使用 GDB 调试一样丝滑。
平时开发与调试,使用 Visual Studio Code 的调试功能就够了,
probe-rs的命令行调试功能主要用于没有图形界面的环境,当然如果要做成品板逆向或者嵌入式安全什么的,也可以使用命令行调试。注意:使用 Visual Studio Code 调试时,若选择中止调试,程序会在此处停住,这与 GDB 的行为恰恰相反。
defmt 调试
比起 probe-rs 这种介入寄存器的调试方法,defmt 显得更简单易用。
加入 defmt 和 defmt-rtt crate,提供支持。
cargo add defmt
cargo add defmt-rtt
把 defmt_rtt 完全链入程序。
// Print panic message to probe console
use panic_probe as _;
use defmt_rtt as _;
开始写调试信息:
// Debug test: Output messages.
warn!("This is a warn.");
debug!("This is a debug.");
trace!("This is a trace.");
info!("This is a info.");
error!("This is an error");
注意,此时应当自动引入 defmt 的宏,若没有引入,则需要手动引入:
use defmt::{debug, error, info, trace, warn};
use stm32f1xx_hal::{
prelude::*,
};
这个引入必须写在 mod app 之内,表示在模块内使用的 crate,否则会报错。
然后使用 cargo run 烧录。
warning: `first-stm-f` (bin "first-stm-f") generated 3 warnings (run `cargo fix --bin "first-stm-f" -p first-stm-f` to apply 2 suggestions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
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% [####################] 7.00 KiB @ 5.36 KiB/s (took 1s)
Programming ✔ 100% [####################] 7.00 KiB @ 5.93 KiB/s (took 1s)
Finished in 2.60s
[WARN ] This is a warn. (first_stm_f first-stm-f/src/main.rs:26)
[INFO ] This is a info. (first_stm_f first-stm-f/src/main.rs:29)
[ERROR] This is an error (first_stm_f first-stm-f/src/main.rs:30)
依旧报 VAPP 电压不足的警告,但程序可以正常运行,且可以看到调试信息。(没钱买全功能 STLink,缺一点就算了)
完美地看到了调试信息,说明 defmt 调试已经成功。但是发现只有 warn、info、error 三种调试信息,debug 和 trace 没有输出。
此时需要指定调试等级,defmt 的日志过滤主要通过 DEFMT_LOG 环境变量控制。
DEFMT_LOG=trace cargo run
必须要让 DEFMT_LOG 环境变量在编译时就要作用,否则依然不会输出 debug 和 trace 级别的调试信息。现在就能看到更详细的信息了:
[WARN ] This is a warn. (first_stm_f first-stm-f/src/main.rs:26)
[DEBUG] This is a debug. (first_stm_f first-stm-f/src/main.rs:27)
[TRACE] This is a trace. (first_stm_f first-stm-f/src/main.rs:28)
[INFO ] This is a info. (first_stm_f first-stm-f/src/main.rs:29)
[ERROR] This is an error (first_stm_f first-stm-f/src/main.rs:30)
如果想要固定这个调试等级,可以在 .cargo/config.toml 中修改:
[env]
DEFMT_LOG = "trace"
CHIPSERIE = "stm32f103"
这里把它改成 debug,就可以只输出 debug 及以上等级的调试信息。
[WARN ] This is a warn. (first_stm_f first-stm-f/src/main.rs:26)
[DEBUG] This is a debug. (first_stm_f first-stm-f/src/main.rs:27)
[INFO ] This is a info. (first_stm_f first-stm-f/src/main.rs:29)
[ERROR] This is an error (first_stm_f first-stm-f/src/main.rs:30)
点亮一个 LED
在此之前,有必要先了解一下 RTIC 的基本结构与工作方法,
虽然我现在想来给初步使用 Rust 开发单片机的人说这些东西就像给刚学会 C 语言的人直接说用 FreeRTOS 一样。不过,我依然认为并行任务是大部分开发的前提。
简单说说 RTIC
RTIC 可以把任务分成两类:
- 硬件任务:由中断触发,例如定时器中断、EXTI 按键中断。
- 软件任务:由其他任务调用
task_name::spawn()触发。
现在我们的 main.rs 文件结构大概长这样:
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true)]
mod app {
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
(Shared {}, Local {}, init::Monotonics())
}
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
continue;
}
}
}
这就是 RTIC 的基本结构,init 是初始化函数,idle 是空闲函数,上一节已经简单说过。
RTIC 使用宏来标记函数作为一个任务,常见的宏标记如下:
#[shared]:多个任务都可能访问的资源。#[local]:只属于某一个任务或上下文的资源。#[init]:系统初始化阶段,只运行一次。#[idle]:没有其他任务运行时执行。#[task]:普通 RTIC 任务。#[task(binds = TIM2)]:绑定到硬件中断的任务。
开始点亮
STM32F103C8T6 Blue Pill 的板载 LED 接在 PC13 引脚上,使用 stm32f1xx-hal 提供的 GPIO 驱动来控制它。
在此之前,我们要手动初始化一下 GPIO,这在以前是 STM32CubeMX 所做的事情,使用 Rust 必须克服这个时钟配置问题。
初始化函数里面添加这几项,慢慢理解:
let device = ctx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(RawConfig {..Default::default()}, &mut flash.acr);
device 是 init 函数的上下文参数,里面包含了所有的外设。FLASH 和 RCC 是两个外设,分别是 Flash 存储器和时钟控制器。constrain() 方法是 stm32f1xx-hal 提供的一个方法,用于将外设的寄存器映射到 HAL 的结构体中,方便后续操作。文档中对这个方法的描述是:限制外设,以确保其能与其他抽象层良好协作。
freeze() 方法是 stm32f1xx-hal 提供的一个方法,用于冻结时钟配置,确保时钟配置不会被修改。RawConfig 是一个结构体,用于配置时钟的参数。
可以看看 RawConfig 的定义:
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RawConfig {
pub hse: Option<u32>,
pub hse_bypass: bool,
pub pllmul: Option<u8>,
pub hpre: HPre,
pub ppre1: PPre,
pub ppre2: PPre,
#[cfg(any(feature = "stm32f103", feature = "connectivity"))]
pub usbpre: UsbPre,
pub adcpre: AdcPre,
pub allow_overclock: bool,
}
这里面的参数都是时钟相关的配置,hse 是外部高速晶振的频率,hse_bypass 是是否使用外部晶振,pllmul 是 PLL 的倍频系数,hpre 是 AHB 总线的分频系数,ppre1 和 ppre2 是 APB1 和 APB2 总线的分频系数,usbpre 是 USB 的分频系数,adcpre 是 ADC 的分频系数,allow_overclock 是是否允许超频。
此处我们使用了 RawConfig {..Default::default()},表示使用默认配置,默认配置是:
impl Default for RawConfig {
fn default() -> Self {
Self {
hse: None,
hse_bypass: false,
pllmul: None,
hpre: HPre::Div1,
ppre1: PPre::Div1,
ppre2: PPre::Div1,
#[cfg(any(feature = "stm32f103", feature = "connectivity"))]
usbpre: UsbPre::Div1_5,
adcpre: AdcPre::Div2,
allow_overclock: false,
}
}
}
- 没有外部晶振,使用内部高速晶振。
- 不使用外部晶振。
- 没有 PLL 倍频,使用默认倍频。
- AHB 总线不分频。
- APB1 总线不分频。
- APB2 总线不分频。
- USB 总线分频为 1.5。
- ADC 总线分频为 2。
- 不允许超频。
如果需要使用 PLL 倍频,可以设置 pllmul 参数,例如:
let mut rcc = rcc.freeze(RawConfig {
pllmul: Some(8), // Set the PLL multiplier to 8
..Default::default()
}, &mut flash.acr);
这样就是将 PLL 倍频设置为 8,也就是将内部高速晶振的频率乘以 8,得到系统时钟频率。
如果确实不知道频率如何设置,可以使用 STM32CubeMX 辅助进行频率解决方案配置。
比如说,STM32CubeMX 给出了如下解决方案:

这个配置中:
- PLLMul x16
- SYSCLK 64MHz
- PCLK1 32MHz
不过,库中给了一个更简便的计算方案,可不手算原始数据,如下:
let mut rcc = rcc.freeze( Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()), &mut flash.acr, );这样,我们写 RCC 也不会很麻烦了。
然后,把 GPIOC 拿出来调用。
let mut gpioc = device.GPIOC.split(&mut rcc);
这里 split() 方法用于将 GPIO 模块拆分为独立的引脚和寄存器,这会重置 GPIO 模块的状态。此处 gpioc 类型为 Parts。
然后开始定义 GPIO 外设,以便于可以直接作为 LED 外设调用:
let mut led = gpioc
.pc13
.into_push_pull_output(&mut gpioc.crh);
into_push_pull_output() 方法用于将 GPIO 引脚配置为推挽输出模式,这样就可以直接控制 LED 的亮灭。
crh
crh是 STM32F1 系列 GPIO 的端口配置高寄存器。F1 系列单片机通常是:
- GPIOC->CRL 控制 PC0 ~ PC7
- GPIOC->CRH 控制 PC8 ~ PC15
所以如果要配置 PC13,就需要使用
crh寄存器。
把此外设设置为低电平:
led.set_low();
即可点亮板载 LED。
为什么是低电平点亮?
Blue Pill 的板载 LED 是共阳极的,也就是 LED 的阳极接在 VCC 上,阴极接在 GPIO 引脚上,所以当 GPIO 输出低电平时,LED 就会点亮。
到此,完整的 init 函数代码如下:
#[init]
fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
let device = ctx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(
Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()),
&mut flash.acr,
);
info!(
"sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz",
rcc.clocks.sysclk().to_Hz(),
rcc.clocks.hclk().to_Hz(),
rcc.clocks.pclk1().to_Hz(),
rcc.clocks.pclk2().to_Hz(),
rcc.clocks.adcclk().to_Hz(),
);
let mut gpioc = device.GPIOC.split(&mut rcc);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
led.set_low();
(
Shared {
// Initialization of shared resources go here
},
Local {
// Initialization of local resources go here
},
init::Monotonics()
)
}
烧录后虽然点亮了 LED,但是这个写法很不雅观。
规范地放置外设
外设可以被 RTIC 的任意一个任务控制,所以必然是共享的。上述这种写法只能说达到了目的,但是不符合工程项目开发要求。于是开始按照要求把外设这些东西好好放起来,然后建立一个新的任务单独来控制这个 LED。(显然初始化里面点灯很不好)
首先,开机先把 LED 关掉:
led.set_high();
然后把 LED 外设放置在 Local 供调用:
#[local]
struct Local {
led: PC13<Output<PushPull>>,
}
GPIO 外设写在 Local 里面时是需要指定其属性的,比如 Output 和 PushPull,且必须与初始化的配置一致。
别忘了,init 返回了一个元组,这个元组必须与上面的 Local 和 Share 一致:
(
Shared {
// Initialization of shared resources go here
},
Local {
// Initialization of local resources go here
led,
},
init::Monotonics()
)
然后建立一个 led_control 任务,专门用来控制 LED。但是需要注意,RTIC 任务必须要一个定时器或者中断来触发,所以我们需要一个中断这个比较丰富的资源来触发这个任务。
先搞个定时器,这里用 EXTI0。添加方法为在 mod 头上的宏标注里写一个 dispatchers。
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])]
现在建立任务:
#[task(local = [led])]
fn led_control(ctx: led_control::Context) {
let led = ctx.local.led;
led.toggle();
}
注意:必须使用 #[task(local = [led])] 来指定这个任务使用的本地资源。
初始化最后调起这个任务:
led_control::spawn().unwrap();
把空闲等待换成 wfi() 指令:
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
cortex_m::asm::wfi();
}
}
警告:
wfi()指令是 ARM Cortex-M 系列处理器的一个指令,用于让处理器进入低功耗模式,等待中断唤醒。但是这样会导致defmt调试信息完全被阻断,所以在调试阶段不使用wfi(),正式编译发布版本时再使用。本文档后续也不使用
wfi()。
编译烧录之,灯正常亮。
现在,完整代码应当如下:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Print panic message to probe console
use defmt_rtt as _;
use panic_probe as _;
#[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])]
mod app {
use defmt::info;
use stm32f1xx_hal::{
gpio::{Output, PushPull, PC13},
prelude::*,
rcc::Config,
};
#[shared]
struct Shared {}
#[local]
struct Local {
led: PC13<Output<PushPull>>,
}
#[init]
fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
let device = ctx.device;
let mut flash = device.FLASH.constrain();
let rcc = device.RCC.constrain();
let mut rcc = rcc.freeze(
Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()),
&mut flash.acr,
);
info!(
"sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz",
rcc.clocks.sysclk().to_Hz(),
rcc.clocks.hclk().to_Hz(),
rcc.clocks.pclk1().to_Hz(),
rcc.clocks.pclk2().to_Hz(),
rcc.clocks.adcclk().to_Hz(),
);
let mut gpioc = device.GPIOC.split(&mut rcc);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
led.set_high();
led_control::spawn().unwrap();
(
Shared {
// Initialization of shared resources go here
},
Local {
// Initialization of local resources go here
led,
},
init::Monotonics(),
)
}
#[task(local = [led])]
fn led_control(ctx: led_control::Context) {
let led = ctx.local.led;
led.toggle();
}
// Optional idle, can be removed if not needed.
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
// cortex_m::asm::wfi();
continue;
}
}
}
キラキラ✨
点亮了就要让它闪烁起来,但是闪烁有很多方法。
cortex_m::asm::delay()
这个方法通用,不用定时器,直接写上去就能用。
#[task(local = [led])]
fn led_control(ctx: led_control::Context) {
let led = ctx.local.led;
loop {
led.toggle();
info!("LED toggled!");
cortex_m::asm::delay(6_400_000);
}
}
但是,代价呢?
- CPU 占用率高:
delay()是一个忙等待函数,CPU 会一直执行NOP,无法处理其他任务。 - 延时为 CPU 时间刻:
delay()的延时是基于 CPU 时钟的,如果 CPU 时钟变化,延时也会变化。 - 延时不精确:途中仍然可以被中断打断,而计时被多个
NOP占住而延长。
spawn_after()
需要注意一个问题:使用此延时方法时,需要在任务中使用 spawn_after() 来触发下一个任务,而不是在任务中使用循环。
#[task(local = [led])]
fn led_control(ctx: led_control::Context) {
ctx.local.led.toggle();
info!("LED toggled!");
led_control::spawn_after(200.millis()).unwrap();
}
这样就是正常的 RTIC 任务触发方式了,spawn_after() 会在指定时间后触发下一个任务,而下一个任务就是本任务。
如果你这样写:
#[task(local = [led])]
fn led_control(ctx: led_control::Context) {
loop {
ctx.local.led.toggle();
info!("LED toggled!");
led_control::spawn_after(200.millis()).unwrap();
}
}
那么你会在第二次循环的时候触发下一个任务,而下一个任务容量不足以再创建一个任务与之并行,导致直接跳 HardFault,所以 RTIC 的任务是不能在任务中使用死循环来保活的。
上述时 RTIC v1.x 的做法,接下来的操作需要你升级到更新的 RTIC 版本。
cargo remove cortex-m-rtic cargo add rtic --features thumbv7-backend cargo add rtic-monotonics --features cortex-m-systick cargo add portable-atomic --features unsafe-assume-single-core去掉
Cargo.toml里stm32f1xx-halcrate 的rticfeature。[dependencies.stm32f1xx-hal] version = "0.11.0" features = ["stm32f103"]去除
Mono标注,改为在根上的写法:// Create a monotonic timer based on the SysTick peripheral with a tick frequency of 1 kHz (1 ms) rtic_monotonics::systick_monotonic!(Mono, 1_000);此处
1_000表示 1 kHz,也就是 1 ms 的时间刻度。这是时间计算精度,如果需要更高精度的时间刻度,可以使用更高的频率,比如 10 kHz 或 100 kHz。如果需要使用
TIM2定时器作为时间刻度,可以使用stm32_tim2_monotonic!宏来创建一个基于TIM2的单调计时器。需在rtic-monotonics上开stm32_tim2feature,但是此举不太方便,此处暂不使用。把任务函数换成
async异步函数。然后换掉prelude::*的提供 crate。改完后,main.rs文件应当如下:#![deny(unsafe_code)] #![no_main] #![no_std] // Print panic message to probe console use defmt_rtt as _; use panic_probe as _; // Create a monotonic timer based on the SysTick peripheral with a tick frequency of 1 kHz (1 ms) rtic_monotonics::systick_monotonic!(Mono, 1_000); #[rtic::app(device = stm32f1xx_hal::pac, peripherals = true, dispatchers = [EXTI0])] mod app { use crate::Mono; use defmt::info; use rtic_monotonics::systick::prelude::*; use stm32f1xx_hal::{ gpio::{Output, PC13, PushPull}, prelude::*, rcc::Config }; #[shared] struct Shared {} #[local] struct Local { led: PC13<Output<PushPull>>, } #[init] fn init(cx: init::Context) -> (Shared, Local) { let device = cx.device; let mut flash = device.FLASH.constrain(); let rcc = device.RCC.constrain(); let mut rcc = rcc.freeze( Config::hsi().sysclk(64.MHz()).pclk1(32.MHz()), &mut flash.acr, ); info!( "sysclk={} Hz hclk={} Hz pclk1={} Hz pclk2={} Hz adcclk={} Hz", rcc.clocks.sysclk().to_Hz(), rcc.clocks.hclk().to_Hz(), rcc.clocks.pclk1().to_Hz(), rcc.clocks.pclk2().to_Hz(), rcc.clocks.adcclk().to_Hz(), ); let mut gpioc = device.GPIOC.split(&mut rcc); let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); led.set_high(); Mono::start(cx.core.SYST, 64_000_000); led_control::spawn().unwrap(); ( Shared { // Initialization of shared resources go here }, Local { // Initialization of local resources go here led, } ) } #[task(local = [led], priority = 1)] async fn led_control(cx: led_control::Context) { loop { cx.local.led.toggle(); info!("LED toggled!"); Mono::delay(200.millis()).await; } } // Optional idle, can be removed if not needed. #[idle] fn idle(_: idle::Context) -> ! { loop { // cortex_m::asm::wfi(); continue; } } }同时
Cargo.toml依赖应当如下:[dependencies] cortex-m = "0.7" cortex-m-rt = "0.7" panic-probe = { version = "1.0.0", features = ["defmt"] } defmt = "1.1.1" defmt-rtt = "1.3.0" rtic = { version = "2.3.1", features = ["thumbv7-backend"] } rtic-monotonics = { version = "2.2.1", features = ["cortex-m-systick"] } portable-atomic = { version = "1.15.0", features = ["unsafe-assume-single-core"] } [dependencies.stm32f1xx-hal] version = "0.11.0" features = ["stm32f103"] [build-dependencies] reqwest = { version = "0.13", features = ["blocking"] }
Mono::delay()
很不幸的是,如果你习惯了 FreeRTOS 的 vTaskDelay() 操作方法,那么上面使用的 spawn_after() 不太适合你。不过 Mono::delay() 方法就很友好了。
#[task(local = [led], priority = 1)]
async fn led_control(cx: led_control::Context) {
loop {
cx.local.led.toggle();
info!("LED toggled!");
Mono::delay(200.millis()).await;
}
}
可见,Mono::delay() 方法可以在异步任务中使用,且不会阻塞其他任务的执行。不过,注意你的任务在 RTIC v2 中必须是异步的,使用 async fn 来定义。这和 FreeRTOS 的 vTaskDelay() 方法类似,但更符合 Rust 的异步编程风格。
Mono::delay_until()
同理,这更类似于 FreeRTOS 的 vTaskDelayUntil() 方法,可以在异步任务中使用,且不会阻塞其他任务的执行。
#[task(local = [led], priority = 1)]
async fn led_control(cx: led_control::Context) {
loop {
let mut now = Mono::now();
now += 1.secs();
cx.local.led.toggle();
info!("LED toggled!");
Mono::delay_until(now).await;
}
}
这样可以使整个任务在 1 秒内执行一次,且不会阻塞其他任务的执行。
到此,你已经学会了如何使用 RTIC 框架在 STM32F103C8T6 上点亮并闪烁一个 LED,同时掌握了 defmt 调试方法和 probe-rs 调试工具的使用。
推荐使用 RTIC v2 的调度方法来构建工程,这更符合 Rust 的异步编程风格,也更适合在嵌入式系统中使用。同时对于习惯使用 FreeRTOS 的开发者来说,RTIC v2 提供了类似的任务调度方法,使得从 FreeRTOS 迁移到 RTIC 更加顺畅。
后续章节都会使用 RTIC v2 作为默认的任务调度框架。



