这篇文章我们将会来讲讲嵌入式系统中非常重要的概念 —— 寄存器。因为单片机对于外界响应和自身功能的控制基本上全部都要通过寄存器进行交互,所以寄存器的使用将会贯穿整个单片机的学习过程。这篇文章将通过手把手重写我们的 blinky 程序来介绍寄存器的概念和操作方法。
文章前半部分会先讲寄存器的基本原理,然后后半部分再通过代码示范寄存器的操作方法。
这里使用的嵌入式平台是 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 < ! { 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 < ! { 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 < ! {// 获取 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 的巨大的优势了。
服务器品牌有哪些?服务器的参数介绍
如何防止脚本重复运行
20亿“分手费”道珍重 高通恩智浦各奔前程
板式给料机链条磨损过快原因分析和解决办法
realme手机官宣旗下6400万像素新机将于9月17日
Cortex-M3入门指南(二):寄存器
机器视觉将是工业4.0和物联网的关键技术
通勤用哪款降噪耳机好?长久佩戴舒适的降噪耳机
五大理由告诉你华为P10值得买吗?
抢攻万亿物联网市场,蓝牙5.0不可小视!
一种基于直接法的动态稠密SLAM方案
北汽新能源一季度销量同比下滑较大 1-2月纯电车型销量依然位列行业第二
诺基亚回归颠覆之作:诺基亚9,全面屏,价格感人
比亚迪储能业务划转至电池事业部
波士顿动力又上新啦!
移动通讯杂散测试
惠斯顿电桥在汽车空气流量传感器上有何应用
无线直读气表远程抄表系统及解决方案介绍
广州联通携手华为建设了全国首个一体化A+P极简站点
我国无人机产业实现腾飞需做到那三点