我们用rust+Embassy开发的新版产品已经投产了一个多月了,经历过近距离的强干扰、连绵的阴雨天失电等考验,初步证明了整个产品体系的稳定性。
经历过开发、测试以及这段时间的运行后,我也发现了Embassy的一些问题,之前的几篇文章都在说它的好处,所以集中说一下它的问题,以帮助大家能正确而全面的进行评估。
供应链难以维持
先概括一下我所认为的Embassy发力点:较为全面的对芯片的支持+async/await的支持。
Embassy支持了几个芯片系列,其它我没用所以无法评价,所以我只针对我使用的STM32来说。这里的较为全面,就STM32系列来说,应该说是非常保守了。
我们因为串口用的很多,所以选的是10串口的STM32F413VG,因为要用到DMA,所以只能用到7个串口,Embassy完全符合手册的说明,所以只要根据CubeMX配置结果来选串口以及对应的DMA通道就可以了,没有任何的问题。
也就是说,Embassy对STM32芯片的支持是全方面的,同时还提供了自己的HAL,所有芯片的操作接口是相同的,这是非常有意义的。
但其第一个问题也就来了:不支持GD32等对应STM32的国产芯片。这在国产替代的当下,显然是一个非常突出的严重问题。
我们在确定rust+Embassy+STM32这条技术路线的时候,当然对供应链做过调查,确认是足以满足我们的供应的,虽然F413目前全国库存只有100多片!!但我们的用量并不是很大,买上一批就够我们用上一段时间的了。
但是,架不住我们其它辅助器件的芯片断供啊!
我们现在不得不考虑全产品都要全面使用国产芯片的技术路线的巨大调整了:(
所以,Embassy的第一个问题就是如何规避供应问题,此外还要面对越来越高的国产替代要求。
函数动态调用消耗巨大
在c中动态调用一个函数非常简单:预先保存好对应的函数指针,需要时直接调用就好了。
rust当然和c一样,可以使用函数指针,但这个函数指针只能调用同步函数,无法执行异步操作。
这就比较麻烦了,我们的状态机、命令行、远程控制,甚至包括debug功能,都需要异步操作,如最简单的将信息打印到控制台,就是异步的,其它如等待、定时、IO操作等等,更都必须是异步的。
我最终的解决办法就是在初始化这些模块时,提供一个spawner,各模块将这个spawner保存起来,然后在回调的同步函数指针中用这个spawner来spaw一个异步任务。
这样的迂回自然带来了spawner保存的开销、异步任务调用的繁琐【需要4个函数的逐步调用才能真正启动c中简单的一个函数指针的执行】,同时还带来了代码书写的繁琐!
这些开销自然还可以忍受,但我在测试的时候,发现了一个巨大的bug:这些命令行、远程控制最多只能执行三四个,再多就会崩溃了!!
经过排查,发现这是由于Embassy启动异步任务的锅,Embassy自动生成的异步任务的调用代码是:
fn system_reboot_inner(
mi: u32,
purpose: DualPurpose,
params: Option<BTreeMap<String, Value>>,
) -> ::embassy_executor::SpawnToken<impl Sized> {
const POOL_SIZE: usize = 1;
static POOL: ::embassy_executor::_export::TaskPoolRef = ::embassy_executor::_export::TaskPoolRef::new();
unsafe {
POOL.get::<_, POOL_SIZE>()
._spawn_async_fn(move || __system_reboot_inner_task(mi, purpose, params))
}
}
即一个任务分配一个任务池,静态的任务池,而spaw的任务是Sized的。
由于时间太紧,我没有进一步的研究Embassy的异步任务机制,但根据这些代码和我所遇到的bug,我有理由相信:Embassy是将所有的异步任务的代码从flash加载到ram中静态保存,任务结束也并不释放!直接导致三四个动态任务执行后内存被消耗一空。
这个bug直接导致我们对于远程控制进行了剪裁,大量的功能从远程控制中取消,同时状态机也暂停使用,虽然在第一个版本中,这是可以接受的,但这无疑导致我在上篇文章中所说的我们选择rust嵌入式的理由大打折扣!
不支持park、block_on等std下的异步功能
Embassy实现了自己的executor,但实现的又不够完整!这主要就表现在park和block_on上。
不支持park,任务就无法一分为二,只能一根筋的自己跑完,简单的说就是无法自己启动一个新任务来执行某些操作。但有些时候,并不适合自己执行,而应该是启动一个子任务放到后台来做【类似python、go中的协程】。
那有些兄弟就会说,可以启动async啊!对,可是正如我上面所说,有时我们是在一个同步函数里,是没办法执行async的。
这时就需要block_on来帮我们在同步函数里执行async函数或闭包。
但是,Embassy的block_on是这个样子的:
pub fn block_on<F: Future>(mut fut: F) -> F::Output {
// safety: we don't move the future after this line.
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
let raw_waker = RawWaker::new(ptr::null(), &VTABLE);
let waker = unsafe { Waker::from_raw(raw_waker) };
let mut cx = Context::from_waker(&waker);
loop {
if let Poll::Ready(res) = fut.as_mut().poll(&mut cx) {
return res;
}
}
}
直接一个死循环反复的调future的poll!cpu直接干满100%,只能等中断才会有反馈,看门狗的喂狗都完蛋了!
结语
概要之,Embassy部分实现了rust的异步机制,提供了async/await,是一个巨大的进步,最典型的就是我之前所讲解的利用await来实现应用级的临界区,这对降低rust在嵌入式编程方面的门槛起到了巨大的作用。
但由于Embassy对rust异步机制支持的不完备,又严重制约了我们灵活的提供一些强大的功能:(
即,Embassy对无需动态执行任务的应用场景是非常良好的,但如果需要动态执行任务,那其显然是无法满足的。