您好,登錄后才能下訂單哦!
今天給大家介紹一下硬件抽象編程該如何選擇C語言及Rust。文章的內(nèi)容小編覺得不錯,現(xiàn)在給大家分享一下,覺得有需要的朋友可以了解一下,希望對大家有所幫助,下面跟著小編的思路一起來閱讀吧。
在 Rust 中使用類型級編程可以使硬件抽象更加安全。
Rust 是一種日益流行的編程語言,被視為硬件接口的最佳選擇。通常會將其與 C 的抽象級別相比較。本文介紹了 Rust 如何通過多種方式處理按位運算,并提供了既安全又易于使用的解決方案。
語言 | 誕生于 | 官方描述 | 總覽 |
---|---|---|---|
C | 1972 年 | C 是一種通用編程語言,具有表達(dá)式簡約、現(xiàn)代的控制流和數(shù)據(jù)結(jié)構(gòu),以及豐富的運算符集等特點。(來源:CS 基礎(chǔ)知識) | C 是(一種)命令式語言,旨在以相對簡單的方式進(jìn)行編譯,從而提供對內(nèi)存的低級訪問。(來源:W3schools.in) |
Rust | 2010 年 | 一種賦予所有人構(gòu)建可靠、高效的軟件的能力的語言(來源:Rust 網(wǎng)站) | Rust 是一種專注于安全性(尤其是安全并發(fā)性)的多范式系統(tǒng)編程語言。(來源:維基百科) |
在系統(tǒng)編程領(lǐng)域,你可能經(jīng)常需要編寫硬件驅(qū)動程序或直接與內(nèi)存映射設(shè)備進(jìn)行交互,而這些交互幾乎總是通過硬件提供的內(nèi)存映射寄存器來完成的。通常,你通過對某些固定寬度的數(shù)字類型進(jìn)行按位運算來與這些寄存器進(jìn)行交互。
例如,假設(shè)一個 8 位寄存器具有三個字段:
+----------+------+-----------+---------+| (unused) | Kind | Interrupt | Enabled |+----------+------+-----------+---------+ 5-7 2-4 1 0
字段名稱下方的數(shù)字規(guī)定了該字段在寄存器中使用的位。要啟用該寄存器,你將寫入值 1
(以二進(jìn)制表示為 0000_0001
)來設(shè)置 Enabled
字段的位。但是,通常情況下,你也不想干擾寄存器中的現(xiàn)有配置。假設(shè)你要在設(shè)備上啟用中斷功能,但也要確保設(shè)備保持啟用狀態(tài)。為此,必須將 Interrupt
字段的值與 Enabled
字段的值結(jié)合起來。你可以通過按位操作來做到這一點:
1 | (1 << 1)
通過將 1 和 2(1
左移一位得到)進(jìn)行“或”(|
)運算得到二進(jìn)制值 0000_0011
。你可以將其寫入寄存器,使其保持啟用狀態(tài),但也啟用中斷功能。
你的頭腦中要記住很多事情,特別是當(dāng)你要在一個完整的系統(tǒng)上和可能有數(shù)百個之多的寄存器打交道時。在實踐上,你可以使用助記符來執(zhí)行此操作,助記符可跟蹤字段在寄存器中的位置以及字段的寬度(即它的上邊界是什么)
下面是這些助記符之一的示例。它們是 C 語言的宏,用右側(cè)的代碼替換它們的出現(xiàn)的地方。這是上面列出的寄存器的簡寫。&
的左側(cè)是該字段的起始位置,而右側(cè)則限制該字段所占的位:
#define REG_ENABLED_FIELD(x) (x << 0) & 1#define REG_INTERRUPT_FIELD(x) (x << 1) & 2#define REG_KIND_FIELD(x) (x << 2) & (7 << 2)
然后,你可以使用這些來抽象化寄存器值的操作,如下所示:
void set_reg_val(reg* u8, val u8); fn enable_reg_with_interrupt(reg* u8) { set_reg_val(reg, REG_ENABLED_FIELD(1) | REG_INTERRUPT_FIELD(1));}
這就是現(xiàn)在的做法。實際上,這就是大多數(shù)驅(qū)動程序在 Linux 內(nèi)核中的使用方式。
有沒有更好的辦法?如果能夠基于對現(xiàn)代編程語言研究得出新的類型系統(tǒng),就可能能夠獲得安全性和可表達(dá)性的好處。也就是說,如何使用更豐富、更具表現(xiàn)力的類型系統(tǒng)來使此過程更安全、更持久?
繼續(xù)用上面的寄存器作為例子:
+----------+------+-----------+---------+| (unused) | Kind | Interrupt | Enabled |+----------+------+-----------+---------+ 5-7 2-4 1 0
你想如何用 Rust 類型來表示它呢?
你將以類似的方式開始,為每個字段的偏移定義常量(即,距最低有效位有多遠(yuǎn))及其掩碼。掩碼是一個值,其二進(jìn)制表示形式可用于更新或讀取寄存器內(nèi)部的字段:
const ENABLED_MASK: u8 = 1;const ENABLED_OFFSET: u8 = 0; const INTERRUPT_MASK: u8 = 2;const INTERRUPT_OFFSET: u8 = 1; const KIND_MASK: u8 = 7 << 2;const KIND_OFFSET: u8 = 2;
接下來,你將聲明一個 Field
類型并進(jìn)行操作,將給定值轉(zhuǎn)換為與其位置相關(guān)的值,以供在寄存器內(nèi)使用:
struct Field { value: u8,} impl Field { fn new(mask: u8, offset: u8, val: u8) -> Self { Field { value: (val << offset) & mask, } }}
最后,你將使用一個 Register
類型,該類型會封裝一個與你的寄存器寬度匹配的數(shù)字類型。 Register
具有 update
函數(shù),可使用給定字段來更新寄存器:
struct Register(u8); impl Register { fn update(&mut self, val: Field) { self.0 = self.0 | field.value; }} fn enable_register(&mut reg) { reg.update(Field::new(ENABLED_MASK, ENABLED_OFFSET, 1));}
使用 Rust,你可以使用數(shù)據(jù)結(jié)構(gòu)來表示字段,將它們與特定的寄存器聯(lián)系起來,并在與硬件交互時提供簡潔明了的工效。這個例子使用了 Rust 提供的最基本的功能。無論如何,添加的結(jié)構(gòu)都會減輕上述 C 示例中的某些晦澀的地方?,F(xiàn)在,字段是個帶有名字的事物,而不是從模糊的按位運算符派生而來的數(shù)字,并且寄存器是具有狀態(tài)的類型 —— 這在硬件上多了一層抽象。
用 Rust 重寫的第一個版本很好,但是并不理想。你必須記住要帶上掩碼和偏移量,并且要手工進(jìn)行臨時計算,這容易出錯。人類不擅長精確且重復(fù)的任務(wù) —— 我們往往會感到疲勞或失去專注力,這會導(dǎo)致錯誤。一次一個寄存器地手動記錄掩碼和偏移量幾乎可以肯定會以糟糕的結(jié)局而告終。這是最好留給機(jī)器的任務(wù)。
其次,從結(jié)構(gòu)上進(jìn)行思考:如果有一種方法可以讓字段的類型攜帶掩碼和偏移信息呢?如果可以在編譯時就發(fā)現(xiàn)硬件寄存器的訪問和交互的實現(xiàn)代碼中存在錯誤,而不是在運行時才發(fā)現(xiàn),該怎么辦?也許你可以依靠一種在編譯時解決問題的常用策略,例如類型。
你可以使用 typenum 來修改前面的示例,該庫在類型級別提供數(shù)字和算術(shù)。在這里,你將使用掩碼和偏移量對 Field
類型進(jìn)行參數(shù)化,使其可用于任何 Field
實例,而無需將其包括在調(diào)用處:
#[macro_use]extern crate typenum; use core::marker::PhantomData; use typenum::*; // Now we'll add Mask and Offset to Field's typestruct Field<Mask: Unsigned, Offset: Unsigned> { value: u8, _mask: PhantomData<Mask>, _offset: PhantomData<Offset>,} // We can use type aliases to give meaningful names to// our fields (and not have to remember their offsets and masks).type RegEnabled = Field<U1, U0>;type RegInterrupt = Field<U2, U1>;type RegKind = Field<op!(U7 << U2), U2>;
現(xiàn)在,當(dāng)重新訪問 Field
的構(gòu)造函數(shù)時,你可以忽略掩碼和偏移量參數(shù),因為類型中包含該信息:
impl<Mask: Unsigned, Offset: Unsigned> Field<Mask, Offset> { fn new(val: u8) -> Self { Field { value: (val << Offset::U8) & Mask::U8, _mask: PhantomData, _offset: PhantomData, } }} // And to enable our register...fn enable_register(&mut reg) { reg.update(RegEnabled::new(1));}
看起來不錯,但是……如果你在給定的值是否適合該字段方面犯了錯誤,會發(fā)生什么?考慮一個簡單的輸入錯誤,你在其中放置了 10
而不是 1
:
fn enable_register(&mut reg) { reg.update(RegEnabled::new(10));}
在上面的代碼中,預(yù)期結(jié)果是什么?好吧,代碼會將啟用位設(shè)置為 0,因為 10&1 = 0
。那真不幸;最好在嘗試寫入之前知道你要寫入字段的值是否適合該字段。事實上,我認(rèn)為截掉錯誤字段值的高位是一種 1未定義的行為(哈)。
如何以一般方式檢查字段的值是否適合其規(guī)定的位置?需要更多類型級別的數(shù)字!
你可以在 Field
中添加 Width
參數(shù),并使用它來驗證給定的值是否適合該字段:
struct Field<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> { value: u8, _mask: PhantomData<Mask>, _offset: PhantomData<Offset>, _width: PhantomData<Width>,} type RegEnabled = Field<U1,U1, U0>;type RegInterrupt = Field<U1, U2, U1>;type RegKind = Field<U3, op!(U7 << U2), U2>; impl<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> Field<Width, Mask, Offset> { fn new(val: u8) -> Option<Self> { if val <= (1 << Width::U8) - 1 { Some(Field { value: (val << Offset::U8) & Mask::U8, _mask: PhantomData, _offset: PhantomData, _width: PhantomData, }) } else { None } }}
現(xiàn)在,只有給定值適合時,你才能構(gòu)造一個 Field
!否則,你將得到 None
信號,該信號指示發(fā)生了錯誤,而不是截掉該值的高位并靜默寫入意外的值。
但是請注意,這將在運行時環(huán)境中引發(fā)錯誤。但是,我們事先知道我們想寫入的值,還記得嗎?鑒于此,我們可以教編譯器完全拒絕具有無效字段值的程序 —— 我們不必等到運行它!
這次,你將向 new
的新實現(xiàn) new_checked
中添加一個特征綁定(where
子句),該函數(shù)要求輸入值小于或等于給定字段用 Width
所能容納的最大可能值:
struct Field<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> { value: u8, _mask: PhantomData<Mask>, _offset: PhantomData<Offset>, _width: PhantomData<Width>,} type RegEnabled = Field<U1, U1, U0>;type RegInterrupt = Field<U1, U2, U1>;type RegKind = Field<U3, op!(U7 << U2), U2>; impl<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> Field<Width, Mask, Offset> { const fn new_checked<V: Unsigned>() -> Self where V: IsLessOrEqual<op!((U1 << Width) - U1), Output = True>, { Field { value: (V::U8 << Offset::U8) & Mask::U8, _mask: PhantomData, _offset: PhantomData, _width: PhantomData, } }}
只有擁有此屬性的數(shù)字才實現(xiàn)此特征,因此,如果使用不適合的數(shù)字,它將無法編譯。讓我們看一看!
fn enable_register(&mut reg) { reg.update(RegEnabled::new_checked::<U10>());}12 | reg.update(RegEnabled::new_checked::<U10>()); | ^^^^^^^^^^^^^^^^ expected struct `typenum::B0`, found struct `typenum::B1` | = note: expected type `typenum::B0` found type `typenum::B1`
new_checked
將無法生成一個程序,因為該字段的值有錯誤的高位。你的輸入錯誤不會在運行時環(huán)境中才爆炸,因為你永遠(yuǎn)無法獲得一個可以運行的工件。
就使內(nèi)存映射的硬件進(jìn)行交互的安全性而言,你已經(jīng)接近 Rust 的極致。但是,你在 C 的第一個示例中所寫的內(nèi)容比最終得到的一鍋粥的類型參數(shù)更簡潔。當(dāng)你談?wù)摑撛诳赡苡袛?shù)百甚至數(shù)千個寄存器時,這樣做是否容易處理?
早些時候,我認(rèn)為手工計算掩碼有問題,但我又做了同樣有問題的事情 —— 盡管是在類型級別。雖然使用這種方法很不錯,但要達(dá)到編寫任何代碼的地步,則需要大量樣板和手動轉(zhuǎn)錄(我在這里談?wù)摰氖穷愋偷耐x詞)。
我們的團(tuán)隊想要像 TockOS mmio 寄存器之類的東西,而以最少的手動轉(zhuǎn)錄生成類型安全的實現(xiàn)。我們得出的結(jié)果是一個宏,該宏生成必要的樣板以獲得類似 Tock 的 API 以及基于類型的邊界檢查。要使用它,請寫下一些有關(guān)寄存器的信息,其字段、寬度和偏移量以及可選的枚舉類的值(你應(yīng)該為字段可能具有的值賦予“含義”):
register! { // The register's name Status, // The type which represents the whole register. u8, // The register's mode, ReadOnly, ReadWrite, or WriteOnly. RW, // And the fields in this register. Fields [ On WIDTH(U1) OFFSET(U0), Dead WIDTH(U1) OFFSET(U1), Color WIDTH(U3) OFFSET(U2) [ Red = U1, Blue = U2, Green = U3, Yellow = U4 ] ]}
由此,你可以生成寄存器和字段類型,如上例所示,其中索引:Width
、Mask
和 Offset
是從一個字段定義的 WIDTH
和 OFFSET
部分的輸入值派生的。另外,請注意,所有這些數(shù)字都是 “類型數(shù)字”;它們將直接進(jìn)入你的 Field
定義!
生成的代碼通過為寄存器及字段指定名稱來為寄存器及其相關(guān)字段提供名稱空間。這很繞口,看起來是這樣的:
mod Status { struct Register(u8); mod On { struct Field; // There is of course more to this definition } mod Dead { struct Field; } mod Color { struct Field; pub const Red: Field = Field::<U1>new(); // &c. }}
生成的 API 包含名義上期望的讀取和寫入的原語,以獲取原始寄存器的值,但它也有辦法獲取單個字段的值、執(zhí)行集合操作以及確定是否設(shè)置了任何(或全部)位集合的方法。你可以閱讀完整生成的 API上的文檔。
將這些定義用于實際設(shè)備會是什么樣?代碼中是否會充斥著類型參數(shù),從而掩蓋了視圖中的實際邏輯?
不會!通過使用類型同義詞和類型推斷,你實際上根本不必考慮程序的類型層面部分。你可以直接與硬件交互,并自動獲得與邊界相關(guān)的保證。
這是一個 UART 寄存器塊的示例。我會跳過寄存器本身的聲明,因為包括在這里就太多了。而是從寄存器“塊”開始,然后幫助編譯器知道如何從指向該塊開頭的指針中查找寄存器。我們通過實現(xiàn) Deref
和 DerefMut
來做到這一點:
#[repr(C)]pub struct UartBlock { rx: UartRX::Register, _padding1: [u32; 15], tx: UartTX::Register, _padding2: [u32; 15], control1: UartControl1::Register,} pub struct Regs { addr: usize,} impl Deref for Regs { type Target = UartBlock; fn deref(&self) -> &UartBlock { unsafe { &*(self.addr as *const UartBlock) } }} impl DerefMut for Regs { fn deref_mut(&mut self) -> &mut UartBlock { unsafe { &mut *(self.addr as *mut UartBlock) } }}
一旦到位,使用這些寄存器就像 read()
和 modify()
一樣簡單:
fn main() { // A pretend register block. let mut x = [0_u32; 33]; let mut regs = Regs { // Some shenanigans to get at `x` as though it were a // pointer. Normally you'd be given some address like // `0xDEADBEEF` over which you'd instantiate a `Regs`. addr: &mut x as *mut [u32; 33] as usize, }; assert_eq!(regs.rx.read(), 0); regs.control1 .modify(UartControl1::Enable::Set + UartControl1::RecvReadyInterrupt::Set); // The first bit and the 10th bit should be set. assert_eq!(regs.control1.read(), 0b_10_0000_0001);}
當(dāng)我們使用運行時值時,我們使用如前所述的選項。這里我使用的是 unwrap
,但是在一個輸入未知的真實程序中,你可能想檢查一下從新調(diào)用中返回的某些東西: 1 2
fn main() { // A pretend register block. let mut x = [0_u32; 33]; let mut regs = Regs { // Some shenanigans to get at `x` as though it were a // pointer. Normally you'd be given some address like // `0xDEADBEEF` over which you'd instantiate a `Regs`. addr: &mut x as *mut [u32; 33] as usize, }; let input = regs.rx.get_field(UartRX::Data::Field::Read).unwrap(); regs.tx.modify(UartTX::Data::Field::new(input).unwrap());}
根據(jù)你的個人痛苦忍耐程度,你可能已經(jīng)注意到這些錯誤幾乎是無法理解的??匆幌挛宜f的不那么微妙的提醒:
error[E0271]: type mismatch resolving `<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>, typenum::B0> as typenum::IsLessOrEqual<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>>>::Output == typenum::B1` --> src/main.rs:12:5 |12 | less_than_ten::<U20>(); | ^^^^^^^^^^^^^^^^^^^^ expected struct `typenum::B0`, found struct `typenum::B1` | = note: expected type `typenum::B0` found type `typenum::B1`
expected struct typenum::B0, found struct typenum::B1
部分是有意義的,但是 typenum::UInt<typenum::UInt, typenum::UInt...
到底是什么呢?好吧,typenum
將數(shù)字表示為二進(jìn)制 cons 單元!像這樣的錯誤使操作變得很困難,尤其是當(dāng)你將多個這些類型級別的數(shù)字限制在狹窄的范圍內(nèi)時,你很難知道它在說哪個數(shù)字。當(dāng)然,除非你一眼就能將巴洛克式二進(jìn)制表示形式轉(zhuǎn)換為十進(jìn)制表示形式。
在第 U100 次試圖從這個混亂中破譯出某些含義之后,我們的一個隊友簡直《瘋了,地獄了,不要再忍受了》,并做了一個小工具 tnfilt
,從這種命名空間的二進(jìn)制 cons 單元的痛苦中解脫出來。tnfilt
將 cons 單元格式的表示法替換為可讓人看懂的十進(jìn)制數(shù)字。我們認(rèn)為其他人也會遇到類似的困難,所以我們分享了 tnfilt。你可以像這樣使用它:
$ cargo build 2>&1 | tnfilt
它將上面的輸出轉(zhuǎn)換為如下所示:
error[E0271]: type mismatch resolving `<U20 as typenum::IsLessOrEqual<U10>>::Output == typenum::B1`
現(xiàn)在這才有意義!
當(dāng)在軟件與硬件進(jìn)行交互時,普遍使用內(nèi)存映射寄存器,并且有無數(shù)種方法來描述這些交互,每種方法在易用性和安全性上都有不同的權(quán)衡。我們發(fā)現(xiàn)使用類型級編程來取得內(nèi)存映射寄存器交互的編譯時檢查可以為我們提供制作更安全軟件的必要信息。該代碼可在 bounded-registers crate(Rust 包)中找到。
以上就是硬件抽象編程該如何選擇C語言及Rust的全部內(nèi)容了,更多與硬件抽象編程該如何選擇C語言及Rust相關(guān)的內(nèi)容可以搜索億速云之前的文章或者瀏覽下面的文章進(jìn)行學(xué)習(xí)哈!相信小編會給大家增添更多知識,希望大家能夠支持一下億速云!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。