Rust for STM32 Developer – 第 1 节 – 调试与 GPIO 初步

调试与 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。

0xE000E004 Interrupt Control Type Register r

0xE000E010 SysTick Control and Status Register r/w
0xE000E014 SysTick Reload Value Register r/w
0xE000E018 SysTick Current Value Register r/wc
0xE000E01C SysTick Calibration Value Register r

0xE000E100 Irq 0 to 31 Set Enable Register r/w
0xE000E11C Irq 224 to 239 Set Enable Register r/w
0xE000E180 Irq 0 to 31 Clear Enable Register r/w
0xE000E19C Irq 224 to 239 Clear Enable Register r/w
0xE000E200 Irq 0 to 31 Set Pending Register r/w
0xE000E21C Irq 224 to 239 Set Pending Register
0xE000E280 Irq 0 to 31 Clear Pending Register
0xE000E29C Irq 224 to 239 Clear Pending Register
0xE000E300 Irq 0 to 31 Active Bit Register
0xE000E31C Irq 224 to 239 Active Bit Register
0xE000E400 Irq 0 to 31 Priority Register
0xE000E4F0 Irq 236 to 239 Priority Register

0xE000ED00 CPUID Base Register r
0xE000ED04 Interrupt Control State Register r/w
0xE000ED08 Vector Table Offset Register r/w
0xE000ED0C Application Interrupt/Reset Control Register r/w
0xE000ED10 System Control Register r/w
0xE000ED14 Configuration Control Register r/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 Debug

这样一来,你就可以随意在代码上面打断点,然后使用 probe-rs 进行调试了。

由于上一节的模板里面包括了 Visual Studio Code 的配置文件,所以你可以直接使用 Visual Studio Code 的调试功能进行调试。

注意:实际调试时发现了问题,defmt 配置默认 dataFormat 为 String,导致终端打印显示乱码,所以需要改变 rttChannelFormats 字段为以下内容。

"rttChannelFormats": [
    {
        "dataFormat": "Defmt",
        "channelNumber": 0,
        "showTimestamps": true,
        "showLocation": true
    }
],

Debug

你不仅能够看到变量值,还能够看到调用栈和寄存器的值,甚至可以双击修改寄存器和变量的值。

监视一栏里有像 IAR 一样的变量变化探测工具,此处还没有正式使用其编写代码,所以暂时没有使用到。在下一任务 GPIO 的时候会使用起来。

Debug

像往常写 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 给出了如下解决方案:

![STM32CubeMX Clock Configuration](https://cdn.jsdelivr.net/gh/zhangnjsd/img-jsdelivr-go/rust-stm/1/> stm32cubemx-clock.png)

这个配置中:

  • 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-hal crate 的 rtic feature。

[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_tim2 feature,但是此举不太方便,此处暂不使用。

把任务函数换成 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 作为默认的任务调度框架。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇