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(备选) 传统调试桥接 系统包管理器安装,或从源码编译
我的建议:新手别在OpenOCD上花太多时间。probe-rs对Rust生态更友好,直接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);
关键点:做DMA传输时,缓冲区一定要放在DMA能访问的内存区域。像DTCM,CPU访问很快,但DMA控制器不一定能直接读写它。我建议把DMA缓冲区放在AXI SRAM(0x2400_0000)里,这是最稳妥的选择。

3.4 链接脚本与启动文件:程序是怎么活过来的

链接脚本(linker script)和启动文件(startup code),是嵌入式里最容易被忽略、但又最关键的两个东西。说白了,它们负责回答一个问题:「芯片上电后,程序怎么从Flash里跑到RAM里,然后执行main函数?」

Rust的 cortex-m-quickstart 模板已经帮你把启动文件封装在 cortex-m-rt crate里了。你不需要手写汇编启动代码,但得理解它干了什么。

启动流程三步走

  1. 取向量表:芯片复位后,从0x0800_0000读取栈指针,从0x0800_0004读取复位向量地址。
  2. 执行复位处理:初始化.data段(把Flash里的初始值拷贝到RAM),清零.bss段。
  3. 调用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 并且没有报错,恭喜你,环境通了。

一个小技巧:第一次烧录后,用调试器看看向量表的前两个word是不是正确的栈指针和复位地址。用probe-rs可以这样:probe-rs debug --chip STM32H743,然后在GDB里打 x/2wx 0x08000000。看到第一个值在RAM范围内,第二个值在Flash范围内,就说明链接脚本和启动文件配合正常。

3.6 本章知识结构图

下面这张图,把整个环境搭建的脉络串起来了。从工具链到项目生成,再到内存配置和启动流程,每一步都环环相扣。

Rust嵌入式开发环境搭建流程 Rust工具链安装 目标架构添加 cargo-generate 烧录工具 cargo generate → cortex-m-quickstart memory.x 内存布局 链接脚本 (linker script) 启动文件 (cortex-m-rt) 上电 → 取向量表 → 复位处理(.data/.bss) → 调用main cargo flash → 烧录验证 DMA缓冲区建议放在AXI SRAM (0x2400_0000),避免DTCM不可达问题

这张图里,从工具链到烧录验证,一共五层。你每次遇到问题,就按这个层次去排查:工具链装了吗?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」章节。
曾经有个坑:我用cortex-m-quickstart模板时,默认的memory.x里RAM大小写的是64K,但我的芯片有128K DTCM。程序编译通过,烧录也成功,但一申请大数组就死机。后来发现是链接脚本里RAM长度设小了,全局变量初始化时写到了未映射的地址空间。所以,拿到新板子第一件事:对着数据手册,把memory.x里的每个数字都核对一遍。