这篇 Blog 将着重介绍如何使用 Rust 语言工具链配置 STM32 嵌入式开发环境,并将指导您使用 Rust 编写一个简易的、基于电磁传感器循迹的智能车程序。

💡 这里所使用的单片机芯片为 STM32F103RCT6。不同的芯片可能产生细微但不大的差别。

安装 Rust STM32 工具链

这里默认已经安装好了rustupcargo 等工具,以及 openocd这一调试工具。

输入命令:

1
2
3
# 注意:不同 STM32 芯片对应的目标可能不同。
# rustup target add thumbv7em-none-eabihf # STM32F4xx / F7xx / L7xx / ...
rustup target add thumbv7m-none-eabi # STM32F1xx / L1xx

💡 这里附上一张 MCU 与不同 Rust 构建目标的参考表。

MCU 系列 Rust target
STM32F0/L0/G0 thumbv6m-none-eabi
STM32F1/L1 thumbv7m-none-eabi
STM32F2/F3/F4/F7/L4/H7 thumbv7em-none-eabithumbv7em-none-eabihf(硬浮点)

为了使得 Rust 可以正常完成交叉编译,我们仍然需要安装交叉编译工具链。

输入命令(这里以 Debian 为例):

1
sudo apt install gcc-arm-none-eabi gdb-multiarch

为了使得我们可以自动化烧录、运行、调试这一全过程,我们还需要安装 probe-rs 工具。

运行命令:

1
cargo install probe-rs

使用简易模板方式创建 STM32 工程

克隆模板仓库

这里有一个 Rust 的仓库模板,尤其适用于基于 VSCode 的开发。

输入命令:

1
git clone https://github.com/redstone6835/example-rust-stm32

以下为叙述方便,将软件仓库根目录视为/ 目录。

这里使用 VSCode 可以直接打开这个文件夹。在打开文件夹之前,请安装 rust-analyzerDependi插件以确保更加完美的 Coding 体验。

💡 如果您使用的是其他编辑器,理论上可以在配置好 Rust 开发环境之后直接以打开任何一个 Rust 工程的形式打开本模板。

注意:本项目默认使用 openocd 进行烧录、调试。使用其他方法烧录可能产生其他问题。

具体细节请参考项目中的 /.vscode/launch.json/.vscode/tasks.json

修改有关配置文件

打开 /Cargo.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[package]
name = "example-rust-stm32"
version = "0.1.0"
edition = "2024"

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1.0.0"
panic-halt = "1.0.0"

stm32f1xx-hal = { version = "0.11.0", features = ["stm32f103", "medium"] }

[profile.dev]
opt-level = 0
debug = true
lto = false
codegen-units = 1
strip = false

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
debug = false
panic = "abort"

package 中的 name 项修改为本工程项目的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[package]
name = "car"
version = "0.1.0"
edition = "2024"

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1.0.0"
panic-halt = "1.0.0"

stm32f1xx-hal = { version = "0.11.0", features = ["stm32f103", "medium"] }

[profile.dev]
opt-level = 0
debug = true
lto = false
codegen-units = 1
strip = false

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
debug = false
panic = "abort"

💡 注意:在本次修改项目名称之后,请同步修改项目根目录文件夹的名称,以确保 VSCode 可以正常找到可执行文件并执行相关操作,并便于管理各个不同项目。

修改链接脚本与 MCU 型号

💡 注意:这里需要提前获知您所正在开发的 MCU 的实际情况(如 Flash 大小,RAM 大小等参数),如果您并不知晓,请查阅 STM32 官方提供的相关数据手册获知情况,否则很可能导致烧录失败甚至程序无法正常通过编译的后果。

打开 /memory.x

1
2
3
4
5
6
7
8
/* memory.x */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

_stack_start = ORIGIN(RAM) + LENGTH(RAM);

将这里的 FLASH 与 RAM 的 LENGTH 段改为相应 MCU 的参数,如 STM32F103RCT6 :

1
2
3
4
5
6
7
8
/* memory.x */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 48K
}

_stack_start = ORIGIN(RAM) + LENGTH(RAM);

我们在这里使用了 xpack 方式进行辅助编译。修改链接脚本之后,仍然需要修改 xpack 的相应配置。打开 /xpack/src/main.rs40 - 43 行处:

1
2
3
4
// 输出占用率
// STM32F103ZET6: Flash 512KB, RAM 64KB
const FLASH_TOTAL: f64 = 512.0 * 1024.0;
const RAM_TOTAL: f64 = 64.0 * 1024.0;

将这里的 Flash 和 RAM 字段同步进行修改:

1
2
3
4
// 输出占用率
// STM32F103ZET6: Flash 256KB, RAM 48KB
const FLASH_TOTAL: f64 = 256.0 * 1024.0;
const RAM_TOTAL: f64 = 48.0 * 1024.0;

💡 可选:修改运行配置

打开 /.cargo/config.toml

1
2
3
4
5
6
7
8
9
[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]
runner = "probe-rs run --chip STM32F103ZE"

rustflags = [
"-C", "link-arg=-Tlink.x",
]

runner 字段的 chip 参数改为具体的 MCU 型号。

1
2
3
4
5
6
7
8
9
[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]
runner = "probe-rs run --chip STM32F103RC"

rustflags = [
"-C", "link-arg=-Tlink.x",
]

Start coding !!!

完成上述配置之后,可以开始正式开发智能车项目了。我们首先研究一下 MCU 各引脚的用法。

引脚定义

引脚 复用外设/功能 程序中的用途
PA4 ADC1_CH4 右侧电感 ADC 采样
PC5 ADC1_CH15 左侧电感 ADC 采样
PA8 GPIO 拨码开关 1
PC9 GPIO 拨码开关 2
PC8 GPIO 拨码开关 3
PC7 GPIO 拨码开关 4
PA11 GPIO LED1
PA15 GPIO LED2
PC12 GPIO LED3
PB3 GPIO LED4
PA12 GPIO 总使能开关(Enable)输入
PC13 GPIO 使能输出至驱动板
PB0 TIM3_CH3 舵机 PWM 输出
PB8 GPIO 左轮控制推挽输出
PB9 GPIO 右轮控制推挽输出
PB6 TIM4_CH1 左轮 PWM 输出
PB7 TIM4_CH2 右轮 PWM 输出
PA9 USART1_TX 调试用串口
PA10 USART1_RX 调试用串口

模板原生代码及相关句柄的意义

打开 /src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;

use stm32f1xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
// 获取外设访问接口
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
// 配置时钟
let mut rcc = dp.RCC.constrain();
let mut delay = cp.SYST.delay(&rcc.clocks);
// GPIOB 时钟开启
let mut gpiob = dp.GPIOB.split(&mut rcc);

// PB5 连接板载 LED
let mut led = gpiob.pb5.into_push_pull_output(&mut gpiob.crl);

loop {
led.toggle(); // 翻转电平
delay.delay_ms(1000_u32);
}
}

这里程序定义了 PB5 引脚的复用(推挽输出),并用 PB5 来控制一个 LED 的闪烁,每次状态持续 1s 之后翻转电平状态。

💡 相应句柄的意义

句柄 抽象层 含义
dp PAC 拿到所有硬件寄存器的所有权(外设级)
cp 核心 PAC 拿到 Cortex-M 内核外设(SysTick、NVIC)
rcc HAL 将不安全的 RCC 变成易用的时钟配置对象
delay HAL 使用 SysTick 生成延时功能

💡 下文中,若未明确指定,默认将提及 Rust 代码视作 /src/main.rs 的一部分。

配置 GPIO 复用并禁用 JTAG

然而我们的程序规模相较于简单的 LED 闪烁程序要大多了,一个 GPIOB 通道完全无法满足,我们需要开辟更多通道:

1
2
3
let mut gpioa = dp.GPIOA.split(&mut rcc);
let mut gpiob = dp.GPIOB.split(&mut rcc);
let mut gpioc = dp.GPIOC.split(&mut rcc);

这里传入rcc 作为参数,避免了如同在 C 语言的 STM32 开发中仍然需要单独启用 GPIO 时钟,大幅提高了代码可读性。

同时,我们注意到,PA15 与 PB3 两个引脚默认被复用为了 JTAG 调试,我们无法直接将其复用为推挽输出,需要禁用 JTAG 调试,释放这两个引脚,才能够将其物理引脚正常复用。这里还需要引入一个新的抽象层:

1
let mut afio = dp.AFIO.constrain(&mut rcc);

💡 afio 句柄提供了重映射、JTAG 配置等安全 API。

这里就可以将两个我们所需要的 JTAG 引脚正常释放,并获得相应句柄:

1
let (pa15_released, pb3_released, _) = afio.mapr.disable_jtag(gpioa.pa15, gpiob.pb3, gpiob.pb4);

配置基本输入输出

根据主板的设计,拨码开关各个开关与使能开关当处于低电平时视为打开,处于高电平时视为关闭,因此我们将这几个引脚配置为上拉输入。四个 LED 灯与使能输出可以使用推挽输出进行控制。

1
2
3
4
5
6
7
8
9
10
11
12
let switch1 = gpioa.pa8.into_pull_up_input(&mut gpioa.crh);
let switch2 = gpioc.pc9.into_pull_up_input(&mut gpioc.crh);
let switch3 = gpioc.pc8.into_pull_up_input(&mut gpioc.crh);
let switch4 = gpioc.pc7.into_pull_up_input(&mut gpioc.crl);

let mut led1_pa11 = gpioa.pa11.into_push_pull_output(&mut gpioa.crh);
let mut led2_pa15 = pa15_released.into_push_pull_output(&mut gpioa.crh);
let mut led3_pc12 = gpioc.pc12.into_push_pull_output(&mut gpioc.crh);
let mut led4_pb3 = pb3_released.into_push_pull_output(&mut gpiob.crl);

let enable_switch_pa12 = gpioa.pa12.into_pull_up_input(&mut gpioa.crh);
let mut enable_out_pc13 = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

这里 &mut gpioa.crh 等类似字段可根据引脚所在频道与编号是否为高八位或低八位获知。如 pa8 位于 gpioa 通道的高八位(8 - 15位视为高八位,0 - 7位视为低八位),则传入 &mut gpioa.crh ,若位于低八位(如 pc7),则传入 &mut gpioc.crl

💡 注意:这里 pa15pb3 句柄的获取需要通过之前获取的 pa15_releasedpb3_released 句柄进行实现。如pb3,若使用 gpioa.pb3 等原生句柄,由于其类型为Pin<'B', 3, Debugger> ,而非我们所预期的 Pin<'B', 3, Input<Floating>> ,其不具有 into_push_pull_output 方法,无法成功获取 led4_pb3 句柄。而 pb3_released 句柄的类型为Pin<'B', 3> ,是通过 JTAG 的释放所获取的新句柄,具有该方法,因此可以成功获取led4_pb3句柄。

配置 ADC 频道句柄

为了获取 PA4 和 PC5 输入进来的传感器信息,我们需要提前配置 ADC1 通道的句柄,并将 PA4 和 PC5 配置为模拟信号输入。

1
2
3
let mut adc1 = adc::Adc::new(dp.ADC1, &mut rcc);
let mut adc1_ch4_pa4 = gpioa.pa4.into_analog(&mut gpioa.crl);
let mut adc1_ch15_pc5 = gpioc.pc5.into_analog(&mut gpioc.crl);

我们可以使用adc1.read(&mut adc1_ch4_pa4).unwrap() 的方式读取 ADC 采集获取的值,并由这个值推理得到转向策略。

配置舵机 PWM 输出

在这里,我们首先定义某一 PWM 中值,使得舵机接受到该占空比的 PWM 波时,舵机角度能够刚好朝向正前方。但是为了适应 C 语言的某个例程,这里使用的是认为占空比最大值为 9999 时的占空比设置,我们暂且称其为等效占空比。

1
2
3
let steer_pwm_duty_center: u16 = 750 - 5; // 舵机 PWM 中值
let steer_pwm_duty_max = steer_pwm_duty_center + 60; // 舵机 PWM 最大值
let steer_pwm_duty_min = steer_pwm_duty_center - 60; // 舵机 PWM 最小值

同时,我们需要将 PB0 配置为复用推挽输出,并配置相应 PWM 频道。这里是 TIM3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将 PB0 配置为 TIM3_CH3 复用推挽输出
let tim3_ch3 = gpiob.pb0.into_alternate_push_pull(&mut gpiob.crl);
// 配置 TIM3 为 PWM 输出,频率 1kHz
let mut steer_pwm =
dp.TIM3.pwm_hz::<Tim3NoRemap, _, _>(
tim3_ch3,
&mut afio.mapr,
1.kHz(),
&mut rcc
);
// 设置占空比为 舵机 PWM 中值
steer_pwm.set_duty(Channel::C3, (steer_pwm_duty_center / 9999) * steer_pwm.get_max_duty());
// 使能舵机 PWM 输出
steer_pwm.enable(Channel::C3);

这里,我们指定 TIM3 频道的频率为 1kHz,并通过等效占空比将占空比设置为真正的中值。

配置左右轮控制流输出

因为左右轮的速度控制是 PWM 控制的,因此我们完全可以使用同样的一种方式来配置相应的句柄。而不同的是,左右轮的转向控制是通过给高电平或低电平实现的,因此我们还需要配置两个推挽输出的引脚进行转向的控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let mut left_wheel_pb8 = gpiob.pb8.into_push_pull_output(&mut gpiob.crh);
let mut right_wheel_pb9 = gpiob.pb9.into_push_pull_output(&mut gpiob.crh);
let tim4_ch1 = gpiob.pb6.into_alternate_push_pull(&mut gpiob.crl);
let tim4_ch2 = gpiob.pb7.into_alternate_push_pull(&mut gpiob.crl);
let mut wheels_pwm =
dp.TIM4.pwm_hz::<Tim4NoRemap, _, _>(
(tim4_ch1, tim4_ch2),
&mut afio.mapr,
1.kHz(),
&mut rcc
);
wheels_pwm.set_duty(Channel::C2, 0);
wheels_pwm.set_duty(Channel::C1, 0);
wheels_pwm.enable(Channel::C2);
wheels_pwm.enable(Channel::C1);

配置调试信息输出

在比赛的过程中,我们常常需要需要获知一个点处具体的传感器输出,从而对车本身进行硬件方面的调试。为了准确获知软件方面的传感器值,我们通常使用串口来直接读取。这里配置了 USART1 串口,实时发送这些信息,以便我们进行调试。

1
2
3
4
5
6
7
8
let usart1_tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh);
let usart1_rx = gpioa.pa10;
let mut serial = dp.USART1
.serial(
(usart1_tx, usart1_rx),
Config::default().baudrate(115200.bps()),
&mut rcc
);

💡 这里使用了 VOFA+JustFloat 协议进行发送。VOFA+ 是一个强大的上位机软件。

进入主循环

Rust STM32 程序的主循环通常使用 loop 死循环,形如:

1
2
3
loop {
// 代码...
}

处理传感器输出时遇到的困难

我们可以使用这样的代码读取传感器的输出,并进行相关的运算:

1
2
let adc_value_right = adc1.read(&mut adc1_ch4_pa4).unwrap();
let adc_value_left = adc1.read(&mut adc1_ch15_pc5).unwrap();

但是,在实际的操作中,我们发现:这样直接读取出来的 ADC 值存在着高达 100 的噪声。为了消除噪声带来的影响,我们必须对数据进行分析,尽可能降低误差。

一种基于正态分布拟合的滤波算法 gauss_filter

我们首先考虑实现快速排序算法,对某一目标数组进行排序。

创建并打开/src/utils.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[inline(always)]
pub fn quicksort(arr: &mut [i32]) {
let len = arr.len();
if len <= 1 {
return;
}
let pivot = arr[len - 1];
let mut i = 0;
for j in 0..len - 1 {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, len - 1);
if i > 0 {
quicksort(&mut arr[0..i]);
}
if i + 1 < len {
quicksort(&mut arr[i+1..]);
}
}

关于快速排序算法的原理,这里不过多赘述。

我们考虑使用一组测得的数据进行正态分布曲线的拟合,将e的指数视为各个数据的权重,计算出加权均值,以该值作为真实值的预估。

为了能够进行正态分布曲线的拟合,我们必须引入一个数学函数库。在嵌入式平台,libm 是一个不错的选择。打开/Cargo.toml ,在dependencies 中写入:

1
libm = "0.2.15”

创建并打开 /src/gauss.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
use crate::utils::quicksort;

pub fn gauss_filter(x: &[i32], n: usize) -> f64 {
if n <= 0 {
return 0.0;
}
if n == 1 {
return x[0] as f64;
}
// 创建一个临时数组来存储排序后的数据
let mut sorted = [0i32; 64]; // 假设最大长度为64,根据实际需求调整
for i in 0..n {
sorted[i] = x[i];
}
quicksort(&mut sorted[0..n]);
// --- 初始估计 ---
let mut sum: f64 = 0.0;
for i in 0..n {
sum += sorted[i] as f64;
}
let mut mu = sum / (n as f64);

let mut var: f64 = 0.0;
for i in 0..n {
let diff = sorted[i] as f64 - mu;
var += diff * diff;
}
var /= (n - 1) as f64;
let mut sigma: f64 = libm::sqrt(var);

if sigma < 1e-6 {
sigma = 1e-6;
}

// --- 迭代更新 ---
let max_iter = 30;
let tol = 1e-6;

for _iter in 0..max_iter {
let mut sum_w: f64 = 0.0;
let mut sum_wx: f64 = 0.0;

for i in 0..n {
let diff: f64 = (sorted[i] as f64 - mu) / sigma;
let exponent: f64 = -0.5 * diff * diff;
let mut weight = libm::exp(exponent);
if weight < 1e-10 {
weight = 1e-10;
}

sum_w += weight;
sum_wx += weight * sorted[i] as f64;
}

let new_mu = sum_wx / sum_w;
if libm::fabs(new_mu - mu) < tol {
mu = new_mu;
break;
}

mu = new_mu;

// 更新 sigma
let mut sum_var = 0.0;
let mut sum_var_w = 0.0;
for i in 0..n {
let diff = sorted[i] as f64 - mu;
let exponent = -0.5 * (diff / sigma) * (diff / sigma);
let mut weight = libm::exp(exponent);
if weight < 1e-10 {
weight = 1e-10;
}

sum_var += weight * diff * diff;
sum_var_w += weight;
}
sigma = libm::sqrt(sum_var / sum_var_w);
if sigma < 1e-6 {
sigma = 1e-6;
}
}

return mu;
}

理论上认为,我们只要有了一组及时测得的 ADC 值数据,通过该滤波算法,可以获取一个相较于直接计算均值更加接近真实值的估计值。

处理传感器数据并控制转向

为了持续化存储每次传感器读取的数据,在整个主循环前面,我们加入:

1
2
let mut adc_value_right_buffer = [0_i32; 33];
let mut adc_value_left_buffer = [0_i32; 33];

作为传感器值的缓冲区。为了能够真正做到及时读取数据,我们在主循环中再加入一个循环,使得在主循环的一次循环之下,我们便可以获知最新的数据。

1
2
3
4
5
6
7
8
9
10
11
for i in (1..33).rev() {
let adc_value_right = adc1.read(&mut adc1_ch4_pa4).unwrap();
let adc_value_left = adc1.read(&mut adc1_ch15_pc5).unwrap();
adc_value_right_buffer[i] = adc_value_right;
adc_value_left_buffer[i] = adc_value_left;
send_adc(&mut serial, adc_value_right as f64, adc_value_left as f64);
delay.delay_us(33_u32);
}
// 计算高斯滤波后的值
let filtered_right = gauss::gauss_filter(&adc_value_right_buffer, 16);
let filtered_left = gauss::gauss_filter(&adc_value_left_buffer, 16);

这里的 send_adc 函数,我们在/src/utils.rs 中给出了定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub fn send_adc<W>(serial : &mut W, adc_right : f64, adc_left : f64)
where
W: _embedded_hal_serial_Write<u8> + core::fmt::Write
{
let tail : [u8; 4] = [0x00, 0x00, 0x80, 0x7f];
for byte in adc_left.to_le_bytes().iter() {
serial.write(*byte).ok();
}
for byte in adc_right.to_le_bytes().iter() {
serial.write(*byte).ok();
}

for byte in tail.iter() {
serial.write(*byte).ok();
}
}

用于向调试串口发送每一次读取的 ADC 值数据,以便调试。

通过实际测试,我们发现:电感距离赛道中心越近,ADC 值越大,反之越小。因此,我们可以用左右 ADC 值的差值来近似描述小车相对于赛道的偏转程度,再利用这个 ADC 值对舵机的相对中值取一个特殊的偏差,从而实现小车的整体转向。

1
2
3
4
5
let diff = (filtered_right - filtered_left) as i32;
let steer_pwm_diff = data_limit(diff as f64 * 0.3, steer_pwm_duty_min as f64, steer_pwm_duty_max as f64) as u16;
let steer_pwm_duty = steer_pwm_duty_center - steer_pwm_diff;
// 设置舵机 PWM 占空比
steer_pwm.set_duty(Channel::C3, (steer_pwm_duty / 9999) * steer_pwm.get_max_duty());

其中,data_limit 函数用于将一个值限定于某两个值之间。其在/src/utils.rs 中定义为:

1
2
3
4
5
6
7
8
9
pub fn data_limit(x: f64, min: f64, max: f64) -> f64 {
if x < min {
min
} else if x > max {
max
} else {
x
}
}

控制左右轮的转速

在我们的基本设计中,使能开关用于控制左右轮是否转动。我们试图建立一个新的“相对转速”,使得左右轮的速度能够以一种直观的数字被描述出来,为正时则正转,反之则反转,且这个数字与输出的 PWM 值可以一一对应。我们令相对转速的绝对值的最大值为 4800,表示最大转速;0 则表示停转。当使能开关打到了高电平时,我们认为小车开机,轮子应当转起来;反之小车关机,轮子停转。

1
2
3
4
5
6
7
8
9
10
let mut wheel_speed_left = 0;
let mut wheel_speed_right = 0;
// 根据使能开关状态控制输出
if enable_switch_pa12.is_high() {
wheel_speed_left = 800;
wheel_speed_right = 800;
enable_out_pc13.set_high();
} else {
enable_out_pc13.set_low();
}

且我们发现,两轮的电机转向是由一个“开关状态”来控制的,当输入低电平时为正转,反之为反转,且反转时的相对转速的绝对值与正转时的互补(即和为4800)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let wheel_left_pwm;
let wheel_right_pwm;
if wheel_speed_left >= 0 {
wheel_left_pwm = wheel_speed_left as u16;
left_wheel_pb8.set_low();
} else {
wheel_left_pwm = (4800 + wheel_speed_left) as u16;
left_wheel_pb8.set_high();
}
if wheel_speed_right >= 0 {
wheel_right_pwm = wheel_speed_right as u16;
right_wheel_pb9.set_low();
} else {
wheel_right_pwm = (4800 + wheel_speed_right) as u16;
right_wheel_pb9.set_high();
}
// 设置左右轮 PWM 占空比
wheels_pwm.set_duty(Channel::C2, (wheel_left_pwm / 4800) * wheels_pwm.get_max_duty());
wheels_pwm.set_duty(Channel::C1, (wheel_right_pwm / 4800) * wheels_pwm.get_max_duty());

使用拨码开关控制板载 LED

我们可以直接读取拨码开关的电平值,从而决定 LED 的电平值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 通过拨码开关调整板子上 LED 灯的亮灭
if switch1.is_low() {
led1_pa11.set_high();
} else {
led1_pa11.set_low();
}
if switch2.is_low() {
led2_pa15.set_high();
} else {
led2_pa15.set_low();
}
if switch3.is_low() {
led3_pc12.set_high();
} else {
led3_pc12.set_low();
}
if switch4.is_low() {
led4_pb3.set_high();
} else {
led4_pb3.set_low();
}

大功告成!

整个工程的完整代码如下:

/src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#![no_std]
#![no_main]

mod gauss;
mod utils;

use cortex_m_rt::entry;
use panic_halt as _;
use stm32f1xx_hal::{adc, pac::{self}, prelude::*, serial::Config, timer::{Channel, Tim3NoRemap, Tim4NoRemap}};

use crate::utils::{data_limit, send_adc};

#[entry]
fn main() -> ! {
let steer_pwm_duty_center: u16 = 750 - 5; // 舵机 PWM 中值
let steer_pwm_duty_max = steer_pwm_duty_center + 60; // 舵机 PWM 最大值
let steer_pwm_duty_min = steer_pwm_duty_center - 60; // 舵机 PWM 最小值

// 拿到硬件外设抽象层句柄
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
// 拿到RCC时钟句柄
let mut rcc = dp.RCC.constrain();
let mut afio = dp.AFIO.constrain(&mut rcc);
// 拿到延时定时器句柄
let mut delay = cp.SYST.delay(&rcc.clocks);

// GPIO 开启
let mut gpioa = dp.GPIOA.split(&mut rcc);
let mut gpiob = dp.GPIOB.split(&mut rcc);
let mut gpioc = dp.GPIOC.split(&mut rcc);

// Release PB3 from JTAG to use it as GPIO
let (pa15_released, pb3_released, _) =
afio.mapr.disable_jtag(gpioa.pa15, gpiob.pb3, gpiob.pb4);
let mut adc1 = adc::Adc::new(dp.ADC1, &mut rcc);
let mut adc1_ch4_pa4 = gpioa.pa4.into_analog(&mut gpioa.crl);
let mut adc1_ch15_pc5 = gpioc.pc5.into_analog(&mut gpioc.crl);

// 配置四个拨码开关为推挽输入
let switch1 = gpioa.pa8.into_pull_up_input(&mut gpioa.crh);
let switch2 = gpioc.pc9.into_pull_up_input(&mut gpioc.crh);
let switch3 = gpioc.pc8.into_pull_up_input(&mut gpioc.crh);
let switch4 = gpioc.pc7.into_pull_up_input(&mut gpioc.crl);

let mut led1_pa11 = gpioa.pa11.into_push_pull_output(&mut gpioa.crh);
let mut led2_pa15 = pa15_released.into_push_pull_output(&mut gpioa.crh);
let mut led3_pc12 = gpioc.pc12.into_push_pull_output(&mut gpioc.crh);
let mut led4_pb3 = pb3_released.into_push_pull_output(&mut gpiob.crl);

// 配置使能开关为推挽输入
let enable_switch_pa12 = gpioa.pa12.into_pull_up_input(&mut gpioa.crh);
let mut enable_out_pc13 = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

// 将 PB0 配置为 TIM3_CH3 复用推挽输出
let tim3_ch3 = gpiob.pb0.into_alternate_push_pull(&mut gpiob.crl);
// 配置 TIM3 为 PWM 输出,频率 1kHz
let mut steer_pwm =
dp.TIM3.pwm_hz::<Tim3NoRemap, _, _>(
tim3_ch3,
&mut afio.mapr,
1.kHz(),
&mut rcc
);
// 设置占空比为 舵机 PWM 中值
steer_pwm.set_duty(Channel::C3, (steer_pwm_duty_center / 9999) * steer_pwm.get_max_duty());
// 使能舵机 PWM 输出
steer_pwm.enable(Channel::C3);

// 配置左右轮控制流输出
let mut left_wheel_pb8 = gpiob.pb8.into_push_pull_output(&mut gpiob.crh);
let mut right_wheel_pb9 = gpiob.pb9.into_push_pull_output(&mut gpiob.crh);
let tim4_ch1 = gpiob.pb6.into_alternate_push_pull(&mut gpiob.crl);
let tim4_ch2 = gpiob.pb7.into_alternate_push_pull(&mut gpiob.crl);
let mut wheels_pwm =
dp.TIM4.pwm_hz::<Tim4NoRemap, _, _>(
(tim4_ch1, tim4_ch2),
&mut afio.mapr,
1.kHz(),
&mut rcc
);
wheels_pwm.set_duty(Channel::C2, 0);
wheels_pwm.set_duty(Channel::C1, 0);
wheels_pwm.enable(Channel::C2);
wheels_pwm.enable(Channel::C1);

// 配置 USART 串口通过 JustFloat 协议输出 ADC 值信息
let usart1_tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh);
let usart1_rx = gpioa.pa10;
let mut serial = dp.USART1
.serial(
(usart1_tx, usart1_rx),
Config::default().baudrate(115200.bps()),
&mut rcc
);

let mut adc_value_right_buffer = [0_i32; 33];
let mut adc_value_left_buffer = [0_i32; 33];

loop {
// 读取 ADC 值入缓冲区
for i in (1..33).rev() {
let adc_value_right = adc1.read(&mut adc1_ch4_pa4).unwrap();
let adc_value_left = adc1.read(&mut adc1_ch15_pc5).unwrap();
adc_value_right_buffer[i] = adc_value_right;
adc_value_left_buffer[i] = adc_value_left;
send_adc(&mut serial, adc_value_right as f64, adc_value_left as f64);
delay.delay_us(33_u32);
}

// 计算高斯滤波后的值
let filtered_right = gauss::gauss_filter(&adc_value_right_buffer, 16);
let filtered_left = gauss::gauss_filter(&adc_value_left_buffer, 16);

let diff = (filtered_right - filtered_left) as i32;
let steer_pwm_diff =
data_limit(diff as f64 * 0.3, steer_pwm_duty_min as f64, steer_pwm_duty_max as f64) as u16;
let steer_pwm_duty = steer_pwm_duty_center - steer_pwm_diff;

// 设置舵机 PWM 占空比
steer_pwm.set_duty(Channel::C3, (steer_pwm_duty / 9999) * steer_pwm.get_max_duty());

let mut wheel_speed_left = 0;
let mut wheel_speed_right = 0;
// 根据使能开关状态控制输出
if enable_switch_pa12.is_high() {
wheel_speed_left = 800;
wheel_speed_right = 800;
enable_out_pc13.set_high();
} else {
enable_out_pc13.set_low();
}

let wheel_left_pwm;
let wheel_right_pwm;
if wheel_speed_left >= 0 {
wheel_left_pwm = wheel_speed_left as u16;
left_wheel_pb8.set_low();
} else {
wheel_left_pwm = (4800 + wheel_speed_left) as u16;
left_wheel_pb8.set_high();
}
if wheel_speed_right >= 0 {
wheel_right_pwm = wheel_speed_right as u16;
right_wheel_pb9.set_low();
} else {
wheel_right_pwm = (4800 + wheel_speed_right) as u16;
right_wheel_pb9.set_high();
}
// 设置左右轮 PWM 占空比
wheels_pwm.set_duty(Channel::C2, (wheel_left_pwm / 4800) * wheels_pwm.get_max_duty());
wheels_pwm.set_duty(Channel::C1, (wheel_right_pwm / 4800) * wheels_pwm.get_max_duty());

// 通过拨码开关调整板子上 LED 灯的亮灭
if switch1.is_low() {
led1_pa11.set_high();
} else {
led1_pa11.set_low();
}
if switch2.is_low() {
led2_pa15.set_high();
} else {
led2_pa15.set_low();
}
if switch3.is_low() {
led3_pc12.set_high();
} else {
led3_pc12.set_low();
}
if switch4.is_low() {
led4_pb3.set_high();
} else {
led4_pb3.set_low();
}
}
}

/src/utils.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
use cortex_m::prelude::_embedded_hal_serial_Write;

pub fn data_limit(x: f64, min: f64, max: f64) -> f64 {
if x < min {
min
} else if x > max {
max
} else {
x
}
}

#[inline(always)]
pub fn quicksort(arr: &mut [i32]) {
let len = arr.len();
if len <= 1 {
return;
}

let pivot = arr[len - 1];
let mut i = 0;

for j in 0..len - 1 {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}

arr.swap(i, len - 1);

if i > 0 {
quicksort(&mut arr[0..i]);
}
if i + 1 < len {
quicksort(&mut arr[i+1..]);
}
}

pub fn send_adc<W>(serial : &mut W, adc_right : f64, adc_left : f64)
where
W: _embedded_hal_serial_Write<u8> + core::fmt::Write
{
let tail : [u8; 4] = [0x00, 0x00, 0x80, 0x7f];
for byte in adc_left.to_le_bytes().iter() {
serial.write(*byte).ok();
}
for byte in adc_right.to_le_bytes().iter() {
serial.write(*byte).ok();
}

for byte in tail.iter() {
serial.write(*byte).ok();
}
}

/src/gauss.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
use crate::utils::quicksort;

pub fn gauss_filter(x: &[i32], n: usize) -> f64 {
if n <= 0 {
return 0.0;
}
if n == 1 {
return x[0] as f64;
}

// 创建一个临时数组来存储排序后的数据
let mut sorted = [0i32; 64]; // 假设最大长度为64,根据实际需求调整
for i in 0..n {
sorted[i] = x[i];
}
quicksort(&mut sorted[0..n]);

// --- 初始估计 ---
let mut sum: f64 = 0.0;
for i in 0..n {
sum += sorted[i] as f64;
}
let mut mu = sum / (n as f64);

let mut var: f64 = 0.0;
for i in 0..n {
let diff = sorted[i] as f64 - mu;
var += diff * diff;
}
var /= (n - 1) as f64;
let mut sigma: f64 = libm::sqrt(var);

if sigma < 1e-6 {
sigma = 1e-6;
}

// --- 迭代更新 ---
let max_iter = 30;
let tol = 1e-6;

for _iter in 0..max_iter {
let mut sum_w: f64 = 0.0;
let mut sum_wx: f64 = 0.0;

for i in 0..n {
let diff: f64 = (sorted[i] as f64 - mu) / sigma;
let exponent: f64 = -0.5 * diff * diff;
let mut weight = libm::exp(exponent);
if weight < 1e-10 {
weight = 1e-10;
}

sum_w += weight;
sum_wx += weight * sorted[i] as f64;
}

let new_mu = sum_wx / sum_w;
if libm::fabs(new_mu - mu) < tol {
mu = new_mu;
break;
}

mu = new_mu;

// 更新 sigma
let mut sum_var = 0.0;
let mut sum_var_w = 0.0;
for i in 0..n {
let diff = sorted[i] as f64 - mu;
let exponent = -0.5 * (diff / sigma) * (diff / sigma);
let mut weight = libm::exp(exponent);
if weight < 1e-10 {
weight = 1e-10;
}

sum_var += weight * diff * diff;
sum_var_w += weight;
}
sigma = libm::sqrt(sum_var / sum_var_w);
if sigma < 1e-6 {
sigma = 1e-6;
}
}

return mu;
}

由这个工程,我们可以看出:使用 Rust 开发的单片机程序,在源代码方面,大大提升了代码的可读性。Rust 的 HAL 库的抽象程度远高于 C 的 HAL 库,因此,在开发的过程中,使用 Rust 无疑是极其方便的。且 Rust 的各个嵌入式库日趋成熟,树莓派 Pico 已经将 Rust 列为了和 C/C++, MicroPython 等同级的语言。在嵌入式方面,Rust 的发展无疑是如日中天的。但是,在配置方面,Rust 的配置仍然相对较为麻烦,没有类似于 CubeMX 的工具,在这方面,Rust 仍然是具有很大的突破空间的。

参考资料

[1] stm32f1xx-hal/examples Rust 嵌入式官方例程;

[2] doc.rust-lang.net/stable/embedded-book 嵌入式 Rust Book

[3] zhuanlan.zhihu.com Rust 嵌入式开发 STM32 & RISC-V

[4] kaisery.github.io Rust 程序设计语言 - 简体中文版

[5] ChatGPT、Gemini、GitHub Copilot 等 AI 工具😋