Open cisen opened 4 years ago
https://zhuanlan.zhihu.com/p/51872048 上一篇文章我们成功跑起了第一个由 Rust 驱动的 Blinky,可以说已经一只脚踏入嵌入式开发的大门了。但是读者如果跟着步骤实践会发现,从编译到烧录运行,整个流程的命令行存在大段的参数,而且GDB 的启动指令重复枯燥。因此,指南第二章将介绍一些技巧来简化整个流程,然后实践一些在嵌入式系统中的调试方法。
首先我们来回顾下编译 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 -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 在启动时会读取并执行项目目录里的 .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 中断一般产生于段错误 (Segment Falut,访问非法内存),或者是整数除以 0 等特殊情况。一旦发生这类型错误,MCU 会终止程序,并且产生一个 Hard Fault 中断信号以供处理,你可以选择从错误中恢复,也可以不作处理让 MCU 陷入默认的死循环函数。
逻辑分析仪
逻辑分析仪
逻辑分析软件
逻辑分析仪用于分析引脚通讯时序信号。与示波器相比,示波器是测量模拟信号的,而逻辑分析仪测量分析数字信号。测量数字信号时,示波器通常可以用来观察有没有信号或者是信号的质量如何,逻辑分析仪主要用来分析信号高低电平时序时间,以及通信的是什么数据。逻辑分析仪还具备强大的数据解析能力,对于一些复杂的协议,示波器显示的是波形,而逻辑分析仪可以直接把十六进制数据解析出来。现在很多逻辑分析仪都具备几十种协议解析器。
使用 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 中断。
直接使用 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
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 等开发平台。
STM32F103 最小系统核心版
仿真器是连接 pc 与单片机的重要模块,主要用于程序烧写与调试。
STLINK V2
串口模块用于 pc 接收单片机的串口信息用以调试,由于现代计算机普遍已经取消了串口接口,所以使用 USB 串口就是最经济可靠的选择。
USB2TTL
Rust 工具链
文章下面将会使用 nightly-msvc channel 的 Rust 编译器工具链(因为作者使用 Windows 平台开发),读者也可以使用 gnu 或者 linux 平台,步骤上如果有所出入相信使用 linux 的老手是可以自己解决的。
除了默认的标准库外,我们还需要提前编译好的 core 核心库。在我们这里添加几个常用的编译目标指令集,rustup 就会自动把核心库下载下来。
另外我们还需要一些传统而好用的二进制工具 (binary tool) 和调试器。在 ARM官网页面 下载适合平台的最新版安装即可。这一步安装的工具包括 arm-none-eabi-nm, arm-none-eabi-gdb, arm-none-eabi-objcopy 还有 arm-none-eabi-size 等等。
最后我们还差 openocd,它负责保持与与仿真器的通讯连接,我们需要使用它来进行烧写和调试指令操作。openocd 的安装途径有很多,建议向购买仿真器的商家索要,或者可以从这里下载(可能需要科学上网)。 注: 上述 3,4 步的工具需要加入 Path 环境变量。
Blinky
Blinky 是嵌入式世界的 hello world —— 让一盏 LED 闪烁。这篇文章的最终目标就是把最小系统版上唯一一颗 LED 灯闪烁起来。
我们先创建一个新的项目。
打开 Cargo.toml 添加几个依赖项。
同一架构的单片机的内存容量往往有很大差异,不同厂家的内存排布也不一定相同,所以这里我们要用 memory.x 文件里定义开发板的内存结构。在项目目录中新建文件 memory.x 并写入:
这里定义了我们这个 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 并写入:
接着打开 src/main,写入:
虽然这段代码看起来毫无作用,但是对于编译来说已经足够了。
可以注意一下这里的 main 函数并不是 Rust 语言内嵌的主函数,事实上,这个主函数仅仅是用户代码的入口,真正的主函数定义在 cortex_m_rt 库中,在启动后负责静态变量和中断向量表的内存初始化,接着才将执行权交回给这里的 main 函数。
执行编译。
至此 Blinky 已经成功编译好了,执行文件应该会出现在 /target/thumbv7m-none-eabi/debug/blinky 。接下来我们要把这个程序烧写到芯片的 ROM 上。首先使用杜邦线连接上仿真器与开发板,对应着接口上的名字,应该很容易将四条连接线接好,四个接口分别是 SWDIO, SWCLK, 3.3V 和 GND。
接着启动 openocd。
这一步以出现 xxxxx.cpu: hardware has x breakpoints, x watchpoints 提示为连接成功。 如果出现了其他错误,先检查是否已经安装仿真器的驱动,4条连接线有没有松动,或者更换一个 USB 口试试。
保留 openocd 终端,再打开一个新的终端启动 GDB (GNU Debugger) ,使用 GDB 进行执行程序烧写:
加载目标文件
连接上 openocd。(openocd 的默认端口为 3333)
重置 MCU,因为在运行状态无法进行烧写。
开始写入
写入后 MCU 默认会暂停在初始状态,这里要手动运行。
好了到目前为止,如果我们的开发板毫无反应,那就对了,我们现在要给它加上最重要的 Blinky 逻辑。
打开 Cargo.toml 再加上两个依赖
修改 src/main.rs
重新编译 Rust。
回到 GDB 终端,此时如果还在运行上一段代码,那按下 Ctrl + C 就可以中断执行。
直接执行 load 指令,GDB 会自动识别到可执行文件的变更并进行覆写。
至此,我们的蓝色 LED 就应该会开始以一秒间隔开始闪烁了!
如果你有幸看到这,那就帮忙点个赞,让更多人看到吧!