网站托管,百度站长工具验证,国内电商平台怎么做,大安移动网站建设笔者一向认为#xff0c;用有限状态自动机来做硬件控制是最好的选择#xff0c;同时又倾向于用文本定义来定义状态机是更好的做法。所以此次用rust开发嵌入式自然也是如此。
状态机实现起来很简单#xff0c;关键是用文本来定义状态机#xff0c;在rust中#xff0c;自然…笔者一向认为用有限状态自动机来做硬件控制是最好的选择同时又倾向于用文本定义来定义状态机是更好的做法。所以此次用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: VecIdent,
}其它语句都和state语句一样都只有idents来记录本语句由哪些标识符组成。
3、解析
然后就是对每种语句进行解析syn已经帮我们完成了中间的工作我们只需要根据我们的语法来提取标识符就可以了
//name语句的识别。name语句的语法格式是【name:smname;】
impl Parse for SMName {//syn已经把TokenStream转换为了识别时更好用的ParseStreamfn parse(input: ParseStream) - ResultSelf {//生成一个探查头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: ResultIdent input.parse();match t {Ok(ident) {idents.push(ident);},Err(_) {//有两种可能let ct: ResultToken![,] 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: VecSMItem
}即状态机就是一系列顺序语句的集合。
这样一来整个状态机的解析就是按上面的约束一个语句一个语句的解析后放入list中即可
impl Parse for StateMachine {fn parse(input: ParseStream) - ResultSelf {let mut list: VecSMItem 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: OptionBTreeMapu8, 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: OptionBTreeMapu8, Value) {//调用实际执行禁止充电任务的charge_close_inner函数let _ charge_close_inner::spawn(param);
}结语
rust中的宏尤其是类函数宏很好用也很强大。如状态机如果不用宏写起来就比较麻烦当然这点麻烦并不足以抵消学习宏的高昂成本。
关键是改起来就要疯掉了增加一个状态、增加一个事件调整几个跃迁这在控制系统开发过程中是常态还是频繁发生、反反复复发生着的。
这时文本定义由于集中在一起不需要频繁的翻页、查找所以注意力高度集中而且也不需要分神去理解程序逻辑就是集中考虑状态机该如何动作就好了。相比用代码编程实现效率高关键bug也会少很多。
说一个最不起眼的好处rust要求静态变量全用大写关键看大写单词非常吃力啊写跃迁的时候一行全是大写单词光在脑子里翻译大写单词了:(
而用宏完全可以在定义的时候都用小写然后扩写成大写在思考状态机的定义的时候就轻松了很多。
当然触发的时候还是得用大写单词但事件触发是分布在各输入处理中的本来就需要大量的翻找和定位这个时候的大写反而比较显眼有助于在翻找分散精力后迅速集中注意力了。