←返回到Kevin'首页发布:2021年3月7日我花了最后一年的建筑键盘,其中包括写入固件的品种定制电路板。
我最初在生锈中写了这个固件,但尽管几年的语言经验丰富了,但我仍然挣扎了很多。我最终得到了我的键盘工作,但它令人尴尬的时间很长一段时间,并不好玩。
经过更多锈蚀和计算经验丰富的朋友杰米布兰登的重复建议,我在Zig重写了固件,这些固件失去了游泳。
我发现这非常令人惊讶,因为我之前从未见过Zig,它是一个由一个主要的PDX时髦,基本上只有一个单一页面的文档。
事实上,这种经验很好,我现在感觉就像转向Zig(一种我用过十几个小时的语言)到生锈(我至少曾至少一千个小时)。
当然,这是对我和我的兴趣相比,这是一个关于这些语言的兴趣。所以我必须首先从系统编程语言中解释我想要的东西。
此外,为了解释为什么我挣扎的原因我将不得不展示很多复杂的代码,我显然不开心。这里的意图不是为了弥补生锈,而是建立我的(缺乏)信誉:这是如此你可以以合理的方式使用Rust的功能,或者如果我完全失去了情节,你可以判断自己。
最后,虽然它的风险落入了可怕的无聊“语言x比y”博客的追踪更好,但我觉得如果我明确比较生锈和锯齿,而不是写一个完全积极的“zig的伟大” “文章。(毕竟,我稳定地忽略了六个月的杰米涌出了Zig,因为“那是伟大的伙伴,但我已经知道生锈了,我只想让我的键盘完成,好吗?”)
我被教育为物理学家和学习的编程,所以我可以制作数据可视化。我的第一语言是PostScript和Ruby(动态,解释语言),我后来移动到JavaScript,所以我可以在Web上绘制克洛尼(使用Clojurescript在网上画画,我花了很多职业生涯。
2017年,我决定学习一个系统语言.Partly这是智慧的好奇心 - 我想更熟悉堆栈,堆,指针和静态类型,这些概念是一个留给我作为Web开发人员的糟糕。但主要是因为我希望系统所承诺的系统语言的能力:
写快速的代码;这可能利用计算机实际工作方式和运行快速运行的方式。
编写可以在最少环境中运行的应用程序,如微控制器或Web组件,在那里它只是不可行(在时间或空间中)携带垃圾收集器,语言运行时等。
我的兴趣不是(并且仍然没有)在操作系统,编程语言设计或安全(关于内存,正式的验证,建模为类型等)中。
我只是想在屏幕上和关闭屏幕上的leplle正方形。
基于其在开源社区的越来越受欢迎和大量的初学者到系统编程文档中,我依靠版本1.18拾取生锈。
从那时起,Rust毫无疑问地帮助我实现了我之后的那些能力:我能够将其编译为WASM的布局引擎,构建和销售快速桌面搜索应用程序(锈盛到电子中),并将RUDER编译为STM32G4微控制器要驾驶轨道SAW机器人(我甚至在寄存器定义中找到了一个错字;完整的“硬模式”嵌入式调试体验!)。
尽管如此,我仍然不觉得生锈感到舒适。它感觉是分歧复杂的 - 似乎每次我在新项目上使用铁锈,我遇到了一些问题,迫使我面对语言/生态系统的新角落。发展我的键盘固件也不例外:我遇到了两个问题,每个都需要学习一个完全新的语言功能。
这些问题并不是真正的嵌入式,但它们代表了过去三年我遇到过锈病的挑战。
如果您想要谷植物嵌入的详细信息或了解为什么我根本正在使用Newfangled语言编写自己的固件,请参阅在构建键盘上的笔记。
我用Rust遇到的第一个挑战是让我的固件从4-Butte Dev-kit PCB到一个无线拆分到单个ATREUS的左/右半部分时,运行硬件
在编译时改变固件的功能被称为“条件编译”。(它需要在编译时间而不是运行时完成,因为微控制器具有有限的程序空间,大约在我的情况下大约为10-100kb。)
RUST对此问题的解决方案是“功能”,它在Cargo.Toml中定义:
[依赖关系] cortex-m =" 0.6" nrf52840-hal = {version =" 0.11" ,可选= true,default-feature = false} nrf52833-hal = {version =" 0.11" ,可选= true,default-feature = false} arraydeque = {version =" 0.4" ,默认特征= false} reapless =" 0.5" [特点] Keytron = [" NRF52833"] Keytron-DK = [" NRF52833"] SplitaPple = [" NRF52840"] Splitapple-left = [" scletapple"] splitapple-ricons = [" scletapple"]#在这里指定默认值,以便Rust-Analyzer可以构建项目;在建立使用时 - 不默认 - 功能要关闭此默认= [" keytron"] nrf52840 = [" nrf52840-hal"] nrf52833 = [" nrf52833-hal& #34;]
例如,为特定键盘硬件设计启用了Keytron功能。硬件取决于NRF52833功能(代表一种微控制器),这取决于NRF52833-HAL箱(将微控制器的外围存储器地址映射到的实际代码) RUDE类型)。
然后,我的生锈代码可以使用属性注释来有条件启用stuff.e.g。,命名空间可以导入特定于微控制器的克拉特:
fn read_keys() - >数据包{让设备=不安全{hw :: filipherals :: lest()}; #[cfg(任何(特征=" keytron"特征=" Keytron-dk")]让你= {设=设备。 P0。在_ 。读 ()。比特();让P1 =设备。 P1。在_ 。读 ()。比特(); //逆变,因为键是活动低GPIO :: P0 :: Pack(!P0)| GPIO :: P1 :: Pack(!P1)}; #[CFG(功能=" scletapple")]让你= gpio :: splitapple :: read_keys();数据包(U)}
可选= true必须添加到Cargo.Toml中的设备箱中(即使源已经有条件要求它们!)
如何在构建静态二进制时启用功能(Cargo Build - 释放 - 不默认 - 功能 - Features" Keytron")
在某些时候,我放弃了尝试将设备外设传递为函数参数,因为我无法弄清楚如何添加条件属性到类型 - “显而易见”的东西不起作用: 有一个整洁的嵌入式框架,RTIC,其主要入口点是一个应用程序注释,它将设备箱为uh,uh,参数: 考虑扫描键盘矩阵:如果我们没有足够的微控制器引脚将每个键盘切换直接连接到引脚,我们可以将带有二极管(单向阀)的开关将开关放入矩阵: 然后,我们将单列设置为高,然后读出行以查找该列的开关的状态。 在此示例中,如果我们设置引脚1.10高(COL0),然后读取0.13(RON1),我们知道按下开关K8。 为单个引脚执行此操作,例如外围端口P0的PIN 10,很简单: 但我的专栏引脚遍布两个端口,所以我想写的是什么:
for(端口,PIN)IN& [(p0,10),(p1,7),..] {端口。 PIN_CNF [PIN]。写(| w | {w.input()。disconnect(); w。dir()。输出(); w}); }
不会飞行,因为现在元组有不同的类型 - (P0,USIZE)和(P1,USIZIZE) - 因此它们不能在同一收集中挂起。
键入pinidx = u8;类型端口= U8; const col_pins:[(port,pinidx); 7] = [(1,10),(1,13),(1,15),(0,2),(0,29),(1,0),(0,17)]; pub fn init_gpio(){for(port,pin_idx)IN& COL_PINS {匹配端口{0 => { 设备 。 P0。 PIN_CNF [* PIN_IDX为USIZE]。写(| w | {w.input()。disconnect(); w。dir()。输出(); w}); 1 => { 设备 。 P1。 PIN_CNF [* PIN_IDX为USIZE]。写(| w | {w.input()。disconnect(); w。dir()。输出(); w}); } _ => {}}}}
但是等等,我听到你问,宏怎么样?哦,是的,我的朋友,我在实际扫描程序中剃了宏牦牛:
PUB FN READ_KEYS() - > U64 {让设备=不安全{Crate :: HW ::外围设备::窃取()};让mut键:u64 = 0;宏_rules! scan_col {($ col_idx:tt; $($ low_idx:tt => $ key:tt,)*)=> {让(端口,pin_idx)= col_pins [$ col_idx]; ///////////////// set col high不安全{match port {0 => { 设备 。 P0。一开始。写(| W | W.位(1<< pin_idx)); 1 => { 设备 。 P1。一开始。写(| W | W.位(1<< pin_idx)); } _ => {}}} cortex_m :: ASM :: Delay(1000); //读取行并进入打包键U64。 //键是1索引。让Val =设备。 P0。在_ 。读 ()。比特(); $(键| =((((Val>> row_pins [$ row_idx])& 1)作为u64)<<($ key-1));)* /////////////////// ////// //设置col低不安全{match port {0 => { 设备 。 P0。 outclr。写(| W | W.位(1<< pin_idx)); 1 => { 设备 。 P1。 outclr。写(| W | W.位(1<< pin_idx)); } _ => {}}}; }; // col_idx; row_idx =>键ID#[CFG(功能=" scletapple-left")] {scan_col! (0; 0 => 1,1 => 8,2 => 15,3 => 21,4 => 27,5 => 33,); scan_col! (1; 0 => 2,1 => 9,2 => 16,3 => 22,4 => 28,5 => 34,); scan_col! (2; 0 => 3,1 => 10,2 => 17,3 => 23,4 => 29,5 => 35,); scan_col! (3; 0 => 4,1 => 11,2 => 18,3 => 24,4 => 30,5 => 36,); scan_col! (4; 0 => 5,1 => 12,2 => 19,3 => 25,4 => 31,5 => 37,); scan_col! (5; 0 => 6,1 => 13,2 => 20,3 => 26,4 => 32,5 => 38,); scan_col! (6; 0 => 7,1 => 14,); }#[CFG(特征=" scletapple-overs")] {scan_col! (0; 0 => 1,1 => 8,2 => 15,3 => 23,4 => 30,5 => 37,); scan_col! (1; 0 => 2,1 => 9,2 => 16,3 => 24,4 => 31,5 => 38,); scan_col! (2; 0 => 3,1 => 10,2 => 17,3 => 25,4 => 32,5 => 39,); scan_col! (3; 0 => 4,1 => 11,2 => 18,3 => 26,4 => 33,5 => 40,); scan_col! (4; 0 => 5,1 => 12,2 => 19,3 => 27,4 => 34,5 => 41,); scan_col! (5; 0 => 6,1 => 13,2 => 20,3 => 28,4 => 35,5 => 42,); scan_col! (6; 0 => 7,1 => 14,2 => 21,3 => 29,4 => 36,5 => 22,); }键}
基本上,每个Scan_Col!宏调用扩展到设置列PIN高的代码中,读出行,并将其状态推向可变键的适当位:U64变量在函数顶部。
如果您想更详细地理解,请抓住您最喜爱的饮料,并使用Rust Book的宏观部分或Rust的宏参考文档花费一些优质的时间。
我对我来的PIN初始化或矩阵扫描代码不满意,但我是最清晰的我能够写入。从Google结果的第一页的“RUST键盘固件”,它看起来像其他Rustacean解决了这个问题:
迭代大量使用和匹配破坏性元组;我喜欢这种宏观方法(我将它拿到我的豪华触摸板),尽管用行/列坐标识别开关意味着每个行/列具有相同数量的交换机,但情况并非总是如此。
依赖于(他们的单词)宏以在T元组结构上实现特征对象的迭代器;我不确定这里发生了什么。
真正的星际的理解水平;我真的不确定在这里发生了什么。
虽然在所有这些解决方案中肯定有很多语言复杂性,但Rust值得比传统方法更卑鄙的荣誉。不失C的臭名昭着的文本预处理器宏(#define,#ifdef等),例如Rust的宏不会导致扩展的莫名语法错误。(所有扩展代码都是选中的类型!)
Rust的工具太好了 - Rust Analyzer是足够的,足以了解在跳过围绕代码时的特征注释,我从未弄清楚C.
鉴于铁锈贡献者的聪明是多么聪明 - 看看他们在公共RFC进程中所做的所有体内讨论和权衡的权衡 - 我很想得出结论,嗯,所有这些复杂性必须是固有的.Perhaps这只是难以做到的努力时间配置和以安全的编译语言有效地迭代独特的类型?
也许,但Zig为令人信服的案例进行了令人信服的案例 - 至少对于我的大流行业余机项目键盘固件 - 我可以通过较少的概念获得。
以下是我如何使用Zig通过不同类型解决条件汇编和迭代的这两个问题。(参见Jamie的帖子,以获得Rust和Zig的更全面的比较。)
完全披露:这是我用Zig写的第一个代码,因此可能存在更加惯用的或整洁的解决方案。
常见的ztron.zig文件然后通过@import(" root")(“root”是编译器entrintpoint导入那些公共常量,所以这是一个循环参考;它很好!)直接使用它们:
没有特别的“功能”语义来学习,货物。要重新排列,或者标志传递给编译器。货物甚至不存在!
要指定要编译的代码,请呃,只需告诉编译器:要编译devkit硬件,请运行zig build-obj dk.zig;对于atreus,zig build-obj atreus.zig。
这是作用,因为Zig仅根据需要评估代码。(不仅仅是导入的文件 - 编译器不介意半写入,不良键入的函数,只要它们未被调用。)
至于键盘矩阵引脚设置?嗯,外围设备仍然是不同的类型,但这是......罚款:
const行=。{。{。端口= P1,。 PIN = 0},。{。端口= P1,。 PIN = 1},。{。端口= P1,。 PIN = 2},。{。端口= P1,。 PIN = 4},}; const cols =。{。{。端口= P0,。 PIN = 13},。{。端口= P1,。 PIN = 15},。{。端口= P0,。 PIN = 17}。{。端口= P0,。 PIN = 20},。{。端口= P0,。 PIN = 22},。{。端口= P0,。 PIN = 24},。{。端口= P0,。 PIN = 9},。{。端口= P0,。 PIN = 10},。{。端口= P0,。 PIN = 4}。{。端口= P0,。 PIN = 26},。{。端口= P0,。 PIN = 2},}; PUB FN initkeyboardgpio()void {inline for(行)| x | { X 。港口 。 pin_cnf [x。别针 ]。修改(。{。dir =。输入,。输入=。连接,。拉扯=。下拉,}); }内联(cols)| x | { X 。港口 。 pin_cnf [x。别针 ]。修改(。{。dir =。输出,。输入=。disconnect,}); }}
这并不是在这里关心生成的机器指令 - 循环实际上是展开的 - 而是语言让我表达我在异构地键入的集合中“循环”的愿望。
const col2row2key =。{。{0,1},。{1,11},。{2,21},。{3,32}},。{。{0,2},。{1,12 },。{2,22}。{3,33}},。{。{0,3},。{1,13},。{2,23},。{3,34}},{ 。{0,4},。{1,14},。{2,24},。{3,35}},。{。{0,5},。{1,15},。{2,25 },。{3,36}},。{2,26},。{3,37}},。{。{0,6},。{1,16},。{2,27}, 。{3,38}},。{。{0,7},。{1,17},。{2,28},。{3,39}},。{。{0,8},。{ 1,18},。{2,29},。{3,40}},。{0,9},。{1,19},。{2,30},。{3,41}} ,。{。{0,10},。{1,20},。{2,31},。{3, 42}},}; PUB FN Readkeys()PackedKeys {var pk = packedkeys。新的 (); inline for(col2row2key)| row2key,col | {//设置col high cols [col]。港口 。一开始。 write_raw(1<< cols [col]。pin);延迟(1000); const val =行[0]。港口 。在 。 read_raw(); inline for(row2key)| row_idx_and_key | {const row_pin =行[row_idx_and_key [0]]。别针 ; PK。键[(row_idx_and_key [1] - 1)] =(1 ==((val> row_pin)& 1)); } //设置Col Low Cols [Col]。港口 。 outclr。 write_raw(1<< cols [col]。pin);返回pk; }
概念上的Zig内联求解Rust的语法宏解决的同一问题(在编译时生成特定于类型的代码),但没有学习LIL'模式匹配/扩展语言的侧面追求。
实际上,由于行/列/交换机布局存在于CONST结构中,因此可以使用它来计算.G,以计算(在编译时)键盘上的开关数:
pub const switch_count = comptime {var n = 0; for(col2row2key)| x | n + = x。 len;返回n; };
我不知道如何从生锈语法宏调用中完成这一点: scan_col! (0; 0 => 1,1 => 8,2 => 15,3 => 21,4 => 27,5 => 33,); (虽然我确定它是可能的 - 专家发现,锈宏可以达到500左右,也许有一天,达到更大的数字。) 使用Zig只是几个小时突出了我的生锈方面,我从未考虑过。特别的是,我对域的无意识的复杂性大大 - “这是系统编程就像” - 是的 事实上,刻意的防锈设计决策的结果。 例如,它现在非常清楚,生锈是一种含别人 ......