最近终于打通了rust嵌入式,值得庆贺!在折腾的过程中发现相关的资料不说少,但合用的太少,所以做个总结,希望能帮到有需要的兄弟。
在这个回答中我说了一下为什么想要启用rust嵌入式,不过当时还是有点低估了rust本身的门槛:(
环境
开发环境很简单:vscode+插件Cortex-Debug,但我实在没精力折腾怎么在vscode中进行debug,就是写完代码直接命令行编译。
相关的工具链请参考:安装工具链。我最后使用的芯片是STM32F103C8T6,所以需要安装
rustup target add thumbv7m-none-eabi
芯片刷新使用JTAG的ARM仿真器,淘宝多的是,选个买得人多的就行。刷程序我用的是JFlash,到官网下载安装后直接运行即可。
之前用rtt的时候,debug都已经被集成到IDE中了,而rust必须还得自己折腾,可折腾半天最后发现个问题:STM32F103C8只有64K的flash,刚写了几个功能debug版本就已经70几K的,只能用release版:
cargo build --release
所以干脆就不debug了,反正现在功能还比较简单,出问题了猜都能猜出是哪挂了:) 后面打通了uart串口的收发,直接看串口输出就是了。
这里需要说明的就是,本来打算用GD32的,但相关的库太少,支持的芯片少不说,而且功能不全,最后还是先选了STM32的芯片。
配置
这个都是比较标准的,主要是做个集中记录,避免以后的新项目少折腾。
.cargo/config.toml
主要是设置交叉编译的目标:
[build]
target = "thumbv7m-none-eabi"
memory.x
/* Linker script for the STM32F103C8T6 */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
根据自己选的芯片设置flash和内存大小即可。
Cargo.toml
主要是配置依赖。我现在用到的是:
[dependencies]
embedded-hal = "0.2.7"
nb = "1"
cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.1"
embedded-alloc = "0.5.1"
panic-halt = "0.2.0"
fugit = "0.3.6"
cortex-m-rtic = "1.1.4"
[dependencies.stm32f1xx-hal]
version = "0.10.0"
features = ["rt", "stm32f103", "medium"]
整个项目的框架我使用了rtic,就是上面依赖中的【cortex-m-rtic】,主要是用它来处理中断,说实话,就我目前rust的水平,其实不用rtic直接写更好理解,但多个中断、时间任务之间的协调实在来不及折腾了。
rtic其实比较简单,麻烦的是需要对rust和硬件的理解足够【当然,rtic能支持多个硬件体系和平台,它的价值在这里,但这一点对我反而价值不大】,需要搞清楚哪些需要自己做,哪些rtic帮我们做了,这很头疼。
大家看看上面rtic的官方指南,自己描个架子,我下面主要说一下自己的处理。
内存管理
rust嵌入式用的是core,所以std中的一些东东用不了,但好在基本的core中都有,包括vec、字符串等,但大家需要看一下rust的说明,官方已经指出:想在no_std环境中使用vec等,必须启用alloc。
所以,我们第一步就是做好相应的内存管理:
1、配置embedded-alloc依赖
有些例子给的是cortex-m-alloc,但这个crate自己都已经说自己挂了,请使用embedded-alloc
2、引用
extern crate alloc;
//引用之后,vec啥的就可以引用到了
use alloc::vec;
use alloc::collections::BTreeMap;
use alloc::string::String;
3、创建堆
//我分配了8k的堆空间
const HEAP_SIZE: usize = 8192;
use embedded_alloc::Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
4、初始化堆
在入口的第一条指令就执行堆的初始化工作:
use core::mem::MaybeUninit;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
入口:如果使用了rtic,就是init函数;否则就是main函数。
时钟
嵌入式编程很多时候我们得自己设时钟:
let mut flash = cx.device.FLASH.constrain();
let rcc = cx.device.RCC.constrain();
let clocks = rcc.cfgr.adcclk(2.MHz()).freeze(&mut flash.acr);
我因为要用到adc,所以这里设了adcclk。
大家在rtic官方的例子中经常看到:
#[monotonic(binds = TIM2, default = true)]
type MicrosecMono = MonoTimerUs<pac::TIM2>;
因为STM32F103只有TIM1,我又要用时钟中断,所以我就去掉了,初始化时只使用:init::Monotonics()。
串口
我用的是USART1,gpio管脚是pa9/pa10,没有使用DMA【被rust折腾惨了,暂时还搞不定】就是中断收发。
let mut afio = cx.device.AFIO.constrain();
let mut gpioa = cx.device.GPIOA.split();
let uart1_pin_tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh);
let uart1_pin_rx = gpioa.pa10;
let uart1 = Serial::new(
cx.device.USART1,
(uart1_pin_tx, uart1_pin_rx),
&mut afio.mapr,
serial::Config::default()
//115200,8N1
.baudrate(115200.bps())
.stopbits(serial::StopBits::STOP1)
.wordlength_8bits()
.parity_none(),
&clocks,
);
let (mut tx, mut rx) = uart1.split();
//监听数据中断
rx.listen();
//监听空闲中断
rx.listen_idle();
发送
我需要将数据打包后发送,所以我的串口发送程序是这样的:
pub fn sent_collect_uart(tx: &mut serial::Tx<USART1>, c:&mut Collect) {
let ps: Vec<u8> = c.into_parket();
let arr: &[u8] = &ps[..];
tx.bwrite_all(arr).unwrap();
}
即将自己写的数据集先打包成一个u8的缓冲区,然后将这个缓冲区转换成一个u8数组,然后用tx的bwrite_all函数发送即可。
注意:不要flush,会挂
由于串口发送在很多地方都会用到,所以我把tx放到了Shared中:
#[shared]
struct Shared {
tx_uart1: Tx<USART1>,
live_random: u32,
}
这样一来,在rtic的任务中就必须以加锁的方式才能使用tx,这就可以保证串口发送是互斥的:
cx.shared.tx_uart1.lock(|tx_uart1| {
sent_collect_uart(tx_uart1, &mut c);
});
接收
串口的接收需要中断,但硬件中断是不应该嵌套的,所以接收完数据的应用处理应该从中断中分离出来。我们在配置并初始化串口后,调用了两个listen函数,这就是分别监听了数据中断和空闲中断
- 数据中断:串口接收到了数据
- 空闲中断:串口超过9bit时间未收到数据信号
组合这两个中断,我们就可以成帧接收数据了。
前面说过,rtic的麻烦是麻烦在需要搞清楚哪些需要我们做,哪些rtic会帮我们做。rtic的例子非常少,文档说的也不是很透彻,需要在新功能上反复尝试才行,好郁闷的:(
串口的数据中断和空闲中断都是绑在USART1号上的,我直接贴代码,然后加注释了:
//用户区的接收处理
fn uart1_recv(buff: vec::Vec<u8>) {
......
}
//串口的接收缓存区,一次最多只能接受1024个字节
const BUFFER_LEN: usize = 1024;
static mut BUFFER: &mut [u8; BUFFER_LEN] = &mut [0; BUFFER_LEN];
static mut WIDX: usize = 0;
//串口1的中断处理函数
#[task(binds = USART1, priority = 3, local = [rx_uart1], shared = [tx_uart1])]
fn uart1(mut cx: uart1::Context) {
//指示本次中断是否可以处理接收到的新数据
let mut b: bool = false;
//如果有新数据,则将其拷贝到buff中再提供给用户处理程序
let mut buff: vec::Vec<u8> = vec![];
//接收端口,不这么做会报move方面的错误,快被折腾疯了:(
let rx = cx.local.rx_uart1;
//接收的时候不允许发送,发送的时候也不会接收,这就是强制将USART变成了单工
cx.shared.tx_uart1.lock(|tx_uart1| {
if rx.is_rx_not_empty() {
//是数据中断,则接收到的数据放到接收缓存
if let Ok(w) = nb::block!(rx.read()) {
unsafe {
BUFFER[WIDX] = w;
WIDX += 1;
if WIDX >= BUFFER_LEN - 1 {
//超出的数据丢弃
//WIDX = 0;
}
}
}
//可以等待空闲中断了
rx.listen_idle();
} else if rx.is_idle() {
//空闲中断,数据接收完毕
b = true;
unsafe {
//将接收到的数据从接收缓存copy到用户空间,以避免被新数据覆盖
buff = vec![0; WIDX];
let to = buff.as_mut_ptr();
let from = BUFFER.as_mut_ptr();
ptr::copy(from, to, WIDX);
WIDX = 0;
}
//除非接收到新的数据,否则不等待空闲中断
rx.unlisten_idle();
}
});
if b {
//这里应该将其放入空闲任务队列,异步执行以尽快结束中断处理
//但我现在还没做到这一步,rust太折腾了:(
uart1_recv(buff);
}
}
时钟中断
stm32只有TIM1,我设了10ms的tick【哈哈,从rtt学到的】:
let mut timer = cx.device.TIM1.counter_ms(&clocks);
timer.start(TIMER_TICK.millis()).unwrap();
timer.listen(Event::Update);
啊,对了,尽可能的用cx.device而不要再自己引用了,现在还搞不清楚为什么,问题太多了,反正就尽量先这么用吧:(
时钟中断的处理函数:
#[task(binds = TIM1_UP, priority = 5, local = [timer])]
fn tick(cx: tick::Context) {
unsafe {
//借鉴rtt,每个时钟中断tick加1
if sys_tick == u32::MAX {
sys_tick = 1;
}else{
sys_tick += 1;
}
}
//可以继续时钟中断
cx.local.timer.clear_interrupt(Event::Update);
//用户任务
//和串口中断一样,应该放到空闲任务队列中异步调度执行
mytask::spawn().unwrap();
//用板载led做个呼吸灯,直观表示还活着
live::spawn().unwrap();
}
注意:绑定的中断号是TIM1_UP
我把时钟中断的优先级设成了5。
数据打包
说实话,在c中这根本就不应该值得多写一个字!可在rust中,我写完都得骄傲的跳三跳!!简直是被rust折腾的死去活来的:(
取到一块buff
fn get_buff(len:usize) -> vec::Vec<u8>{
let vec:vec::Vec<u8> = vec![0; len];
vec
}
向buff中写入数据
基本函数
fn write_buff(from:*const u8, to:*mut u8, len:usize) -> *mut u8 {
unsafe {
ptr::copy(from, to, len);
to.add(len)
}
}
有了基本函数,就可以写各种类型的数据了:
//写u16:
fn write_buff_short(to:*mut u8, v:u16) -> *mut u8 {
let from = &v as *const u16;
write_buff(from as *const u8, to, 2)
}
//写字节:
fn write_buff_byte(to:*mut u8, v:u8) -> *mut u8 {
let from = &v as *const u8;
write_buff(from, to, 1)
}
//写u32:
fn write_buff_int(to:*mut u8, v:u32) -> *mut u8 {
let from = &v as *const u32;
write_buff(from as *const u8, to, 4)
}
//写f32:
fn write_buff_float(to:*mut u8, v:f32) -> *mut u8 {
let from = &v as *const f32;
write_buff(from as *const u8, to, 4)
}
//写字符串:
fn write_buff_str(to:*mut u8, v:&str) -> *mut u8 {
let from = v.as_ptr();
write_buff(from, to, v.len())
}
真被rust的借用、生命周期给折腾的欲死欲仙的:(
打包
我用伪码写一下:
impl <'a> Collect<'a> {
......其它代码
//给自己的数据结构打包
pub fn into_parket(&mut self) -> vec::Vec<u8>{
//获取缓冲区
let mut buff = get_buff(self.tl as usize);
//得到缓冲区的基址
let base = buff.as_mut_ptr();
//每写入一个数据,prt就会指向下一个待写入的地址
let mut ptr: *mut u8 = base;
unsafe {
//打入包头
ptr = write_buff_byte(ptr, b'D');
//移动指针
ptr = base.add(OFFSET_DATA_LEN as usize);
write_buff_short(ptr, self.tl);
......其它代码
//开始打入数据
ptr = base.add(OFFSET_BODY as usize);
let pbody = ptr;
......其它代码
//crc
ptr = base.add(OFFSET_CRC as usize);
//crc是数据区的校验和,所以要从包身开始,计算总数去掉包头的长度
let crc = crc_xor(pbody, (self.tl - 12) as u32);
write_buff_byte(ptr, crc);
}
buff
}
}
需要注意:虽然写数据是用unsafe封了可以强制读写,但如果自己没算清楚,buff的读写超范围了,出了unsafe就会挂!所以,在打包完成后,调试期间应该立刻写一个print语句,如果没看到相应的提示,那自然就说明指针移动的时候算错了。
杂项
其它adc和gpio,包括业务处理都很简单,各种例子都可以参考,不复赘述。
dispatchers
我现在还没搞懂,rtic的app为什么需要一个dispatchers,还必须是和自己用到的中断都不一样的一个硬件中断源,我就用了官方例子中的SPI1:
#[rtic::app(device = stm32f1xx_hal::pac, dispatchers = [SPI1])]
mod app {
代码布局
这个也很折腾,rtic::app它本身是一个宏!所以我们写的代码,并不是编译器阅读到的最终代码,所以我最终就没用官方例子的:
use ......
mod app {
use super::*;
即和其它rust程序一样,在代码的开头就引用需要的crate。我是:
#![no_std]
#![no_main]
use panic_halt as _;
extern crate alloc;
mod 自己写的模块;
#[rtic::app(device = stm32f1xx_hal::pac, dispatchers = [SPI1])]
mod app {
use ......
包括静态数据的定义都是放到app模块里面,这样就不会有问题。
idle
这个很简单:
#[idle]
fn idle(_cx: idle::Context) -> ! {
loop {
cortex_m::asm::wfi();
//官方例子中的也可以
//rtic::export::wfi()
}
}
即便不用idle也没问题,但一呢,根据官方的说法,用了idle会比较节省【当然,我们做了一个10ms的时钟】;二呢,就是可以在idle中做我们的用户任务管理。