cisen / blog

Time waits for no one.
135 stars 20 forks source link

Rust 嵌入式开发环境搭建指南【转】 #864

Open cisen opened 4 years ago

cisen commented 4 years ago

https://zhuanlan.zhihu.com/p/51733085 https://stackoverflow.com/questions/58985995/cs32f103c8t6-blue-pill-clone-cannot-flash-from-ac6-systemworkbench https://github.com/cisen/stm-rust-preemptive

因为这是本专栏的第一篇文章,所以我打算先在这里介绍下专栏的写作目标。

Rust 是一种系统编程语言。 它有着惊人的运行速度,能够防止段错误,并保证线程安全。

Rust 官方一直标榜着自己是系统编程语言,然而最根本的系统编程就是嵌入式系统开发。如果不能在嵌入式系统里大施拳脚,那么 Rust 就没有底气能与 C 语言叫板。经过了 3 年迭代,Rust 在嵌入式开发领域已经日渐成型,并且官方也成立了嵌入式工作组特别关注 Rust 嵌入式库与工具链的开发,同时也在不断完善The embedded rust book。这里推荐大家关注工作组的 newsletter,里面有很多工作组最新工作进展。

而本专栏将会更面向于嵌入式开发的入门教程和实践,也就是说,本专栏的文章并不假定读者拥有任何嵌入式开发的知识或经验,但是要求读者有一定 Rust 语言基础,比如说熟悉借用所有权系统,懂得使用 unsafe 手动操作内存结构等等。

专栏文章会分为几大类:

单片机架构的基础知识 Rust 嵌入式开发的技巧 各种可以跟着动手的实践项目

希望通过本专栏可以吸引 Rust 小伙伴加入嵌入式领域,同时拐骗一波正在使用 C 语言开发嵌入式的水深火热的程序员。

准备

为了能够自己动手实践嵌入式开发,我们需要先准备好一些材料:

STM32F103 最小系统开发板 (约 10 元) STLINK V2 仿真器 (约 20 元) 母对母杜邦线 USB 转 TTL 串口模块 (约 5 元)

STM32F103 是现在应用非常广泛,性能强大而且成本低廉的一款单片机,拥有着高达 72Mhz 的主频率,完全吊打 Arduino 等开发平台。 166af91338c8ec1a

STM32F103 最小系统核心版

仿真器是连接 pc 与单片机的重要模块,主要用于程序烧写与调试。 v2-a55275841c947fb6b84d5f3bdcff6acc_720w

STLINK V2

串口模块用于 pc 接收单片机的串口信息用以调试,由于现代计算机普遍已经取消了串口接口,所以使用 USB 串口就是最经济可靠的选择。

v2-7bae51192d78082b63d33a2f1be3577e_720w

USB2TTL

Rust 工具链

文章下面将会使用 nightly-msvc channel 的 Rust 编译器工具链(因为作者使用 Windows 平台开发),读者也可以使用 gnu 或者 linux 平台,步骤上如果有所出入相信使用 linux 的老手是可以自己解决的。

> rustup default nightly-msvc
info: using existing install for 'nightly-x86_64-pc-windows-msvc'
info: default toolchain set to 'nightly-x86_64-pc-windows-msvc'
nightly-x86_64-pc-windows-msvc unchanged - rustc 1.32.0-nightly (36a50c29f 2018-11-09)
  1. 除了默认的标准库外,我们还需要提前编译好的 core 核心库。在我们这里添加几个常用的编译目标指令集,rustup 就会自动把核心库下载下来。

    > rustup target add thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf
    info: downloading component 'rust-std' for 'thumbv6m-none-eabi'
    info: downloading component 'rust-std' for 'thumbv7m-none-eabi'
    info: downloading component 'rust-std' for 'thumbv7em-none-eabi'
    info: downloading component 'rust-std' for 'thumbv7em-none-eabihf'
  2. 另外我们还需要一些传统而好用的二进制工具 (binary tool) 和调试器。在 ARM官网页面 下载适合平台的最新版安装即可。这一步安装的工具包括 arm-none-eabi-nm, arm-none-eabi-gdb, arm-none-eabi-objcopy 还有 arm-none-eabi-size 等等。

  3. 最后我们还差 openocd,它负责保持与与仿真器的通讯连接,我们需要使用它来进行烧写和调试指令操作。openocd 的安装途径有很多,建议向购买仿真器的商家索要,或者可以从这里下载(可能需要科学上网)。 注: 上述 3,4 步的工具需要加入 Path 环境变量。

Blinky

Blinky 是嵌入式世界的 hello world —— 让一盏 LED 闪烁。这篇文章的最终目标就是把最小系统版上唯一一颗 LED 灯闪烁起来。

我们先创建一个新的项目。

> cargo new blinky
     Created binary (application) `blinky` package

打开 Cargo.toml 添加几个依赖项。

[dependencies]
cortex-m = "0.5.8"        # cortex-m 核心指令集
cortex-m-rt = "0.6.5"     # 最小运行时,负责启动内存初始化
panic-halt = "0.2.0"      # 定义发生 panic 时采取立即停机的行为

同一架构的单片机的内存容量往往有很大差异,不同厂家的内存排布也不一定相同,所以这里我们要用 memory.x 文件里定义开发板的内存结构。在项目目录中新建文件 memory.x 并写入:

MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 128K
  RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

这里定义了我们这个 MCU 拥有 128k ROM 和 20k RAM,内存起点分别在 0x08000000 和 0x20000000。

memory.x 事实上是一段链接器脚本 (Linker Script),链接器脚本用来在内存中规划如何排布代码和静态变量。很明显仅靠这小段脚本还不足以声明好运行所需的所有段 (SECTION)。幸运的是,cortex-m-rt 运行库已经为我们写好了通用的链接脚本,我们仅仅需要在编译时将名为 memory.x 的内存定义脚本放在编译目录,memory.x 就会被自动 include 到模板中。 所以这里需要一段编译时自动拷贝 memory.x 的 build script。在项目目录中新建文件 build.rs 并写入:

use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

fn main() {
    // Put the linker script somewhere the linker can find it
    let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
    File::create(out.join("memory.x"))
        .unwrap()
        .write_all(include_bytes!("memory.x"))
        .unwrap();
    println!("cargo:rustc-link-search={}", out.display());

    // Only re-run the build script when memory.x is changed,
    // instead of when any part of the source code changes.
    println!("cargo:rerun-if-changed=memory.x");
}

接着打开 src/main,写入:

#![no_std]
#![no_main]

extern crate panic_halt;

use cortex_m::asm;
use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    asm::nop();

    loop { }
}

虽然这段代码看起来毫无作用,但是对于编译来说已经足够了。

可以注意一下这里的 main 函数并不是 Rust 语言内嵌的主函数,事实上,这个主函数仅仅是用户代码的入口,真正的主函数定义在 cortex_m_rt 库中,在启动后负责静态变量和中断向量表的内存初始化,接着才将执行权交回给这里的 main 函数。

执行编译。

> cargo build --target thumbv7m-none-eabi
    Compiling blinky v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.62s

至此 Blinky 已经成功编译好了,执行文件应该会出现在 /target/thumbv7m-none-eabi/debug/blinky 。接下来我们要把这个程序烧写到芯片的 ROM 上。首先使用杜邦线连接上仿真器与开发板,对应着接口上的名字,应该很容易将四条连接线接好,四个接口分别是 SWDIO, SWCLK, 3.3V 和 GND。

接着启动 openocd。

> openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg
64-bits Open On-Chip Debugger 0.10.0-dev-00289-g5eb5e34 (2016-09-03-09:40)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html

...

Polling target stm32f1x.cpu failed, trying to reexamine
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints

这一步以出现 xxxxx.cpu: hardware has x breakpoints, x watchpoints 提示为连接成功。 如果出现了其他错误,先检查是否已经安装仿真器的驱动,4条连接线有没有松动,或者更换一个 USB 口试试。

保留 openocd 终端,再打开一个新的终端启动 GDB (GNU Debugger) ,使用 GDB 进行执行程序烧写:

> arm-none-eabi-gdb
GNU gdb (GNU Tools for ARM Embedded Processors 6-2017-q1-update) 7.12.1.20170215-git

...

For help, type "help".
(gdb)

加载目标文件

(gdb) file ./target/thumbv7m-none-eabi/debug/blinky
Reading symbols from ./target/thumbv7m-none-eabi/debug/blinky...done.

连接上 openocd。(openocd 的默认端口为 3333)

(gdb) target remote :3333
Remote debugging using :3333
0x00000000 in ?? ()

重置 MCU,因为在运行状态无法进行烧写。

(gdb) monitor reset halt
stm32f1x.cpu: target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x0800016c msp: 0x20000260

开始写入

(gdb) load
Start address 0x0, load size 0
Transfer rate: 0 bits in <1 sec.

写入后 MCU 默认会暂停在初始状态,这里要手动运行。

(gdb) continue
Continuing.

好了到目前为止,如果我们的开发板毫无反应,那就对了,我们现在要给它加上最重要的 Blinky 逻辑。

打开 Cargo.toml 再加上两个依赖

stm32f103xx-hal = { git = "https://github.com/japaric/stm32f103xx-hal.git" }    # MCU 外围部件操作的统一接口
nb = "0.1"    # stm32f103xx-hal 的异步阻塞模块,用来实现时钟等待同步

修改 src/main.rs

#![no_std]
#![no_main]

extern crate panic_halt;
extern crate stm32f103xx_hal as hal;
#[macro_use]
extern crate nb;

use cortex_m_rt::entry;

use hal::prelude::*;
use hal::stm32f103xx;
use hal::timer::Timer;

#[entry]
fn main() -> ! {
  let cp = cortex_m::Peripherals::take().unwrap();
  let dp = stm32f103xx::Peripherals::take().unwrap();

  let mut flash = dp.FLASH.constrain();
  let mut rcc = dp.RCC.constrain();

  // 设置时钟总线
  let clocks = rcc.cfgr.freeze(&mut flash.acr);

  // 设置通用引脚 (GPIO)
  let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);

  // LED 对应的 PC13 引脚
  let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);

  // 淘宝上有些版本的核心板的 LED 会接在 PB12 引脚上,这样的话用下面两行替换
  // let mut gpiob = dp.GPIOB.split(&mut rcc.apb2);
  // let mut led = gpiob.pb12.into_push_pull_output(&mut gpiob.crh);

  let mut timer = Timer::syst(cp.SYST, 1.hz(), clocks);
  loop {
      block!(timer.wait()).unwrap();
      // 点亮 LED
      led.set_high();
      block!(timer.wait()).unwrap();
      // 关闭 LED
      led.set_low();
  }
}

重新编译 Rust。

> cargo build --target thumbv7m-none-eabi
    Compiling blinky v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.1s 

回到 GDB 终端,此时如果还在运行上一段代码,那按下 Ctrl + C 就可以中断执行。

Continuing.
Program received signal SIGINT, Interrupt.
0x08000240 in ?? ()
(gdb) 

直接执行 load 指令,GDB 会自动识别到可执行文件的变更并进行覆写。

(gdb) load
Start address 0x0, load size 0
Transfer rate: 0 bits in <1 sec.
(gdb) continue
Continuing. 

至此,我们的蓝色 LED 就应该会开始以一秒间隔开始闪烁了!

如果你有幸看到这,那就帮忙点个赞,让更多人看到吧!

cisen commented 4 years ago

https://zhuanlan.zhihu.com/p/51872048 上一篇文章我们成功跑起了第一个由 Rust 驱动的 Blinky,可以说已经一只脚踏入嵌入式开发的大门了。但是读者如果跟着步骤实践会发现,从编译到烧录运行,整个流程的命令行存在大段的参数,而且GDB 的启动指令重复枯燥。因此,指南第二章将介绍一些技巧来简化整个流程,然后实践一些在嵌入式系统中的调试方法。

Cargo

首先我们来回顾下编译 Rust 源码的命令:

> cargo build --target thumbv7m-none-eabi

这里的编译目标可以换用 .cargo/config 来指定。在项目目录新建文件夹 .cargo 并新建文件 config, 写入:

[build]
target = "thumbv7m-none-eabi"

之前我们使用 rustup 添加的几个目标平台其实是对应了几个不同的 Cortex 指令集,它们的对应关系是:

Target                    |  Architecture
------------------------------------------------------------
thumbv6m-none-eabi        |  Cortex-M0 and Cortex-M0+
thumbv7m-none-eabi        |  Cortex-M3
thumbv7em-none-eabi       |  Cortex-M4 and Cortex-M7 (no FPU)
thumbv7em-none-eabihf     |  Cortex-M4F and Cortex-M7F (with FPU)

STM32F103 的架构为 Cortex-M3,所以这里我们指定的是 thumbv7m-none-eabi。

另外,如果不指定链接器,rustc 会使用默认的 LLD 进行链接,然而 LLD 并不能完全兼容嵌入式指令集,因此编译的可执行文件会丢失调试符号。为了之后能够使用 GDB 进行调试,我们这里将链接器指定为 gcc,修改 .cargo/config :

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]
rustflags = [
  "-C", "linker=arm-none-eabi-gcc",
  "-C", "link-arg=-Wl,-Tlink.x",
  "-C", "link-arg=-nostartfiles",
]

现在就可以直接使用 cargo build 指令了:

> cargo build
    Compiling blinky v0.1.0
    Finished dev [unoptimized + debuginfo]

Openocd

上一章启动 openocd 的命令:

> openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg

为了简化,我们在项目目录里新建文件 openocd.cfg,写入:

source [find interface/stlink-v2.cfg]
source [find target/stm32f1x.cfg]

之后要在这个项目目录里启动 openocd,只需要简单地:

> openocd
64-bits Open On-Chip Debugger 0.10.0-dev-00289-g5eb5e34 (2016-09-03-09:40)
Licensed under GNU GPL v2

...

Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints

GDB

GDB 在启动时会读取并执行项目目录里的 .gdbinit 文件,文件里的每一行对应一条 GDB 指令。新建文件 .gdbinit,写入:

file ./target/thumbv7m-none-eabi/debug/blinky
target remote :3333
monitor reset halt
load

我们试下启动 GDB:

> arm-none-eabi-gdb
GNU gdb (GNU Tools for ARM Embedded Processors 6-2017-q1-update) 7.12.1.20170215-git

...

warning: File "C:\Users\Andy\Documents\Code\Rust\blinky2\.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
        add-auto-load-safe-path C:\Users\Andy\Documents\Code\Rust\blinky2\.gdbinit
line to your configuration file "C:\Users\Andy/.gdbinit".
To completely disable this security protection add
        set auto-load safe-path /
line to your configuration file "C:\Users\Andy/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
        info "(gdb)Auto-loading safe path"
(gdb)

与预期不同,项目目录里的 .gdbinit 文件并没有被使用,这是因为安全设置把它过滤屏蔽了。因此我们还需要设置 GDB 的全局安全配置。

进入用户根目录(Windows 系统下位于 C:\Users\UserName),新建文件 .gdbinit,写入:

set auto-load safe-path /

然后我们回到项目目录,再次启动 GDB。

> arm-none-eabi-gdb
GNU gdb (GNU Tools for ARM Embedded Processors 6-2017-q1-update) 7.12.1.20170215-git

...

0x20000004 in ?? ()
stm32f1x.cpu: target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x0800016c msp: 0x20000260
Start address 0x0, load size 0
Transfer rate: 0 bits in <1 sec.
(gdb)

可以看到 .gdbinit 里的初始化指令已经成功执行了。

事实上我们还可以设置一些快捷 GDB 指令,比如说使用 monitor reset halt 重置单片机是一个非常高频的操作,然而每次都输入这么长的指令是很麻烦的。

我们可以打开位于用户根目录的全局 .gdbinit 文件,在文件后面追加:

set auto-load safe-path /

define reset
    monitor reset halt
end

这样我们在 GDB 中就可以直接使用 reset 指令来进行重置了:

(gdb) reset
stm32f1x.cpu: target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x0800016c msp: 0x20000260

调试

嵌入式系统中的调试往往比 PC 困难的多,因为嵌入式程序直接跑在硬件上,没有系统帮助线程隔离,也没有标准输入输出 (Standard IO)。有时候嵌入式系统程序还是根本无法调试的,因为调试行为本身可能就会影响到单片机的实时时间顺序。虽然在嵌入式上调试很困难,但是还是有不少工具手段可以帮助我们:

串口输入输出 Hard Fault 中断 逻辑分析仪 GDB 单步调试

Hard Fault 中断

Hard Fault 中断一般产生于段错误 (Segment Falut,访问非法内存),或者是整数除以 0 等特殊情况。一旦发生这类型错误,MCU 会终止程序,并且产生一个 Hard Fault 中断信号以供处理,你可以选择从错误中恢复,也可以不作处理让 MCU 陷入默认的死循环函数。

逻辑分析仪

v2-58439d3de4ba19fd0781b4853596603d_720w

逻辑分析仪 v2-a8e48d6c1ad0a083cb7b280b5e83d6a5_720w

逻辑分析软件

逻辑分析仪用于分析引脚通讯时序信号。与示波器相比,示波器是测量模拟信号的,而逻辑分析仪测量分析数字信号。测量数字信号时,示波器通常可以用来观察有没有信号或者是信号的质量如何,逻辑分析仪主要用来分析信号高低电平时序时间,以及通信的是什么数据。逻辑分析仪还具备强大的数据解析能力,对于一些复杂的协议,示波器显示的是波形,而逻辑分析仪可以直接把十六进制数据解析出来。现在很多逻辑分析仪都具备几十种协议解析器。

使用 GDB 调试

使用 GDB 可以在线对 MCU 进行单步调试,下断点,条件断点,读写局部变量,查看堆栈等操作,是嵌入式调试非常重要的工具。下面我会通过一个斐波那契数列的例子简单示范 GDB 的基础操作。

我们将 src/main.rs 改为斐波那契数列计算程序:

#![no_std]
#![no_main]

extern crate panic_halt;
extern crate stm32f103xx_hal as hal;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    let mut n = 0;

    loop {
        let fib = fib(n);
        n += 1;
    }
}

fn fib(n: usize) -> usize {
    if n < 2 {
        1
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

编译然后启动 GDB:

> arm-none-eabi-gdb
...
Loading section .vector_table, size 0x130 lma 0x8000000
Loading section .text, size 0x3ce lma 0x8000130
Loading section .rodata, size 0x194 lma 0x8000500
Start address 0x80001ee, load size 1682
Transfer rate: 6 KB/sec, 560 bytes/write.
(gdb)

给 main 函数加上断点:

(gdb) break main
Breakpoint 1 at 0x80001be: file src\main.rs, line 11.

我们让程序运行到断点处:

(gdb) continue
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, main () at src\main.rs:11
11          let mut n = 0;

程序成功停在了 main 函数的入口处,这里我们可以使用 list 指令查看光标附近的代码:

(gdb) list
6
7       use cortex_m_rt::entry;
8
9       #[entry]
10      fn main() -> ! {
11          let mut n = 0;
12
13          loop {
14              let fib = fib(n);
15              n += 1;

使用 next 指令单步执行:

(gdb) next
13          loop {

直接回车会执行上一条指令 (也就是 next):

(gdb)
14              let fib = fib(n);

使用 step 指令可以跳入函数:

(gdb) step
blinky::fib::h7d40020be56f8bb6 (n=1) at src\main.rs:20
20          if n < 2 {

使用 backtrace 查看调用堆栈:

(gdb) backtrace
#0  blinky::fib::h7d40020be56f8bb6 (n=1) at src\main.rs:21
#1  0x080001c8 in main () at src\main.rs:14

这里还可以使用 up 和 down 指令在调用堆栈上上下移动。

使用 finish 指令继续执行直到函数返回,并打印返回值:

(gdb) finish
Run till exit from #0  blinky::fib::h7d40020be56f8bb6 (n=1) at src\main.rs:21
0x080001c8 in main () at src\main.rs:14
14              let fib = fib(n);
Value returned is $1 = 1

注意到这里为止只是 fib 函数返回了值,但还没赋值给 fib 变量,我们可以查看本地变量来验证一下:

(gdb) info locals
n = 1

单步运行让程序进行赋值,再查看来验证一下:

(gdb) next
15              n += 1;
(gdb) info locals
fib = 1
n = 1

可以使用 set 指令修改变量:

(gdb) set fib=500
(gdb) info locals
fib = 500
n = 1

最后我们可以通过 info breakpoints 查看已设置的断点:

(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x080001be in main at src\main.rs:11
        breakpoint already hit 1 time

使用 delete 指令删除对应断点:

(gdb) delete 1
(gdb) info breakpoints
No breakpoints or watchpoints.

事实上,在日常使用中我们往往会使用简短版的 GDB 指令,比如说 list -> l, break -> b, info locals -> i lo, next -> n 等等,简写只要不与其他指令产生歧义, GDB 都能识别。另外还有一个指令 tbreak 可以用来设置一次性断点,顾名思义,这个断点会在触发中断后自动删除。

软断点

通过 break 指令设置的断点被称为硬件断点 (hardward breakpoint),从 openocd 的提示可以看出,这款单片机拥有最高 6 个硬件断点,也就是说我们设置的断点数量是有限制的。

Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints

还有另一种设置断点的方法,就是使用 arm 指令集特有的 bkpt 指令,实现软件断点,这种断点方式没有数量限制。由于高级语言里不包含这个指令,所以我们要通过内嵌汇编来实现这个功能,所幸的是,cortex_m 库已经为我们封装好了这个汇编代码。修改源代码:

#![no_std]
#![no_main]

extern crate panic_halt;
extern crate stm32f103xx_hal as hal;

use cortex_m::asm;
use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {
        asm::bkpt();
    }
}

编译并运行:

(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0800036c in __bkpt ()

可以看到程序停在了软断点处,使用 up 指令沿着调用堆栈可以回溯到调用库函数的代码处:

(gdb) up
#1  0x08000136 in cortex_m::asm::bkpt::h0732619f5c574313 ()
    at C:\Users\Andy\.cargo\registry\src\github.com-1ecc6299db9ec823\cortex-m-0.5.8\src/asm.rs:19
19                  __bkpt();
(gdb) up
#2  main () at src\main.rs:13
13              asm::bkpt();

注意:在非调试模式下(即没有连接调试器)的情况下触发 bkpt 会导致 Hard Fault 中断。

使用 VS Code 调试

直接使用 GDB 进行调试有时不够直观,也较为繁琐,适用于临时或简单的调试。对于大型项目,我们一般喜欢使用 IDE 来辅助调试。

打开 VS Code,安装 Native Debug 插件,转到调试面板添加配置,选择 C++ (GDB/LLDB),修改 .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug GDB",
            "type": "gdb",
            "request": "attach",
            "executable": "./target/thumbv7m-none-eabi/debug/blinky",
            "target": "localhost:3333",
            "cwd": "${workspaceRoot}",
            "gdbpath": "arm-none-eabi-gdb",
            "remote": true,
            "autorun": [
                "monitor reset halt",
                "load"
            ]
        }
    ]
}

点击开始调试,即可开始享受 VS Code 的现代化调试体验加成: v2-a00fe9ffa5a82e5172f6322ce1ae78bc_r

VS Code Debugger