文章前半部分會(huì)先講寄存器的基本原理,然后后半部分再通過(guò)代碼示范寄存器的操作方法。
這里使用的嵌入式平臺(tái)是 STM32F103,它的的寄存器手冊(cè)可以在 這里 下載。
寄存器操作
在之前我們說(shuō)過(guò): 寄存器指代的是一段特殊的內(nèi)存地址區(qū)域,但是它沒(méi)有實(shí)際對(duì)應(yīng)的 SRAM (Static Random-Access Memor, 靜態(tài)隨機(jī)存取存儲(chǔ)器) 存儲(chǔ),對(duì)寄存器的操作與對(duì)內(nèi)存的操作完全一致,可以將寄存器當(dāng)作內(nèi)存來(lái)讀寫(xiě),而對(duì)寄存器內(nèi)存段的讀寫(xiě)將會(huì)被轉(zhuǎn)化為總線(xiàn)上與外設(shè)的數(shù)據(jù)交換。 所以對(duì)寄存器的操作實(shí)際上就是對(duì)特殊地址的內(nèi)存進(jìn)行讀寫(xiě)操作。在手冊(cè)中我們可以找到各寄存器的起始地址 (28頁(yè)):
我們拿 GPIOA 外設(shè)的寄存器來(lái)做個(gè)例子,我們跳到手冊(cè)中 GPIO 的章節(jié) (115頁(yè)),這里有一張表格列出了 GPIO_BSRR 寄存器的結(jié)構(gòu)。 這個(gè)寄存器到底有什么用并不重要,我們這里只需掌握如何讀懂寄存器表格:

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)亮:

GPIO 配置寄存器
單片機(jī)的引腳往往兼有多種功能,比如輸入或輸出,因此在使用引腳之前要通過(guò)配置寄存器配置它的功能。 我們注意到這里出現(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 擁有以下幾種模式:
- 輸入浮空
- 輸入上拉
- 輸入下拉
- 模擬輸入
- 開(kāi)漏輸出
- 推挽式輸出
- 推挽式復(fù)用功能 ─ 開(kāi)漏復(fù)用功能
輸入可以理解為讀取引腳上的電平,相反,輸出就是控制引腳電平。因?yàn)槲覀兿胍ㄟ^(guò)控制引腳電平來(lái)點(diǎn)亮 LED,所以我們這里選擇輸出模式。
輸出模式有 推挽式輸出 和 開(kāi)漏輸出 兩種。推挽輸出模式下引腳可以自行輸出高低兩種電平,但是電流驅(qū)動(dòng)力較弱,適合于和數(shù)字元件通訊或驅(qū)動(dòng) LED;開(kāi)漏輸出只有低電平和截止兩種狀態(tài),所以需要在電路上加上 上拉電阻 (一端電源一端接引腳的電阻) 才能在截止?fàn)顟B(tài)下輸出高電平,開(kāi)漏輸出的電流驅(qū)動(dòng)能力更強(qiáng), 適合于做電流型的驅(qū)動(dòng)。 這里我們選擇最簡(jiǎn)單的推挽式輸出模式就可以了。 查閱手冊(cè)我們可以找到配置寄存器的結(jié)構(gòu) (114頁(yè)):
GPIO 置位/復(fù)位寄存器
置位/復(fù)位寄存器專(zhuān)門(mén)用于操作引腳輸出電平,對(duì) BR (R意為Reset) 寫(xiě)1會(huì)讓對(duì)應(yīng)引腳輸出低電平,對(duì) BS (S意為Set) 寫(xiě)1會(huì)讓對(duì)應(yīng)引腳輸出高電平。操作十分簡(jiǎn)單,這里就不贅述了。
RCC 總線(xiàn)開(kāi)關(guān)
總線(xiàn)就是之前提到過(guò)的時(shí)間總線(xiàn) APB1 和 APB2。單片機(jī)中的任何外設(shè)都需要從總線(xiàn)上獲取時(shí)間信號(hào),然而在單片機(jī)啟動(dòng)復(fù)位后,所有外設(shè)都是默認(rèn)關(guān)閉來(lái)節(jié)省能源,因此在使用外設(shè)前需要手動(dòng)打開(kāi)總線(xiàn)開(kāi)關(guān)。 RCC (Reset and Clock Control,復(fù)位和時(shí)鐘控制器) 負(fù)責(zé)單片機(jī)時(shí)間總線(xiàn)相關(guān)的配置,它的 APB2ENR 寄存器用于開(kāi)關(guān) APB2 總線(xiàn)上的外設(shè)。而 GPIO 外設(shè)位于 APB2 總線(xiàn)上,我們查找 RCC_APB2ENR 寄存器 (95頁(yè)):

Blinky 示例
我們打開(kāi)之前文章建立的工程項(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 中的依賴(lài)。在這里我們暫時(shí)沒(méi)有使用 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)存寫(xiě)入操作,這是因?yàn)槿绻褂?ptr::write() 函數(shù),編譯器有可能會(huì)把內(nèi)存的寫(xiě)入操作優(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è)肉眼可見(jiàn)的延遲。
其實(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è)問(wèn)題。 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)行寫(xiě)操作,這個(gè)庫(kù)對(duì)寄存器的讀寫(xiě)操作都被包含在了閉包中,這樣庫(kù)可以在讀寫(xiě)前后執(zhí)行一些保險(xiǎn)操作(重置寄存器值或關(guān)閉中斷)。w 是 apb2enr 的寫(xiě)入器,我們對(duì)其調(diào)用 w.iopben().enabled() 和之前使用 unsafe 寫(xiě)入內(nèi)存完全等價(jià),而且 zero-cost,編譯后的指令一般不會(huì)有差別。
同理我們對(duì) GPIOC 的操作可以改寫(xiě)為:
// 配置 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)非常驚艷,但是這還沒(méi)能完全發(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 總線(xiàn)開(kāi)關(guān)。同樣,這個(gè)庫(kù)也是 zero-cost 的。 修改 Cargo.toml,添加依賴(lài):
[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è)類(lèi)型。
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)打開(kāi)總線(xiàn)開(kāi)關(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)打開(kāi)總線(xiàn)開(kāi)關(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)存操作方法,然后展示了如何通過(guò) Rust 強(qiáng)大的抽象能力將零散的內(nèi)存操作隱藏在安全的操作接口后面,并且還基于 embedded-hal 對(duì)寄存器操作的邏輯再一次抽象,得到了安全且容易使用的 API,還可以根據(jù)需要靈活選擇抽象級(jí)別。相信讀者已經(jīng)能感受到Rust 在嵌入式領(lǐng)域相對(duì)于 C 的巨大的優(yōu)勢(shì)了。
審核編輯 :李倩
-
寄存器
+關(guān)注
關(guān)注
31文章
5421瀏覽量
123307 -
存儲(chǔ)器
+關(guān)注
關(guān)注
38文章
7633瀏覽量
166390 -
總線(xiàn)
+關(guān)注
關(guān)注
10文章
2947瀏覽量
89336 -
Cortex
+關(guān)注
關(guān)注
2文章
203瀏覽量
47139
發(fā)布評(píng)論請(qǐng)先 登錄
【圖書(shū)分享】《ARM Cortex-M3權(quán)威指南》
ARM Cortex-M3系統(tǒng)該如何去設(shè)計(jì)呢
ARM Cortex-M3 DesignStart? Eval RTL和FPGA快速入門(mén)指南
Cortex-M3 技術(shù)參考手冊(cè)
ARM Cortex-M3權(quán)威指南
Cortex-M3寄存器組資料下載

Cortex-M3處理器內(nèi)核與基于Cortex-M3的MCU關(guān)系

ARM Cortex-M3系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)

Cortex-M3 內(nèi)部寄存器

Cortex-M3寄存器等基礎(chǔ)知識(shí)

評(píng)論