3. Rust嵌入式开发环境搭建:使用cargo-generate创建项目、配置memory.x、链接脚本与启动文件
好,咱们正式开始动手。前面两章聊了DMA和中断的理论,但说实话,没有实际跑起来的环境,那些都是纸上谈兵。这一章,我就带你从零搭一个Rust嵌入式开发环境。不是那种「装个IDE点下一步」的教程,而是真正理解每个文件是干什么的。
我个人习惯,先把工具链理清楚,再动手建项目。这样出了问题,你知道去哪找原因。
3.1 工具链准备:你真正需要装什么
先列个清单。别急着全装,对照着来。
| 组件 | 用途 | 安装命令 / 说明 |
|---|---|---|
| Rust 工具链 | 编译器、包管理器 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| 目标架构支持 | 交叉编译到ARM Cortex-M | rustup target add thumbv7em-none-eabihf |
| cargo-generate | 从模板快速生成项目 | cargo install cargo-generate |
| cargo-flash / probe-rs | 烧录与调试 | cargo install probe-rs-cli |
| OpenOCD(备选) | 传统调试桥接 | 系统包管理器安装,或从源码编译 |
cargo flash就能烧录,省去一堆GDB脚本的麻烦。
装完验证一下:
rustc --version
rustup target list --installed | grep thumbv7em
cargo generate --version
输出里能看到 thumbv7em-none-eabihf 就对了。这个target名字很长,说白了就是「ARM Cortex-M7,硬浮点,无操作系统」。我刚开始也被这串名字搞晕过,后来记住一个规律:none 表示裸机,eabihf 表示硬浮点ABI。
3.2 使用cargo-generate创建项目
手动搭项目结构太累了。社区有个好模板叫 cortex-m-quickstart,咱们直接用。
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
命令跑起来后,会问你项目名字。我一般起名叫 dma_demo。生成完的目录结构长这样:
dma_demo/
├── .cargo/
│ └── config.toml # 链接器配置、runner设置
├── src/
│ └── main.rs # 入口文件
├── memory.x # 内存布局
├── build.rs # 构建脚本
├── Cargo.toml
└── openocd.cfg # 调试配置(可选)
这里有个坑,我踩过。模板默认的 .cargo/config.toml 里,runner指向的是 arm-none-eabi-gdb。如果你用probe-rs,得改成:
# .cargo/config.toml
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32H743"
[build]
target = "thumbv7em-none-eabihf"
--chip 参数必须和你板子的MCU型号完全一致。写错一个字,烧录就会报错。我曾经把STM32H743写成了STM32H742,排查了半小时。
3.3 配置memory.x:你的芯片内存地图
memory.x 这个文件,说白了就是告诉链接器:「我的Flash从哪开始,RAM从哪开始,各有多大」。它直接决定了你的程序能不能跑起来。
以STM32H743为例,它的内存分布是这样的:
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| FLASH | 0x0800_0000 | 2 MB | 程序存储,XIP执行 |
| RAM (DTCM) | 0x2000_0000 | 128 KB | 紧耦合内存,CPU直接访问,延迟低 |
| RAM (AXI SRAM) | 0x2400_0000 | 512 KB | DMA传输首选,带宽高 |
| RAM (SRAM1/2/3) | 0x3000_0000 | 288 KB | 通用SRAM,可被DMA访问 |
对应的 memory.x 配置:
/* memory.x - STM32H743 */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2M
DTCM : ORIGIN = 0x20000000, LENGTH = 128K
RAM : ORIGIN = 0x24000000, LENGTH = 512K
}
/* 栈和堆放在DTCM里,因为CPU访问它最快 */
_stack_start = ORIGIN(DTCM) + LENGTH(DTCM);
_heap_start = ORIGIN(RAM);
_heap_end = ORIGIN(RAM) + LENGTH(RAM);
3.4 链接脚本与启动文件:程序是怎么活过来的
链接脚本(linker script)和启动文件(startup code),是嵌入式里最容易被忽略、但又最关键的两个东西。说白了,它们负责回答一个问题:「芯片上电后,程序怎么从Flash里跑到RAM里,然后执行main函数?」
Rust的 cortex-m-quickstart 模板已经帮你把启动文件封装在 cortex-m-rt crate里了。你不需要手写汇编启动代码,但得理解它干了什么。
启动流程三步走
- 取向量表:芯片复位后,从0x0800_0000读取栈指针,从0x0800_0004读取复位向量地址。
- 执行复位处理:初始化.data段(把Flash里的初始值拷贝到RAM),清零.bss段。
- 调用main:跳转到Rust的入口函数。
对应的链接脚本片段(由cortex-m-rt自动生成):
/* 向量表放在Flash开头 */
SECTIONS
{
.vector_table : ALIGN(4)
{
KEEP(*(.vector_table))
. = ALIGN(4);
} > FLASH
.text : ALIGN(4)
{
*(.text .text.*);
. = ALIGN(4);
} > FLASH
/* .data段:初始值在Flash,运行时在RAM */
.data : ALIGN(4)
{
_sdata = .;
*(.data .data.*);
_edata = .;
} > RAM AT > FLASH
/* .bss段:运行时清零 */
.bss : ALIGN(4)
{
_sbss = .;
*(.bss .bss.*);
_ebss = .;
} > RAM
}
嗯,这里要注意。如果你自己定义了全局变量,并且希望它初始值不为0,链接器会自动把它放到.data段,并在启动时从Flash拷贝到RAM。这个过程是透明的,但如果你把大数组放在.data里,启动时间会变长。我有个项目就是因为一个4KB的查找表放在.data里,导致启动慢了十几毫秒,后来改到Flash里直接读取才解决。
3.5 验证环境:烧录一个最小程序
环境搭没搭好,跑个最小程序就知道了。在 src/main.rs 里写:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {}
}
然后编译烧录:
cargo build --release
cargo flash --chip STM32H743 --release
如果看到 Finished in X.XXs 并且没有报错,恭喜你,环境通了。
probe-rs debug --chip STM32H743,然后在GDB里打 x/2wx 0x08000000。看到第一个值在RAM范围内,第二个值在Flash范围内,就说明链接脚本和启动文件配合正常。
3.6 本章知识结构图
下面这张图,把整个环境搭建的脉络串起来了。从工具链到项目生成,再到内存配置和启动流程,每一步都环环相扣。
这张图里,从工具链到烧录验证,一共五层。你每次遇到问题,就按这个层次去排查:工具链装了吗?target加了吗?memory.x地址对不对?链接脚本有没有覆盖默认段?启动文件有没有正确初始化?一层一层查,基本不会漏。
3.7 常见问题与避坑
- 编译报错 "no memory region specified for section .vector_table":八成是memory.x没被链接器找到。检查build.rs里有没有
include!("memory.x"),或者链接参数里有没有指定-Tmemory.x。 - 烧录后程序不跑,调试器停在HardFault:我遇到过两次。一次是栈指针设错了,一次是中断向量表对齐不对。检查
_stack_start是否在RAM范围内,向量表是否4字节对齐。 - DMA传输数据全是0:缓冲区地址不对。DMA控制器只能访问特定的总线矩阵,查芯片参考手册的「DMA memory map」章节。