使用Rust进行stm32嵌入式开发之Blink

rust作为一个内存安全并且无运行时(runtime)的现代开发语言,其中的卖点之一就是能够进行底层开发。 那么,如果用rust替代嵌入式开发中常用的C/C++语言会是怎么样的一种体验呢?因此,我们这里尝试使用rust 针对stm32f103c8的最小系统板进行开发。

开始之前

在开始之前,先描述一下我们目前的硬件和软件环境。这里用的stm32f103c8在国内应该算是比较常见的一类cortex-m3 单片机,性价比要优于之前的atmega系列,后者现在在ardunio中发光发热。在某宝上,现成的最小系统板20以内就能搞定了。 对于嵌入式开发,我们还需要能够有一条用于ISP的下载器,针对不同的厂商,也会有不同的下载方式。对于stm32系列, 相对比较方便廉价的方式,是用stlink进行下载,某宝上20以内也能搞定。如果,有usart串口线的话,也能够用串口线下载, 但这里的串口并不能直接用PC的串口,主要是因为电压不同。软件方面,除了rust之外,还需要安装openocd,对于windows, 还需要安装armv7的交叉编译器。由于硬件相关的编程会和实际的硬件直接相关,因此文中的程序可能需要根据你的实际硬件进行调整。

Rust环境安装

stm32f1系列的芯片属于cortex-m3系列的单片机,所采用的指令集为armv7。这个与我们平时采用的amd64指令集或者树莓派 所使用的指令集是不一样的。因此,需要安装对应的编译环境。

rustup target add thumbv7m-none-eabi

通过以上命令让rust能够进行stm32f1程序的编译,但由于嵌入式开发的资源限制以及对于单片机来说,并没有操作系统的存在。 因此,针对嵌入式开发的rust并不能使用std库。

Rust项目代码

针对嵌入式的代码,也能够像一般的程序那样使用cargo进行构建和依赖管理。只是针对嵌入式的场景,需要做一些调整和配置。

memory.x文件

与我们平时的程序不同的是,针对嵌入式的程序需要根据单片机的硬件设定程序中内存的分布,便于编译器对程序进行构建。 memory.x就是用来定义这些分布的参数的。针对stm32f103c8的情况进行设置,对于stm32f1系列的单片机, 主要修改的是FLASH中的LENGTH中的值,调整成不同FLASH ROM的容量。

MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 64K /* 定义Flash ROM的容量为64K */
  RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

项目cargo的配置文件 .cargo/config

针对cargo也许要进行设定,设定的文件为 .cargo/config。以下的内容定义了cargo的编译方式,以及链接参数等。 对于windows的用户,可能需要采用arm-none-eabi-ld的链接器进行链接。

[target.thumbv7m-none-eabi]

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
  "-C", "link-arg=-Tlink.x",
  # 在windows下,可能需要使用下面被注释的链接方式
  # "-C", "linker=arm-none-eabi-ld",
]

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

cargo.toml 文件定义依赖

接下来的配置基本和一般的rust项目类似了,针对stm32的嵌入式开发,我们需要引入一些嵌入式相关的依赖。

[package]
name = "blink"
version = "0.1.0"
authors = ["bezalel.xyz"]
edition = "2018"

[dependencies]
cortex-m = "*"
cortex-m-rt = "*"
cortex-m-semihosting = "*"
panic-halt = "*"
embedded-hal = "*"
nb = "*"

[dependencies.stm32f1xx-hal]
version = "*"
features = ["rt", "stm32f103", "medium"]

[[bin]]
name = "blink"
test = false
bench = false

[profile.release]
codegen-units = 1
debug = false
lto = true

程序主文件 src/main.rs

由于嵌入式开发与常规的应用不同,因此需要在文件一开始定义不使用std库,并且,根据硬件的实际情况设置系统时钟以及初始化, 以下的程序将会让PB12引脚所接的LED频烦闪烁。

#![no_std]
#![no_main]

use panic_halt as _;
use cortex_m_rt::entry;
use embedded_hal::digital::v2::OutputPin;
use nb::block;
use stm32f1xx_hal::{
    prelude::*,
    pac,
    timer::Timer
};

#[entry]
fn main() -> ! {

    let core = cortex_m::Peripherals::take().unwrap();
    let device = pac::Peripherals::take().unwrap();
    let mut flash = device.FLASH.constrain();
    let mut rcc = device.RCC.constrain();
    let mut _afio = device.AFIO.constrain(&mut rcc.apb2);
    
    // 设定stm32的时钟,采用外部8MHz的晶振,通过内部倍频达到72MHz的系统时钟。
    let clocks = rcc
        .cfgr
        .use_hse(8.mhz())
        .sysclk(72.mhz())
        .pclk1(36.mhz())
        .freeze(&mut flash.acr);

    // 定义定时器设置为10Hz,即每秒10次。
    let mut timer = Timer::syst(core.SYST, &clocks).start_count_down(10.hz());

    // 定义用于闪烁指示灯的引脚,这边采用的是PB12。
    let mut gpiob = device.GPIOB.split(&mut rcc.apb2);
    let mut led = gpiob
        .pb12
        .into_push_pull_output(&mut gpiob.crh);

    // 进入主循环,反复切换引脚电平,达到闪烁的目的
    loop {
        block!(timer.wait()).unwrap();
        led.set_high().unwrap();
        block!(timer.wait()).unwrap();
        led.set_low().unwrap();
    }
}

编译与程序上载

编译和构建基本和正常的程序一样,这里直接构建release版的代码进行上传。

cargo build --release

这里采用的是stlink进行代码上载,按照要求链接好stlink与stm32f1后,使用openocd进行上载。 如果要使用USART串口进行上传,可以使用stm32官方的STM32CubeProgrammer。

openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg \
-c "program target/thumbv7m-none-eabi/release/blink verify reset exit"

总结

本文所描写的只是利用rust进行stm32f1系列单片机的开发,实际使用中还是会遇到许多的问题。 例如rust的内存安全机制,对于嵌入式开发而言,还是会遇到一些阻碍。但相对C/C++而言, 也能提升代码的可靠性。openocd除了能上载代码外,其实本职的工作是结合调试工具实现在系统调试。 硬件的开发比起软件而言,遇到的问题和意外会更多。对于硬件的连线等也对开发的人而言,有更高的要求, 可能一个疏忽,就造成不可挽回的损失。也可能理论上一切正常,但运行时却各种问题和意外。 这里只是对于嵌入式开发做了一个简单的入门。关于更多的内容可参考 cortex-m-quickstart 项目