rust嵌入式开发补充

本文是对rust嵌入式开发的补充,就当时遗留的一些问题进行增补与修正。

RTIC中的任务处理

在上篇文章中还不是很理解rtic的工作机制。但写东东进行总结的好处就体现出来了,在上篇文章中提到了rtic的app入口本就是一个进程宏,所以在写完文章后就想,那就看看这个宏到底干了些什么吧。

想到就干。执行:

cargo expand > src/app.rs

然后在main.rs中引用一下【这样在vscode中读代码的时候,就可以随时跳转来来看源码】:

mode app;

经过对扩展了宏之后的代码的研读,搞清楚了几个问题:

RTIC的任务分发

rtic不是一个RTOS,它只是一个基于中断的任务分发系统

所以,上篇文章中有很多概念就都错了,因为那还是基于一个OS中多任务【或线程】的认识。

在rtic中,不管是中断处理,还是用户的任务函数,如live、mytask等,在rtic中都是一视同仁:全部都是在中断响应函数调用我们自己编写的任务函数。

而rtic所提供的标准的中断响应函数都是一样的:

  • 设置优先级,由程序员以priority指定
  • 为用户任务函数准备参数,核心就是local和shared两个结构
  • 调用程序员所写的用户任务函数

所以,dispatchers的作用就是选一个没有实际使用到的硬件中断源,然后在实际的硬件中断响应函数中调用用户任务函数的spawn()时,rtic就会将这个调用放入到一个任务队列中,然后以软件触发的方式触发这个中断。

同时,rtic会接管这个硬件中断的响应函数,然后在这个中断响应函数中从任务队列中逐次提取相应的用户任务函数来执行。

所以,使用了rtic后根本不需要再自己实现时钟任务或空闲任务队列,来自己做多任务的调度了。只需要和硬件中断一样,编写好自己的用户任务,然后在相应的硬件中断响应任务中spawn()即可。

所以,如上篇文章中,就需要三个用户任务函数,分别在两个硬件中断中进行触发:

tick函数【TIM1_UP硬件中断的响应函数】中触发mytask和live:
//用户任务
mytask::spawn().unwrap();
//用板载led做个呼吸灯,直观表示还活着
live::spawn().unwrap();uart1函数【USART1硬件中断的响应函数】中触发uart1_recv:
//串口1接收到数据
if new_data {//将接收到的数据放入用户空间cx.shared.buff_uart1_recv.lock(|buff_uart1_recv| {*buff_uart1_recv = Some(buff);});uart1_recv::spawn().unwrap();
}

也就是说,rtic中有两种任务,一种是硬件中断的处理函数,由rtic内置的硬件中断响应函数进行触发;一种是用户的任务处理函数,必须在前一种的硬件中断处理函数中以spawn()进行触发,然后rtic以软中断的方式触发dispatchers的中断,然后在此中断的响应函数中进行调度来执行用户的任务处理函数。

即,rtic中的任务其最终来源都是指定的硬件中断。

RTIC的任务处理

rtic中所有的任务函数,都等价于硬件中断响应函数。所以就有两个问题需要考虑:中断处理,数据安全。

中断处理

由于所有的任务函数都是中断响应函数,所以我们不得不研究一下rtic中对中断的处理方式。回答是:没有任何特殊的处理,主要依靠MCU芯片的中断处理机制。

arm芯片本身有其强大的中断处理机制,简单的说就是高优先级的中断可以抢占低优先级的中断。其也没有核心数据的概念,也就不需要特殊的中断保护机制。

所以rtic的中断处理非常简单:

  • 在初始化时,只允许用户绑定的那几个硬件中断,以及作为dispatchers源的那个硬件中断,其它中断全部关闭
  • 所有的任务函数,全部根据优先级由MCU芯片来执行中断的响应调度
  • rtic唯一要做的就是在用户任务spawn()时按优先级排队放入任务队列,然后在dispatchers时依次调度
数据安全

arm不管有没有核心数据,只根据优先级进行中断处理函数的抢占与调度。但rtic则必须提供相应的数据安全保护。

因此,在rtic视角就存在三种数据:

  • rtic系统数据,用于保存所有的系统数据和用户数据
  • 任务的本地数据,只用于某个任务函数的数据
  • 任务的共享数据,可在两个以上的任务函数间共享的数据

由于rust有强大的借用约束,所以系统数据和任务数据的隔离非常简单:所有的数据都由rtic拥有并管理,在需要调度任务函数执行【不管是硬件中断函数还是用户任务函数】时,rtic从用户数据区中引用【借出】该函数所指定的数据,然后创建相应的上下文,然后作为参数交给该函数执行。

也就是说,任务函数所需要的数据,不管是local的还是shared的,都来自rtic数据区中相应数据的引用【借用】,然后创建一个局部的上下文,将这些数据引用复制进来。

这也是rtic中的闭包无法跨线程使用的原因,因为rtic任务函数中的上下文是一个栈上的局部变量,其将随着任务函数的执行结束而被销毁,所以想如java或go中以闭包的方式将一些处理推入任务队列以在时钟任务队列或空闲任务队列中异步执行,是根本不可能的!

展开说,即java或go中可将闭包所使用的数据不从栈上分配,而是在堆中分配,则当闭包执行完毕时,对这块数据区的引用为0,就可以通过gc进行回收了。

但rust没有这样的机制,如果引用了上下文中的数据,就需要将这些数据自己copy到堆上,即创建一个Box,然后将上下文中的数据保存进去,然后将相应的函数指针和Box一起保存到时钟任务队列上,然后在需要的时候调用。但又何必使用闭包呢?!

而这,正好也就是rtic对local和shared两种数据的处理方式:在栈上创建对应的上下文数据结构,然后从保存到堆上的rtic的系统数据空间中复制需要的数据引用【借用】,再将上下文提供给任务函数使用。

当然,只用于某个任务函数的local数据这么处理没问题,但共享数据就不可以了,因为任务函数的调度是可以被更高优先级的任务函数抢占的。

这样一来,如果低优先级的任务函数正在使用共享数据,就可能导致错误。

rtic对此的处理是互斥,即共享数据都需要以加锁的方式才能使用,以保证共享数据的安全。

但这很容易就有一个困惑:如果低优先级的任务函数加锁后被高优先级任务函数抢占,其也要使用共享数据,那是否会导致死锁?!!

这是我们使用惯了OS或高级编程语言的习惯性认知。但rust嵌入式,起码在STMS32F103芯片上,它是一个单核的、没有运行时提供的阻塞任务队列来支持Mutex原语。

所以rtic对共享数据的加锁非常简单,每次加锁时,设置一个优先级天花板:

  • 当这个天花板是MCU的最高优先级【STMS32F103特性时是16】时,则函数执行前会关闭中断,函数执行完毕再打开中断
  • 否则,会临时把MCU的中断响应水平提升到天花板,低于或等于这个天花板的中断就暂时不响应了,等任务函数执行完再恢复【请注意,中断抢占可以嵌套,也就是说,这个屏蔽中断的天花板可以一步步提高,然后再一步步恢复】

rtic一般会把这个天花板设为需要加锁的任务函数的优先级。

也就是说,共享数据的锁,只对低于或等于该任务函数的其它函数起作用【其实,由于那些任务函数的优先级低,其也根本不会来抢占本函数的执行】,对于高优先级的任务函数是不起作用的!

所以呢,上篇文章中串口接收中断中对发送端口加锁的做法是没有任何意义的,因为串口发送时其所在的live和mytask任务函数的优先级只有1,而串口接收中断的优先级是3,其会抢占串口发送的执行,只有当串口接收完毕后,才会继续串口的发送。

也就是说,在当前的配置下,串口本就工作于单工模式,且串口发送的优先级是低于串口接收的,只有为串口配置了DMA模式,其才会处于双工模式。

我们搞清楚了rtic的共享数据加锁机制,自然也就明白了一个关键原则:如果需要在两个任务函数间共享某个数据,则这两个任务函数必须设为同一个优先级,否则就必须考虑高优先级函数的抢占会破坏低优先级任务函数的工作

总结

优先级是rtic正常工作的关键,建议应设置自己的一套优先级标准。笔者目前的考虑是:

  • 所有的用户任务函数,都是统一的最低优先级:1
  • 任何硬件中断响应函数之间不应共享数据
  • 用户任务对于从硬件中断响应函数共享来的数据应尽快消费,即必须考虑任务的实时性,如果实时性无法保证,则必须考虑使用缓存,然后和硬件中断响应函数之间商定一个消费协议【如设置标记、留存大小等】

至此,回看上篇文章中对串口接收的处理,还是受了rtt等RTOS的影响,所以自作聪明的增加了所谓上半程、下半程的多余设计:如果在串口接收处理还没有消费完接收到的数据时,又有新的数据进入,则之前提交给串口接收处理函数的共享数据【buff_uart1_recv】依然要被接收中断修改。我现在对rust的经验还不够,还无法说清楚具体会发生什么,但显然,系统崩溃、业务错误的风险肯定出现了。

所以,合理的设计应该是设置一个未消费标识,阻止接收中断在之前接收到的数据消费完毕之前接收新的数据。当然,由于我们给的串口缓存足够的大,更好的选择是实现环形接收缓存。

当然,哪怕串口的波特率被设为了115200,但触发一个接收数据中断的时间起码也需要:236微秒。在STM32F103工作在36M频率时,也足以执行8500多条指令,只要整个应用系统不是太复杂,没有非常多的功能或非常高频的其它外部中断,暂时还是不太需要担心消费不过来的。

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

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

相关文章

(Java企业 / 公司项目)分布式事务Seata详解(含Seata+Nacos组合使用)

一. Seata介绍 Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,其内部版本在阿里系内部一直扮演着应用架构层数据一致性的中间件角色,帮助经济体平稳的度过历年的双11&…

数字化转型与数据化思维

什么是数字化转型,什么是数据化思维,它们之间有什么关系? 数字化转型(Digital Transformation)是指企业或组织利用数字技术从根本上改变其业务活动、流程、文化和商业模式的过程,以适应不断变化的市场环境…

【Vue3/Vue2】判断设备是移动端还是pc端跳转不同路由router

Vue3代码 APP文件中写入js代码 1、首先,通过isMobile()函数判断用户的设备类型。该函数使用正则表达式匹配navigator.userAgent字符串,以确定用户是在移动设备上访问网页还是在桌面设备上访问网页 2、然后,在onMounted()钩子函数中&#…

http的tcp连接

http的tcp连接 三次握手 1、客户端第一次发起握手,请求建立tcp连接。 2、服务端接收到请求,知道客户端发送正常,为了让客户端知道服务端发送和接收信息正常,发起第二次握手,告诉客户端接收到了请求,并答…

vue3 - 自定义弹框组件

写了一个弹框组件 <template><transition name"modal-fade"><div v-if"showFlag" class"myModal"><div class"content"><div class"topBox"><div class"leftTitle"><spa…

线性代数——行列式按行(列)展开

目录 一、余子式&#xff1a;将行列式某元素所在行和列的元素全去掉 剩余部分所构成的行列式&#xff0c;称为该元素的余子式 二、代数余子式 三、行列式等于它的任一行&#xff08;列&#xff09;的各元素与对应代数余子式乘积之和 四、行列式某行元素&#xff08;列&…

单机物理机部署Datax

一、概述 DataX 是阿里巴巴开源的一个异构数据源离线同步工具&#xff0c;致力于实现包括关系型数据库(MySQL、Oracle等)、HDFS、Hive、ODPS、HBase、FTP等各种异构数据源之间稳定高效的数据同步功能。 源码地址&#xff1a;https://github.com/alibaba/DataX 为了解决异构数据…

什么是云服务器?云服务器的工作原理是介绍

阿里云服务器ECS英文全程Elastic Compute Service&#xff0c;云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;阿里云提供多种云服务器ECS实例规格&#xff0c;如经济型e实例、通用算力型u1、ECS计算型c7、通用型g7、GPU实例等&#xff0c;阿里云百科aliyunbai…

Linux shell jq工具操作文档(jq --help使用示例)

文章目录 jq工具介绍jq --help解读英文中文 使用示例1. 使用最简单的过滤器。将输入复制到输出&#xff0c;不做任何修改&#xff08;除了格式化&#xff09;2. 使用 -c 选项进行紧凑输出而非美化输出3. 使用 -n 选项以 null 作为单一输入值&#xff08;用于创建新json&#xf…

STL——stack容器和queue容器详解

目录 &#x1f4a1;stack &#x1f4a1;基本概念 常用接口 &#x1f4a1;queue &#x1f4a1;基本概念 &#x1f4a1;常用接口 &#x1f4a1;stack &#x1f4a1;基本概念 栈&#xff08;stack&#xff09;&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端…

OpenGL 网格拾取坐标(Qt)

文章目录 一、简介二、代码实现三、实现效果参考资料一、简介 有时候我们希望通过鼠标来拾取某个网格中的坐标,这就涉及到一个很有趣的场景:光线投射,也就是求取一条射线与网格的交点,这里如果我们采用普通遍历网格中的每个面片的方式,当网格的面片数据量很大时计算效率就…

Oracle VARCHAR和VARCHAR2区别

在Oracle数据库中&#xff0c;VARCHAR和VARCHAR2是两种不同的数据类型&#xff0c;它们的区别如下&#xff1a; 1.存储空间 VARCHAR和VARCHAR2在存储空间上有所不同。在Oracle 7及以下版本中&#xff0c;VARCHAR类型的长度是固定的&#xff0c;如果存储的数据长度小于定义的长…

pyside6 捕捉主窗口关闭后,进行释放相关的资源

import sys from PySide6 import QtGui from PySide6.QtWidgets import QWidget,QApplication,QMessageBoxclass Message(QWidget):def __init__(self):# 如果希望窗口内嵌于其他部件&#xff0c;可添加parent参数super(Message, self).__init__()# 调用初始化方法self.initUI(…

学习方法论:PQ4R 提升理解力

Learning Methodology: PQ4R for Enhanced Comprehension 学习方法论&#xff1a;PQ4R 提升理解力 The PQ4R method, devised by Thomas and Robinson, is an effective learning strategy that promotes better understanding and retention of information. It consists of…

Python基本语法与变量的相关介绍

python基本语法与变量 python语句的缩进 Python代码块使用缩进对齐表示代码逻辑&#xff0c;Python每段代码块缩进的空白数量可以任意&#xff0c;但要确保同段代码块语句必须包含相同的缩进空白数量。建议在代码块的每个缩进层次使用单个制表符或两个空格或四个空格 , 切记不…

Redis分布式锁的Java实现之道

摘要&#xff1a; 在当今的微服务架构中&#xff0c;分布式锁是一个非常重要的概念。它允许我们在多个服务之间同步操作&#xff0c;确保数据的一致性和完整性。而Redis作为一种高性能的内存数据存储系统&#xff0c;常常被用来实现分布式锁。 一、分布式锁的基本概念 在分布…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK设置软件触发模式(C++)

Baumer工业相机堡盟工业相机如何通过NEOAPISDK设置硬件触发模式&#xff08;C&#xff09; Baumer工业相机Baumer工业相机NEOAPISDK和软触发模式的技术背景Baumer工业相机通过NEOAPI SDK设置软件触发模式功能1.引用合适的类文件2.通过NEOAPI SDK实现软件触发采集图像的功能 Bau…

jQuery选择器(二) 过滤选择器及可见性过滤选择器的使用

Hi i,m JinXiang ⭐ 前言 ⭐ 本篇文章主要介绍在 jQuery中过滤选择器及可见性过滤选择器的使用以及部分理论知识 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主收将持续更新学习记录获&#xff0c;友友们有任…

flutter使用get库管理路由,并设页面跳转动画和常见动画

get库还是非常强大的一个仓库&#xff0c;里面包含了非常常用的一些方法&#xff0c;比如路由管理&#xff0c;这是最常见和最常用的一个功能了&#xff0c;我们可以先配置一个路由对象&#xff0c;然后在里面配置路由列表&#xff0c;并且设置路由跳转方式。 第一种方式&…

教师资格证照片分辨率怎么调?教师资格证上传照片要求

最近教师资格证考试开始报名了&#xff0c;在报名之前需要准备好一些必备的材料&#xff0c;比如证件照片&#xff0c;报名考试平台一般都会对上传的证件照有具体的要求&#xff0c;比如考生本人近6个月以内的免冠正面证件照&#xff1b;照片格式及大小&#xff1a;JPG/JPEG格式…