rust嵌入式开发

最近终于打通了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中做我们的用户任务管理。

相关推荐
大风起兮121 小时前
STM32HAL库中RTC闹钟设置时分秒,年月日
stm32·嵌入式硬件
QQ5471760522 小时前
stm32实现回调功能
stm32·单片机·嵌入式硬件
wenchm3 小时前
细说STM32F407单片机轮询方式读写SPI FLASH W25Q16BV
stm32·单片机·嵌入式硬件
委员3 小时前
基于NodeMCU的物联网电灯控制系统设计
单片机·物联网·嵌入式·nodemcu··lu_asr01·gy-302
北国无红豆3 小时前
【CAN总线】STM32的CAN外设
c语言·stm32·嵌入式硬件
单片机学习之路4 小时前
【C语言】结构
c语言·开发语言·stm32·单片机·51单片机
brrdg_sefg4 小时前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_748230945 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
m0_748254095 小时前
STM32--超声波模块(HC—SR04)(标准库+HAL库)
stm32·单片机·嵌入式硬件
逝灮6 小时前
【蓝桥杯——物联网设计与开发】基础模块8 - RTC
stm32·单片机·嵌入式硬件·mcu·物联网·蓝桥杯·rtc