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