内存映射寄存器
嵌入式系统只能通过执行正常的 Rust 代码并在 RAM 中移动数据来实现。如果我们想要将任何信息输入或输出到我们的系统(无论是闪烁 LED、检测按钮按下还是与某个总线上的片外外设通信),我们都需要进入外设及其“内存映射寄存器”的世界。
您可能会发现,访问微控制器中外设所需的代码已经编写好了,并且处于以下级别之一:
- 微体系结构板条箱 - 这种板条箱处理微控制器使用的处理器内核的任何有用例程,以及所有使用该特定类型处理器内核的微控制器共有的任何外设。例如,cortex-m 板条箱为您提供了启用和禁用中断的功能,这些功能对于所有基于 Cortex-M 的微控制器都是相同的。它还允许您访问所有基于 Cortex-M 的微控制器中包含的“SysTick”外设。
- 外设访问板条箱 (PAC) - 这种板条箱是对您使用的特定微控制器型号定义的各种内存包装寄存器的薄包装器。例如,tm4c123x 用于德州仪器 Tiva-C TM4C123 系列,或 stm32f30x 用于意法半导体 STM32F30x 系列。在这里,您将直接与寄存器交互,遵循微控制器技术参考手册中给出的每个外设的操作说明。
- HAL 板条箱 - 这些板条箱为您的特定处理器提供更友好的 API,通常是通过实现 embedded-hal 中定义的一些通用特征。例如,此板条箱可能提供一个
Serial
结构体,其构造函数接受一组适当的 GPIO 引脚和波特率,并提供某种write_byte
函数用于发送数据。有关 embedded-hal 的更多信息,请参见有关 可移植性 的章节。 - 板条箱 - 这些板条箱比 HAL 板条箱更进一步,通过预先配置各种外设和 GPIO 引脚来适应您使用的特定开发套件或板,例如 stm32f3-discovery 用于 STM32F3DISCOVERY 板。
板条箱
如果您是嵌入式 Rust 的新手,板条箱是完美的起点。它们很好地抽象了学习此主题时可能让人不知所措的硬件细节,并使标准任务变得容易,例如打开或关闭 LED。它公开的功能在不同的板之间差异很大。由于本书旨在保持硬件无关性,因此本书不会涵盖板条箱。
如果您想尝试使用 STM32F3DISCOVERY 板,强烈建议您查看 stm32f3-discovery 板条箱,它提供了闪烁板 LED、访问其罗盘、蓝牙等功能。 Discovery 书籍提供了对板条箱使用的极佳介绍。
但是,如果您正在使用尚未拥有专用板条箱的系统,或者您需要现有板条箱未提供的功能,请继续阅读,因为我们将从最底层开始,使用微体系结构板条箱。
微体系结构板条箱
让我们看看所有基于 Cortex-M 的微控制器共有的 SysTick 外设。我们可以在 cortex-m 板条箱中找到一个相当底层的 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
结构体。有关更多信息,请参见 外设 部分。
使用外设访问板条箱 (PAC)
如果我们只限于每个 Cortex-M 中包含的基本外设,那么我们的嵌入式软件开发将无法走得太远。在某些时候,我们需要编写一些特定于我们使用的特定微控制器的代码。在本例中,让我们假设我们有一个德州仪器 TM4C123 - 一个中等 80MHz Cortex-M4,具有 256 KiB 的 Flash。我们将引入 tm4c123x 板条箱来使用此芯片。
#![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());
}
我们访问 PWM0
外设的方式与我们之前访问 SYST
外设的方式完全相同,只是我们调用了 tm4c123x::Peripherals::take()
。由于此板条箱是使用 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 板条箱
芯片的 HAL 库通常通过为 PAC 公开的原始结构实现自定义 Trait 来工作。通常,此 Trait 会为单个外设定义一个名为 `constrain()` 的函数,或为像具有多个引脚的 GPIO 端口这样的东西定义一个名为 `split()` 的函数。此函数将使用底层原始外设结构并返回具有更高层 API 的新对象。此 API 也可能执行一些操作,例如让串行端口 `new` 函数需要对某些 `Clock` 结构进行借用,而该结构只能通过调用配置 PLL 并设置所有时钟频率的函数来生成。这样,在没有先配置时钟速率的情况下,就无法静态地创建串行端口对象,或者串行端口对象无法将波特率错误地转换为时钟节拍。一些库甚至为每个 GPIO 引脚的状态定义了特殊的 Trait,要求用户将引脚置于正确状态(例如,通过选择适当的备用功能模式)然后再将引脚传递给外设。所有这些都没有运行时成本!
让我们看一个例子
#![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();
}
}