rust开发环境_Rust 嵌入式开发环境搭建指南 (二):日常除虫
上一篇文章我们成功跑起了第一个由 Rust 驱动的 Blinky,可以说已经一只脚踏入嵌入式开发的大门了。但是读者如果跟着步骤实践会发现,从编译到烧录运行,整个流程的命令行存在大段的参数,而且GDB 的启动指令重复枯燥。因此,指南第二章将介绍一些技巧来简化整个流程,然后实践一些在嵌入式系统中的调试方法。Cargo首先我们来回顾下编译 Rust 源码的命令:> cargo build --ta
上一篇文章我们成功跑起了第一个由 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:UsersAndyDocumentsCodeRustblinky2.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:UsersAndyDocumentsCodeRustblinky2.gdbinit
line to your configuration file "C:UsersAndy/.gdbinit".
To completely disable this security protection add
set auto-load safe-path /
line to your configuration file "C:UsersAndy/.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:UsersUserName),新建文件 .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 陷入默认的死循环函数。
逻辑分析仪
逻辑分析仪
逻辑分析软件
逻辑分析仪用于分析引脚通讯时序信号。与示波器相比,示波器是测量模拟信号的,而逻辑分析仪测量分析数字信号。测量数字信号时,示波器通常可以用来观察有没有信号或者是信号的质量如何,逻辑分析仪主要用来分析信号高低电平时序时间,以及通信的是什么数据。逻辑分析仪还具备强大的数据解析能力,对于一些复杂的协议,示波器显示的是波形,而逻辑分析仪可以直接把十六进制数据解析出来。现在很多逻辑分析仪都具备几十种协议解析器。
使用 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 srcmain.rs, line 11.
我们让程序运行到断点处:
(gdb) continue
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 1, main () at srcmain.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 srcmain.rs:20
20 if n < 2 {
使用 backtrace 查看调用堆栈:
(gdb) backtrace
#0 blinky::fib::h7d40020be56f8bb6 (n=1) at srcmain.rs:21
#1 0x080001c8 in main () at srcmain.rs:14
这里还可以使用 up 和 down 指令在调用堆栈上上下移动。
使用 finish 指令继续执行直到函数返回,并打印返回值:
(gdb) finish
Run till exit from #0 blinky::fib::h7d40020be56f8bb6 (n=1) at srcmain.rs:21
0x080001c8 in main () at srcmain.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 srcmain.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:UsersAndy.cargoregistrysrcgithub.com-1ecc6299db9ec823cortex-m-0.5.8src/asm.rs:19
19 __bkpt();
(gdb) up
#2 main () at srcmain.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 的现代化调试体验加成:
VS Code Debugger
如果你有幸看到这,那就帮忙点个赞,让更多人看到吧!
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)