引
因为这是本专栏的第一篇文章,所以我打算先在这里介绍下专栏的写作目标。
Rust 是一种系统编程语言。 它有着惊人的运行速度,能够防止段错误,并保证线程安全。
Rust 官方一直标榜着自己是系统编程语言,然而最根本的系统编程就是嵌入式系统开发。如果不能在嵌入式系统里大施拳脚,那么 Rust 就没有底气能与 C 语言叫板。经过了 3 年迭代,Rust 在嵌入式开发领域已经日渐成型,并且官方也成立了嵌入式工作组特别关注 Rust 嵌入式库与工具链的开发,同时也在不断完善The embedded rust book。这里推荐大家关注工作组的 newsletter,里面有很多工作组最新工作进展。
而本专栏将会更面向于嵌入式开发的入门教程和实践,也就是说,本专栏的文章并不假定读者拥有任何嵌入式开发的知识或经验,但是要求读者有一定 Rust 语言基础,比如说熟悉借用所有权系统,懂得使用 unsafe
手动操作内存结构等等。
专栏文章会分为几大类:
- 单片机架构的基础知识
- Rust 嵌入式开发的技巧
- 各种可以跟着动手的实践项目
希望通过本专栏可以吸引 Rust 小伙伴加入嵌入式领域,<del>同时拐骗一波正在使用 C 语言开发嵌入式的水深火热的程序员。</del>
准备
为了能够自己动手实践嵌入式开发,我们需要先准备好一些材料:
- 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
的老手是可以自己解决的。
> 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)
2. 除了默认的标准库外,我们还需要提前编译好的 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'
3. 另外我们还需要一些传统而好用的二进制工具 (binary tool
) 和调试器。在 ARM官网页面 下载适合平台的最新版安装即可。这一步安装的工具包括 arm-none-eabi-nm
, arm-none-eabi-gdb
, arm-none-eabi-objcopy
还有 arm-none-eabi-size
等等。
4. 最后我们还差 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 就应该会开始以一秒间隔开始闪烁了!
如果你有幸看到这,那就帮忙点个赞,让更多人看到吧!