rust嵌入式开发之RTICvsEmbassy
RTIC和Embassy是目前rust嵌入式开发中比较热门的两个框架。本来呢,针对RTIC的移植已经完成了一小半,但在移植过程中感受到了RTIC的不足,正好跳出来全面考察下embassy,本文就是根据目前的尝试结果做个对比总结。
RTIC和Embassy是两种完全不同的思路:
1、RTIC是基于MCU强大的中断体系以中断来驱动,所以RTIC的重心是放到了数据的隔离上,可参考rust嵌入式开发补充,整个框架,不管是处理还是规模都比较简单
2、Embassy则是基于rust语言的异步特性【async和await】、Send等来实现线程化并确保安全
也就是说,一个依托底层MCU,一个依托rust语言,所以分别形成了自己的特色。
RTIC的优缺点
RTIC的优点就是简便,一旦理解了RTIC的工作机制,开发起来只要牢记三个方面就好了:
- 中断驱动:所有的任务调用源头都是某个硬件中断,是天然的事件-响应模型,以此模型来编写各个处理任务,然后将这些任务分配到相应的硬件中断中spawn就好了,逻辑非常清晰、简洁
- 数据隔离:RTIC将数据分为了本地和共享两类,本地数据为某个任务独享,其它任务全都无法访问;共享数据为指定的某几个任务共享,使用时需加锁来互斥使用。确保了数据安全,而且数据的访问逻辑也非常清晰,不容易混乱,使用上也非常简单,不会出现复杂的Option<Arc<Mutex<…>>>的形式【RTIC自己做了,提供给用户任务时就是复制过来的数据指针了】
- 优先级:RTIC依靠MCU进行任务的调度,而MCU不管其它,只依靠中断许可和优先级进行调度。所以,优先级就是RTIC工作的基石,如果用户任务较多,就需要仔细考虑这些任务的调配和优先级的匹配问题
RTIC的缺点就是过于简单,对rust的特性支持不够,面对稍微复杂点的任务,就有些力不从心了。
首先,上面讲了RTIC良好的数据隔离机制,但反过来,由于用户任务中的数据,都来自RTIC从自己所管理的全局数据中的复制,所以RTIC的用户任务中,很难使用闭包,因为其所有的数据都是从栈上分配的,一旦离开所在用户任务就会被销毁,而RTIC中所有任务都是中断驱动。这两点决定了RTIC中的闭包无法异步使用,但不能脱离本地来异步使用的闭包,一点价值都没有的。
而如果自己用box来保存与复制,那何不干脆另起一个用户任务呢?!
但每个RTIC的用户任务的参数都是通过宏扩写出来的,所以用户任务很难通过指针的方式按需调度,这对状态机、控制台命令、远程控制等功能很不友好。
也就是说,RTIC最大的问题就是不灵活,一旦需要灵活的根据实际情况来动态调用用户任务,RTIC就会很笨拙。需要通过静态函数和统一的参数表,来为用户任务的调用提供参数转换和过渡。这对于状态机还好,但对于控制台命令和远程控制就很不友好了。
其次,RTIC是用过程宏来完成规模庞大的任务和数据的扩写工作的【对rust的过程宏还不太理解的可参考:rust嵌入式之用类函数宏简写状态机定义】,但由于其对过程宏的重度使用,使得我们编写自己的过程宏然后和RTIC进行衔接时,就不能触碰所有的数据和用户任务,只能通过上面提到的静态函数来进行过渡。
而这,自然就会大幅度的削弱过程宏的强大与便利了。
Embassy的优缺点
Embassy我才刚开始尝试,所以就只能对比上面RTIC的优缺点来说了。
1、Embassy既然是依托rust的async和await来调度任务,所以其必须实现自己的一个运行时来完成中断接管、poll登记与分发、基于优先级的任务调度等等
运行时的存在,自然会提供更好的灵活性和弹性,更有利于充分发挥rust作为现代语言的优势与强大。
2、Embassy提供了强大的线程间通信手段,如Channel和mbox,这就非常便于复杂业务场景下各功能部件之间的密切协作了
3、任务的创建手段更灵活、更便利,如:
方式一:独立任务模式:
#[embassy_executor::task]
async fn writer(mut tx: UartTx<'static, USART1, DMA1_CH4>) {
#串口1通过一个Channel接收要发送的字符串,然后以DMA方式发送
loop {
let buf = CHANNEL.receive().await;
unwrap!(tx.write(buf.as_bytes()).await);
}
}
在主程序中启动该任务:
unwrap!(spawner.spawn(writer(tx)));
如此,不管在哪,只要通过Channel发送字符串就可以执行:
CHANNEL.send(s).await;
方式二:闭包模式:
//主程序中定义一个闭包
let blinky = async {
loop {
//注意,这里直接使用了主程序创建的led和delay两个局部变量【rust会自动将其分配到堆上去】
//其它闭包也可以使用这两个变量【对于没有实现Copy的,需要考虑借用问题】
//当然,如果多个闭包同时使用到某个复杂的数据结构,需要实现Send或用Mutex等实现线程间数据保护
led.toggle();
Timer::after_millis(delay).await;
}
};
//将这个闭包启动
futures::join!(blinky, ...其它闭包...);
RTIC可以同样很简单的实现方式一,但根本无法实现方式二。而对于大量简单的小任务来说,显然方式二更简便,同时还省去了RTIC中复杂的数据隔离措施,更轻更快。
此外,Embassy对过程宏的使用不是太过严重,甚至还可以在Embassy项目中支持RTIC框架,结合其对闭包的友好支持,我们完全可以在用过程宏来定义状态机、控制台命令时直接用闭包来实现动作或命令的实现函数,既省去了过渡函数的繁琐,还更容易阅读与理解,降低了bug的几率。
当然,Embassy不提供如RTIC般的数据隔离与保护,所以线程间的数据保护就需要程序员自己来承担了。
总结
如开头所述,RTIC其实是一种中断驱动的嵌入式任务调度思想的rust实现【增强了数据隔离与保护】,而Embassy则是rust语言用于嵌入式环境的一种实现【所有其有两个发力点:运行时和基于自己HAL的各种芯片的适配】。
显然,RTIC更容易上手,更针对固定功能的嵌入式开发;而Embassy更有rust味道,更灵活、更强大,给予了程序员更丰富的支持,同时也对程序员提出了rust特有的高要求、高门槛。
当然,rust嵌入式开发还有tock,在Embassy之前,笔者也是更看好tock,但仔细研究了几天,笔者认为就嵌入式来说,tock属于过度设计了:
- 资源占用过于庞大,flash需256K起,ram需64K起,对于大多数的嵌入式应用来说,实无必要
- tock的一个诉求是彼此隔离所以安全的app,但对于嵌入式来说,实在有些过分了
所以,tock是针对一个复杂产品的中控来设计的。由于我们目前尚无此需求,所以我们显然不是tock预期中的目标群体。因此很快就转向Embassy了。