- 主题:小新不懂就问:自己设计的开发板固件怎么搞定的?
我又忍不住又要来安利rust了...
我最近通过一个项目已经基本完全迁移到rust上开发了。
目前在手撸一个运控。
我是感觉到rust的抽象能力实在是太爽了,比如我是先定义mm,um,丝,inch等公制,英制单位,(注意丝这个单位英文里没有对应的单词,于是我直接用了中文...)
比如驱动频率有hz,khz,mhz。旋转运动有rpm,feedrate有mm/min,ipm等单位...
写死之后基本上不可能出错。因为整个系统不允许任何出现无量纲的数字...
比如设定电机转速得写成set_rotary_speed(3000.rpm()),不能直接写个3000进去。
比如pwm频率得写成10.khz(),不能直接写10000。
然后除了量纲的区分外,每个值的适用领域也有区分。
比如move_to如果传的是Position(100.mm())表示绝对定位100mm,如果没有做过homing就会报错。
但这没有homing的情况下可以传Displacement(100.mm()),表示从当前位置开始算的相对位移。
最后,不仅传入的数字不允许出现无量纲,这些数值之间内部是否可以计算的关系,计算的结果类型也都准确定义了,比如Position-Position得到Distance等等,这样不管是外部给别人用,还是内部写逻辑,还是debug dump内部状态,都很清晰并且不太可能写错。
最后回到rtos的问题。rust有个框架叫RTIC,用async/await的形式直接就提供了异步支持,直接拿来用就行了:
#[task(
priority = 2,
shared = [led],
local = [tog: bool = true],
)]
async fn heartbeat(mut c: heartbeat::Context) {
loop {
info!("heartbeat, tog = {}", *c.local.tog);
if *c.local.tog {
c.shared.led.lock(|l| l.set_high().unwrap());
} else {
c.shared.led.lock(|l| l.set_low().unwrap());
}
*c.local.tog = !*c.local.tog;
rtic_monotonics::systick::Systick::delay(500.millis()).await;
}
}
#[task(
priority = 1,
shared = [timer, cap_displacement_sensor],
)]
async fn displacement_update(mut c: displacement_update::Context) {
loop {
let now = c.shared.timer.lock(|timer| timer.get_counter());
info!("now = {}", now.ticks());
c.shared.cap_displacement_sensor.lock(|cds| {
info!("{:?}", cds.read_spi_and_feed(now));
});
// Period to read the displacement.
rtic_monotonics::systick::Systick::delay(10.millis()).await;
}
}
总的来说,这个就叫降维打击,实在是太爽了。rust会逼着你去思考,逼着你只能规划好资源的分配逻辑,逼着你只能写出正确的代码。
比如上面的代码有很多unwrap,看着很丑。这种丑就是rust在逼你把代码写成带错误处理的模式。
因为我目前故障处理这块还没做完,所以先unwrap跑起来再说。但等把所有的故障都规划好设计好,并把代码都改成带故障处理的模式之后,这些unwrap就不会再存在了。
最主要的是,我前几年尝试切rust,每次都被它陡峭的学习曲线打击了,导致半途而废。但现在有chatgpt,有啥不懂得直接请教ai就行了。学rust难度降低了一个数量级,现在转真心不是什么问题了。
【 在 dismoon 的大作中提到: 】
: 我都底层C重写了一遍类RTOS,写固件是个问题?
:
--
修改:lvsoft FROM 121.225.189.*
FROM 121.225.189.*
没深刻体会过rust开发的好处,是无法理解我前面说的“降维打击”这四个字的厚重的。
所谓的后期调试阶段之类的优劣,只有在同层次的情况下才好谈。而rust压根就不需要什么后期调试阶段。
举个例子,比如我这块的代码:
let spi_mosi = pins.gpio15.into_push_pull_output();
let spi_miso = pins.gpio12.into_floating_input();
let spi_sclk = pins.gpio14.into_floating_input();
let spi = bsp::hal::spi::Spi::<_, _, _, 8>::new(pac.SPI1, (spi_mosi, spi_miso, spi_sclk));
let mut spi = spi.init_slave(&mut pac.RESETS, embedded_hal::spi::MODE_3);
注意看最后一行,一开始我也不知道怎么用spi,抄的example里面例子。example里用的是spi.init,这个是master模式。而我需要的slave模式要用init_slave初始化,但这一切我当时都不知道。
放c环境里原样照抄的代码肯定是能跑的。然后你就会发现不work,然后你就得拿示波器测来测去。运气不好没第一时间怀疑到这里,可能还要搭个最小系统,好多天就搭进去了。这就是所谓的“后期调试阶段”。
但在rust里,这里原样照抄来的spi.init是编译不过的。因为rust知道spi.init是master模式,而master模式会把sck配置成输出,而我在前面的声明中定义了spi sck是输入。编译器直接就会把这个矛盾指出来。这就是所谓的最好的解决问题的办法,是让这个问题直接不存在。这就是所谓的你会被逼着只能写出尽量正确的代码。
而且,rust本身就是个很完美的hal抽象层。事实上我现在的代码就可以同时跑在rp2040和esp32上。
包括esp32的Xtensa core和risc-v core。
并且,因为我还要被结构,机械设计等各方面的问题分心。所以firmware这块是我朋友和我一起写的。他是cs出生,连spi是个啥都不知道,纯嵌入式新手。而且他在地球另一头,跟我有时空阻隔,连个硬件开发环境都没有,根本就不可能联调。但他写的代码就是拿过来直接跑,直接就ok的。后期调试?项目交接?这些都不存在。
至于招聘难度,这个肯定会比招c要难。但就如我前面说的,在ai的辅助下一个老手熟练掌握rust,也就是一个月的时间而已。而rust+ai释放的生产力足以顶一个团队。找到一两个高手让他转rust就行,这一点并不难。
【 在 conepoint 的大作中提到: 】
: 嵌入式是纯C的天下,因为提升的那点开发效率相对后期调试阶段代码可读性要求与项目交接人员招聘难度,可能得不偿失。
:
--
FROM 121.225.189.*
看reference manual,自己从操作寄存器开始封装,从来不用别人的代码,也是一种风格。
这个可以确保你不会犯我这种错误。
那么当我掏出zero cost hal,能各个硬件平台到处迁移,包括这个具备高时间精度的async/await异步框架直接拿来就用,你又如何应对呢?
工程问题几个要素都要兼顾的,不用别人的代码什么都自己干,这也是我很喜欢的风格。
事实上我最近这个项目连钣金都没让别人干,为了控制装配精度,钣金相关的问题我都是自己解决的。
但什么都自己搞,这是一种兜底能力,不能一直用的。就像打仗你出奇兵制胜,但你不能只靠奇兵打仗一样。
否则效率就是问题了,而rust的优势就是能让你不用仔细透彻的理解别人的东西,同时用起来还不容易出错,这是更先进的工具所带来的更高的生产力。
这就好比我们都为了某个高标准的目标,选择不用货架成熟产品而是自己手搓一个。
但你拿锉刀手搓,我开CNC手搓,这区别还是很大的...
【 在 dismoon 的大作中提到: 】
: 原来版上包括吕大都是不看手册直接开干的么?
: 我几乎是理解了一块芯片的内核,外设才会做项目,从来不用别人的example代码
:
--
修改:lvsoft FROM 121.225.189.*
FROM 121.225.189.*
不是...我没有花半天时间啊...
编译器都指出这个问题了,我跟进去一看spi.init的内部细节知道它初始化成master模式了,然后我一看init_slave就在旁边,那当然一下子就改好了。我一开始会这么写是因为stm32的hal下面spi就只有一个init入口,master还是slave是作为参数传进去的。我只是不熟悉rust版的rp2040_hal的spi的用法,并不是不知道spi的主从关系。我又不是我朋友...他不懂spi我怎么可能不懂嘛...
这里举这个例子是让大家体会到rust对严格约束追求到什么程度,以及这么做能带来什么潜在的好处。spi这里只是举一个容易理解的例子。但凡干工程的都能理解,很多时候修bug可能只要改一个字符,但找到这个字符在哪里才是最难的事情。而rust可以从源头就尽量规避掉这种问题。
【 在 dismoon 的大作中提到: 】
: 这个就看各人看资料的能力了,
: 比如你前面几贴举得例子,因为不用rust,又不看资料,拉了个SPI库函数过来用,结果没有搞明白主从,浪费了半天调试时间
: 而我,花了半天看完资料,代码写完没有发生不工作的情况,没有浪费半天,但是我看资料用了半天,时间一样,但是我搞懂了很多东西,时间差不多,而我变强了
: ...................
--
FROM 121.225.189.*
cs也不全是追求极致的抽象。有些学院派确实在抽象这方面搞的有点走火入魔。这种我也一样该反对就反对。
硬件领域追求成本,追求极致的挖掘性能,这些我也是同意的。
我在这些问题上比较实用主义。我的观点是追求任何方向的极致都是可以,但不能走火入魔。
不过追求极致和走火入魔往往也就在一线之间。就说rust开发者内部,也是存在两派的观点在斗争呢。
所以说具体的各种优劣细节我不是那么的关心,这一点自己冷暖自知,自己觉得舒服就行。
这个问题,更重要的是横向扩展和纵向扩展的差别。
除了极少数天才,大部分人都是普通人,大家天赋差不多,用时间来换技能点的效率也差不多。技能树横着点还是竖着点是个人的不同风格,反正技能点点下去都会变强,本也无所谓差别。
但纵向扩展很怕踩错坑,然后把技能点点在了会随时间贬值的技能上,并且因为巨大的沉没成本还不容易跳坑。
所以我是很不喜欢去做精通诸如某个器件的具体细节,把手册通读一遍之类的事情。这个就是练屠龙技,然后出山之后很容易找不到龙...尤其是现在还有ai辅助,量产型屠龙刀人手一把的时代...
我当年刚学硬件的时候就听说过一位大牛精通MSP430。他2000年前后就玩了10年msp430,寄存器就跟刻在dna上的一样,不看手册手撸汇编信手拈来。当年是很牛,然而现在这还有啥意义嘛。
所以要把技能点在拥有更多可能性,有更长期回报的事情上。而rust就是其中的一种。
【 在 dismoon 的大作中提到: 】
: 其实这也是一种学CS和学EE的区别
: 学CS的,追求泛用性,觉得能各平台迁徙而且没有extra amount of work是优雅
: 学EE的,追求把硬件的性能压榨干是优雅,从来不考虑代码的泛用,最多就是考虑的能不能维护
: ...................
--
修改:lvsoft FROM 121.225.189.*
FROM 121.225.189.*