目录
2.3 内存映射寄存器
2.3.1 从底层开始
2.3.2 使用外围访问包(PAC)
2.3.3 使用HAL Crate
2.3 内存映射寄存器
嵌入式系统只能通过执行正常的Rust 代码和在RAM中移动数据来实现这一目标。如果我们想让任何信息进入或退出我们的系统(无论是闪烁的LED,检测按钮按下或与某类总线上的非芯片外设通信),我们将不得不进入外设和它们的“内存映射寄存器”的世界。
您可能会发现,访问微控制器中外围设备所需的代码已经在以下某个级别编写:
- Micro-architecture Crate - 这种crate 可以处理微控制器使用的处理器内核常用的任何有用例程,以及使用该特定类型处理器内核的所有微控制器通用的任何外设。例如,Cortex-M crate 为您提供启用和禁用中断的功能,这与所有基于Cortex-M的微控制器相同。它还允许您访问所有基于Cortex-M的微控制器所包含的“Systick”外围设备。
- Peripheral Access Crate (PAC) - 这种crate 是各种内存包装寄存器上的薄包装,这些寄存器是为您所使用的微控制器的特定型号定义的。例如,TM4C123X用于德州仪器Tiva-C TM4C123系列,或STM32F30X用于ST-Micro STM32F30X系列。在这里,您将直接与寄存器进行交互,遵循微控制器技术参考手册中给出的每个外围设备的操作说明。
- HAL Crate - 这些crates 为您的特定处理器提供了一个更加用户友好的API,通常通过实现在embedded-hal中定义的一些公共特性来实现。例如,这个crate 可能提供一个Serial结构,带有一个采用适当GPIO管脚集和波特率的构造函数,并提供某种类型的
write_byte
函数来发送数据。有关 embedded-hal的更多信息,请参阅可移植性一章。 - Board Crate - 这些crates 比 HAL Crate更进一步,方法是预先配置各种外围设备和GPIO管脚,以适合您使用的特定开发工具包或板,例如用于STM32F3DISCOVERY开发板的 F3。
2.3.1 从底层开始
让我们看看所有基于Cortex-M的微控制器所共有的Systick外围设备。我们可以在 cortex-m crate中找到一个底层的API,我们可以这样使用它:
use cortex_m::peripheral::{syst, Peripherals};
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
let mut 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结构。 有关更多信息,请参阅“外围设备”部分。
2.3.2 使用外围访问包(PAC)
如果我们把自己局限于每个Cortex-M中包含的基本外围设备,那么嵌入式软件的开发就不会有太大进展。在某种程度上,我们需要编写一些特定于我们使用的特定微控制器的代码。在这个例子中,假设我们有一个德州仪器TM4C123—一个中等80MHz的Cortex-M4,有256 kib的闪存。我们要把TM4C123X的crate 用在这个芯片上。
#![no_std]
#![no_main]
extern crate panic_halt; // 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()。由于这个crate是使用svd2rust自动生成的,所以寄存器字段的访问函数是关闭的,而不是数字参数。虽然这看起来像很多代码,但Rust编译器可以使用它为我们执行一系列检查,然后生成与手工编写的汇编程序非常接近的机器代码!如果自动生成的代码无法确定特定访问器函数的所有可能参数是否有效(例如,如果SVD将寄存器定义为32位,但没有说明其中某些32位值是否具有特殊含义),则该函数被标记为不安全(unsafe
)。当使用bits()函数设置LOAD和COMPA子字段时,我们可以在上面的示例中看到这一点。
读取(Reading)
函数的作用是:返回一个对象,该对象以只读方式访问该寄存器中的各个子字段,如制造商为该芯片定义的SVD文件所示。您可以在TM4C123X文档中的这个特定的寄存器、这个特定的外设、这个特定的芯片上找到所有可用的特殊R返回类型的函数。
if pwm.ctl.read().globalsync0().is_set() {
// Do a thing
}
写入(Writing)
write()函数采用带有单个参数的闭包。 通常我们称之为w。然后,该参数提供对该寄存器内各个子字段的读写访问,如该芯片的制造商SVD文件所定义。同样,您可以在TM4C123X文档的“W”上找到此特定寄存器、此特定外设、此特定芯片上的所有可用功能。请注意,我们没有设置的所有子字段将被设置为我们的默认值-寄存器中的任何现有内容都将丢失。
pwm.ctl.write(|w| w.globalsync0().clear_bit());
修改(Modifying)
如果我们只想更改这个寄存器中的一个子字段,而不更改其他子字段,我们可以使用Modifying函数。 此函数接受一个包含两个参数的闭包-一个用于读取,另一个用于写入。通常我们分别称之为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!
2.3.3 使用HAL Crate
芯片的HAL crate 通常通过为PAC暴露的原始结构实施自定义特征来工作。通常,这个特性将为单个外设定义一个名为constran()的函数,或者为具有多个管脚的GPIO端口定义split()。此函数将使用底层的原始外围设备结构,并返回具有更高级别API的新对象。这个API也可以做一些事情,比如串口new函数需要引用一些Clock structure,这只能通过调用配置PLL的函数并设置所有时钟频率来生成。通过这种方式,在没有先配置时钟速率的情况下创建串行端口对象,或者串行端口对象将波特率错误地转换为时钟节拍是绝对不可能的。有些Crate甚至为每个GPIO引脚所处的状态定义了特殊的特性,要求用户在将引脚传入外设之前将引脚置于正确的状态(例如,通过选择适当的备用功能模式)。无需运行时间成本!
让我们看一个例子:
#![no_std]
#![no_main]
extern crate panic_halt; // 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();
}
}