目标:在 Windows 上,用 Rust 从零搭出一个 STM32H743 工程------外置 25MHz 晶振,SYSCLK 跑到 480MHz,通过 RTT 输出日志,并用 PG2~PG6 跑流水灯。
全程使用
cargo构建 +probe-rs烧录,不碰 Keil / CubeIDE。
一、为什么用 Rust 写 STM32?
- 内存安全 :
no_std下没有 GC,编译期杜绝空指针、缓冲区溢出等一整类问题。 - 零成本抽象 :HAL 的封装在
--release下基本等价于手写寄存器。 - 类型安全的引脚与时钟 :引脚复用、时钟树在编译期校验,配错直接编译不过------这是 C 宏做不到的。
- 生态 :
stm32-rs提供全系列 PAC(寄存器访问层)与配套 HAL,cargo一条命令搞定依赖与构建。
代价是学习曲线略陡、生态不如 C 庞大。但对想认真做嵌入式的新项目,值得一试。
二、开发环境准备
2.1 Rust 工具链
powershell
# 安装 rustup(已装可跳过)
# 添加 Cortex-M7 目标(H743 是 M7 + 双精度 FPU + DSP)
rustup target add thumbv7em-none-eabihf
# 建议顺手升级到最新版,避免依赖要求更高的 MSRV
rustup update stable
2.2 烧录 / 调试工具:probe-rs
probe-run 已经停止维护(其依赖 time crate 的新版本会因 MSRV 直接编译失败),现在统一用 probe-rs:
powershell
cargo install probe-rs-tools
注意:
probe-rs-tools没有cli这个 feature,不要写成--features cli。
2.3 调试器驱动(最容易卡住的一步)
| 调试器 | probe-rs 需要的驱动 | 是否需要额外操作 |
|---|---|---|
| ST-Link V2/V3 | ST 官方驱动 | 一般免驱 |
| CMSIS-DAP / DAP-Link | Windows 原生 HID | 免驱 |
| J-Link | WinUSB | 需用 Zadig 替换 SEGGER 驱动 |
J-Link 默认安装的是 SEGGER 官方驱动,probe-rs 打不开。用 Zadig:
Options→List All Devices- 下拉框选中
J-Link - 目标驱动选
WinUSB(或libusb-win32) Replace Driver
代价:替换后 SEGGER 自家的 J-Link Commander / RTT Viewer 就不能用了,需要时再用 Zadig 切回来。
2.4 国内镜像(可选,强烈建议)
新建 C:\Users\<你>\.cargo\config.toml:
toml
[source.crates-io]
replace-with = 'rsproxy-sparse'
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
三、创建工程骨架
powershell
cargo new stm32h743_rust_project
cd stm32h743_rust_project
最终目录结构:
text
stm32h743_rust_project/
├── .cargo/
│ └── config.toml # 目标芯片、链接参数、运行器
├── build.rs # 让链接器找到 memory.x
├── memory.x # 内存布局
├── Cargo.toml
└── src/
└── main.rs
3.1 Cargo.toml
toml
[package]
name = "stm32h743_rust_project"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7.7"
cortex-m-rt = "0.7.5"
rtt-target = "0.5.0"
panic-rtt-target = "0.1.3"
stm32h7xx-hal = { version = "0.16.0", features = ["stm32h743v"] }
[[bin]]
name = "stm32h743_rust_project"
path = "src/main.rs"
[profile.release]
codegen-units = 1
debug = true
lto = true
opt-level = "s"
关键点:feature 用 stm32h743v 而不是 stm32h743。
stm32h743对应早期硅片(Rev Y/X),最高 400MHz;stm32h743v对应 Rev V ,会自动带入revision_v+rm0433,才能用 VOS Scale0 跑满 480MHz。
3.2 .cargo/config.toml
toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# 用 probe-rs 烧录 + 自动回显 RTT
runner = "probe-rs run --chip STM32H743ZITx"
[target.thumbv7em-none-eabihf]
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv7em-none-eabihf"
--chip STM32H743ZITx里的型号按你板子实际芯片改(如STM32H743VITx)。-Tlink.x是cortex-m-rt提供的链接脚本。
3.3 memory.x
ld
/* STM32H743 内存布局
* FLASH 2MB @ 0x08000000
* RAM 512KB @ 0x24000000 AXI SRAM,放 .data/.bss/堆/栈
* 栈顶由 cortex-m-rt 自动放在 RAM 末尾。
*/
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2048K
RAM : ORIGIN = 0x24000000, LENGTH = 512K
}
H743 还有 128KB 的 DTCM(@0x20000000,速度最快)。要单独用 DTCM 需自定义链接脚本,属于进阶内容,这里先用最简布局。
3.4 build.rs
rust
//! 让链接器在工程根目录搜索 memory.x
//! cortex-m-rt 的 link.x 通过 `INCLUDE memory.x` 引用它
fn main() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
println!("cargo:rustc-link-search={}", manifest_dir);
}
没有这个文件,链接会报 cannot find linker script memory.x。
3.5 src/main.rs
rust
//! STM32H743 最小工程
//!
//! - 时钟:外置 25MHz 晶振 → PLL1 → SYSCLK 480MHz
//! - 输出:RTT 日志(`rprintln!`)+ PG2~PG6 流水灯
//!
//! 前提:芯片为 Revision V(DBGMCU_IDCODE = 0x2003_6450),支持 VOS Scale0。
//! 故 Cargo.toml 启用 `stm32h743v` feature(自动带入 revision_v + rm0433)。
//! PLL1 的 M/N/P 由 HAL 按目标频率反推,无需(也无法)手写。
#![no_std]
#![no_main]
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use stm32h7xx_hal::{gpio::ErasedPin, pac, prelude::*, rcc::PllConfigStrategy, time::Hertz};
/// 外置晶振频率 (MHz)
const HSE_MHZ: u32 = 25;
/// 目标系统主频 (MHz)
const SYSCLK_MHZ: u32 = 480;
/// 流水灯每步点亮时长 (ms)
const STEP_MS: u32 = 200;
#[cortex_m_rt::entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("==== STM32H743 boot ====");
let _cp = cortex_m::Peripherals::take().unwrap();
let dp = pac::Peripherals::take().unwrap();
// VOS Scale0:跑 480MHz 的必要条件
let pwr = dp.PWR.constrain().vos0(&dp.SYSCFG).freeze();
// HSE 25MHz → PLL1 → 480MHz(Iterative 策略自动求解分频)
let ccdr = dp
.RCC
.constrain()
.use_hse(Hertz::MHz(HSE_MHZ))
.sys_ck(Hertz::MHz(SYSCLK_MHZ))
.pll1_strategy(PllConfigStrategy::Iterative)
.freeze(pwr, &dp.SYSCFG);
let sysclk_hz = ccdr.clocks.sys_ck().to_Hz();
rprintln!("SYSCLK = {} Hz", sysclk_hz);
// PG2~PG6 推挽输出;.erase() 抹掉引脚编号泛型,便于放进同一数组
let gpiog = dp.GPIOG.split(ccdr.peripheral.GPIOG);
let mut leds: [ErasedPin<_>; 5] = [
gpiog.pg2.into_push_pull_output().erase(),
gpiog.pg3.into_push_pull_output().erase(),
gpiog.pg4.into_push_pull_output().erase(),
gpiog.pg5.into_push_pull_output().erase(),
gpiog.pg6.into_push_pull_output().erase(),
];
// 全部熄灭(本板低电平点亮,高电平熄灭)
for led in &mut leds {
let _ = led.set_high();
}
// 流水灯:逐个点亮 STEP_MS 毫秒
let step_cycles = sysclk_hz / 1000 * STEP_MS;
let mut round = 0u32;
loop {
for led in &mut leds {
let _ = led.set_low();
cortex_m::asm::delay(step_cycles);
let _ = led.set_high();
}
round += 1;
rprintln!("round #{}", round);
}
}
四、代码详解
4.1 #![no_std] / #![no_main] / #[entry]
裸机没有操作系统,不能用标准库、也没有 main 的启动逻辑。cortex-m-rt 提供启动代码(设置栈指针、初始化 .data/.bss),#[cortex_m_rt::entry] 标记真正的入口。
4.2 时钟:25MHz HSE → 480MHz
H743 的 PLL 参数受 VCO 频率范围约束(Rev V 上限 836MHz),手算 M/N/P 很容易踩坑。stm32h7xx-hal 的做法是只给目标频率,让 HAL 反推:
rust
.use_hse(Hertz::MHz(25)) // 启用外置 25MHz 晶振
.sys_ck(Hertz::MHz(480)) // 目标主频 480MHz
.pll1_strategy(PllConfigStrategy::Iterative) // 自动求解分频
注意 :
PllConfig结构体里没有m/n/p字段,HAL 也没有pll1_m()这类方法------别去手写分频参数,编译器会直接报no method named。
而 480MHz 还依赖供电等级:
rust
let pwr = dp.PWR.constrain().vos0(&dp.SYSCFG).freeze();
- VOS Scale0 → 最高 480MHz(仅 Rev V 支持)
- VOS Scale1(默认)→ 最高 400MHz
若芯片不是 Rev V,只能 Scale1 + 400MHz,否则运行时会断言失败。
4.3 RTT 日志输出
rtt-target 把日志写入芯片的 RTT 缓冲区,调试器经 SWD 读取,不占用串口:
rust
rtt_init_print!();
rprintln!("SYSCLK = {} Hz", sysclk_hz);
panic-rtt-target 让 panic! 也走 RTT,崩溃时能在终端直接看到原因。
4.4 GPIO 流水灯
PG2~PG6 配置为推挽输出。into_push_pull_output() 返回的类型带有引脚编号泛型,直接放进数组会类型不一致;.erase() 抹掉编号后统一为 ErasedPin<_>,方便用循环批量操作。
五、编译、烧录、看日志
powershell
# 仅编译,检查代码
cargo build --release
# 编译 + 烧录 + 自动启动 RTT(runner 已在 config.toml 配好)
cargo run --release
正常应看到:
text
==== STM32H743 boot ====
SYSCLK = 480000000 Hz
round #1
round #2
round #3
...
同时 PG2~PG6 依次点亮,形成流水灯。
只想编译不想烧录时 ,把 .cargo/config.toml 里的 runner 行注释掉即可。
六、踩坑记录
坑 1:probe-run 装不上
error: could not compile `time`
probe-run 已弃用,其依赖的 time 新版本 MSRV 过高。换用 probe-rs:
powershell
cargo install probe-rs-tools
坑 2:probe-rs-tools 报 does not contain this feature: cli
该包没有 cli feature,去掉 --features cli。
坑 3:assertion failed: sys_d1cpre_ck <= sys_d1cpre_ck_max
供电等级不够。目标 480MHz 必须 VOS Scale0(.vos0(&dp.SYSCFG)),且需要 Rev V 芯片 + stm32h743v feature。
坑 4:Failed to open probe: ... use Zadig to install WinUSB
J-Link 还是 SEGGER 驱动。用 Zadig 替换为 WinUSB(见 2.3)。
坑 5:怎么确认芯片是不是 Rev V?
读 DBGMCU_IDCODE 寄存器。H743 的 DBGMCU 基址是 0x5C001000 (不是 F4 系列的 0xE0042000):
powershell
probe-rs read --chip STM32H743ZITx b32 0x5C001000 1
输出 20036450 时:
DEV_ID = 0x450→ STM32H743/753REV_ID = 0x2003→ Revision V(支持 480MHz)
版本对照:0x1001=Rev Z,0x1003=Rev Y,0x2001=Rev X,0x2003=Rev V。
七、小结
用 Rust 写 STM32H743 的完整链路其实很清爽:
rustup target add thumbv7em-none-eabihfcargo install probe-rs-tools+ 用 Zadig 给 J-Link 装 WinUSB- 配
Cargo.toml/.cargo/config.toml/memory.x/build.rs - 写
main.rs,cargo run --release一键烧录 + RTT 看日志
难点集中在时钟 (VOS 等级 + PLL 策略)和调试器驱动两处,把这两点搞定,剩下就是纯粹的 Rust 了。
引脚、时钟频率、LED 极性请按自己板子调整;本文以 PG2~PG6、外置 25MHz 晶振为例。
参考
- stm32h7xx-hal 文档与
pwr.rs/rcc源码 - STM32H742/743/750/753 勘误表 DM00368411(硅片版本编码)
- STM32H743 参考手册 RM0433
- probe-rs 官方文档