文章前半部分会先讲寄存器的基本原理,然后后半部分再通过代码示范寄存器的操作方法。
这里使用的嵌入式平台是 STM32F103,它的的寄存器手册可以在 这里 下载。
寄存器操作
在之前我们说过: 寄存器指代的是一段特殊的内存地址区域,但是它没有实际对应的 SRAM (Static Random-Access Memor, 静态随机存取存储器) 存储,对寄存器的操作与对内存的操作完全一致,可以将寄存器当作内存来读写,而对寄存器内存段的读写将会被转化为总线上与外设的数据交换。 所以对寄存器的操作实际上就是对特殊地址的内存进行读写操作。在手册中我们可以找到各寄存器的起始地址 (28页):我们拿 GPIOA 外设的寄存器来做个例子,我们跳到手册中 GPIO 的章节 (115页),这里有一张表格列出了 GPIO_BSRR 寄存器的结构。 这个寄存器到底有什么用并不重要,我们这里只需掌握如何读懂寄存器表格: 第一行是偏移地址。偏移地址指明了这个寄存器相对于外设寄存器区段的位置,从起始地址表中我们可以知道 GPIOA 寄存器区段的起始地址是 0x4001_0800,而 GPIO_BSRR 的偏移地址为 0x10,因此 GPIOA 的 GPIOA_BSRR 寄存器的真正地址即为 0x4001_0800 + 0x10 = 0x4001_0810。 下面的两行格子是寄存器位的说明。格子上的数字是位偏移地址,格子中间的是位的名称,格子下面的是可读写性,这里格子下方都是 w,也就是说这些位都是只写位。 根据下方说明,如果我们要对 ODR3(另一个寄存器的位) 清0,我们就要对 BR3 写1。这个操作实际上就是对 0x4001_0810 内存地址写 0x1 << 19 (除第19位以外都是0的32位无符号整数)。 使用 Rust 来操作就是这样:
core::write_volatile(0x4001_0810 as *mut u32, 1 << 19);
GPIO(通用接口)
Blinky 的原理很简单,只需定时改变连接 LED 的引脚的电平,就可以让 LED 闪烁起来了。我们查看核心板的电路原理图可以发现 LED 被连接在了 PC13 引脚上,而且从原理图中可以看出 LED 采用了共阳极接法,当引脚输出低电平时 LED 才会点亮: STM32F103C8T6 引脚图 注意:有的 STM32F103 核心板 LED 会连接在 PB12 引脚上,需要查看原理图来确定。 STM32 中的引脚被分为了 GPIOA,GPIOB,GPIOC,GPIOD ... 等等多个组,每组中各控制有 16 个引脚,每个组都是一个独立的外设。 在这里,我们需要学习 GPIO 两个关键寄存器:配置寄存器 (GPIOx_CRL,GPIOx_CRH) 和置位/复位寄存器 (GPIOx_BSRR)。(寄存器名中的 x 即为 GPIO 分组中的 A, B, C .. 等等)GPIO 配置寄存器
单片机的引脚往往兼有多种功能,比如输入或输出,因此在使用引脚之前要通过配置寄存器配置它的功能。 我们注意到这里出现了两个配置寄存器 GPIOx_CRL 和 GPIOx_CRH,这其实是配置寄存器的高/低部分,低寄存器 (GPIOx_CRL) 负责配置 0..7 号引脚,高寄存器 (GPIOx_CRH) 负责配置 8..15 号引脚。GPIO 拥有以下几种模式:
- 输入浮空
- 输入上拉
- 输入下拉
- vwin 输入
- 开漏输出
- 推挽式输出
- 推挽式复用功能 ─ 开漏复用功能
输入可以理解为读取引脚上的电平,相反,输出就是控制引脚电平。因为我们想要通过控制引脚电平来点亮 LED,所以我们这里选择输出模式。
输出模式有 推挽式输出 和 开漏输出 两种。推挽输出模式下引脚可以自行输出高低两种电平,但是电流驱动力较弱,适合于和数字元件通讯或驱动 LED;开漏输出只有低电平和截止两种状态,所以需要在电路上加上 上拉电阻 (一端电源一端接引脚的电阻) 才能在截止状态下输出高电平,开漏输出的电流驱动能力更强, 适合于做电流型的驱动。 这里我们选择最简单的推挽式输出模式就可以了。 查阅手册我们可以找到配置寄存器的结构 (114页): PC13 引脚对应了 MODE13 和 CNF13 两段寄存器位,我们将 MODE13 设置为输出模式即 0x11 (最大速度指的是最大电平翻转频率,这里任选一个都行),然后将 CNF13 设为 0x00 就可以推挽输出了。GPIO 置位/复位寄存器
置位/复位寄存器专门用于操作引脚输出电平,对 BR (R意为Reset) 写1会让对应引脚输出低电平,对 BS (S意为Set) 写1会让对应引脚输出高电平。操作十分简单,这里就不赘述了。RCC 总线开关
总线就是之前提到过的时间总线 APB1 和 APB2。单片机中的任何外设都需要从总线上获取时间信号,然而在单片机启动复位后,所有外设都是默认关闭来节省能源,因此在使用外设前需要手动打开总线开关。 RCC (Reset and Clock Control,复位和时钟控制器) 负责单片机时间总线相关的配置,它的 APB2ENR 寄存器用于开关 APB2 总线上的外设。而 GPIO 外设位于 APB2 总线上,我们查找 RCC_APB2ENR 寄存器 (95页): 从图中可知,对 APB2ENR 的 IOPCEN 写 1 就可以启动 GPIOC 外设。Blinky 示例
我们打开之前文章建立的工程项目,修改 src/main.rs 恢复为最小可编译版本:
#![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"
我们根据手册的信息定义寄存器的地址:
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 函数。
#[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() 进行内存写入操作,这是因为如果使用 ptr::write() 函数,编译器有可能会把内存的写入操作优化掉或者调换执行顺序,这在内存操作上可以提高效率,但在寄存器上会完全改变我们程序的意图,导致不可预测的后果。对寄存器的读操作也同样不能使用 ptr::read() 而要使用 ptr::read_volatile()。
此时编译运行就能看到点亮的 LED 了。
接下来我们制造一个简单的延迟函数:
fn delay() {
for _ in 0..2_000 {
asm::nop();
}
}
这里使用了一个汇编函数 nop,即为 No Operation。它会空转耗费 CPU 一个时钟周期,然后我们再对它循环来得到一个肉眼可见的延迟。
其实按照 Cortex-M3 72MHz 的时钟速率来计算,2000 周期级别的延迟也应该在毫秒级以下,然而这里的延迟竟然可以达到半秒左右。这是因为在单片机刚启动的时候,芯片默认采用了启动较快但是频率较低的内部时钟,频率大概在 40kHz 左右,一般情况下我们在复位后要设置 RCC 的寄存器将时钟源转为外部高速时钟,这部分我们留到之后再细讲。
修改 loop 循环:
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 语言中操作寄存器的方法,简单直接。虽然这样可用,但是可以看出这样操作的语义非常模糊,常常需要反复翻查手册,而且这样会大量使用 unsafe 内存操作,很容易发生人为错误。幸好,Rust 为我们提供了更安全的抽象,可以极大地改善以上两个问题。 stm32f103xx 库安全地封装了寄存器的操作接口,而且它是由 svd2rust 自动生成的,所以可以杜绝人工错误。在 这里 可以找到它的文档。 我们来看看怎样使用这个库:
// 获取 Peripheralslet dp = stm32f103xx::take().unwrap();// 启用 GPIOCdp.RCC.apb2enr.write(|w| w.iopben().enabled());
第一行的 stm32f103xx::take() 只会在第一次调用时返回 Some(dp),这样避免了存在多个寄存器实例而的导致数据竞争。
Peripherals 是一个结构体,它拥有所有外设的接口定义,比如说这里的 RCC。可以对 RCC 的 apb2enr 寄存器进行写操作,这个库对寄存器的读写操作都被包含在了闭包中,这样库可以在读写前后执行一些保险操作(重置寄存器值或关闭中断)。w 是 apb2enr 的写入器,我们对其调用 w.iopben().enabled() 和之前使用 unsafe 写入内存完全等价,而且 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 中保证了不会出现任何内存错误。
Blinky:再抽象
stm32f103xx 的表现非常惊艳,但是这还没能完全发掘 Rust 的潜力。嵌入式工作组为我们提供了 embedded-hal 抽象库,stm32f103xx-hal 就是 embedded-hal 在 stm32f103 上的具体实现。stm32f103xx-hal 库在 stm32f103xx 的基础上再次抽象封装了寄存器的逻辑细节。比如说,stm32f103xx-hal 可以在我们使用 GPIOC 前自动启用 apb2enr 总线开关。同样,这个库也是 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 默认实现于外设结构体(比如说 RCC)上来提供 constrain() 转换函数。constrain() 会将 stm32f103xx 的外设实例转化为 stm32f103xx-hal 中的外设类型。
let dp = stm32f103xx::Peripherals::take().unwrap();// 将 RCC 寄存器结构体转换为进一步抽象的 hal 结构体let mut rcc = dp.RCC.constrain();// 获取 GPIOC 实例,这里会自动打开总线开关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 寄存器结构体转换为进一步抽象的 hal 结构体 let mut rcc = dp.RCC.constrain();// 获取 GPIOC 实例,这里会自动打开总线开关 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
这篇文章篇幅较长,从寄存器原理一直讲到了内存操作方法,然后展示了如何通过 Rust 强大的抽象能力将零散的内存操作隐藏在安全的操作接口后面,并且还基于 embedded-hal 对寄存器操作的逻辑再一次抽象,得到了安全且容易使用的 API,还可以根据需要灵活选择抽象级别。相信读者已经能感受到Rust 在嵌入式领域相对于 C 的巨大的优势了。
审核编辑 :李倩
-
寄存器
+关注
关注
31文章
5336浏览量
120224 -
存储器
+关注
关注
38文章
7484浏览量
163759 -
总线
+关注
关注
10文章
2878浏览量
88050 -
Cortex
+关注
关注
2文章
202浏览量
46478
发布评论请先 登录
相关推荐
评论