# 内存映射寄存器 - The Embedded Rust Book

# 内存映射寄存器

到目前为止目前我们学了嵌入式系统如何执行常规的Rust代码,如何操作内存中的数据。如果我们想获取或者修改系统的任何信息(例如，闪烁LED，检测到按钮的按下或与某种总线上的外设进行通信)，我们将不得不深入了解外设及其“内存映射寄存器”。

现在已经存在不少访问外设的crate,他们可以大致进行如下分类:

- 处理器架构相关Crate (Micro-architecture Crate) - 这种crate比较通用, 可处理CPU相关的通用例程以及一些通用外设。例如，cortex-mcrate为您提供了启用和禁用中断的功能，这些功能对于所有基于Cortex-M的CPU都是相同的。它还使您可以访问所有基于Cortex-M的微控制器附带的时钟外设(SysTick)。

- 外设相关Crate(PAC) 这种crate实际上是对特定CPU型号的内存映射寄存器的一个简单封装。例如，tm4c123x这个crate是对德州仪器(TI)Tiva-C TM4C123系列CPU的封装，stm32f30x这个crate是对ST-Micro STM32F30x系列CPU的封装。借助这些crate，您可以按照CPU参考手册中给出的每个外设的操作说明直接与寄存器进行交互。

- HAL crate - 这些crate通过实现embedded-hal中定义的一些常见Trait，来提供更友好的处理器相关API。例如，此crate可能提供一个`Serial`结构体，该结构体提供一个构造函数来配置一组GPIO引脚和波特率，并提供某种`write_byte`函数来发送数据。有关embedded-hal的更多信息，请参见可移植性一章。

- 开发板相关crate - 通过预先配置各种外设和GPIO引脚以适合特定的开发板，例如针对TM32F3DISCOVERY开发板的F3crate，这些crate相比HAL类crate,更易用。

## 从底层开始

让我们看一下SysTick外设, 所有Cortex-M的微控制器都有这个外设。我们可以在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中没有“延迟多少毫秒”这样的函数接口,因此我们必须使用`while`循环来实现它。注意，在调用 `Peripherals::take()`之前，我们无法访问`SYST`结构体--这可确保整个程序中只有一个`SYST`实例。有关更多信息，请参见外围设备部分。

## 使用外设crate(PAC)

如果我们只能操控Cortex-M附带的基本外围设备，那么我们在嵌入式软件开发方面就只能是小打小闹。总有一天，我们需要编写一些针对我们正在使用的特定微控制器的代码。在此示例中，假设我们使用德州仪器(TI)TM4C123这款款处理器(具有256 KiB Flash,80MHz的Cortex-M4)。需要引入tm4c123xcrate以使用此芯片。

    #![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());
    }

除了调用`tm4c123x::Peripherals::take()`之外，我们访问PWM0外设的方式与之前访问SYST外设的方式完全相同。由于此crate是使用svd2rust自动生成的，因此我们寄存器的访问函数采用闭包而不是数字参数。尽管这看起来有点绕，但是Rust编译器可以为我们执行很多检查以及优化，然后生成与手写汇编代码非常接近的机器代码！自动生成的代码如果无法确定特定函数的参数的所有可能值均有效(例如，SVD将寄存器定义为32位整数，但实际上只有其中的某些值才有特殊含义,才有意义)，则该函数被标记为“不安全”。我们在上面的示例中使用`bits()` 函数设置 `load` 和`compa` 子字段时可以看到这一点。

### 读访问

`read()` 函数返回一个对象R，该对象只有对该寄存器中各个子字段的只读访问权限，这些权限由制造商的该芯片的SVD文件定义。R上面定义的所有函数功能,您可以在[tm4c123x文档] tm4c123x文档R中找到针对此款处理器,此种外设的具体寄存器的定义。

    if pwm.ctl.read().globalsync0().is_set() {
        // Do a thing
    }

### 写访问

`write()`函数的参数是一个带有单个参数的闭包。通常我们将其称为 `w`。根据制造商关于此芯片的SVD文件，此参数可对该寄存器内的各个子字段进行读写访问。同样，`w`上面定义所有函数功能,您可以在[tm4c123x文档] 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,一般是通过为这个芯片的`PAC`中的结构体实现自定义的Trait.通常，这个自定义crate为单体外设定义一个名为 `constrain()` 的函数，为具有多个引脚的GPIO端口之类的外设定义 `split()` 函数。该函数将消耗底层的原始外围设备结构，并返回具有更高级别API的新对象。这个API可能还会做一些事情，例如让串口`new`函数需要`Clock`结构体的借用，这个Clock结构体只能通过调用特定函数来生成,而这个函数会配置PLL并设置时钟频率。这样在没有先配置时钟频率的情况下，就不可能创建串口对象, 否则串口对象有可能将波特率误转换为错误的时钟滴答。一些crate甚至为每个GPIO引脚可以处于的状态定义了特殊的Trait，要求用户在将引脚传递到外设之前将其置于正确的状态(例如，通过选择适当的可选功能模式)。更重要的是,这些都是零成本抽象！

让我们来看一个例子：

    #![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();
        }
    }