rust嵌入式之用类函数宏简写状态机定义

这篇具有很好参考价值的文章主要介绍了rust嵌入式之用类函数宏简写状态机定义。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

笔者一向认为,用有限状态自动机来做硬件控制是最好的选择,同时又倾向于用文本定义来定义状态机是更好的做法。所以此次用rust开发嵌入式自然也是如此。

状态机实现起来很简单,关键是用文本来定义状态机,在rust中,自然是用宏来实现。

在折腾的过程中,又是发现各种解说文章铺天盖地的,但真正有用的不多,都是泛泛而谈。所以还是老样子,写篇文章讲一下自己经过痛苦折腾后的实现,希望能帮到需要的兄弟。

目标

我希望实现的状态机的定义是:

//充电控制状态机
stateMachine!{
    name: sm_charge;
    init: charge_close, charge_close;
    state: charge_close, charge_open, charge_close_wait;
    event: event_charge_close, event_charge_open, event_charge_timeout;
    active: charge_close, charge_open, charge_start_timer;
    trans: charge_close, event_charge_open, charge_open, charge_open;
    trans: charge_open, event_charge_close, charge_close_wait, charge_start_timer;
    trans: charge_close_wait, event_charge_timeout, charge_close, charge_close;
}

即,用一个宏,以文本的方式完成整个状态机的定义【在init函数之外】,然后在init函数执行初始化时执行:

let smi_charge = sm_charge_init();

就可以完成全部的初始化的工作。然后就可以将状态机实例smi_charge放入shared中使用了。

状态机的实现非常简单,这不是我们的重点,我们主要展示如何编写一个类函数宏来定义并初始化状态机。

状态机定义了8种语句:

1、name,状态机名字,形式【name:smname;

2、init,状态机初始设置,形式【init:initstate, initfunc[可选];

3、state,状态机的状态列表,形式【state:state1, state2, …;

4、event,状态机的事件列表,形式【event:event1, event2, …;

5、active,状态机的动作列表,形式【active:active1, active2, …;

6、trans,状态机的跃迁,形式【trans:from_state, event, to_state, active[可选];

7、trans_else,状态机的跌落,形式【trans_else:from_state, to_state, active[可选];

8、force,状态机的强制跃迁,形式【force:event, to_state, active[可选];

前面的12345,有且仅有一次,后面的678可重复多次,78也可忽略。

每种语句以一个关键字开头,跟一个英文的冒号,然后是单个或多个标识符【标识符之间以英文逗号分隔】,最后跟一个英文的分号作为结尾。

准备

这个很多文章都讲到,我就集中整理一下,免得大家再去翻。

1、过程宏是在编译的时候执行的,所以过程宏必须以crate的方式创建,而不能是模块。所以,在项目主目录下执行:

mkdir macro_sm
cd macro_sm
cargo init --lib

注意:macro_sm和项目的src目录平级

2、macro_sm的Cargo.toml:

[dependencies]
proc-macro2 = "1.0.76"
quote = "1.0.35"
syn = { version = "2.0.48", features = ["full","extra-traits"] } 
	
[lib]
proc-macro = true

然后就可以在macro_sm的src目录中的lib.rs文件中编写宏了。

3、在主项目的的Cargo.toml中添加依赖:

[dependencies]
macro_sm = { path='./macro_sm' }

4、在主项目的main.rs中引用:

extern crate macro_sm;
use macro_sm::stateMachine;

然后就可以使用sm宏定义自己的状态机了。

类函数宏的工作机制

类函数宏的工作包括四步:

  • 将stateMachine!{…}定义中花括号之间的内容进行解析,识别为一个个rust词法单元【Token】组成的TokenStream
  • 将此TokenStream转换为自定义数据结构形式的数据
  • 根据转换后的数据生成想要的rust语法块
  • 将生成的rust语法块再次转换为TokenStream

本质上,类函数宏最终的成果和java中的反射是一样的,都是向程序中注入已经良好实现过的代码。但java是动态的,而rust则是在编译时一次性完成的。

第一步和第四步,rust的编译器以及syn已经帮我们做完了,我们的主要工作就是二、三两步。所以我们的工作主要分为三个阶段:语句解析、文章解析、语义扩写

  • 语句解析:将name、init、state等我们自定义的语句一一识别并从中提取我们需要的数据
  • 文章解析:将这八种语句一一识别出来后,整合为我们对状态机的完整描述
  • 语义扩写:根据得到的状态机描述,将其翻译为状态机的函数调用代码等以创建对应的自动机

强调一点:在第一步我们说了,对我们自定义的内容首先是识别为rust的词法单元,所以不管我们如何定义,都必须符合rust的词法要求【不是语法要求,语法是我们自己定义的,如我上面自定义的八种语句】,即标识符必须是rust中的合法标识符;如果rust识别为表达式,我们就只能当做表达式来用。

如,【:::】即连续三个英文冒号,rust会识别为一个类引用符【::】和一个冒号,我们就不能按自己的想法随意使用,将这三个英文冒号当做自己的一个词汇。

所以,类函数宏本质上是用rust的词汇,根据我们自定义的语法来造句,在理解了用这个语法书写的文章的意图后注入对应的代码

语句解析

看一下上面状态机的八种语句,其格式都是【识别是哪种语句的关键字】【英文冒号】【数量不定的标识符,如果多个标识符则以英文逗号分隔】【英文分号】。

所以我们的工作包括三步:

1、准备词汇

可以看出,词汇有三种:关键字;英文的冒号、逗号、分号;标识符。后两者syn已经帮我们解析完了,关键字syn也提供了相应的处理函数,我们只需要根据其提供的工具来定义这八个关键字即可:

mod kw {
    syn::custom_keyword!(name);
    syn::custom_keyword!(init);
    syn::custom_keyword!(state);
    syn::custom_keyword!(event);
    syn::custom_keyword!(active);
    syn::custom_keyword!(trans);
    syn::custom_keyword!(trans_else);
    syn::custom_keyword!(force);
}

2、准备数据结构

状态机定义的这八种语句,大家仔细琢磨一下,其实关键的就是两种信息:什么类型的语句,以及这些语句中都包含了哪些标识符。

按rust的习惯,这两种信息分别用两类数据结构来表示:

  • 每一种语句,我们都需要一种数据结构来保存该语句识别出来的信息
  • 再定义一个枚举,来表示属于哪一种类型的语句

语句的定义是:

name语句:

struct SMName {
	name: Ident,
}

state语句:

struct SMState {
	idents: Vec<Ident>,
}

其它语句都和state语句一样,都只有idents来记录本语句由哪些标识符组成。

3、解析

然后就是对每种语句进行解析,syn已经帮我们完成了中间的工作,我们只需要根据我们的语法来提取标识符就可以了:

//name语句的识别。name语句的语法格式是【name:smname;】
impl Parse for SMName {
	//syn已经把TokenStream转换为了识别时更好用的ParseStream
    fn parse(input: ParseStream) -> Result<Self> {
	    //生成一个探查头
        let lookahead = input.lookahead1();
        //name语句是以name关键字开头,所以要先检查是不是这样;peek不移动读取游标
        if lookahead.peek(kw::name) {
	        //从流中提取name关键字,但对我们没用,所以直接丢弃;parse如果成功会移动读取游标
            let _: kw::name = input.parse()?;
            //提取英文冒号,还是没用,直接丢弃
			//如果name后跟的不是英文冒号,会提取失败,最后的问号就会立刻结束对name语句的识别并返回错误
            let _: Token![:] = input.parse()?;
            //提取出名字对应的标识符
            let name: Ident = input.parse()?;
            //name语句是以英文分号结尾的,检查是否如此,并丢弃
            let _: Token![;] = input.parse()?;
            Ok(SMName {
	            //识别并提取成功,返回SMName来保存识别结果
                name,
            })
        }else{
	        //不是name语句
            Err(lookahead.error())
        }
    }
}

其它七种语句都是一个以上的标识符,所以只是在识别冒号和分号之间做一个循环即可:

let _: Token![:] = input.parse()?;
let mut b = true;
while b {
    //识别并提取标识符
    let t: Result<Ident> = input.parse();
    match t {
        Ok(ident) => {
            idents.push(ident);
        },
        Err(_) => {
            //有两种可能
            let ct: Result<Token![,]> = input.parse();
            match ct {
	            //一种是标识符后跟着其它类型的词汇,就停止识别
                Err(_) => b = false,
	            //一种是标识符后跟着逗号,表示没完,需要继续
                _ => (),
            }
        },
    }
}
let _: Token![;] = input.parse()?;

由于那七种语句都是这么识别的,所以把上面的语句写成一个函数来用就好了。

到这,我们就完成了对八种语句的识别。然后我们用一个枚举来提供各语句的类别信息:

enum SMItem {
    Name(SMName),
    Init(SMInit),
    State(SMState),
    Event(SMEvent),
    Active(SMActive),
    Trans(SMTrans),
    Else(SMTransElse),
    Force(SMForce),
}
文章解析

有了句子,我们就可以将之组合运用来写自己的文章了。但笔者如今满打满算开始看rust都没满两个月,syn的例子又太少,实在来不了挥洒写意,所以干脆的约定死了八种语句的语义约束:就按我一开始给出的语句顺序一个个来,前五种一个不能少,后三者可重复,最后两种可省略。

而在上面,我们用枚举SMItem来综合八种语句,这就大大简化了我们对状态机的描述:

struct StateMachine {
    list: Vec<SMItem>
}

即状态机就是一系列顺序语句的集合。

这样一来,整个状态机的解析就是按上面的约束,一个语句一个语句的解析后放入list中即可:

impl Parse for StateMachine {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut list: Vec<SMItem> = vec![];
        list.push(SMItem::Name(SMName::parse(input)?));
        list.push(SMItem::Init(SMInit::parse(input)?));
        list.push(SMItem::State(SMState::parse(input)?));
        list.push(SMItem::Event(SMEvent::parse(input)?));
        list.push(SMItem::Active(SMActive::parse(input)?));
        loop {
            let tr = SMTrans::parse(input);
            match tr {
                Ok(item) => list.push(SMItem::Trans(item)),
                Err(_) => break,
            }
        }
        loop {
            let tr = SMTransElse::parse(input);
            match tr {
                Ok(item) => list.push(SMItem::Else(item)),
                Err(_) => break,
            }
        }
        loop {
            let tr = SMForce::parse(input);
            match tr {
                Ok(item) => list.push(SMItem::Force(item)),
                Err(_) => break,
            }
        }
        Ok(SM { list })
    }
}

有了对整个状态机的解析,我们就完成了第二步工作:从rust词汇中得到我们需要的数据。

现在,我们就可以完成类函数宏的上半部分的编写了:

#[proc_macro]
pub fn stateMachine(tokens: TokenStream) -> TokenStream {
	//加了proc_macro属性宏的sm函数,就是我们自己编写的sm宏
	//其参数tokens就是stateMachine!{...}执行时花括号中的文本被识别为rust词汇后的结果
	//然后我们将tokens解析为我们自己的SM数据结构
    let mut data = parse_macro_input!(tokens as StateMachine);

	//下面就是用得到的数据来生成我们需要的代码了	
}
语义扩写

得到了状态机的描述,我们就可以根据这些描述数据,来生成状态机定义的代码了。

简单的说,就是根据这些数据,拼出一个字符串,然后将这个字符串翻译为TokenStream输出,rust编译器就会将这个字符串其当做代码进行编译了。即

  • rust编译器在编译我们的源代码的时候,读到了stateMachine!{…},就会把花括号中的文本解析为rust词汇流,然后调用另一个crate中的stateMachine函数
  • stateMachine函数将编译器送入的rust词汇流翻译成一个字符串,然后将这个字符串转换成另一个rust词汇流,返回给rust编译器
  • rust编译器就会将原本的【stateMachine!{…}】用得到的rust词汇流进行整体替换

所以,我们生成的代码,就是rust代码,所以不仅仅要符合rust词法,还要符合rust语法。

由于基本都差不多,我们就只以状态的定义和跃迁的定义进行说明。

我实现的状态机的状态和事件,都是u8的静态变量,所以:

//这些生成代码,就接在上面从tokens中提取出data之后
let mut order = 0;
let mut tss = String::new();
data.list.retain_mut(|item|{
	//从状态机的各语句中只提出state语句来扩写
    match item {
        SMItem::State(SMState{ idents, ..}) => {
            for ident in idents.iter() {
                tss = format!("{}\nstatic {}: u8 = {};\n", tss, ident.to_string().to_uppercase(), order);
                order += 1;
            }
			//retain_mut如果返回false会删除掉该项
            false
        },
        _ => true
    }
});
tss += "\n";

就是将【state: charge_close, charge_open, charge_close_wait;】的状态语句,生成对应的代码:

static CHARGE_CLOSE: u8 = 0;
static CHARGE_OPEN: u8 = 1;
static CHARGE_CLOSE_WAIT: u8 = 2;

跃迁【trans】是同样的处理框架,只是由于其active可选,所以:

let mut active_name = "None".to_owned();

如果trans语句中的标识符是四个的话,就修改active_name:

active_name = format!("Some({})", ident.to_string());

由于rust中的字符串拼太麻烦,所以我用了quote,但需要在调用前将字符串转换为标识符【字符串带引号的】:

let ident_from: syn::Ident = syn::parse_str(from.as_str()).expect("Unable to parse");
let ident_event: syn::Ident = syn::parse_str(event.as_str()).expect("Unable to parse");
let ident_to: syn::Ident = syn::parse_str(to.as_str()).expect("Unable to parse");
//active_name如果有则形如【Some(...)】,在rust词法中,这是一个表达式
let ident_active_name: syn::Expr = syn::parse_str(active_name.as_str()).expect("Unable to parse");
//用quote来扩写trans语句对应的add_trans函数调用
let ts_init = quote!(
	let _ = &sm.add_trans(#ident_from, #ident_event, #ident_to, #ident_active_name);
);
//我还是将其转换为了字符串
rs += ts_init.to_string().as_mut_str();

然后扩写出一个名为【sname_init】的函数,将init、trans、trans_else、force这几种语句扩写后的代码块包含进来:

fn sm_charge_init() -> state_machine::SMInstance {
    let mut state_machine = State_machine::new(CHARGE_CLOSE, Some(charge_close));
    //trans语句扩写后的代码块
    let _ = &state_machine
        .add_trans(CHARGE_CLOSE, EVENT_CHARGE_OPEN, CHARGE_OPEN, Some(charge_open));
	......
    //trans_else语句扩写后的代码块,如果有的话
    //force语句扩写后的代码块,如果有的话
    
    //根据创建好的状态机,生成其实例
    return State_machine::instance(sm);
}

最终,整个rs字符串包括,state和event语句扩写为对应的静态变量声明语句,active语句扩写为一组动作函数,name语句、init语句、trans语句、trans_else语句、force语句这五种语句扩写为上面的sm_charge_init语句。

在stateMachine的最后,我们将生成的字符串再翻译回TokenStream:

	//显示我们生成的代码
    eprint!("State_machine:{}\n", rs);
	//将这段代码翻译为rust词汇流
    let mut ts: TokenStream = rs.parse().unwrap();
	//返回结果
    ts
}//state_machine函数结束

这样,在init函数中,只要调用sm_charge_init函数,就可以得到该状态机的实例了:

let smi_charge = sm_charge_init();

将其放入shared中,在需要时触发事件即可:

if voltage > VOLTAGE_15V {
    let sr = cx.shared.smi_charge.lock(|smi_charge| {
        //电池电压超过15伏时,触发禁止充电事件
        smi_charge.happen(EVENT_CHARGE_CLOSE, None)
    });
}

注意:rtic中的任务无法通过闭包的形式来调用【参考我上篇文章的说明】,所以需要先手工编写rtic的任务函数:

#[task(priority = 1, shared = [out_charge, state_charge])]
fn charge_close_inner(mut cx: charge_close_inner::Context, param: Option<BTreeMap<u8, Value>>) {
    //禁止充电
    cx.shared.out_charge.lock(|out_charge| {                
        out_charge.set_high()
    });
    cx.shared.state_charge.lock(|state_charge| {                
        *state_charge = 0;
    });
    let _ = send_packet::spawn();
}

然后我们就可以扩写active语句中的charge_close动作为对此任务函数的调用入口函数了:

fn charge_close(param: Option<BTreeMap<u8, Value>>) {
	//调用实际执行禁止充电任务的charge_close_inner函数
    let _ = charge_close_inner::spawn(param);
}

结语

rust中的宏,尤其是类函数宏,很好用也很强大。如状态机,如果不用宏,写起来就比较麻烦,当然这点麻烦并不足以抵消学习宏的高昂成本。

关键是改起来就要疯掉了,增加一个状态、增加一个事件,调整几个跃迁,这在控制系统开发过程中是常态,还是频繁发生、反反复复发生着的。

这时,文本定义由于集中在一起,不需要频繁的翻页、查找,所以注意力高度集中;而且也不需要分神去理解程序逻辑,就是集中考虑状态机该如何动作就好了。相比用代码编程实现,效率高,关键bug也会少很多。

说一个最不起眼的好处:rust要求静态变量全用大写,关键看大写单词非常吃力啊,写跃迁的时候,一行全是大写单词,光在脑子里翻译大写单词了:(

而用宏,完全可以在定义的时候都用小写,然后扩写成大写,在思考状态机的定义的时候,就轻松了很多。

当然,触发的时候,还是得用大写单词,但事件触发是分布在各输入处理中的,本来就需要大量的翻找和定位,这个时候的大写反而比较显眼,有助于在翻找分散精力后迅速集中注意力了。文章来源地址https://www.toymoban.com/news/detail-814255.html

到了这里,关于rust嵌入式之用类函数宏简写状态机定义的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 嵌入式软件测试笔记12 | 什么是状态转换测试?如何开展?

    嵌入式系统有些表现出基于状态的行为,设计此系统可使用基于状态的建模; 在设计过程中,创建的模型可作为测试设计的基础; 以下将描述基于状态的模型来导出测试用例的技术。 此技术目标是:验证事件、动作、行为与转台转换之间的关系; 通过此技术,可判定系统基

    2024年02月17日
    浏览(60)
  • 【嵌入式——C++】 数组与函数

    C++ 支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。 声明数组 初始化数组 数组的访问 声明数组 初始化数组 数组的访问 函数是一组一起执行一个任务的语句。每个 C++ 程序都至

    2024年01月18日
    浏览(37)
  • 嵌入式毕设分享 stm32的人体健康状态检测系统(项目开源)

    🔥 这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。 为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天

    2024年03月22日
    浏览(78)
  • 【小黑嵌入式系统第六课】嵌入式系统软件设计基础——C语言简述、程序涉及规范、多任务程序设计、状态机建模(FSM)、模块化设计、事件触发、时间触发

    上一课: 【小黑嵌入式系统第五课】嵌入式系统开发流程——开发工具、交叉开发环境、开发过程(生成调试测试)、发展趋势 下一课: 【小黑嵌入式系统第七课】PSoC® 5LP 开发套件(CY8CKIT-050B )——PSoC® 5LP主芯片、I/O系统、GPIO控制LED流水灯的实现 1、为什么要用C语言? 理

    2024年02月06日
    浏览(64)
  • 上位机图像处理和嵌入式模块部署(自定义算法)

    【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】         我们在使用opencv的时候,虽然大部分算法都不需要我们自己重头开始编写,但是总有一些关于我们自己产品的know-how,是之前库里本来就不存在的。所以,这个时候,我们还是希望

    2024年01月24日
    浏览(47)
  • 自定义循环队列、软件定时器、事件集,实用嵌入式代码库

    本篇博文分享一个很实用的嵌入式代码库。 它可灵活应用到有无RTOS的程序中,采用C语言面向对象的思路实现各个功能,尽可能最大化的复用代码,目前为止工具包包含: 循环队列、软件定时器、事件集 。 网址:https://download.csdn.net/download/m0_38106923/87765476?spm=1001.2014.3001.550

    2024年02月03日
    浏览(56)
  • 嵌入式linux_C应用学习之API函数

    pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径信息,如:“./src_file”、\\\"/home/dengtao/hello.c\\\"等;如果 pathname 是一个符号链接,会对其进行解引用。 flags:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,如O_RDONLY,O_CREAT,

    2024年01月15日
    浏览(45)
  • 嵌入式设计中对于只有两种状态的变量存储设计,如何高效的对循迹小车进行偏差量化

    (1)在嵌入式程序设计中,我们常常会要对各类传感器进行数据存储。大多时候的传感器,例如红外光传感器,返回的数据要么是0,要么是1。因此,只需要一bit就能够存储。而很多人却常常使用char型数组存储,这样真正申请到的内存只使用了八分之一。对于MCU这种空间宝贵

    2024年02月12日
    浏览(44)
  • FPGA嵌入式开发一些Xilinx SDK库函数的理解

    最近在测试AXI Quad SPI这个IP核的端口时序,搭建BD后导出到硬件,在SDK中导入xspi_intr_example.c的源文件,在师兄的帮助下,浅浅研究了一下代码。 首先,需要修改源程序中的错误,参照CSDN文章:ZYNQ中断示例修改 做出以下修改: 此外,还要对中断驱动实例名称进行修改: 以下

    2024年02月12日
    浏览(50)
  • 在嵌入式设备中用多项式快速计算三角函数和方根

    惯性传感器的倾角计算要用到三角函数. 在 MCS-51, Cortex M0, M3 之类的芯片上编程时, 能使用的资源是非常有限, 通常只有两位数KB的Flash, 个位数KB的RAM. 如果要使用三角函数和开方就要引入 math.h, 会消耗掉10KB以上的Flash空间. 在很多情况下受硬件资源限制无法使用 math.h, 这时候使

    2024年03月09日
    浏览(80)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包