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 = 64KRAM : 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加1if 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;......其它代码//crcptr = 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中做我们的用户任务管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/613364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JSON Crack数据可视化工具结合内网穿透实现公网访问

文章目录 1. 在Linux上使用Docker安装JSONCrack2. 安装Cpolar内网穿透工具3. 配置JSON Crack界面公网地址4. 远程访问 JSONCrack 界面5. 固定 JSONCrack公网地址 JSON Crack 是一款免费的开源数据可视化应用程序&#xff0c;能够将 JSON、YAML、XML、CSV 等数据格式可视化为交互…

什么是多态?

多态是方法的多态&#xff0c;属性没有多态 多态存在父子之间 父类与子类之间要有联系&#xff0c;没有联系则会出现异常 --- String 与 Person 本质&#xff1a;父类引用指向子类对象 --- Person son new Son(); 如何构成多态&#xff1f; 建立父子类 子类重写父类方法…

Redis:原理速成+项目实战——Redis实战7(优惠券秒杀+细节解决超卖、一人一单问题)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis实战6&#xff08;封装缓存工具&#xff08;高级写法&#xff09;&&缓存总…

【数据库学习】hive

1&#xff0c;HIVE Hadoop 的数据仓库处理工具&#xff0c;数据存储在Hadoop 兼容的文件系统&#xff08;例如&#xff0c;Amazon S3、HDFS&#xff09;中。hive 在加载数据过程中不会对数据进行任何的修改&#xff0c;只是将数据移动到HDFS 中hive 设定的目录下。 1&#xf…

【数据库学习】ClickHouse(ck)

1&#xff0c;ClickHouse&#xff08;CK&#xff09; 是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。 1&#xff09;特性 按列存储&#xff0c;列越多速度越慢&#xff1b; 按列存储&#xff0c;数据更容易压缩&#xff08;类型相同、区分度&#xff09;&#xff1b…

Python如何使用Excel文件

使用Python操作Office——EXCEL 首先介绍下office win32 com接口&#xff0c;这个是MS为自动化提供的操作接口&#xff0c;比如我们打开一个EXCEL文档&#xff0c;就可以在里面编辑VB脚本&#xff0c;实现我们自己的效果。对于这种一本万利的买卖&#xff0c;Python怎么能放过…

D3121是什么?主要有哪些特点呢?为什么可以应用在车载音响系统上

D3121 是一块对地能动冲放大器集成电路&#xff0c;该电路能有效消除由线 路电阻所引起的问题及噪声。所需外围电容小&#xff0c;便于设计时小型化的同 时可靠性不降低。广泛应用于车载音响系统内。 D3121 系列采用 DIP8 、 SOP8 、 SIP8 的封装形式封装。 主要特点&#…

IP风险画像:源头防范网络攻击的全面策略

在当今数字化的时代&#xff0c;网络攻击呈现多样化和复杂化的趋势&#xff0c;为了确保网络的安全&#xff0c;制定全面的IP风险画像并从源头防范网络攻击是至关重要的。ip数据云将探讨如何通过建立IP风险画像来识别和应对潜在的威胁&#xff0c;从而实现更加安全可靠的网络环…

【STM32】HAL库的RCC复位状态判断及NVIC系统软件复位

【STM32】HAL库的RCC复位状态判断及NVIC系统软件复位 在实际开发中 有时候会遇到复位状态不同 导致结果不同的情况 比如在上电复位时 电压不稳定 可能导致一些外部芯片无法正常工作 从而导致进行了错误的操作流程 所以 可以在程序运行后 加一个复位状态判断 用来检测是否正常复…

2024最新面试经验分享

目录 重点掌握的知识点JavaMySQLRedis 微服务分布式系统项目亮点场景题/设计题短链抢红包多租户 开放性问题自我介绍为什么跳槽团队规模如何带团队如何看待加班职业规划 主要针对Java程序员&#xff0c;当然也包含一些通用的内容。 重点掌握的知识点 需要重点掌握的知识点必须…

git中常用的tag命令

1、创建一个tag git tag v1.02、创建带有注释的tag git tag -a v1.0 -m "release version 1.0"3、查看tag git tag4、查看tag的详细信息 git show v1.05、推送tag到远程仓库 git push --tags 6、删除某个tag git tag -d v1.07、切换到某个tag git checkout v1…

网络服务DHCP与DNS

一 DHCP的工作原理&#xff08;租约过程&#xff09; 分类 1&#xff09;自动分配&#xff1a;分配到一个IP地址后永久使用 &#xff08;2&#xff09;手动分配&#xff1a;由DHCP服务器管理员指定IP&#xff08;打印机、报销系统&#xff09;把mac地址和ip地址做一个一一对…

无人机群ros通信

单架无人机与地面站通信 在一个局域网内获取无人机的机载电脑ip 通过地面站ssh到机载电脑&#xff0c;实现通信 多架无人机与地面站通信 在ROS基础上&#xff0c;配置主机和从机&#xff0c;实现主机和从机的话题联通 配置hosts 在主机和从机的/etc/hosts文件中&#xff0c…

MySQL 从零开始:03 基本入门语句

文章目录 1、连接数据库1.1 命令提示符登陆1.2 MySQL 8.0 Command Line Client 登陆1.3 MySQL Workbench 登陆 2、基本语句2.1 查看所有库2.2 创建库2.3 删除库2.4 选择数据库2.5 查看表2.6 创建表2.7 删除表2.8 改表名2.9 清空表 在上一小节中介绍了 MySQL 数据库的安装&#…

【Android】Dialog弹出软键盘时把布局顶起来的实现

【Android】Dialog弹出软键盘时把布局顶起来的实现 demo效果如下&#xff1a; 代码实现 class AICodeDialog(val activity: BaseActivity) : Dialog(activity),View.OnClickListener, AIRedPkgView {private lateinit var mBgView: Viewprivate lateinit var mClose: Viewp…

Leetcode19-差的绝对值为K的数对数目(2006)

1、题目 给你一个整数数组 nums 和一个整数 k &#xff0c;请你返回数对 (i, j) 的数目&#xff0c;满足 i < j 且 |nums[i] - nums[j]| k 。 |x| 的值定义为&#xff1a; 如果 x > 0 &#xff0c;那么值为 x 。 如果 x < 0 &#xff0c;那么值为 -x 。 示例 1&a…

ML:2-4理解python如何实现forward prop

文章目录 1. 单层上的向前传播forward prop2. 前向传播的一般实现3. 通用人工智能 1. 单层上的向前传播forward prop 【了解在python中如何实现forward prop】 继续使用咖啡烘焙模型&#xff1a; 了解经过每一个神经元的预测过程。设置每一个神经元的w&#xff0c;b值。从而得…

接口测试工具:Postman的高级用法

Postman 是一款功能强大的 API 开发和测试工具&#xff0c;以下是一些高级用法的详细介绍和操作步骤。【文末有配套视频教程和免费的资料文档领取】 一、环境和全局变量 环境变量允许你设置特定于环境&#xff08;如开发、测试、生产&#xff09;的变量&#xff0c;全局变量则…

C语言之扫雷小游戏的实现【含递归展开】

文章目录 前言一、扫雷游戏代码设计思路二、设计扫雷代码1.创建菜单函数2.实现9x9扫雷3.初始化棋盘4.打印棋盘5.随机布置雷的位置6.排查雷的信息7.递归展开 三、源码1.新建一个test.c源文件2.新建一个game.c源文件3.创建一个game.h头文件 前言 扫雷游戏是1992年发行的一款大众类…

第十一章 后端编译与优化

文章目录 11.1 概述11.2 即时编译器11.2.1 解释器与编译器11.2.2 编译对象与触发条件11.2.3 编译过程 11.3 提前编译器11.4 编译器优化技术11.4.1 方法内联11.4.2 逃逸分析11.4.3 公共子表达式11.4.4 数组边界检查消除 11.1 概述 如果我们把字节码看作是程序语言的一种中间表示…