内存映射寄存器
嵌入式系统仅仅通过执行普通的 Rust 代码和在 RAM 中移动数据是无法到达很远的。如果我们想将任何信息输入或输出我们的系统(无论是闪烁 LED、检测按钮按下还是与某种总线上的片外外设通信),我们都必须深入到外围设备及其“内存映射寄存器”的世界。
您可能会发现,您访问微控制器中的外围设备所需的代码已经编写好,并且处于以下级别之一
- 微架构 Crate - 这种 crate 处理您的微控制器使用的处理器内核通用的任何有用例程,以及使用该特定类型处理器内核的所有微控制器通用的任何外围设备。例如,cortex-m crate 为您提供了启用和禁用中断的函数,这对于所有基于 Cortex-M 的微控制器都是相同的。它还使您可以访问所有基于 Cortex-M 的微控制器都包含的“SysTick”外围设备。
- 外围设备访问 Crate (PAC) - 这种 crate 是对为您的微控制器特定部件号定义的各种内存包装寄存器的薄包装器。例如,tm4c123x 用于德州仪器 Tiva-C TM4C123 系列,或 stm32f30x 用于 ST-Micro STM32F30x 系列。在这里,您将直接与寄存器交互,遵循微控制器的技术参考手册中给出的每个外围设备的操作说明。
- HAL Crate - 这些 crate 为您的特定处理器提供更用户友好的 API,通常通过实现 embedded-hal 中定义的一些通用 traits。例如,此 crate 可能提供一个
Serial
结构体,其构造函数接受一组合适的 GPIO 引脚和波特率,并提供一些用于发送数据的write_byte
函数。有关 embedded-hal 的更多信息,请参阅有关 可移植性 的章节。 - 开发板 Crate - 这些 crate 比 HAL Crate 更进一步,通过预先配置各种外围设备和 GPIO 引脚来适应您正在使用的特定开发套件或开发板,例如用于 STM32F3DISCOVERY 开发板的 stm32f3-discovery。
开发板 Crate
如果您是嵌入式 Rust 的新手,开发板 crate 是一个完美的起点。它们很好地抽象了在开始学习这个主题时可能会让人不知所措的硬件细节,并使诸如打开或关闭 LED 之类的标准任务变得容易。它公开的功能在不同的开发板之间差异很大。由于本书旨在保持硬件无关性,因此本书不会涵盖开发板 crate。
如果您想尝试 STM32F3DISCOVERY 开发板,强烈建议您查看 stm32f3-discovery 开发板 crate,它提供了闪烁开发板 LED、访问其指南针、蓝牙等功能。Discovery 书籍为如何使用开发板 crate 提供了很好的介绍。
但是,如果您正在处理一个尚未拥有专用开发板 crate 的系统,或者您需要现有 crate 未提供的功能,请继续阅读,因为我们将从最底层开始,即微架构 crate。
微架构 crate
让我们看一下所有基于 Cortex-M 的微控制器通用的 SysTick 外围设备。我们可以在 cortex-m crate 中找到一个相当底层的 API,我们可以这样使用它
#![no_std]
#![no_main]
use cortex_m::peripheral::{syst, Peripherals};
use cortex_m_rt::entry;
use panic_halt as _;
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take().unwrap();
let mut systick = peripherals.SYST;
systick.set_clock_source(syst::SystClkSource::Core);
systick.set_reload(1_000);
systick.clear_current();
systick.enable_counter();
while !systick.has_wrapped() {
// Loop
}
loop {}
}
SYST
结构体上的函数与 ARM 技术参考手册中为此外围设备定义的功能非常接近。这个 API 中没有任何关于“延迟 X 毫秒”的内容 - 我们必须使用 while
循环粗略地自己实现它。请注意,在我们调用 Peripherals::take()
之前,我们无法访问我们的 SYST
结构体 - 这是一个特殊例程,它保证我们的整个程序中只有一个 SYST
结构体。有关更多信息,请参阅外围设备部分。
使用外围设备访问 Crate (PAC)
如果我们仅限于每个 Cortex-M 中包含的基本外围设备,我们的嵌入式软件开发将无法取得太大进展。在某些时候,我们需要编写一些特定于我们正在使用的特定微控制器的代码。在此示例中,我们假设我们有一个德州仪器 TM4C123 - 一个中等的 80MHz Cortex-M4,带有 256 KiB 的闪存。我们将引入 tm4c123x crate 来利用这个芯片。
#![no_std]
#![no_main]
use panic_halt as _; // panic handler
use cortex_m_rt::entry;
use tm4c123x;
#[entry]
pub fn init() -> (Delay, Leds) {
let cp = cortex_m::Peripherals::take().unwrap();
let p = tm4c123x::Peripherals::take().unwrap();
let pwm = p.PWM0;
pwm.ctl.write(|w| w.globalsync0().clear_bit());
// Mode = 1 => Count up/down mode
pwm._2_ctl.write(|w| w.enable().set_bit().mode().set_bit());
pwm._2_gena.write(|w| w.actcmpau().zero().actcmpad().one());
// 528 cycles (264 up and down) = 4 loops per video line (2112 cycles)
pwm._2_load.write(|w| unsafe { w.load().bits(263) });
pwm._2_cmpa.write(|w| unsafe { w.compa().bits(64) });
pwm.enable.write(|w| w.pwm4en().set_bit());
}
我们以与之前访问 SYST
外围设备完全相同的方式访问了 PWM0
外围设备,只是我们调用了 tm4c123x::Peripherals::take()
。由于此 crate 是使用 svd2rust 自动生成的,因此我们寄存器字段的访问函数采用闭包,而不是数字参数。虽然这看起来像很多代码,但 Rust 编译器可以使用它为我们执行一系列检查,然后生成非常接近手写汇编程序的机器代码!当自动生成的代码无法确定特定访问器函数的所有可能参数都有效时(例如,如果 SVD 将寄存器定义为 32 位,但没有说明这些 32 位值中的某些值是否具有特殊含义),则该函数将标记为 unsafe
。在上面的示例中,当使用 bits()
函数设置 load
和 compa
子字段时,我们可以看到这一点。
读取
read()
函数返回一个对象,该对象提供对此寄存器内各个子字段的只读访问权限,如该芯片制造商的 SVD 文件中所定义的那样。您可以在 tm4c123x 文档中找到此特定芯片上此特定外围设备的此特定寄存器的特殊 R
返回类型的所有可用函数。
if pwm.ctl.read().globalsync0().is_set() {
// Do a thing
}
写入
write()
函数接受一个带有单个参数的闭包。通常我们称之为 w
。然后,此参数提供对此寄存器内各个子字段的读写访问权限,如该芯片制造商的 SVD 文件中所定义的那样。同样,您可以在 tm4c123x 文档中找到此特定芯片上此特定外围设备的此特定寄存器的 “w” 的所有可用函数。请注意,我们没有设置的所有子字段都将设置为默认值 - 寄存器中的任何现有内容都将丢失。
pwm.ctl.write(|w| w.globalsync0().clear_bit());
修改
如果我们只想更改此寄存器中的一个特定子字段,而保持其他子字段不变,我们可以使用 modify
函数。此函数接受一个带有两个参数的闭包 - 一个用于读取,一个用于写入。通常我们分别称它们为 r
和 w
。r
参数可用于检查寄存器的当前内容,w
参数可用于修改寄存器内容。
pwm.ctl.modify(|r, w| w.globalsync0().clear_bit());
modify
函数真正展示了这里闭包的强大功能。在 C 中,我们必须先读取到某个临时值,修改正确的位,然后再写回该值。这意味着有相当大的出错范围
uint32_t temp = pwm0.ctl.read();
temp |= PWM0_CTL_GLOBALSYNC0;
pwm0.ctl.write(temp);
uint32_t temp2 = pwm0.enable.read();
temp2 |= PWM0_ENABLE_PWM4EN;
pwm0.enable.write(temp); // Uh oh! Wrong variable!
使用 HAL crate
芯片的 HAL crate 通常通过为 PAC 公开的原始结构实现自定义 Trait 来工作。通常,此 trait 将为单个外围设备定义一个名为 constrain()
的函数,或者为具有多个引脚的 GPIO 端口定义一个名为 split()
的函数。此函数将使用底层原始外围设备结构并返回一个具有更高级别 API 的新对象。此 API 可能还会执行一些操作,例如使串口 new
函数需要借用某个 Clock
结构体,该结构体只能通过调用配置 PLL 并设置所有时钟频率的函数来生成。这样,在未先配置时钟速率的情况下,或在串口对象将波特率错误地转换为时钟滴答的情况下,静态地不可能创建串口对象。某些 crate 甚至为每个 GPIO 引脚可以处于的状态定义特殊的 traits,要求用户在将引脚传递到外围设备之前将引脚置于正确的状态(例如,通过选择合适的替代功能模式)。所有这些都没有运行时成本!
让我们看一个例子
#![no_std]
#![no_main]
use panic_halt as _; // panic handler
use cortex_m_rt::entry;
use tm4c123x_hal as hal;
use tm4c123x_hal::prelude::*;
use tm4c123x_hal::serial::{NewlineMode, Serial};
use tm4c123x_hal::sysctl;
#[entry]
fn main() -> ! {
let p = hal::Peripherals::take().unwrap();
let cp = hal::CorePeripherals::take().unwrap();
// Wrap up the SYSCTL struct into an object with a higher-layer API
let mut sc = p.SYSCTL.constrain();
// Pick our oscillation settings
sc.clock_setup.oscillator = sysctl::Oscillator::Main(
sysctl::CrystalFrequency::_16mhz,
sysctl::SystemClock::UsePll(sysctl::PllOutputFrequency::_80_00mhz),
);
// Configure the PLL with those settings
let clocks = sc.clock_setup.freeze();
// Wrap up the GPIO_PORTA struct into an object with a higher-layer API.
// Note it needs to borrow `sc.power_control` so it can power up the GPIO
// peripheral automatically.
let mut porta = p.GPIO_PORTA.split(&sc.power_control);
// Activate the UART.
let uart = Serial::uart0(
p.UART0,
// The transmit pin
porta
.pa1
.into_af_push_pull::<hal::gpio::AF1>(&mut porta.control),
// The receive pin
porta
.pa0
.into_af_push_pull::<hal::gpio::AF1>(&mut porta.control),
// No RTS or CTS required
(),
(),
// The baud rate
115200_u32.bps(),
// Output handling
NewlineMode::SwapLFtoCRLF,
// We need the clock rates to calculate the baud rate divisors
&clocks,
// We need this to power up the UART peripheral
&sc.power_control,
);
loop {
writeln!(uart, "Hello, World!\r\n").unwrap();
}
}