這篇文章我們將會來講講嵌入式系統(tǒng)中非常重要的概念 —— 寄存器。因為單片機對于外界響應(yīng)和自身功能的控制基本上全部都要通過寄存器進行交互,所以寄存器的使用將會貫穿整個單片機的學習過程。這篇文章將通過手把手重寫我們的 Blinky 程序來介紹寄存器的概念和操作方法。
文章前半部分會先講寄存器的基本原理,然后后半部分再通過代碼示范寄存器的操作方法。
這里使用的嵌入式平臺是 STM32F103,它的的寄存器手冊可以在 這里 下載。
寄存器操作
在之前我們說過: ? 寄存器指代的是一段特殊的內(nèi)存地址區(qū)域,但是它沒有實際對應(yīng)的 SRAM (Static Random-Access Memor, 靜態(tài)隨機存取存儲器) 存儲,對寄存器的操作與對內(nèi)存的操作完全一致,可以將寄存器當作內(nèi)存來讀寫,而對寄存器內(nèi)存段的讀寫將會被轉(zhuǎn)化為總線上與外設(shè)的數(shù)據(jù)交換。 ? 所以對寄存器的操作實際上就是對特殊地址的內(nèi)存進行讀寫操作。在手冊中我們可以找到各寄存器的起始地址 (28頁): ?
我們拿 GPIOA 外設(shè)的寄存器來做個例子,我們跳到手冊中 GPIO 的章節(jié) (115頁),這里有一張表格列出了 GPIO_BSRR 寄存器的結(jié)構(gòu)。 ? 這個寄存器到底有什么用并不重要,我們這里只需掌握如何讀懂寄存器表格: ?
? 第一行是偏移地址。偏移地址指明了這個寄存器相對于外設(shè)寄存器區(qū)段的位置,從起始地址表中我們可以知道 GPIOA 寄存器區(qū)段的起始地址是 0x4001_0800,而 GPIO_BSRR 的偏移地址為 0x10,因此 GPIOA 的 GPIOA_BSRR 寄存器的真正地址即為 0x4001_0800 + 0x10 = 0x4001_0810。 ? 下面的兩行格子是寄存器位的說明。格子上的數(shù)字是位偏移地址,格子中間的是位的名稱,格子下面的是可讀寫性,這里格子下方都是 w,也就是說這些位都是只寫位。 ? 根據(jù)下方說明,如果我們要對 ODR3(另一個寄存器的位) 清0,我們就要對 BR3 寫1。這個操作實際上就是對 0x4001_0810 內(nèi)存地址寫 0x1 << 19 (除第19位以外都是0的32位無符號整數(shù))。 ? 使用 Rust 來操作就是這樣: ?
core::write_volatile(0x4001_0810 as *mut u32, 1 << 19);
? ? ? ?GPIO(通用接口)Blinky 的原理很簡單,只需定時改變連接 LED 的引腳的電平,就可以讓 LED 閃爍起來了。我們查看核心板的電路原理圖可以發(fā)現(xiàn) LED 被連接在了 PC13 引腳上,而且從原理圖中可以看出 LED 采用了共陽極接法,當引腳輸出低電平時 LED 才會點亮: ?
?
STM32F103C8T6 引腳圖 ? 注意:有的 STM32F103 核心板 LED 會連接在 PB12 引腳上,需要查看原理圖來確定。 ? STM32 中的引腳被分為了 GPIOA,GPIOB,GPIOC,GPIOD ... 等等多個組,每組中各控制有 16 個引腳,每個組都是一個獨立的外設(shè)。 ? 在這里,我們需要學習 GPIO 兩個關(guān)鍵寄存器:配置寄存器 (GPIOx_CRL,GPIOx_CRH) 和置位/復(fù)位寄存器 (GPIOx_BSRR)。(寄存器名中的 x 即為 GPIO 分組中的 A, B, C .. 等等) ? ?
GPIO 配置寄存器
單片機的引腳往往兼有多種功能,比如輸入或輸出,因此在使用引腳之前要通過配置寄存器配置它的功能。 ? 我們注意到這里出現(xiàn)了兩個配置寄存器 GPIOx_CRL 和 GPIOx_CRH,這其實是配置寄存器的高/低部分,低寄存器 (GPIOx_CRL) 負責配置 0..7 號引腳,高寄存器 (GPIOx_CRH) 負責配置 8..15 號引腳。 ?
GPIO 擁有以下幾種模式:
輸入浮空
輸入上拉
輸入下拉
模擬輸入
開漏輸出
推挽式輸出
推挽式復(fù)用功能 ─ 開漏復(fù)用功能
輸入可以理解為讀取引腳上的電平,相反,輸出就是控制引腳電平。因為我們想要通過控制引腳電平來點亮 LED,所以我們這里選擇輸出模式。
輸出模式有 推挽式輸出 和 開漏輸出 兩種。推挽輸出模式下引腳可以自行輸出高低兩種電平,但是電流驅(qū)動力較弱,適合于和數(shù)字元件通訊或驅(qū)動 LED;開漏輸出只有低電平和截止兩種狀態(tài),所以需要在電路上加上 上拉電阻 (一端電源一端接引腳的電阻) 才能在截止狀態(tài)下輸出高電平,開漏輸出的電流驅(qū)動能力更強, 適合于做電流型的驅(qū)動。 ? 這里我們選擇最簡單的推挽式輸出模式就可以了。 ? 查閱手冊我們可以找到配置寄存器的結(jié)構(gòu) (114頁): ?
? PC13 引腳對應(yīng)了 MODE13 和 CNF13 兩段寄存器位,我們將 MODE13 設(shè)置為輸出模式即 0x11 (最大速度指的是最大電平翻轉(zhuǎn)頻率,這里任選一個都行),然后將 CNF13 設(shè)為 0x00 就可以推挽輸出了。 ? ?
GPIO 置位/復(fù)位寄存器
置位/復(fù)位寄存器專門用于操作引腳輸出電平,對 BR (R意為Reset) 寫1會讓對應(yīng)引腳輸出低電平,對 BS (S意為Set) 寫1會讓對應(yīng)引腳輸出高電平。操作十分簡單,這里就不贅述了。 ?
? ?
RCC 總線開關(guān)
總線就是之前提到過的時間總線 APB1 和 APB2。單片機中的任何外設(shè)都需要從總線上獲取時間信號,然而在單片機啟動復(fù)位后,所有外設(shè)都是默認關(guān)閉來節(jié)省能源,因此在使用外設(shè)前需要手動打開總線開關(guān)。 ? RCC (Reset and Clock Control,復(fù)位和時鐘控制器) 負責單片機時間總線相關(guān)的配置,它的 APB2ENR 寄存器用于開關(guān) APB2 總線上的外設(shè)。而 GPIO 外設(shè)位于 APB2 總線上,我們查找 RCC_APB2ENR 寄存器 (95頁): ?
?
? 從圖中可知,對 APB2ENR 的 IOPCEN 寫 1 就可以啟動 GPIOC 外設(shè)。 ? ?
Blinky 示例
我們打開之前文章建立的工程項目,修改 src/main.rs 恢復(fù)為最小可編譯版本:
#![no_std] #![no_main] extern crate panic_halt; use core::ptr; use cortex_m::asm; use cortex_m_rt::entry; use stm32f103xx; #[entry] fn main() -> ! { asm::nop(); loop { } }修改 Cargo.toml 中的依賴。在這里我們暫時沒有使用 stm32f103xx 的寄存器功能,只是讓編譯器自動鏈接它提供的中斷向量表,否則會無法編譯:
[denpendencies] cortex-m = "0.5.8" cortex-m-rt = "0.6.5" panic-halt = "0.2.0" stm32f103xx = "0.11"我們根據(jù)手冊的信息定義寄存器的地址:
const RCC_APB2ENR: *mut u32 = (0x4002_1000 + 0x18) as *mut u32; const GPIOC_CRH: *mut u32 = (0x4001_1000 + 0x04) as *mut u32; const GPIOC_BSRR: *mut u32 = (0x4001_1000 + 0x10) as *mut u32;再定義要用到的寄存器位偏移量:
const APB2ENR_IOPCEN: usize = 4; const CRH_MODE13: usize = 20; const BSRR_BS13: usize = 13; const BSRR_BR13: usize = 13 + 16;修改 main 函數(shù)。
#[entry] fn main() -> ! { unsafe { // 啟用 GPIOC ptr::write_volatile(RCC_APB2ENR, 1 << APB2ENR_IOPCEN); // 配置 GPIOC - PC13 為推挽輸出 ptr::write_volatile(GPIOC_CRH, 0b0011 << CRH_MODE13); // 重置 PC13 以輸出低電平 ptr::write_volatile(GPIOC_BSRR, 1 << BSRR_BR13); } loop { } }注意這里使用了 ptr::write_volatile() 進行內(nèi)存寫入操作,這是因為如果使用 ptr::write() 函數(shù),編譯器有可能會把內(nèi)存的寫入操作優(yōu)化掉或者調(diào)換執(zhí)行順序,這在內(nèi)存操作上可以提高效率,但在寄存器上會完全改變我們程序的意圖,導(dǎo)致不可預(yù)測的后果。對寄存器的讀操作也同樣不能使用 ptr::read() 而要使用 ptr::read_volatile()。 ? 此時編譯運行就能看到點亮的 LED 了。 ? 接下來我們制造一個簡單的延遲函數(shù):
fn delay() { for _ in 0..2_000 { asm::nop(); } }這里使用了一個匯編函數(shù) nop,即為 No Operation。它會空轉(zhuǎn)耗費 CPU 一個時鐘周期,然后我們再對它循環(huán)來得到一個肉眼可見的延遲。 ? 其實按照 Cortex-M3 72MHz 的時鐘速率來計算,2000 周期級別的延遲也應(yīng)該在毫秒級以下,然而這里的延遲竟然可以達到半秒左右。這是因為在單片機剛啟動的時候,芯片默認采用了啟動較快但是頻率較低的內(nèi)部時鐘,頻率大概在 40kHz 左右,一般情況下我們在復(fù)位后要設(shè)置 RCC 的寄存器將時鐘源轉(zhuǎn)為外部高速時鐘,這部分我們留到之后再細講。 ? 修改 loop 循環(huán):
loop { delay(); // Reset:輸出低電平,點亮 LED unsafe { ptr::write_volatile(GPIOC_BSRR, 1 << BSRR_BR13); } delay(); // Set:輸出高電平,LED 熄滅 unsafe { ptr::write_volatile(GPIOC_BSRR, 1 << BSRR_BS13); } }至此我們的寄存器版本的 Blinky 就完成了!下面是完整代碼:
#![no_std] #![no_main] extern crate panic_halt; use core::ptr; use stm32f103xx; use cortex_m::asm; use cortex_m_rt::entry; const RCC_APB2ENR: *mut u32 = (0x4002_1000 + 0x18) as *mut u32; const GPIOC_CRH: *mut u32 = (0x4001_1000 + 0x04) as *mut u32; const GPIOC_BSRR: *mut u32 = (0x4001_1000 + 0x10) as *mut u32; const APB2ENR_IOPCEN: usize = 4; const CRH_MODE13: usize = 20; const BSRR_BS13: usize = 13; const BSRR_BR13: usize = 13 + 16; #[entry] fn main() -> ! { unsafe { // 啟用 GPIOC ptr::write_volatile(RCC_APB2ENR, 1 << APB2ENR_IOPCEN); // 配置 GPIOC - PC13 為推挽輸出 ptr::write_volatile(GPIOC_CRH, 0b0011 << CRH_MODE13); // 重置 PC13 以輸出低電平 ptr::write_volatile(GPIOC_BSRR, 1 << BSRR_BR13); } loop { delay(); // Reset:輸出低電平,點亮 LED unsafe { ptr::write_volatile(GPIOC_BSRR, 1 << BSRR_BR13); } delay(); // Set:輸出高電平,LED 熄滅 unsafe { ptr::write_volatile(GPIOC_BSRR, 1 << BSRR_BS13); } } } fn delay() { for _ in 0..2_000 { asm::nop(); } }? ?Blinky:抽象
上面代碼中使用的就是 C 語言中操作寄存器的方法,簡單直接。雖然這樣可用,但是可以看出這樣操作的語義非常模糊,常常需要反復(fù)翻查手冊,而且這樣會大量使用 unsafe 內(nèi)存操作,很容易發(fā)生人為錯誤。幸好,Rust 為我們提供了更安全的抽象,可以極大地改善以上兩個問題。 ? stm32f103xx 庫安全地封裝了寄存器的操作接口,而且它是由 svd2rust 自動生成的,所以可以杜絕人工錯誤。在 這里 可以找到它的文檔。 ? 我們來看看怎樣使用這個庫:
// 獲取 Peripheralslet dp = stm32f103xx::take().unwrap();// 啟用 GPIOCdp.RCC.apb2enr.write(|w| w.iopben().enabled());第一行的 stm32f103xx::take() 只會在第一次調(diào)用時返回 Some(dp),這樣避免了存在多個寄存器實例而的導(dǎo)致數(shù)據(jù)競爭。 ? Peripherals 是一個結(jié)構(gòu)體,它擁有所有外設(shè)的接口定義,比如說這里的 RCC。可以對 RCC 的 apb2enr 寄存器進行寫操作,這個庫對寄存器的讀寫操作都被包含在了閉包中,這樣庫可以在讀寫前后執(zhí)行一些保險操作(重置寄存器值或關(guān)閉中斷)。w 是 apb2enr 的寫入器,我們對其調(diào)用 w.iopben().enabled() 和之前使用 unsafe 寫入內(nèi)存完全等價,而且 zero-cost,編譯后的指令一般不會有差別。 ? 同理我們對 GPIOC 的操作可以改寫為:
// 配置 PC13dp.GPIOC.crh.write(|w| w.mode13().output().cnf13().push());// Setdp.GPIOC.bsrr.write(|w| w.bs13().set());// Resetdp.GPIOC.bsrr.write(|w| w.br13().reset());完整代碼:
#![no_std] #![no_main] extern crate panic_halt; use core::ptr; use stm32f103xx; use cortex_m::asm; use cortex_m_rt::entry; #[entry] fn main() -> ! { // 獲取 Peripherals let dp = stm32f103xx::take().unwrap(); // 啟用 GPIOC dp.RCC.apb2enr.write(|w| w.iopben().enabled()); // 配置 PC13 dp.GPIOC.crh.write(|w| w.mode13().output().cnf13().push()); loop { delay(); // Reset:輸出低電平,點亮 LED dp.GPIOC.bsrr.write(|w| w.br13().reset()); delay(); // Set:輸出高電平,LED 熄滅 dp.GPIOC.bsrr.write(|w| w.bs13().set()); } } fn delay() { for _ in 0..2_000 { asm::nop(); } }相比于 C style 的寄存器操作,svd2rust 封裝了所有寄存器地址信息,而且不需要使用任何 unsafe 代碼,這在 Rust 中保證了不會出現(xiàn)任何內(nèi)存錯誤。 ? ?
Blinky:再抽象
stm32f103xx 的表現(xiàn)非常驚艷,但是這還沒能完全發(fā)掘 Rust 的潛力。嵌入式工作組為我們提供了 embedded-hal 抽象庫,stm32f103xx-hal 就是 embedded-hal 在 stm32f103 上的具體實現(xiàn)。stm32f103xx-hal 庫在 stm32f103xx 的基礎(chǔ)上再次抽象封裝了寄存器的邏輯細節(jié)。比如說,stm32f103xx-hal 可以在我們使用 GPIOC 前自動啟用 apb2enr 總線開關(guān)。同樣,這個庫也是 zero-cost 的。 ? 修改 Cargo.toml,添加依賴:
[dependencies.stm32f103xx-hal]features = ["rt"]git = "https://github.com/japaric/stm32f103xx-hal"在 src/main.rs 里引入 hal:
extern crate stm32f103xx_hal as hal;use hal::*;hal::prelude 中定義了許多 trait,這些 trait 默認實現(xiàn)于外設(shè)結(jié)構(gòu)體(比如說 RCC)上來提供 constrain() 轉(zhuǎn)換函數(shù)。constrain() 會將 stm32f103xx 的外設(shè)實例轉(zhuǎn)化為 stm32f103xx-hal 中的外設(shè)類型。
let dp = stm32f103xx::Peripherals::take().unwrap();// 將 RCC 寄存器結(jié)構(gòu)體轉(zhuǎn)換為進一步抽象的 hal 結(jié)構(gòu)體let mut rcc = dp.RCC.constrain();// 獲取 GPIOC 實例,這里會自動打開總線開關(guān)let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);// 獲取 PC13 實例,并進行引腳配置let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);// 輸出高電平led.set_high();// 輸出低電平led.set_low();完整代碼:
#![no_std]#![no_main]extern crate panic_halt;extern crate stm32f103xx_hal as hal;use core::ptr;use stm32f103xx;use cortex_m::asm;use cortex_m_rt::entry;use hal::*;#[entry]fn main() -> ! {// 獲取 Peripherals let dp = stm32f103xx::take().unwrap();// 將 RCC 寄存器結(jié)構(gòu)體轉(zhuǎn)換為進一步抽象的 hal 結(jié)構(gòu)體 let mut rcc = dp.RCC.constrain();// 獲取 GPIOC 實例,這里會自動打開總線開關(guān) let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);// 獲取 PC13 實例,并進行引腳配置 let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); loop { delay();// 輸出低電平 led.set_low(); delay();// 輸出高電平 led.set_high(); }}fn delay() {for _ in 0..2_000 { asm::nop(); }}
Conclusion
這篇文章篇幅較長,從寄存器原理一直講到了內(nèi)存操作方法,然后展示了如何通過 Rust 強大的抽象能力將零散的內(nèi)存操作隱藏在安全的操作接口后面,并且還基于 embedded-hal 對寄存器操作的邏輯再一次抽象,得到了安全且容易使用的 API,還可以根據(jù)需要靈活選擇抽象級別。相信讀者已經(jīng)能感受到Rust 在嵌入式領(lǐng)域相對于 C 的巨大的優(yōu)勢了。
編輯:黃飛
?
評論
查看更多