作者:张汉东

学习笔记系列在线阅读地址:https://zhanghandong.github.io/raspberrypi-os-tutorials-notes/


这是我对官方 Operating System development tutorials in Rust on the Raspberry Pi 的学习笔记。以此来学习 Rust 裸机(Bare-Matel)编程。

学习笔记代码仓库:https://github.com/ZhangHanDong/raspberrypi-os-tutorials-notes,欢迎反馈交流。

注意:如果需要动手实现,请使用官方教程源码仓库配合本学习笔记。

当前已更新:

  • ARM 汇编基础
  • 00: 准备工作
  • 01: 循环等待
  • 02: 初始化执行环境(runtime)
  • 03: 硬核输出 Hello World
  • 04: 安全访问全局数据结构
  • 05: 驱动:GPIO 和 UART

本文节选第五章内容。


驱动:GPIO 和 UART

注意:这章不支持 BSP=rpi4 make qemu

一些前置知识

Arm 物理内存 vs Arm 虚拟地址空间

Arm 与物理内存等外设统一编址在4GB(32位)的地址空间中。而 x86 是将内存单独编址在一个地址空间,外设I/O端口在另外的地址空间,要访问IO地址空间需要用专门的指令操作。

Linux的每个进程都有4GB的虚拟地址空间,其中13GB是每个进程独占的用户空间,3GB4GB是所有进程共享的内核空间(0xC0000000~0xFFFFFFFF)。因此虚拟地址空间包括内核空间与用户空间。linux的实虚地址的转换就是有MMU通过页表的形式完成转换的。

Arm 虚拟地址空间布局是一个标准的linux kernel实现,地址空间被分割成 1G 内核空间和 3G 用户空间,内核空间地址范围为0xC0000000 - 0xEFFFFFFF, 用户空间地址范围为 0x00000000 - 0xBFFFFFFF

通过操作内存来访问外设,这种方式称为 memory-mapped IO

需要注意:外设(Peripherals)的物理地址空间为 0x20000000+16MB,一般在内核虚拟地址中被映射到0x7E000000+16MB

树莓派 4b bcm 2711 文档中描述了ARM所见地址之间的映射:

rpi4: 0xFE200000 - 0xFE2000b3 : gpio@ 0x7E200000

只需把 fe200000 减去GPIO-BaseAddr(0x00200000),就可获得树莓派的PeripheralBaseAddr(PBase):0xFE000000。

GPIO 与 UART

在学习这一章之前,还是需要一些前置知识的。对树莓派如果不了解的话,很难理解它的代码在做什么。这部分内容,如果你暂时不想看,或者已经了解过,可以跳过。需要的时候可以回头看看。

GPIO(General Purpose I/O Ports)意思为通用输入/输出端口,通俗地说,就是一些引脚,可以通过它们输出高低电平或者通过它们读入引脚的状态-是高电平或是低电平。GPIO是个比较重要的概念,用户可以通过GPIO口和硬件进行数据交互(如UART),控制硬件工作(如LED、蜂鸣器等),读取硬件的工作状态信号(如中断信号)等。GPIO口的使用非常广泛。掌握了GPIO,差不多相当于掌握了操作硬件的能力。

以树莓派 4B 为例,其 40 针脚见下图:

image.png

UART(Universal Asynchronous Receiver/Transmitter),是一种串行通信协议,其中数据是串行传输的,一次传输一个字节的数据,即逐位传输。作为把并行输入信号转成串行输出信号的芯片,UART 通常被集成于其他通讯接口的连结上。

为了和树莓派串口通信,我们将树莓派的 UART 针脚连接到个人电脑(下面简称 PC)上。

UART 的端口至少有 RX、TX 和地线三个针脚。RX 负责读取,TX 负责输出。如果有两个 UART 端口,它们的连接方式如下:

image.png

注意这里是:usb2ttl(RXD) <-> gpio(TXD) 以及 usb2ttl(TXD) <-> gpio(RXD)。另外注意,不要连接usb2tt 上的电源线到树莓派,小心烧坏树莓派。

uart 协议层

协议层中,规定了数据包的内容,它由起始位、主体数据、校验位以及停止位组成,通信双方的数据包格式要约定一致才能正常收发数据 。

  • 波特率:异步通信中由于没有时钟信号,所以2个通信设备需约定好波特率,常见的有4800、9600、115200等。
  • 通信的起始和停止信号:串口通信的一个数据包从起始信号开始,知道停止信号结束。数据包的起始信号由一个逻辑0的数据位表示,而数据包的停止信号可由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。
  • 有效数据:在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为8位或9位长。
  • 数据校验:在有效数据之后,有一个可选的数据校验位。由于数据通信相对容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)以及无校验(noparity)。
  • 奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为:01101001,此时总共有 4 个“1”,为达到奇校验效果,校验位为“1”,最后传输的数据将是 8 位的有效数据加上 1 位的校验位总共 9 位。偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。0 校验是不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”

uart 波特率计算

公式:

image.png

其中,fck为 USART 时钟, USARTDIV 是一个存放在波特率寄存器(USART_BRR)的一个无符号定点数。其中 DIV_Mantissa[11:0]位定义 USARTDIV 的整数部分,DIV_Fraction[3:0]位定义 USARTDIV 的小数部分。

例如:DIV_Mantissa=24(0x18)DIV_Fraction=10(0x0A),此时 USART_BRR 值为0x18A;那么USARTDIV的小数位10/16=0.625;整数位24,最终USARTDIV的值为24.625

波特率的常用值有 2400960019200115200

如何设定寄存器值得到波特率的值?

假设:串口设置为961200 8N1,即 波特率为 961200,8数据位,N表示没有校验位,1位停止位。

而在内核 config.txt中将时钟设置为 48MHZinit_uart_clock=48000000)。

那么波特率(baud)计算为: (48_000_000 / 16) / 921_600 = 3.2552083。这意味着整数部分(DIV_Mantissa)为3,小数部分是0.2552083

根据PL011技术参考手册进行的 DIV_Fraction计算: INTEGER( (0.2552083 * 64) + 0.5 ) = 16INTEGER代表取整。

因此生成的波特率分频器(baud rate divider)为 3 + 16/64 = 3.25,那么生成的波特率是48_000_000 / (16 * 3.25) = 923_077。误差率为 ((923_077 - 921_600) / 921_600) * 100 = 0.16%

那么反过来,如果想用波特率为 115200 8N1,则:

  • 整数部分:(48000000/16)/115200 = 26.0416666667
  • 小数部分: INTEGER((0.0416666667 * 64) + 0.5) = 3

设置正确的波特率比较重要。

树莓派启动流程

树莓派在设计的时候,为了节省成本,没有使用掉电非易失性存储介质,也就在板子上不能找到相关的flash。这样芯片启动的程序只能放在sd卡里面了。本来U盘启动也是一种方式,但是树莓派4代之前的设计并不是很好,导致这种启动方式不可以使用。树莓派4也有一些尝试,但是目前树莓派4最主流的方式还是使用SD卡启动。

Raspberry Pi 4具有一个SPI连接的EEPROM(4MBits / 512KB)。其中包含用于启动系统的代码,并替换了先前在SD卡的启动分区中找到的bootcode.bin。如果你的树莓派4通电了但是一直无法启动系统,且绿灯常亮不闪,需要检查两种情况:

  1. 有没有插入 sd 卡。
  2. 如果有sd卡,那么就是EEPROM被损坏了。你需要重新格式化sd卡,并去官网下载Recovery固件修复即可。

在树莓派裸机实验中,从 sd 卡启动系统必须包含必要的文件:

  1. bootcode.bin (树莓派4不需要,之前的需要)。
  2. config.txt
  3. kernel8.img,内核镜像
  4. start4.elf
  5. bcm2711-rip-4.dtb

启动流程:

  1. 芯片上电,执行固化在内部的 first-stage bootloader,用于加载sd卡中的bootcode.bin文件。但是在树莓派4上,不需要这个文件,因为有了有了SPI的EEPROM。
  2. 启动 GPU。ARM Cortex-A72 Core处于standby状态,VideoCore IV GPU Core负责启动系统。
  3. bootcode.bin读取到了128K大小的二级缓存(L2 Cache)中。开始执行bootcode.bin代码。用于初始化RAM,并且把start4.elf加载到内存中,并且去读取config.txt中的配置信息,设置这些配置信息。当然,树莓派4就没有bootcode.bin什么事了。
  4. bcm2711-rpi-4-b.dtb文件也是需要的,如果不存在也会影响串口的输出,也就是只会有乱码输出。所以推测start4.elf文件也会去读取设备树文件,然后设置一些基本的参数。

config.txt 配置信息

  1. enable_uart=1 // 表示使用的是miniUART
  2. arm_64bit=0 // 告诉arm要启动的程序是32位的
  3. core_freq=250 // 设置arm的频率
  4. kernel=kernel7.img // 表示从start4.elf执行的程序是哪一个
  5. kernel_address=0x8000 // 表示需要执行的内存地址,这个地址就是裸机程序在链接时的入口地址

这些配置并不是每个都必须,主要看实际情况。前期理解芯片的启动过程有助于对后面编写裸机代码的分析。

重要

以上概念,需要配合本章代码和真实的树莓派实机来验证才能有更深刻的领会。

代码释意

第五章算是一个里程碑。

前四章完成了从树莓派裸机到建立 Rust 执行环境,但都是基于QEMU。从第五章开始支持真实的树莓派执行内核代码。

所以,为了和真实的树莓派通信,我们需要实现两个驱动程序。

引导程序

引导程序基本和上一章代码一样。只是在 src/_arch/aarch64/cpu.rs 里增加了针对树莓派3的相关代码:

  1. pub use asm::nop;
  2. /// Spin for `n` cycles.
  3. #[cfg(feature = "bsp_rpi3")]
  4. #[inline(always)]
  5. pub fn spin_for_cycles(n: usize) {
  6. for _ in 0..n {
  7. asm::nop();
  8. }
  9. }

这个后面会用到。此处使用条件编译,指定bsp_rpi3 feature。使用汇编的nop操作实现cpu 空等待。

内核初始化

打开 src/main.rs ,看到 kernel_init 函数有了很大变化。

  1. // 因为只有单核(core0)被激活执行初始化代码,能保证正确执行顺序,所以现在是安全的
  2. /// # Safety
  3. ///
  4. /// - Only a single core must be active and running this function.
  5. /// - The init calls in this function must appear in the correct order.
  6. unsafe fn kernel_init() -> ! {
  7. // 此处增加了 驱动管理
  8. use driver::interface::DriverManager;
  9. // 迭代驱动实例初始化,如果失败则 panic
  10. for i in bsp::driver::driver_manager().all_device_drivers().iter() {
  11. if let Err(x) = i.init() {
  12. panic!("Error loading driver: {}: {}", i.compatible(), x);
  13. }
  14. }
  15. //
  16. bsp::driver::driver_manager().post_device_driver_init();
  17. // println! is usable from here on.
  18. // 这个是安全的函数
  19. // Transition from unsafe to safe.
  20. kernel_main()
  21. }
  22. /// The main function running after the early init.
  23. fn kernel_main() -> ! {
  24. use bsp::console::console;
  25. use console::interface::All;
  26. use driver::interface::DriverManager;
  27. println!(
  28. "[0] {} version {}",
  29. env!("CARGO_PKG_NAME"),
  30. env!("CARGO_PKG_VERSION")
  31. );
  32. println!("[1] Booting on: {}", bsp::board_name());
  33. // 打印驱动加载过程
  34. println!("[2] Drivers loaded:");
  35. for (i, driver) in bsp::driver::driver_manager()
  36. .all_device_drivers()
  37. .iter()
  38. .enumerate()
  39. {
  40. println!(" {}. {}", i + 1, driver.compatible());
  41. }
  42. println!(
  43. "[3] Chars written: {}",
  44. bsp::console::console().chars_written()
  45. );
  46. // 下面打印回显信息
  47. println!("[4] Echoing input now");
  48. // 进入回显模式之前,请丢弃所有接收到的噪音字符
  49. // Discard any spurious received characters before going into echo mode.
  50. console().clear_rx();
  51. loop {
  52. let c = bsp::console::console().read_char();
  53. bsp::console::console().write_char(c);
  54. }
  55. }

内存映射

MMIO 映射物理内存代码被定义在 src/bsp/raspberrypi/memory.rs中。

  1. //--------------------------------------------------------------------------------------------------
  2. // Public Definitions
  3. //--------------------------------------------------------------------------------------------------
  4. /// The board's physical memory map.
  5. #[rustfmt::skip]
  6. pub(super) mod map {
  7. pub const GPIO_OFFSET: usize = 0x0020_0000;
  8. pub const UART_OFFSET: usize = 0x0020_1000;
  9. /// Physical devices.
  10. #[cfg(feature = "bsp_rpi3")]
  11. pub mod mmio {
  12. use super::*;
  13. pub const START: usize = 0x3F00_0000;
  14. pub const GPIO_START: usize = START + GPIO_OFFSET;
  15. pub const PL011_UART_START: usize = START + UART_OFFSET;
  16. }
  17. // 注意,树莓派4 的物理内存基址为 0xFE00_0000 (前置知识有描述)
  18. /// Physical devices.
  19. #[cfg(feature = "bsp_rpi4")]
  20. pub mod mmio {
  21. use super::*;
  22. pub const START: usize = 0xFE00_0000;
  23. pub const GPIO_START: usize = START + GPIO_OFFSET;
  24. pub const PL011_UART_START: usize = START + UART_OFFSET;
  25. }
  26. }

驱动

在看 src/bsp/raspberrypi.rs 代码:

  1. //--------------------------------------------------------------------------------------------------
  2. // Global instances
  3. // 这里定义俩全局静态变量 GPIO 和 PL011_UART ,用于持有相应的基址。
  4. // --------------------------------------------------------------------------------------------------
  5. use super::device_driver;
  6. static GPIO: device_driver::GPIO =
  7. unsafe { device_driver::GPIO::new(memory::map::mmio::GPIO_START) };
  8. static PL011_UART: device_driver::PL011Uart =
  9. unsafe { device_driver::PL011Uart::new(memory::map::mmio::PL011_UART_START) };
  10. //--------------------------------------------------------------------------------------------------
  11. // Public Code
  12. //--------------------------------------------------------------------------------------------------
  13. /// Board identification.
  14. pub fn board_name() -> &'static str {
  15. #[cfg(feature = "bsp_rpi3")]
  16. {
  17. "Raspberry Pi 3"
  18. }
  19. #[cfg(feature = "bsp_rpi4")]
  20. {
  21. "Raspberry Pi 4"
  22. }
  23. }

接下来我们看一下 src/driver.rs 代码。

  1. // 为驱动定义一个 interface 模块,当命名空间使用
  2. // 该模块定义了两个 trait,规范了驱动的行为 和 管理操作
  3. /// Driver interfaces.
  4. pub mod interface {
  5. /// Device Driver functions.
  6. pub trait DeviceDriver {
  7. // 设备树基本属性之一,用于指定兼容的系统
  8. /// Return a compatibility string for identifying the driver.
  9. fn compatible(&self) -> &'static str;
  10. // 这段 Unsafe Rust 代码写的很标准
  11. // 初始化函数 init 为 unsafe 操作,因为在初始化的时候,驱动可能会对整个系统产生影响,所以这里加上 `#Safety` 注释来说明这种情况,并且为函数加上 `unsafe` 标签。
  12. // 整个函数是由内核调用以启动设备
  13. /// Called by the kernel to bring up the device.
  14. ///
  15. /// # Safety
  16. ///
  17. /// - During init, drivers might do stuff with system-wide impact.
  18. unsafe fn init(&self) -> Result<(), &'static str> {
  19. Ok(())
  20. }
  21. }
  22. /// Device driver management functions.
  23. /// `BSP` 应该提供一个全局实例
  24. /// The `BSP` is supposed to supply one global instance.
  25. pub trait DriverManager {
  26. // 返回所有实例化驱动的引用集合(切片)
  27. // 该函数返回 DeviceDriver trait对象,用 `'static`是因为该trait中方法返回都是 `'static str`。
  28. /// Return a slice of references to all `BSP`-instantiated drivers.
  29. ///
  30. /// # Safety
  31. /// 设备的顺序是调用`DeviceDriver::init()`的顺序
  32. /// - The order of devices is the order in which `DeviceDriver::init()` is called.
  33. fn all_device_drivers(&self) -> &[&'static (dyn DeviceDriver + Sync)];
  34. // 驱动程序初始化后运行的初始化代码
  35. /// Initialization code that runs after driver init.
  36. ///
  37. /// For example, device driver code that depends on other drivers already being online.
  38. fn post_device_driver_init(&self);
  39. }
  40. }

具体的驱动代码实现在 src/bsp/device_driver/bcm.rs 模块中。

先来看一下 src/bsp/device_driver/common.rs

  1. //! Common device driver code.
  2. use core::{marker::PhantomData, ops};
  3. //--------------------------------------------------------------------------------------------------
  4. // Public Definitions
  5. //--------------------------------------------------------------------------------------------------
  6. // 对 MMIO 指针地址做了一个 Rust 包装,此处引入 `PhantomData<fn() -> T>`,为的保证只能传入 `'static`的引用。
  7. pub struct MMIODerefWrapper<T> {
  8. start_addr: usize,
  9. phantom: PhantomData<fn() -> T>,
  10. }
  11. //--------------------------------------------------------------------------------------------------
  12. // Public Code
  13. //--------------------------------------------------------------------------------------------------
  14. impl<T> MMIODerefWrapper<T> {
  15. /// Create an instance.
  16. pub const unsafe fn new(start_addr: usize) -> Self {
  17. Self {
  18. start_addr,
  19. phantom: PhantomData,
  20. }
  21. }
  22. }
  23. // 为 `MMIODerefWrapper<T>` 实现 Deref ,作为智能指针使用,返回一个引用
  24. impl<T> ops::Deref for MMIODerefWrapper<T> {
  25. type Target = T;
  26. fn deref(&self) -> &Self::Target {
  27. unsafe { &*(self.start_addr as *const _) }
  28. }
  29. }

再看 src/bsp/device_driver/bcm/bcm2xxx_gpio.rs,这是 GPIO 驱动的具体实现。下面摘录一些只和树莓派4b相关的关键代码:

  1. use crate::{
  2. bsp::device_driver::common::MMIODerefWrapper, driver, synchronization,
  3. synchronization::NullLock,
  4. };
  5. // 此处用到了 `register-rs` 库,是一个类型安全的 MMIO 和 CPU 寄存器访问的库。
  6. use register::{mmio::*, register_bitfields, register_structs};
  7. // GPIO registers.
  8. // 下面连接是 GPIO 寄存器的一些规格文档
  9. // Descriptions taken from
  10. // - https://github.com/raspberrypi/documentation/files/1888662/BCM2837-ARM-Peripherals.-.Revised.-.V2-1.pdf
  11. // - https://datasheets.raspberrypi.org/bcm2711/bcm2711-peripherals.pdf
  12. // Raspberry Pi 3/4 具有两个UART器件:PL011 UART和 mini UART。
  13. // 而 PL011 UART连接到蓝牙模块,而 mini UART 用作主要 UART。
  14. // 但我们可以初始化 GPIO 寄存器来直接使用 PL011 UART 来替代 mini UART。
  15. // 此宏为 `register-rs` 库提供,用于定义 MMIO 寄存器
  16. register_bitfields! {
  17. u32,
  18. // 为了使用 PL011 UART
  19. // 需要将 GPFSEL1寄存器 的 FSEL14 和 FSEL15 位字段设置为与 AltFunc0 功能相对应的 0b100 地址。
  20. /// GPIO Function Select 1
  21. GPFSEL1 [
  22. /// Pin 15
  23. FSEL15 OFFSET(15) NUMBITS(3) [
  24. Input = 0b000,
  25. Output = 0b001,
  26. AltFunc0 = 0b100 // PL011 UART RX
  27. ],
  28. /// Pin 14
  29. FSEL14 OFFSET(12) NUMBITS(3) [
  30. Input = 0b000,
  31. Output = 0b001,
  32. AltFunc0 = 0b100 // PL011 UART TX
  33. ]
  34. ],
  35. // 为了使用 PL011 UART
  36. // 通过将GPPUD寄存器设置为0
  37. // 树莓派3需要,此处省略
  38. // ... ...
  39. // 为了使用 PL011 UART
  40. // 将GPIO_PUP_PDN_CNTRL_REG0 寄存器 GPIO_PUP_PDN_CNTRL15 和 GPIO_PUP_PDN_CNTRL14位字段设置为1来关闭 Pullup 来启用这些引脚。
  41. /// GPIO Pull-up / Pull-down Register 0
  42. ///
  43. /// BCM2711 only.
  44. GPIO_PUP_PDN_CNTRL_REG0 [
  45. /// Pin 15
  46. GPIO_PUP_PDN_CNTRL15 OFFSET(30) NUMBITS(2) [
  47. NoResistor = 0b00,
  48. PullUp = 0b01
  49. ],
  50. /// Pin 14
  51. GPIO_PUP_PDN_CNTRL14 OFFSET(28) NUMBITS(2) [
  52. NoResistor = 0b00,
  53. PullUp = 0b01
  54. ]
  55. ]
  56. }
  57. register_structs! {
  58. #[allow(non_snake_case)]
  59. RegisterBlock {
  60. (0x00 => _reserved1),
  61. (0x04 => GPFSEL1: ReadWrite<u32, GPFSEL1::Register>),
  62. (0x08 => _reserved2),
  63. (0x94 => GPPUD: ReadWrite<u32, GPPUD::Register>),
  64. (0x98 => GPPUDCLK0: ReadWrite<u32, GPPUDCLK0::Register>),
  65. (0x9C => _reserved3),
  66. (0xE4 => GPIO_PUP_PDN_CNTRL_REG0: ReadWrite<u32, GPIO_PUP_PDN_CNTRL_REG0::Register>),
  67. (0xE8 => @END),
  68. }
  69. }
  70. // 关联的MMIO寄存器的抽象
  71. /// Abstraction for the associated MMIO registers.
  72. type Registers = MMIODerefWrapper<RegisterBlock>;
  73. //--------------------------------------------------------------------------------------------------
  74. // Public Definitions
  75. //--------------------------------------------------------------------------------------------------
  76. pub struct GPIOInner {
  77. registers: Registers,
  78. }
  79. // Export the inner struct so that BSPs can use it for the panic handler.
  80. pub use GPIOInner as PanicGPIO;
  81. // GPIO 硬件抽象
  82. /// Representation of the GPIO HW.
  83. pub struct GPIO {
  84. inner: NullLock<GPIOInner>,
  85. }
  86. //--------------------------------------------------------------------------------------------------
  87. // Public Code
  88. //--------------------------------------------------------------------------------------------------
  89. impl GPIOInner {
  90. /// Create an instance.
  91. ///
  92. /// # Safety
  93. /// 此处用户必须确保提供正确的 MMIO start 地址,所以用 unsafe 标记函数
  94. /// - The user must ensure to provide a correct MMIO start address.
  95. pub const unsafe fn new(mmio_start_addr: usize) -> Self {
  96. Self {
  97. registers: Registers::new(mmio_start_addr),
  98. }
  99. }
  100. // 关闭引脚 14 和 15 的 pull-up/down
  101. /// Disable pull-up/down on pins 14 and 15.
  102. #[cfg(feature = "bsp_rpi4")]
  103. fn disable_pud_14_15_bcm2711(&mut self) {
  104. self.registers.GPIO_PUP_PDN_CNTRL_REG0.write(
  105. GPIO_PUP_PDN_CNTRL_REG0::GPIO_PUP_PDN_CNTRL15::PullUp
  106. + GPIO_PUP_PDN_CNTRL_REG0::GPIO_PUP_PDN_CNTRL14::PullUp,
  107. );
  108. }
  109. // 将 PL011 UART 映射为标准输出
  110. /// Map PL011 UART as standard output.
  111. ///
  112. /// TX to pin 14
  113. /// RX to pin 15
  114. pub fn map_pl011_uart(&mut self) {
  115. // Select the UART on pins 14 and 15.
  116. self.registers
  117. .GPFSEL1
  118. .modify(GPFSEL1::FSEL15::AltFunc0 + GPFSEL1::FSEL14::AltFunc0);
  119. // Disable pull-up/down on pins 14 and 15.
  120. #[cfg(feature = "bsp_rpi3")]
  121. self.disable_pud_14_15_bcm2837();
  122. #[cfg(feature = "bsp_rpi4")]
  123. self.disable_pud_14_15_bcm2711();
  124. }
  125. }
  126. impl GPIO {
  127. /// Create an instance.
  128. ///
  129. /// # Safety
  130. ///
  131. /// - The user must ensure to provide a correct MMIO start address.
  132. pub const unsafe fn new(mmio_start_addr: usize) -> Self {
  133. Self {
  134. inner: NullLock::new(GPIOInner::new(mmio_start_addr)),
  135. }
  136. }
  137. /// Concurrency safe version of `GPIOInner.map_pl011_uart()`
  138. pub fn map_pl011_uart(&self) {
  139. self.inner.lock(|inner| inner.map_pl011_uart())
  140. }
  141. }
  142. //------------------------------------------------------------------------------
  143. // OS Interface Code
  144. //------------------------------------------------------------------------------
  145. use synchronization::interface::Mutex;
  146. // 注意,这里还有个 init 方法使用默认实现。
  147. impl driver::interface::DeviceDriver for GPIO {
  148. fn compatible(&self) -> &'static str {
  149. "BCM GPIO"
  150. }
  151. }

接下来看 src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs中 PL011 uart 的驱动实现。

只摘录关键代码聊聊。定义寄存器和 GPIO 驱动类似。有一些关于 uart 读取的代码就没贴出来。

  1. use register::{mmio::*, register_bitfields, register_structs};
  2. impl PL011UartInner {
  3. // ... ...
  4. pub fn init(&mut self) {
  5. self.flush();
  6. // Turn the UART off temporarily.
  7. self.registers.CR.set(0);
  8. // Clear all pending interrupts.
  9. self.registers.ICR.write(ICR::ALL::CLEAR);
  10. // Set the baud rate, 8N1 and FIFO enabled.
  11. // 这里设置波特率,关于波特率的计算可以查看本章前置知识
  12. // 这里有一个坑:
  13. // 现在这个注释的波特率设置最终值为 `921600`,
  14. // 但是实际在真实树莓派硬件执行的时候,
  15. // 有的 utf2ttl 可能不支持这么高的波特率,所以可能会出现乱码。
  16. // 如果遇到乱码输出可以尝试将波特率改为 115200 ,对应设置(26,3)
  17. // self.registers.IBRD.write(IBRD::BAUD_DIVINT.val(3));
  18. // self.registers.FBRD.write(FBRD::BAUD_DIVFRAC.val(16));
  19. self.registers.IBRD.write(IBRD::BAUD_DIVINT.val(26));
  20. self.registers.FBRD.write(FBRD::BAUD_DIVFRAC.val(3));
  21. self.registers
  22. .LCR_H
  23. .write(LCR_H::WLEN::EightBit + LCR_H::FEN::FifosEnabled);
  24. // Turn the UART on.
  25. self.registers
  26. .CR
  27. .write(CR::UARTEN::Enabled + CR::TXE::Enabled + CR::RXE::Enabled);
  28. }
  29. }
  30. // ... ...
  31. impl driver::interface::DeviceDriver for PL011Uart {
  32. fn compatible(&self) -> &'static str {
  33. "BCM PL011 UART"
  34. }
  35. // 这里使用了同步锁,在当前示例下,不需要这个也可以
  36. // 因为内核初始化的时候只绑定了单核
  37. unsafe fn init(&self) -> Result<(), &'static str> {
  38. self.inner.lock(|inner| inner.init());
  39. Ok(())
  40. }
  41. }

驱动管理

驱动定义好以后,就可以管理了。看 src/bsp/raspberrypi/driver.rs 中代码:

  1. // 定义设备驱动管理器,这里是两个元素的固定大小数组
  2. /// Device Driver Manager type.
  3. struct BSPDriverManager {
  4. device_drivers: [&'static (dyn DeviceDriver + Sync); 2],
  5. }
  6. // 创建一个静态常量,持有 GPIO 和 PL011_UART 的引用地址
  7. static BSP_DRIVER_MANAGER: BSPDriverManager = BSPDriverManager {
  8. device_drivers: [&super::GPIO, &super::PL011_UART],
  9. };
  10. //--------------------------------------------------------------------------------------------------
  11. // Public Code
  12. //--------------------------------------------------------------------------------------------------
  13. /// Return a reference to the driver manager.
  14. pub fn driver_manager() -> &'static impl driver::interface::DriverManager {
  15. &BSP_DRIVER_MANAGER
  16. }
  17. //------------------------------------------------------------------------------
  18. // OS Interface Code
  19. //------------------------------------------------------------------------------
  20. use driver::interface::DeviceDriver;
  21. // 实现 DriverManager trait
  22. impl driver::interface::DriverManager for BSPDriverManager {
  23. fn all_device_drivers(&self) -> &[&'static (dyn DeviceDriver + Sync)] {
  24. &self.device_drivers[..]
  25. }
  26. // 在 GPIO 启动以后执行 映射 pl011_uart 引脚
  27. fn post_device_driver_init(&self) {
  28. // Configure PL011Uart's output pins.
  29. super::GPIO.map_pl011_uart();
  30. }
  31. }

代码在真实树莓派上执行过程

如果没有树莓派,在 qemu 也可以。但是如果在真实硬件上启动内核,需要注意避开一些坑。我分享一下我的流程:

  1. 在测试内核之前,先安装树莓派官方的 Respbian OS 操作系统。这样做可以完整测试树莓派硬件的功能完整性,为后续启动 rust 实现的内核排坑。
  2. 如果遇到无法启动 OS 的情况,观察绿灯是否闪烁,排除是不是EEPROM损坏了。
  3. 如果遇到输出字符乱码,请确认内核设置的波特率,和你的串口调试工具波特率是否一致。
  4. 确保 usb2ttl RX/TX 连线对接正确。并且 usb2ttl 驱动安装正确。
  5. 烧录工具推荐:balenaetcher。官方推荐的不太好使。如果展示烧录失败,可以点击 skip 跳过 验证那一环节。
  6. 串口输出推荐使用 Rust 实现的这个工具:https://github.com/umaYnit/rust-serial-tool。不推荐 官方教程自带的这个 Ruby 实现,因为在 mac m1下有点坑。

小结

前五章,以一个非常短小精悍的 Rust 内核程序,为我们展示了如何在树莓派裸机上运行 Rust 代码。为后面的操作系统实现奠定了初步的基础。

在实际树莓派硬件上执行的时候,遇到坑不要轻易放弃,多半是因为知识经验不足导致的,此时正是学习的机会。

参考

  1. https://www.raspberrypi.com.tw/tutorial/faq/
  2. https://zhuanlan.zhihu.com/p/136806005
  3. https://github.com/bztsrc/raspi3-tutorial
  4. https://datasheets.raspberrypi.org/bcm2711/bcm2711-peripherals.pdf
  5. https://e-mailky.github.io/2016-12-06-dts-introduce