前言

本教材仍在撰写和调整中

第零章:环境配置与预备知识

0.1 硬件平台介绍

飞腾派开发板硬件平台详解

飞腾派是由萤火工场研发的国产自主可控开源硬件平台,定位于工业控制、嵌入式开发及教育领域,兼具高性能与丰富外设接口,适配多种操作系统生态。

1. 处理器与架构

  • 核心配置:搭载飞腾定制四核处理器,采用ARM v8指令集,包含:
    • 2×FTC664核(主频1.8GHz)
    • 2×FTC310核(主频1.5GHz)
  • 架构特性
    • 支持大小核协同调度
    • 集成硬件虚拟化支持
    • 适用于实时操作系统(RTOS)及边缘计算任务

2. 内存与存储

  • 内存
    • 板载64位DDR4内存
    • 提供2GB/4GB双版本
    • 注:4GB版本采用鑫存储颗粒,2GB版本采用兆易创新颗粒
  • 存储
    • 支持MicroSD卡或eMMC模块启动
    • 默认从SD卡加载系统
    • 推荐使用≥16GB容量的存储介质

3. 网络与无线连接

  • 有线网络
    • 双路千兆以太网接口(RJ45)
    • 支持DHCP自动分配IP或静态配置
  • 无线通信
    • 板载WiFi 6(2.4G/5G双频)
    • 蓝牙4.2/BLE4.2
    • 集成陶瓷天线
  • 移动网络扩展
    • 通过Mini-PCIe接口可扩展4G/5G通信模组

4. 扩展接口与多媒体能力

高速接口

  • Mini-PCIe×1:支持AI加速卡(如NPU模块)、4G/5G模组等扩展
  • USB接口
    • USB 3.0 Host×1
    • USB 2.0 Host×3

工业接口

  • CAN FD×2:用于工业总线通信
  • UART调试口×2
  • **MIO(多功能IO)**×2:可配置为UART/I2C模式
  • GPIO×29:支持自定义外设驱动开发

音视频输出

  • HDMI
    • 最高支持1080P@60fps视频输出
    • 兼容H.264/H.265硬解码
  • 3.5mm音频接口:支持音频播放与录制

5. 物理规格与供电

  • 尺寸:119mm × 93mm(紧凑型设计)
  • 供电
    • 12V/3A直流电源
    • 工作温度范围:0~50℃
  • 散热:需外接风扇(注意出风方向与电源极性)

6. 操作系统支持与开源生态

  • 目前兼容系统
    • Debian 11(Phytium Pi OS)
    • Ubuntu
    • OpenKylin
    • RT-Thread
  • 开发工具链
    • Python、Qt、OpenCV等
    • 适配机器视觉(如Halcon)及AI框架
  • 社区资源
    • 开源硬件设计文档
    • 萤火工场社区软件仓库

为何选择飞腾派进行ARCEOS驱动开发?

  • 国产化适配:全国产处理器架构,符合信创场景需求
  • 外设驱动全覆盖:丰富接口(CAN、I2C、SPI等)为驱动开发提供完整硬件基础
  • 实时性扩展:支持Xenomai/Linux-RT,便于验证实时任务调度性能

硬件资源参考
萤火工场官方文档

0.2 开发环境准备

0.2.1 运行环境

基础运行环境可以参考如下网址,搭建wsl+qemu的开发环境

https://rcore-os.cn/arceos-tutorial-book/ch01-00.html

vscode使用

由于是使用的wsl(inux子系统)作为基础编译环境,默认提供的是命令行搭配vim的方式来编辑文件。这可能并不是大部分人喜欢的开发环境,所以推荐使用vscode搭配remote-ssh插件来进行开发,并且该插件对于wsl是有很好的兼容性的,安装插件后直接选择连接wsl即可使用。

安装remote-ssh插件 连接到wsl 打开远程文件夹

qemu 补充

在第一节的指导手册上使用的qemu版本是7.0.2,而很多模拟的外设是在后续版本才加到qemu中的。所以推荐从官网安装最新版或指定版本qemu官网

以10.0.2 版本为例子,使用如下命令即可完成qemu的安装。

wget https://download.qemu.org/qemu-10.0.2.tar.xz
tar xvJf qemu-10.0.2.tar.xz
cd qemu-10.0.2
make
make install

0.2.2 飞腾派上运行Arceos

首先运行Arceos需要依赖其他系统来提供uboot,所以运行Arceos第一步是先烧录提供的飞腾派OS。

烧录飞腾派OS

https://pan.baidu.com/s/1pStiyqohrB3SxHAFFk8R6Q (提取码:dzdv)

注释: 5-系统镜像/1-PhytiumPIOS(基于Debian)/phytiumpiosv2.1资料包/4G内存-optee版/sdcard.img.7z,解压缩。

下载解压后使用烧录工具将系统镜像烧录到TF卡,之后将TF插到飞腾派的卡槽中,最后连接电源线上电。 烧录工具推荐使用 balenaEtcher
下载地址 也可以使用win32 disk image 下载地址

注释: 常用的手机以及派上的小卡正式名叫做TF或者说microSD 大的是SD卡当然统称为sd卡也是可以的 具体区别可查看 TF卡与SD卡

注意引导方式选择从sd卡启动!!! 启动模式

注释: 如果使用的是带emmc的版本可以直接从emmc启动,是当前版本的麒麟os启动会禁用风扇请注意,过热可能会损坏飞腾派。

启动后连接串口,可以看到如下打印即说明系统成功启动, phyiumpi 账号:root 密码:root

编译及运行

首先快速验证请参考这个链接,可以通过ostool来快速验证当前开发环境是完整可用的,避免后面在进行了较多code后因为难以验证而放弃。

最快速验证

下载Arceos

git clone  https://github.com/rcore-os/arceos.git

编译Arceos

make A=examples/helloworld ARCH=aarch64 PLATFORM=aarch64-phytium-pi  FEARURES="irq" SMP=4 LOG=info

之后会在example/helloword 文件夹内生成对应的可执行文件helloworld_aarch64-phytium-pi.bin 将该文件拷贝到u盘中后将u盘插入飞腾派,之后重启飞腾派并在重启过程中在串口工具中输入enter以进入cmd模式,成功进入后串口打印如下图所示。 命令行模式

执行如下指令即可将本地编译的Arceos部署到飞腾派上运行

usb start
fatload usb 0 0x90000000 helloworld_aarch64-phytium-pi.bin
go 0x90000000

运行Arceos

0.3 前置知识引导

本章节主要针对在飞腾派上对arceos开发驱动所需的一些前置知识进行简单的介绍

What is Arceos?

Arceos是基于组件化思想构造、以 Rust 为主要开发语言、Unikernel 形态的操作系统。与传统操作系统的构建方式不同,组件是构成 ArceOS 的基本元素。

What is Rust?

Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目。从 2009 年开始,得到了 Mozilla 研究院的资助,2010 年项目对外公布,2010 ~ 2011 年间实现自举。自此以后,Rust 在部分重构 -> 崩溃的边缘反复横跳(历程极其艰辛),终于,在 2015 年 5 月 15 日发布 1.0 版。

  • 相比 Go 语言,Rust 语言表达能力更强,性能更高。同时线程安全方面 Rust 也更强,不容易写出错误的代码。包管理 Rust 也更好,Go 虽然在 1.10 版本后提供了包管理,但是目前还比不上 Rust 。

  • 相比 C++ 语言,Rust 与 C++ 的性能旗鼓相当,但是在安全性方面 Rust 会更优,特别是使用第三方库时,Rust 的严格要求会让三方库的质量明显高很多。

  • 相比 Java 语言,除了极少数纯粹的数字计算性能,Rust 的性能全面领先于 Java 。同时 Rust 占用内存小的多,因此实现同等规模的服务,Rust 所需的硬件成本会显著降低。

这里是部分 Rust 学习相关资料的链接:

  • Rust程序设计语言中文版(https://www.rustwiki.org.cn/zh-CN/book/ch01-02-hello-world.html)
  • Rust语言圣经(https://course.rs/into-rust.html)
  • Rustling(编程题库,可以在实践中对照学习具体用法,具体练习方法在仓库文档中)(https://github.com/LearningOS/rustling-25S-template)

What is Unikernel?

Unikernel 是操作系统内核设计的一种架构(或称形态),从下图对比可以看出它与其它内核架构的显著区别: Unikernel 相对其它内核架构有三个特点:

单特权级:应用与内核都处于同一特权级 - 即内核态,这使得应用在访问内核时,不需要特权级的切换。 单地址空间:应用没有单独的地址空间,而是共享内核的地址空间,所以在运行中,也不存在应用与内核地址空间切换的问题。 单应用:整个操作系统有且仅有一个应用,所以没有多应用之间隔离、共享及切换的问题。 所以相对于其它内核架构,Unikernel 设计实现的复杂度更低,运行效率相对较高,但在安全隔离方面,它的能力最弱。Unikernel 有它适合的特定的应用领域和场景。

ArceOS 选择 Unikernel 作为起步,希望为将来支持其它的内核架构建立基础。本实验指导正是对应这一阶段,从零开始一步一步的构建 Unikernel 形态的操作系统。Unikernel 本身这种简化的设计,可以让我们暂时忽略那些复杂的方面,把精力集中到最核心的问题上。

上图就是 ArceOS 的整体架构,由apps、crates、modules组成

  • apps: 应用程序。它的运行需要依赖于modules组件库。
  • modules: ArceOS的组件库。
  • crates: 通用的基础库。为modules实现提供支持。

本开发手册主要针对 ArceOS 在 Phytium-Pi上开发驱动进行辅助说明,因此对于apps部分不作过多说明,主要对于目前已实现的crates和modules进行说明以辅助开发人员查询

Crates

  • allocator: 内存分配算法,包括:bitmap、buddy、slab、tlsf。
  • arm_gic: ARM通用中断控制器 (GICv2) 。
  • arm_pl011: ARM串行通信接口,用于处理器和外部设备之间的串行通信 。
  • axerrno: ArceOS的错误码定义。
  • axfs_devfs: ArceOS的设备(Device)文件系统,是axfs_vfs一种实现。
  • axfs_ramfs: ArceOS的内存(RAM)文件系统,是axfs_vfs一种实现。
  • axfs_vfs: ArceOS的虚拟文件系统接口。
  • axio: no_std环境下的I/O traits 。
  • capability: Capability-based security 通过设置访问权限控制对系统资源的访问。
  • crate_interface: 提供一种在 crate 中定义接口(特征)的方法,其目的是解决循环依赖。
  • driver_block: 通用的块存储(磁盘)驱动程序的接口定义。
  • driver_common: ArceOS的通用设备驱动接口定义,包括:disk、serial port、 ethernet card、GPU。
  • driver_display: 通用的图形设备驱动程序接口定义。
  • driver_net: 通用的网络设备 (NIC) 驱动程序定义。
  • driver_pci: 定义对PCI总线操作。
  • driver_virtio: 实现在driver_common定义的驱动。
  • flatten_objects: 为每个存储对象分配一个唯一的ID。
  • handler_table: 无锁的事件处理程序表。
  • kernel_guard: 利用RAII创建具有本地IRQ或禁用抢占的临界区,用于在内核中实现自旋锁。
  • lazy_init: 延迟初始化。
  • linked_list: 链表。
  • memory_addr: 提供理物理和虚拟地址操作的辅助函数。
  • page_table: 页表。
  • page_table_entry: 页表项。
  • percpu: per-CPU的数据结构。
  • percpu_macros: per-CPU的数据结构的宏实现。
  • ratio: 比率相关计算。
  • scheduler: 统一的调度算法接口,包括:cfs、fifo、round_robin。
  • slab_allocator: no_std 环境下的 Slab 分配器(一种内存管理算法)。
  • spinlock: no_std 环境下的自旋锁实现。
  • timer_list: 定时器,在计时器到期时触发。
  • tuple_for_each: 提供遍历tuple字段的宏和方法。

crates可以在 https://crates.io/ 进行具体查询

Modules

  • axalloc: ArceOS 的全局内存分配器.
  • axconfig: ArceOS 特定平台编译的常量和参数配置。
  • axdisplay: ArceOS 的图形化模块。
  • axdma: ArceOS 中为需要直接内存访问的设备驱动提供DMA支持。
  • axdriver: ArceOS 的设备驱动模块。
  • axfs: ArceOS 的文件系统模块。
  • axhal: ArceOS硬件抽象层,为特定平台的操作提供统一的API。
  • axlog: ArceOS 多个级别日志记录宏,包括:error、warn、info、debug、trace。
  • axmm: ArceOS 的内存管理模块,提供虚拟内存地址空间抽象,支持线性映射和按需分配映射
  • axnet: ArceOS 的网络模块,包括:IpAddr、TcpSocket、UdpSocket、DnsSocket等。
  • axns: ArceOS 中用于命名空间管理功能的模块
  • axruntime: ArceOS 的运行时库,负责系统启动和初始化序列,协调其他模块的初始化过程,是应用程序运行的基础环境。
  • axsync: ArceOS 提供的同步操作模块,包括:Mutex、spin。
  • axtask: ArceOS 的任务调度管理模块,包括:任务创建、调度、休眠、销毁等。

值得注意的是,并非所有模块都是必需的,其中axruntime、axhal、axconfig、axlog在所有构建中都会被启用并编译,而其他模块则会根据启动的功能特性进行选择性的编译,使得ArceOS可以根据不同的需求进行定制化构建

ArceOS最新主线仓库(https://github.com/arceos-org/arceos)

Phytium-Pi

  • 顶层接口视图与说明
  • 底层接口视图与说明

第一章:硬件控制类驱动

1.1 GPIO驱动开发

GPIO 介绍

GPIOGeneral Purpose Input Output(通用输入/输出) 的缩写,也就意味着这种类型的外设可以配置为多种输入/输出类型。单根GPIO的模型可以简单理解为一根导线。 导线的一端留给硬件工程师,他们可以将这一端任意的连接到他们想要的地方,然后告诉驱动工程师,他们想要 ”这根线“ 起到什么作用;导线的一端连接到cpu核心,驱动工程师通过cpu配置这个模块为指定的功能。

一般来说,GPIO可以用于获取某个的高低电平,作为cpu中断触发源等等。 下面的两个实验将分别验证gpio的 irq输出 工作原理。

memory mapped io

关于这个的实现原理,本章不做过多的解释,否则篇幅将会过长。简单的来说,有了这个技术之后,外设对于cpu来说就是为一块物理内存,cpu可以像操作内存一样来操作设备。在没有这个技术之前,一些古老的设备,像intel的16位cpu,必须使用in/out一些特殊的端口来访问外设。

qemu平台关机实验

对于一些简单的设备,qemu能够很好的进行模拟。因此,对于部分没有开发板而想尝试进行驱动开发学习的同学,我们提供了基于qemu的部分实验。

实验原理

  • gpio 模块介绍

    本次实验使用的gpio模块为pl061, 它是arm提供的一个集成化的gpio模块,具有8个引脚,具备常见的gpio功能, qemu也能对这个设备进行模拟(可以使用 qemu-system-aarch64 --device help 来查看qemu支持的设备)。

  • pl061 相关寄存器介绍(注意:不同的外设有不同的寄存器,对于驱动工程师来说,阅读外设datasheet是必不可少的技能。所以建议暂时先跳过本节,阅读后再来验证自己的想法)

    • GPIOISGPIOIEV

      这两个寄存器的宽度都为为8bit,对应8个引脚。这两个寄存器共同决定了某个引脚的中断触发方法。他们按如下的方式决定中断类型。

      GPIOIS_iGPIOEV_i引脚i中断方式
      00下降沿触发
      01上升沿触发
      10低电平触发
      11高电平触发
  • GPIOIE

    全称是 PrimeCell GPIO interrupt enable register,翻译过来就是中断使能寄存器。宽度为8bit,对应8个引脚。每一个bit用来配置对应引脚的是否使能中断,1是使能,0是不使能。

实验过程

  • 在arceos代码仓库下,使用example为helloworld,先尝试运行得到以下结果。

    运行结果 arceos git:(main)✗ make A=examples/helloworld PLATFORM=aarch64-qemu-virt ARCH=aarch64 LOG=debug FEATURES="driver-ramdisk,irq" run ACCEL=n GRAPHIC=n
      ... # skip part build log
      axconfig-gen configs/defconfig.toml configs/platforms/aarch64-qemu-virt.toml  -w smp=1 -w arch=aarch64 -w platform=aarch64-qemu-virt -o "/Users/jp/code/arceos/.axconfig.toml" -c "/Users/jp/code/arceos/.axconfig.toml"
      Building App: helloworld, Arch: aarch64, Platform: aarch64-qemu-virt, App type: rust
      cargo -C examples/helloworld build -Z unstable-options --target aarch64-unknown-none-softfloat --target-dir /Users/jp/code/arceos/target --release  --features "axstd/log-level-debug axstd/driver-ramdisk axstd/irq"
      Finished `release` profile [optimized] target(s) in 0.08s
      rust-objcopy --binary-architecture=aarch64 examples/helloworld/helloworld_aarch64-qemu-virt.elf --strip-all -O binary examples/helloworld/helloworld_aarch64-qemu-virt.bin
      Running on qemu...
      qemu-system-aarch64 -m 128M -smp 1 -cpu cortex-a72 -machine virt -kernel examples/helloworld/helloworld_aarch64-qemu-virt.bin -nographic
    
          d8888                            .d88888b.   .d8888b.
          d88888                           d88P" "Y88b d88P  Y88b
          d88P888                           888     888 Y88b.
      d88P 888 888d888  .d8888b  .d88b.  888     888  "Y888b.
      d88P  888 888P"   d88P"    d8P  Y8b 888     888     "Y88b.
      d88P   888 888     888      88888888 888     888       "888
      d8888888888 888     Y88b.    Y8b.     Y88b. .d88P Y88b  d88P
      d88P     888 888      "Y8888P  "Y8888   "Y88888P"   "Y8888P"
    
      arch = aarch64
      platform = aarch64-qemu-virt
      target = aarch64-unknown-none-softfloat
      build_mode = release
      log_level = debug
      smp = 1
    
      [  0.001902 0 axruntime:130] Logging is enabled.
      [  0.002488 0 axruntime:131] Primary CPU 0 started, dtb = 0x44000000.
      [  0.002738 0 axruntime:133] Found physcial memory regions:
      [  0.002968 0 axruntime:135]   [PA:0x40200000, PA:0x40206000) .text (READ | EXECUTE | RESERVED)
      [  0.003304 0 axruntime:135]   [PA:0x40206000, PA:0x40209000) .rodata (READ | RESERVED)
      [  0.003502 0 axruntime:135]   [PA:0x40209000, PA:0x4020d000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
      [  0.003714 0 axruntime:135]   [PA:0x4020d000, PA:0x4024d000) boot stack (READ | WRITE | RESERVED)
      [  0.003892 0 axruntime:135]   [PA:0x4024d000, PA:0x40250000) .bss (READ | WRITE | RESERVED)
      [  0.004080 0 axruntime:135]   [PA:0x40250000, PA:0x48000000) free memory (READ | WRITE | FREE)
      [  0.004290 0 axruntime:135]   [PA:0x9000000, PA:0x9001000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.004482 0 axruntime:135]   [PA:0x9100000, PA:0x9101000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.004662 0 axruntime:135]   [PA:0x8000000, PA:0x8020000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.004806 0 axruntime:135]   [PA:0xa000000, PA:0xa004000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.004948 0 axruntime:135]   [PA:0x10000000, PA:0x3eff0000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.005098 0 axruntime:135]   [PA:0x4010000000, PA:0x4020000000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.005284 0 axruntime:150] Initialize platform devices...
      [  0.005420 0 axhal::platform::aarch64_common::gic:51] Initialize GICv2...
      [  0.006258 0 axruntime:176] Initialize interrupt handlers...
      [  0.006466 0 axhal::irq:32] irq=30 enabled
      [  0.006830 0 axruntime:188] Primary CPU 0 init OK.
      Hello, world!
      [  0.007086 0 axruntime:201] main task exited: exit_code=0
      [  0.007248 0 axhal::platform::aarch64_common::psci:98] Shutting down...
    
  • 由于目前arceos是unikernel模式,特权级为el1,所以可以直接在 main.c 中操作设备地址(需要注意的是,这不是一种正确的做法。但对于初学者,为了不在一开始就去研究arecos的复杂代码框架,可以短暂的把实现代码写在这儿。),将pl061模块的三号引脚配置为irq模式。

    #![allow(unused)]
    fn main() {
    // examples/helloworld/main.c
    ...
    /// 0x9030000 是 qemu模拟的aarch64-qemu-virt机器的 pl061模块的基地址(物理地址)。
    /// irq number 39 从 aarch64-qemu-virt机器的设备树中找到,7 + 外部中断base(32) = 39
    unsafe fn set_gpio_irq_enable() {
        // PHYS_VIRT_OFFSET 是 arceos 初始化时,将物理内存映射时进行的偏移。
        let base_addr = (0x9030000 + PHYS_VIRT_OFFSET) as *mut u8;
        // pl061的3号引脚
        let pin = 3;
        // 将interrupt设置为边缘触发
        let gpio_is = base_addr.add(0x404);
        *gpio_is = *gpio_is & !(1 << pin);
        // 设置触发事件
        let gpio_iev = base_addr.add(0x40c);
        *gpio_iev = *gpio_iev & !(1 << pin);
        
        // 设置中断使能
        let gpio_ie = base_addr.add(0x410);
        *gpio_ie = 0;
        *gpio_ie = *gpio_ie | (1 << pin);
    
        fn shut_down() {
           println!("shutdown function called");
           unsafe {
               let base_addr = (0x9030000 + PHYS_VIRT_OFFSET) as *mut u8;
               let pin = 3;
               // clear interrupt
               let gpio_ic = base_addr.add(0x41c);
               *gpio_ic = (1 << pin);
               // 关机命令
               core::arch::asm!(
                   "mov w0, #0x18;
                   hlt #0xf000"
               )
           }
        };
        register_handler(39, shut_down);
        println!("set irq done");
    }
    }
  • 并将中断号注册到 GIC(generic interrupt controller)中,中断号是39。

    #![allow(unused)]
    fn main() {
    // examples/helloworld/main.c
    ...
    /// 0x9030000 是 qemu模拟的aarch64-qemu-virt机器的 pl061模块的基地址(物理地址)。
    /// irq number 39 从 aarch64-qemu-virt机器的设备树中找到,7 + 外部中断base(32) = 39
    unsafe fn set_gpio_irq_enable() {
       // PHYS_VIRT_OFFSET 是 arceos 初始化时,将物理内存映射时进行的偏移。
    let base_addr = (0x9030000 + PHYS_VIRT_OFFSET) as *mut u8;
    // pl061的3号引脚
    let pin = 3;
    // 将interrupt设置为边缘触发
    let gpio_is = base_addr.add(0x404);
    *gpio_is = *gpio_is & !(1 << pin);
    // 设置触发事件
    let gpio_iev = base_addr.add(0x40c);
    *gpio_iev = *gpio_iev & !(1 << pin);
    
    // 设置中断使能
    let gpio_ie = base_addr.add(0x410);
    *gpio_ie = 0;
    *gpio_ie = *gpio_ie | (1 << pin);
    
        fn shut_down() {
            println!("shutdown function called");
            unsafe {
                let base_addr = (0x9030000 + PHYS_VIRT_OFFSET) as *mut u8;
                let pin = 3;
                // clear interrupt
                let gpio_ic = base_addr.add(0x41c);
                *gpio_ic = (1 << pin);
                // 关机命令,可以用别的函数替代
                core::arch::asm!(
                    "mov w0, #0x18;
                    hlt #0xf000"
                )
            }
        };
        // register handler 会同时将注册和在gic中使能中断完成。
        register_handler(39, shut_down);
        println!("set irq done");
    }
    }
  • 在main中死循环,等待gpio触发中断。

    #[cfg_attr(feature = "axstd", unsafe(no_mangle))]
    fn main() {
      println!("Hello, world!");
      unsafe {
          set_gpio_irq_enable();
      }
      println!("loop started!");
      loop {
          sleep(time::Duration::from_millis(10));
      }
    }
  • 重新执行第一步命令,若无报错,输入 ctrl + a + c 进入qemu的console模式,输入system_powerdown时,qemu会模拟一次中断。

    ...
      [  0.005842 0 axhal::platform::aarch64_common::gic:51] Initialize GICv2...
      [  0.006358 0 axruntime:176] Initialize interrupt handlers...
      [  0.006554 0 axhal::irq:32] irq=30 enabled
      [  0.007232 0 axruntime:188] Primary CPU 0 init OK.
      Hello, world!
      GPIORIS=0x0
      [  0.007688 0 axhal::irq:32] irq=39 enabled
      set irq done
      loop started!
      QEMU 9.2.0 monitor - type 'help' for more information
      (qemu) syst
      system_powerdown  system_reset      system_wakeup     
      (qemu) system_powerdown 
    
  • GIC 会将中断分发给 arm 某个核心(由于我们是单核,不存在分发), cpu对我们注册的关机函数进行回调。

      ...
      (qemu) system_powerdown 
      (qemu) shutdown function called
      [ 50.194414 0 axruntime::lang_items:5] panicked at /Users/jp/.cargo/registry/src/mirrors.ustc.edu.cn-38d0e5eb5da2abae/axcpu-0.1.0/src/aarch64/trap.rs:112:13:
      Unhandled synchronous exception @ 0xffff0000402010b0: ESR=0x2000000 (EC 0b000000, ISS 0x0)
      [ 50.195002 0 axhal::platform::aarch64_common::psci:98] Shutting down...
    

飞腾派点灯实验

实验原理

基本思想是将GPIO配置为作为输出模式,对应的,这个GPIO可以输出为高电平或者低电平。我们都学过初中物理,知道当一个led灯两侧有足够的电压和电流的时候,它就会亮。不过一般的GPIO线输出电流能力都不强,不足以驱动一个led灯。所以一般会用以下两种方式来实现:

  • led正极接电源,负极接gpio。gpio输出为低时,led点亮。
  • led接 mos 管的gate。一般来说,GPIO为高电压时,会使得mos管闭合,led点亮;反之则熄灭。

参照飞腾派的硬件原理图,板子上有一个灯可以被我们控制,也就是led20,控制方法为第二种方法,控制GPIO线为GPIO1_8。 cpu_run gpio1_8

当然,如果你愿意,飞腾派开发板提供了很多GPIO的拓展线。你可以自己实现一套电路来点亮外接的led灯。

我们最终要实现led灯的"心跳"效果,即 亮1秒,暗1秒,如此往复循环。

寄存器定义

notice: 飞腾派上的GPIO模块操作方式都是统一的,唯一的区别是有的gpio需要先进行引脚复用的配置。具体操作可以参照参考飞腾派裸机开发手册进行配置。本次使用的引脚不需要配置,因为笔者没有找到这个GPIO的引脚复用配置寄存器。按照官方文档描述,没有找到就是不需要配置,所以可以认为这个引脚只有gpio功能。

对于输入输出,我们只关心以下三个寄存器,他们的定义可以在飞腾派软件开发手册很轻松的找到(而且是中文~),这里就不做复制粘贴的工作了。

  • GPIO_SWPORT_DR (0x00)
  • GPIO_SWPORT_DDR (0x04)
  • GPIO_EXT_PORT (0x08)

实验过程

  • 编写驱动代码,实现 set_dirset_data 操作,示例代码如下:
    #![allow(unused)]
    fn main() {
    use bitflags::bitflags;
    use safe_mmio::fields::ReadWrite;
    use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
    
    #[derive(Clone, Eq, FromBytes, Immutable, IntoBytes, KnownLayout, PartialEq)]
    #[repr(C, align(4))]
    pub struct PhitiumGpio {
      data: ReadWrite<GpioPins>,
      resv: ReadWrite<u16>,
      dir: ReadWrite<GpioPins>,
      resv2: ReadWrite<u16>,
    }
    
    #[repr(transparent)]
    #[derive(Copy, Clone, Debug, Eq, FromBytes, Immutable, IntoBytes, KnownLayout, PartialEq)]
    pub struct GpioPins(u16);
    
    bitflags! {
      impl GpioPins: u16 {
          const p0 = 1<<0;
          const p1 = 1<<1;
          const p2 = 1<<2;
          const p3 = 1<<3;
          const p4 = 1<<4;
          const p5 = 1<<5;
          const p6 = 1<<6;
          const p7 = 1<<7;
          const p8 = 1<<8;
          const p9 = 1<<9;
          const p10 = 1<<10;
          const p11 = 1<<11;
          const p12 = 1<<12;
          const p13 = 1<<13;
          const p14 = 1<<14;
          const p15 = 1<<15;
      }
    }
    
    impl PhitiumGpio {
      pub fn new(base: usize) -> &'static mut Self {
          let b = base as *mut PhitiumGpio;
          unsafe { &mut (*b) }
      }
      pub fn set_pin_dir(&mut self, pin: GpioPins, dir: bool) {
          let mut status = self.dir.0.bits();
          debug!("dir data = {status}");
          let pb = pin.bits();
          if dir == true {
              status |= pb;
          } else {
              status &= !pb;
          }
          debug!("dir data = {status}");
          self.dir.0 = (GpioPins::from_bits_truncate(status));
      }
      pub fn set_pin_data(&mut self, pin: GpioPins, data: bool) {
          let mut status = self.dir.0.bits();
          debug!(" data = {status}");
          let pb = pin.bits();
          if data == true {
              status |= pb;
          } else {
              status &= !pb;
          }
          debug!(" data = {status}");
          self.data.0 = (GpioPins::from_bits_truncate(status));
      }
    }
    pub use crate::mem::phys_to_virt;
    pub use memory_addr::PhysAddr;
    
    pub const BASE1: PhysAddr = pa!(0x28035000);
    }
  • 由于我们目前暂时没有文件系统,无法通过读写文件的方式来控制GPIO。这里直接在main.rs中实例一个GPIO控制器进行相关初始化。
    // examples/helloworld/src/main.rs
    
    #![cfg_attr(feature = "axstd", no_std)]
    #![cfg_attr(feature = "axstd", no_main)]
    
    use core::time;
    
    #[cfg(feature = "axstd")]
    use axstd::println;
    
    use axstd::thread::sleep;
    
    #[cfg_attr(feature = "axstd", unsafe(no_mangle))]
    fn main() {
        println!("Hello, world!");
        let gpio0 = axhal::platform::gpio::PhitiumGpio::new(
            axhal::platform::gpio::phys_to_virt(axhal::platform::gpio::BASE1).into(),
        );
        let p = axhal::platform::gpio::GpioPins::p8;
        gpio0.set_pin_dir(p, true);
        let mut data = false;
        loop {
           sleep(time::Duration::from_secs(1));
           gpio0.set_pin_data(p, data);
           println!("current data: {data}");
           data = !data;
        }
    }
    
  • 创建一个大loop,在这个loop中,我们不停的将pin 8的值进行反转,反转一次,sleep 1s,这样就实现了1s灭,1s亮的效果。
  // examples/helloworld/src/main.rs

  #![cfg_attr(feature = "axstd", no_std)]
  #![cfg_attr(feature = "axstd", no_main)]

  use core::time;

  #[cfg(feature = "axstd")]
  use axstd::println;

  use axstd::thread::sleep;

  #[cfg_attr(feature = "axstd", unsafe(no_mangle))]
  fn main() {
      println!("Hello, world!");
      let gpio0 = axhal::platform::gpio::PhitiumGpio::new(
         axhal::platform::gpio::phys_to_virt(axhal::platform::gpio::BASE1).into(),
      );
      let p = axhal::platform::gpio::GpioPins::p8;
      gpio0.set_pin_dir(p, true);
      et mut data = false;
      loop {
          sleep(time::Duration::from_secs(1));
          gpio0.set_pin_data(p, data);
          println!("current data: {data}");
          data = !data;
      }
 } 
  • 通过make A=examples/helloworld ARCH=aarch64 PLATFORM=aarch64-phytium-pi FEATURES=irq LOG=debug进行编译,并烧入飞腾派运行。下面是运行日志以及实拍。
运行结果
Starting kernel ...


      d8888                            .d88888b.   .d8888b.
      d88888                           d88P" "Y88b d88P  Y88b
    d88P888                           888     888 Y88b.
    d88P 888 888d888  .d8888b  .d88b.  888     888  "Y888b.
  d88P  888 888P"   d88P"    d8P  Y8b 888     888     "Y88b.
  d88P   888 888     888      88888888 888     888       "888
d8888888888 888     Y88b.    Y8b.     Y88b. .d88P Y88b  d88P
d88P     888 888      "Y8888P  "Y8888   "Y88888P"   "Y8888P"

arch = aarch64
platform = aarch64-phytium-pi
target = aarch64-unknown-none-softfloat
build_mode = release
log_level = trace
smp = 1

[ 13.461312 0 axruntime:130] Logging is enabled.
[ 13.467040 0 axruntime:131] Primary CPU 0 started, dtb = 0xf9c29000.
[ 13.474591 0 axruntime:133] Found physcial memory regions:
[ 13.481276 0 axruntime:135]   [PA:0x90000000, PA:0x90007000) .text (READ | EXECUTE | RESERVED)
[ 13.491083 0 axruntime:135]   [PA:0x90007000, PA:0x9000a000) .rodata (READ | RESERVED)
[ 13.500197 0 axruntime:135]   [PA:0x9000a000, PA:0x9000e000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
[ 13.511655 0 axruntime:135]   [PA:0x9000e000, PA:0x9004e000) boot stack (READ | WRITE | RESERVED)
[ 13.521724 0 axruntime:135]   [PA:0x9004e000, PA:0x90051000) .bss (READ | WRITE | RESERVED)
[ 13.531272 0 axruntime:135]   [PA:0x90051000, PA:0x100000000) free memory (READ | WRITE | FREE)
[ 13.541167 0 axruntime:135]   [PA:0x2800c000, PA:0x2800d000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.551496 0 axruntime:135]   [PA:0x2800d000, PA:0x2800e000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.561825 0 axruntime:135]   [PA:0x2800e000, PA:0x2800f000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.572155 0 axruntime:135]   [PA:0x2800f000, PA:0x28010000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.582484 0 axruntime:135]   [PA:0x30000000, PA:0x38000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.592813 0 axruntime:135]   [PA:0x40000000, PA:0x50000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.603142 0 axruntime:135]   [PA:0x58000000, PA:0x80000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.613471 0 axruntime:135]   [PA:0x28014000, PA:0x28016000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.623800 0 axruntime:135]   [PA:0x28016000, PA:0x28018000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.634130 0 axruntime:135]   [PA:0x28018000, PA:0x2801a000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.644459 0 axruntime:135]   [PA:0x2801a000, PA:0x2801c000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.654788 0 axruntime:135]   [PA:0x2801c000, PA:0x2801e000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.665117 0 axruntime:135]   [PA:0x28034000, PA:0x28035000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.675446 0 axruntime:135]   [PA:0x28035000, PA:0x28036000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.685776 0 axruntime:135]   [PA:0x28036000, PA:0x28037000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.696105 0 axruntime:135]   [PA:0x28037000, PA:0x28038000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.706434 0 axruntime:135]   [PA:0x28038000, PA:0x28039000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.716763 0 axruntime:135]   [PA:0x28039000, PA:0x2803a000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.727093 0 axruntime:150] Initialize platform devices...
[ 13.733776 0 axhal::platform::aarch64_common::gic:51] Initialize GICv2...
[ 13.741897 0 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
[ 13.750182 0 axhal::platform::aarch64_common::gic:27] GICD set enable: 116 true
[ 13.758688 0 axruntime:176] Initialize interrupt handlers...
[ 13.765545 0 axhal::platform::aarch64_common::gic:36] register handler irq 30
[ 13.773878 0 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
[ 13.782298 0 axruntime:188] Primary CPU 0 init OK.
Hello, world!
[ 13.789589 0 axhal::platform::aarch64_phytium_pi::gpio:46] dir data = 0
[ 13.797401 0 axhal::platform::aarch64_phytium_pi::gpio:53] dir data = 256
[ 14.805386 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 14.810239 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 0
current data: false
[ 15.819614 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 15.824467 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 256
current data: true
[ 16.833928 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 16.838781 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 0
current data: false
[ 17.848156 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 17.853009 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 256
current data: true
[ 18.862470 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 18.867323 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 0
current data: false
[ 19.876698 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 19.881551 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 256
current data: true
[ 20.891012 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 20.895865 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 0
current data: false
[ 21.905240 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 21.910093 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 256
current data: true
[ 22.919554 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 22.924407 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 0
current data: false
[ 23.933782 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 23.938635 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 256
current data: true
[ 24.948097 0 axhal::platform::aarch64_phytium_pi::gpio:58]  data = 256
[ 24.952950 0 axhal::platform::aarch64_phytium_pi::gpio:65]  data = 0
current data: false

led亮

led_off

优化代码

  • 目前驱动代码位于 examples/helloworld/main.c 中,这不是一种正确的做法。参考 modules/axhal/src/platform/aarch64_common/pl011.rs 的实现,在同级目录下实现 pl061.rs。 rust 提供了如 tock_registers 这样的可以用来定义寄存器的crate,用起来!
  • 关机函数实际上是触发了一个异常而导致的关机,当把上一步完成后,换成 axhal::misc::terminate 来优雅的关机!
  • 实验2的完整代码在这儿

参考资料

1.2 PWM驱动开发

1.3 复位与引脚复用驱动

第二章:时钟管理类驱动

2.1 NOC时钟驱动

generic timer介绍

generic timer arm公司提出的一种硬件上的设计框架。在早期单核时代,每个SOC vendor厂商购买arm core的IP,然后自己设计soc上的timers。这就导致了对于每一种soc,驱动代码都会不同。此外,linux对硬件timer也有一定的要求,有些硬件厂商设计的timer支持的不是很好。在进入多核时代之后,核与核之间有同步的需求,为单核设计timer的方案会变的更加复杂。由此,arm公司提供了timer的硬件设计,集成在了自己的多核结构中。也就是generic_timer.

对于软件开发者来说,总体框架如下:

非虚拟化:

system_counter

虚拟化:

system_counter_virtual

每一个核有单独的timer,以及一个全局的system counter,它们通过system time bus相连。此外,对于虚拟化场景,还额外增加了CNTVOFF这个寄存器,主要作用是对于让虚拟机感受不到非调度时间的流逝。对于这个架构保证了对于所有的核来说,timer的clock都是一致的。产生的中断位于gic的ppi区,产生的中断会分发到对应的核上。

主要寄存器介绍

  • CNTFRQ_EL0: EL1级别物理system counter时钟的频率,对于EL1及以上,这个寄存器是只读的。
  • CNTP_CTL_EL0: EL1级别物理timer的控制器,用于开启/关闭这个核的timer中断
  • CNTP_TVAL_EL0:EL1级别物理timer的时钟值,当system counter值 >= 这个值,会产生一个可能的中断
  • CNTPCT_EL0:EL1级别物理timer的比较值,当这个值被写时,会将 system_count + CNTPCT_EL0 写入 CNTP_TVAL_EL0。

多核时钟驱动实验步骤

  • timer 初始化代码

    • main core获取 system counter的 frequency
    #![allow(unused)]
    fn main() {
      /// Early stage initialization: stores the timer frequency.
      pub(crate) fn init_early() {
          let freq = CNTFRQ_EL0.get();
          unsafe {
              // crate::time::NANOS_PER_SEC = 1_000_000_000;
              CNTPCT_TO_NANOS_RATIO = Ratio::new(crate::time::NANOS_PER_SEC as u32, freq as u32);
              NANOS_TO_CNTPCT_RATIO = CNTPCT_TO_NANOS_RATIO.inverse();
          }
      }
    }
  • 每个核都开启timer中断,注册timer回调

    #![allow(unused)]
    fn main() {
      /// Set a one-shot timer.
      ///
      /// A timer interrupt will be triggered at the specified monotonic time deadline (in nanoseconds).
      #[cfg(feature = "irq")]
      pub fn set_oneshot_timer(deadline_ns: u64) {
          let cnptct = CNTPCT_EL0.get();
          let cnptct_deadline = nanos_to_ticks(deadline_ns);
          if cnptct < cnptct_deadline {
              let interval = cnptct_deadline - cnptct;
              debug_assert!(interval <= u32::MAX as u64);
              CNTP_TVAL_EL0.set(interval);
          } else {
              CNTP_TVAL_EL0.set(0);
          }
      }
      #[cfg(feature = "irq")]
      fn init_interrupt() {
          use axhal::time::TIMER_IRQ_NUM;
    
          // Setup timer interrupt handler
          const PERIODIC_INTERVAL_NANOS: u64 =
              axhal::time::NANOS_PER_SEC / axconfig::TICKS_PER_SEC as u64;
    
          #[percpu::def_percpu]
          static NEXT_DEADLINE: u64 = 0;
    
          fn update_timer() {
              let now_ns = axhal::time::monotonic_time_nanos();
              // Safety: we have disabled preemption in IRQ handler.
              let mut deadline = unsafe { NEXT_DEADLINE.read_current_raw() };
              if now_ns >= deadline {
                  deadline = now_ns + PERIODIC_INTERVAL_NANOS;
              }
              unsafe { NEXT_DEADLINE.write_current_raw(deadline + PERIODIC_INTERVAL_NANOS) };
              axhal::time::set_oneshot_timer(deadline);
          }
    
          axhal::irq::register_handler(TIMER_IRQ_NUM, || {
              update_timer();
              #[cfg(feature = "multitask")]
              axtask::on_timer_tick();
          });
    
          // Enable IRQs before starting app
          axhal::asm::enable_irqs();
      }
    
      pub(crate) fn init_percpu() {
          #[cfg(feature = "irq")]
          {
              CNTP_CTL_EL0.write(CNTP_CTL_EL0::ENABLE::SET);
              // 设置为0,马上就会触发一次中断。
              CNTP_TVAL_EL0.set(0);
              // gic 中断时能
              crate::platform::irq::set_enable(crate::platform::irq::TIMER_IRQ_NUM, true);
          }
      }
    }
  • 通过这个命令make A=examples/helloworld ARCH=aarch64 PLATFORM=aarch64-qemu-virt FEATURES=irq LOG=trace ACCEL=n SMP=4 run在qemu上运行得到下列结果

    运行结果 qemu-system-aarch64 -m 128M -smp 4 -cpu cortex-a72 -machine virt -kernel examples/helloworld/helloworld_aarch64-qemu-virt.bin -nographic
      d8888                            .d88888b.   .d8888b.
      d88888                           d88P" "Y88b d88P  Y88b
      d88P888                           888     888 Y88b.
      d88P 888 888d888  .d8888b  .d88b.  888     888  "Y888b.
      d88P  888 888P"   d88P"    d8P  Y8b 888     888     "Y88b.
      d88P   888 888     888      88888888 888     888       "888
      d8888888888 888     Y88b.    Y8b.     Y88b. .d88P Y88b  d88P
      d88P     888 888      "Y8888P  "Y8888   "Y88888P"   "Y8888P"
    
      arch = aarch64
      platform = aarch64-qemu-virt
      target = aarch64-unknown-none-softfloat
      build_mode = release
      log_level = trace
      smp = 4
    
      [  0.003638 axruntime:130] Logging is enabled.
      [  0.004146 axruntime:131] Primary CPU 0 started, dtb = 0x44000000.
      [  0.004338 axruntime:133] Found physcial memory regions:
      [  0.004530 axruntime:135]   [PA:0x40200000, PA:0x40207000) .text (READ | EXECUTE | RESERVED)
      [  0.004790 axruntime:135]   [PA:0x40207000, PA:0x4020a000) .rodata (READ | RESERVED)
      [  0.004938 axruntime:135]   [PA:0x4020a000, PA:0x4020e000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
      [  0.005096 axruntime:135]   [PA:0x4020e000, PA:0x4030e000) boot stack (READ | WRITE | RESERVED)
      [  0.005232 axruntime:135]   [PA:0x4030e000, PA:0x40311000) .bss (READ | WRITE | RESERVED)
      [  0.005372 axruntime:135]   [PA:0x40311000, PA:0x48000000) free memory (READ | WRITE | FREE)
      [  0.005530 axruntime:135]   [PA:0x9000000, PA:0x9001000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.005672 axruntime:135]   [PA:0x9100000, PA:0x9101000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.005808 axruntime:135]   [PA:0x8000000, PA:0x8020000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.005944 axruntime:135]   [PA:0xa000000, PA:0xa004000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.006080 axruntime:135]   [PA:0x10000000, PA:0x3eff0000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.006216 axruntime:135]   [PA:0x4010000000, PA:0x4020000000) mmio (READ | WRITE | DEVICE | RESERVED)
      [  0.006386 axruntime:150] Initialize platform devices...
      [  0.006506 axhal::platform::aarch64_common::gic:51] Initialize GICv2...
      [  0.007098 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
      [  0.007574 axhal::platform::aarch64_common::gic:27] GICD set enable: 33 true
      [  0.007854 axruntime::mp:20] starting CPU 1...
      [  0.007976 axhal::platform::aarch64_common::psci:115] Starting CPU 1 ON ...
      [  0.008234 axruntime::mp:37] Secondary CPU 1 started.
      [  0.008236 axruntime::mp:20] starting CPU 2...
      [  0.008672 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
      [  0.008768 axhal::platform::aarch64_common::psci:115] Starting CPU 2 ON ...
      [  0.008974 axruntime::mp:47] Secondary CPU 1 init OK.
      [  0.009110 axruntime::mp:37] Secondary CPU 2 started.
      [  0.009300 axruntime::mp:20] starting CPU 3...
      [  0.009550 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
      [  0.009712 axhal::platform::aarch64_common::psci:115] Starting CPU 3 ON ...
      [  0.009934 axruntime::mp:47] Secondary CPU 2 init OK.
      [  0.010186 axruntime::mp:37] Secondary CPU 3 started.
      [  0.010218 axruntime:176] Initialize interrupt handlers...
      [  0.010548 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
      [  0.010790 axruntime::mp:47] Secondary CPU 3 init OK.
      [  0.010818 axhal::platform::aarch64_common::gic:36] register handler irq 30
      [  0.011058 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
      [  0.011578 axhal::irq:18] IRQ 30
      [  0.012612 axruntime:188] Primary CPU 0 init OK.
      Hello, world!
      [  0.012760 3 axhal::irq:18] IRQ 30
      [  0.012750 1 axhal::irq:18] IRQ 30
      [  0.012944 2 axhal::irq:18] IRQ 30
      [  0.024292 0 axhal::irq:18] IRQ 30
      [  0.024338 2 axhal::irq:18] IRQ 30
      [  0.024350 3 axhal::irq:18] IRQ 30
      [  0.024344 1 axhal::irq:18] IRQ 30
      [  0.034532 2 axhal::irq:18] IRQ 30
      [  0.034566 1 axhal::irq:18] IRQ 30
      [  0.034568 0 axhal::irq:18] IRQ 30
      [  0.034564 3 axhal::irq:18] IRQ 30
      [  0.043722 2 axhal::irq:18] IRQ 30
      [  0.043730 0 axhal::irq:18] IRQ 30
      [  0.043724 1 axhal::irq:18] IRQ 30
      [  0.043730 3 axhal::irq:18] IRQ 30
      [  0.053962 2 axhal::irq:18] IRQ 30
      [  0.053986 0 axhal::irq:18] IRQ 30
    
  • 可以看出,4个cpu的中断每10ms被触发一次,符合预期。

实验总结

验证了arm多核架构下的timer。

参考资料

arm_generic_timer generic_timer_in_linux

2.2 时钟源驱动

2.3 看门狗驱动

看门狗是为了保障系统在因为某些错误导致挂起时,用来恢复系统所设计的一个硬件.其内部有个定时器,如果在定时器超时时间内,没有周期性地去"喂"(通常以设置某个寄存器,或者拉升/低某个引脚的形式)看门狗,则会导致看门狗重启整个系统.所以,从用户使用的角度来看,用户通常是会设计一个周期性"喂"看门狗的程序,如果内核崩溃恐慌导致系统挂起,则该程序无法继续"喂"狗,看门狗超时导致系统重启.

如上所述,一个看门狗驱动,至少需要提供让用户可以设置超时,启动/关闭看门狗,"喂"狗等等的接口.因此,在飞腾派Linux内核源码的include/linux/watchdog.h文件中,我们可以看到这样的定义

/** struct watchdog_ops - The watchdog-devices operations
 *
 * @owner:	The module owner.
 * @start:	The routine for starting the watchdog device.
 * @stop:	The routine for stopping the watchdog device.
 * @ping:	The routine that sends a keepalive ping to the watchdog device.
 * @status:	The routine that shows the status of the watchdog device.
 * @set_timeout:The routine for setting the watchdog devices timeout value (in seconds).
 * @set_pretimeout:The routine for setting the watchdog devices pretimeout.
 * @get_timeleft:The routine that gets the time left before a reset (in seconds).
 * @restart:	The routine for restarting the machine.
 * @ioctl:	The routines that handles extra ioctl calls.
 *
 * The watchdog_ops structure contains a list of low-level operations
 * that control a watchdog device. It also contains the module that owns
 * these operations. The start function is mandatory, all other
 * functions are optional.
 */
struct watchdog_ops {
	struct module *owner;
	/* mandatory operations */
	int (*start)(struct watchdog_device *);
	/* optional operations */
	int (*stop)(struct watchdog_device *);
	int (*ping)(struct watchdog_device *);
	unsigned int (*status)(struct watchdog_device *);
	int (*set_timeout)(struct watchdog_device *, unsigned int);
	int (*set_pretimeout)(struct watchdog_device *, unsigned int);
	unsigned int (*get_timeleft)(struct watchdog_device *);
	int (*restart)(struct watchdog_device *, unsigned long, void *);
	long (*ioctl)(struct watchdog_device *, unsigned int, unsigned long);
};

上面的结构体描述了,Linux下,一个看门狗设备需要提供的操作接口.而这些,由驱动实现.本节我们只会分析其中的startset_timeoutping接口,其它接口的说明,超出了本节范围,其中ping方法既是用来给用户提供一个"喂"狗的接口的,而另两个,顾名思义,分别是启动看门狗和设置看门狗超时的功能的.

接下来,我们会结合飞腾派的文档和Linux内核源码,分析如何在Arceos中实现对应的接口.要说明的是,飞腾派中的看门狗的设计遵照ARM-Sbsa规范,其驱动实现位于drivers/watchdog/sbsa-gwdt下.我们可以看到其中对应的方法实现,分别是

static const struct watchdog_ops sbsa_gwdt_ops = {
	.owner		= THIS_MODULE,
	.start		= sbsa_gwdt_start,
	.stop		= sbsa_gwdt_stop,
	.ping		= sbsa_gwdt_keepalive,
	.set_timeout	= sbsa_gwdt_set_timeout,
	.get_timeleft	= sbsa_gwdt_get_timeleft,
};

继续看下去,我们会发现sbsa_gwdt_startsbsa_gwdt_set_timeoutsbsa_gwdt_keepalive三个函数的实现异常简单,没有太多"魔法"在里面.如下

static void sbsa_gwdt_reg_write(u64 val, struct sbsa_gwdt *gwdt)
{
	if (gwdt->version == 0)
		writel((u32)val, gwdt->control_base + SBSA_GWDT_WOR);
	else
		lo_hi_writeq(val, gwdt->control_base + SBSA_GWDT_WOR);
}
/*
 * watchdog operation functions
 */
static int sbsa_gwdt_set_timeout(struct watchdog_device *wdd,
				 unsigned int timeout)
{
	struct sbsa_gwdt *gwdt = watchdog_get_drvdata(wdd);

	wdd->timeout = timeout;
	timeout = clamp_t(unsigned int, timeout, 1, wdd->max_hw_heartbeat_ms / 1000);

	if (action)
		sbsa_gwdt_reg_write((u64)gwdt->clk * timeout, gwdt);
	else
		/*
		 * In the single stage mode, The first signal (WS0) is ignored,
		 * the timeout is (WOR * 2), so the WOR should be configured
		 * to half value of timeout.
		 */
		sbsa_gwdt_reg_write(((u64)gwdt->clk / 2) * timeout, gwdt);

	return 0;
}

static int sbsa_gwdt_keepalive(struct watchdog_device *wdd)
{
	struct sbsa_gwdt *gwdt = watchdog_get_drvdata(wdd);

	/*
	 * Writing WRR for an explicit watchdog refresh.
	 * You can write anyting (like 0).
	 */
	writel(0, gwdt->refresh_base + SBSA_GWDT_WRR);

	return 0;
}

static int sbsa_gwdt_start(struct watchdog_device *wdd)
{
	struct sbsa_gwdt *gwdt = watchdog_get_drvdata(wdd);

	/* writing WCS will cause an explicit watchdog refresh */
	writel(SBSA_GWDT_WCS_EN, gwdt->control_base + SBSA_GWDT_WCS);

	return 0;
}

本质上,是对SBSA_GWDT_WORSBSA_GWDT_WRRSBSA_GWDT_WCS三个寄存器的操作.关于这些寄存器的描述,我们可以在飞腾派软件软件编程手册V1.0上找到.

WDT_WRR (0x0000)

读写复位值描述
WDT_WRR31:0RW0x0Watchdog更新寄存器.写操作会重新开始看门狗计数,读返回0

WDT_WOR (0x1008)

读写复位值描述
WDT_WOR31:0RO0x3000000Watchdog清除寄存器

WDT_WCS (0x1000)

读写复位值描述
reserved31:3RO0x0保留
Ws12RO0x0二次超时,读返回当前ws1的值
Ws01RO0x0一次超时,读返回当期ws0的值
Wdt_en0RW0x0Watchdog使能信号,高有效,常规复位和热保护都会清0.

对该三个寄存器的写入,会直接导致sys_cnt+WOR寄存器储存的值被更新到WCV寄存器中.而sys_cnt的计数值大于当前WCV寄存器存储的比较值则会导致看门狗超时.

为了让Arceos支持对这三个寄存器进行读取写入,我们需要将三个寄存器的地址空间映射到内核虚拟空间中,在configs/platforms/aarch64-phytium-pi.toml文件中,修改mmio-regions部分,添加上对看门狗的支持.

#
# Device specifications
#
[devices]
# MMIO regions with format (`base_paddr`, `size`).
mmio-regions = [
    [0x2800_C000, 0x1000],  # UART 0
    [0x2800_D000, 0x1000],  # UART 1
    [0x2800_E000, 0x1000],  # UART 2
    [0x2800_F000, 0x1000],  # UART 3

    [0x3000_0000, 0x800_0000],  # other devices
    [0x4000_0000, 0x1000_0000], # Pcie ecam

    [0x5800_0000, 0x2800_0000], # 32-bit MMIO space

    [0x2801_4000, 0x2000],  # MIO0 - I2C
    [0x2801_6000, 0x2000],  # MIO1 - I2C
    [0x2801_8000, 0x2000],  # MIO2 - I2C
    [0x2801_A000, 0x2000],  # MIO3 - I2C
    [0x2801_C000, 0x2000],  # MIO4 - I2C

    [0x000_2803_4000, 0x1000],  # GPIO0
    [0x000_2803_5000, 0x1000],  # GPIO1
    [0x000_2803_6000, 0x1000],  # GPIO2
    [0x000_2803_7000, 0x1000],  # GPIO3
    [0x000_2803_8000, 0x1000],  # GPIO4
    [0x000_2803_9000, 0x1000],  # GPIO5
    [0x000_2804_0000, 0x2000],  # WDT0
    [0x000_2804_2000, 0x2000],  # WDT1
] 

# Watchdog Address
wdt0-paddr = 0x2804_0000         # uint
wdt1-paddr = 0x2804_2000         # uint

axhal/src/platform/aarch64_common文件夹下创建sbsa_wdt.rs文件,之后我们就可以参照之前linux源码中的实现,来完成看门狗使能,设置超时和关闭看门狗的功能

#![allow(unused)]
fn main() {
/// Start Watchdog
pub fn start_watchdog() {
    axlog::debug!("starting watchdog");
    let wdt = WDT.lock();
    unsafe {
        // Enable Watchdog Timer
        core::ptr::write_volatile(
            (wdt.0 + SBSA_GWDT_WCS) as *mut u32, 0x1);
    }
}

pub fn set_watchdog_timeout(timeout: u32) {
    axlog::debug!("set watchdog timeout: {timeout}");
    let clk = CNTFRQ_EL0.get();
    let wdt = WDT.lock();
    unsafe {
        core::ptr::write_volatile(
            (wdt.0 + SBSA_GWDT_WOR) as *mut u32, (clk * timeout as u64 / 2) as u32);
    }
}

pub fn stop_watchdog() {
    axlog::debug!("stopping watchdog");
    let wdt = WDT.lock();
    unsafe {
        // Disable Watchdog Timer
        core::ptr::write_volatile(
            (wdt.0 + SBSA_GWDT_WCS) as *mut u32, 0x0);
    }
}

pub fn ping_watchdog() {
    axlog::debug!("feeding watchdog");
    let wdt = WDT.lock();
    unsafe {
        // Write to Watchdog Timer Reset Register to reset the timer
        core::ptr::write_volatile(
            (wdt.0 + SBSA_GWDT_WRR) as *mut u32, 0);
    }
}
}

接下来,我们进行测试,我们可以选择在内核中启用看门狗,并设置超时时间为6秒,

#![allow(unused)]
fn main() {
super::aarch64_common::sbsa_wdt::set_watchdog_timeout(6);
super::aarch64_common::sbsa_wdt::start_watchdog();
}

同时编写个测试应用

fn main() {
    println!("Hello, world!");
    let mut count = 1usize;
    loop {
        println!("count {count}");
        count += 1;
        axstd::thread::sleep(core::time::Duration::from_secs(1));
    }
}

可观察到如下现象,应用程序计时到第6秒时,设备重启了.

第三章:外设协议类驱动

3.1 UART串口驱动

UART 介绍

uart: universal asynchronous receiver/transmitter,通用异步发送/接受器,是嵌入式设备中最常用全双工通信协议之一。关于它的详细介绍,可以参考 这篇文章。对于软件工程师来说,以下几个概念是必须要掌握的。

  • 波特率

    波特率就是双方进行通信的时钟速率。由于uart里面并没有时钟线,这也就意味着通信双方需要预先约定好具体的通信速率是多少。使用过类似于xshell这样的串口工具的同学应该就知道,在配置串口的时候,这个值配置不对就会导致屏幕上出现乱码。常用的波特率有115200,9600等。

  • 数据宽度

    一次传输的数据宽度,可以是5-8的一个值,常见是8bit,因为这对软件开发者来说最友好。毕竟,你也不想把一个u8拆成5bits和3bits来发送吧~

  • 停止位

    一个特殊的bit,用来告诉对方这一次传输结束了。可以配置为1位或者2位。

  • 奇偶校验

    在数据传输时增加额外一个1bit,用来使得这一个bit+数据的每一位为1的总数为奇数或者偶数。发送端在发送时,计算出奇偶校验位应该为1或者为0,接收端在接受时,就可以根据这一位校验出数据传输是否出错。当然,这个校验非常不准确,一般不开。

  • 8N1

    这是一种缩写,意味着8个数据位,没有奇偶校验,1位停止位。这是最常见的串口配置。

一些高级的uart芯片集成了调制/解调的功能,这涉及到了通信原理的一些知识,其配置也对应的复杂更多。飞腾派上搭载9线串口就具有这个功能,不过幸运的是本次实验不涉及这块内容,所以在看寄存器定义时,也不需要对这部分有深入理解。

飞腾派 UART 串口驱动实验

实验目标

  • 编写代码实现串口驱动
    • 初始化为8N1模式,波特率为115200,非中断模式
    • 没有tx/rx FIFO队列,单次接收/发送1一个byte。
  • 验证串口通信的基本原理

实验原理

根据原理图,我们使用飞腾派的UART2串口,它被连线到J1端子板的8(tx),10(rx)口上。用杜邦线将8口与10口相连接,uart2 tx发出的数据会被uart rx口被收到。 连线图

我们主要关注下面几个寄存器,这些寄存器的定义可以在飞腾派软件开发手册里找到。

  • UARTDR: 数据寄存器,用来传输/接受最大8位的数据
  • UARTRSR/UARTECR: 接受状态寄存器,用来查看接收过程中是否产生了错误,写这个寄存器可以清除错误。
  • UARTFR:标志寄存器,存了各种各样的标志,包括设备忙,发送队列是否为空等等。发送接收都需要查看这个寄存器。
  • UARTIBRD:存储波特率经过算法转变后的整数部分
  • UARTFBRD:存储波特率经过算法转变后的小数部分
  • UARTLCR_H:线控寄存器,控制uart的一些行为。
  • UARTLCR:控制寄存器,控制uart的一些行为。
  • UARTIMSC:控制是否打开中断。

uart初始化步骤

参考 飞腾派软件开发手册 的 5.22.1.1章节。

发送数据操作流程

参考 飞腾派软件开发手册 的 5.22.1.2章节。

接收数据操作流程

参考 飞腾派软件开发手册 的 5.22.1.3章节。

实验步骤

  • 根据数据手册,编写驱动代码。计算波特率的小数部分的值有一个小trick。很多低端嵌入式设备可能不支持浮点运算,为了驱动的通用性,你需要实现一种不用浮点的方法来计算出小数部分的divider的方法。如果没有什么思路,可以参考下面代码的 get_ti_tf 函数。实现之后别忘了针对这类函数写一些简单的测试代码,它会省去后期大量的调试工作。
#![allow(unused)]
fn main() {
// modules/axhal/src/platform/aarch64_phytium_pi/uart.rs

use core::ptr::NonNull;

use tock_registers::{
    interfaces::{Readable, Writeable},
    register_bitfields, register_structs,
    registers::{ReadOnly, ReadWrite, WriteOnly},
};

register_structs! {
    PhytiumUartRegs {
        /// Data Register.
        (0x00 => dr: ReadWrite<u32, DATA::Register>),
        (0x04 => _reserved0),
        /// Flag Register.
        (0x18 => fr: ReadOnly<u32, FLAG::Register>),
        (0x1c => _reserved1),
        ///
        (0x24 => tibd: ReadWrite<u32>),
        ///
        (0x28 => tfbd: ReadWrite<u32>),
        /// Control register.
        (0x2c => cr_h: ReadWrite<u32, CONTROLH::Register>),
        (0x30 => cr_l: ReadWrite<u32,CONTROLL::Register>),
        /// Interrupt FIFO Level Select Register.
        (0x34 => ifls: ReadWrite<u32>),
        /// Interrupt Mask Set Clear Register.
        (0x38 => imsc: ReadWrite<u32>),
        /// Raw Interrupt Status Register.
        (0x3c => ris: ReadOnly<u32>),
        /// Masked Interrupt Status Register.
        (0x40 => mis: ReadOnly<u32>),
        /// Interrupt Clear Register.
        (0x44 => icr: WriteOnly<u32>),
        (0x48 => @END),
    }
}

register_bitfields![u32,
    DATA [
        RAW OFFSET(0) NUMBITS(8),
        FE OFFSET(9) NUMBITS(1),
        PE OFFSET(10) NUMBITS(1),
        BE OFFSET(11) NUMBITS(1),
        OE OFFSET(12) NUMBITS(1),
    ],
    FLAG [
        CTS OFFSET(0) NUMBITS(1),
        DSR OFFSET(1) NUMBITS(1),
        DCD OFFSET(2) NUMBITS(1),
        BUSY OFFSET(3) NUMBITS(1),
        RXFE OFFSET(4) NUMBITS(1),
        TXFF OFFSET(5) NUMBITS(1),
        RXFF OFFSET(6) NUMBITS(1),
        TXFE OFFSET(7) NUMBITS(1),
    ],
    CONTROLH [
        BRK OFFSET(0) NUMBITS(1) [],
        PEN OFFSET(1) NUMBITS(1) [],
        EPS OFFSET(2) NUMBITS(1) [],
        STP2 OFFSET(3) NUMBITS(1) [],
        FEN OFFSET(4) NUMBITS(1) [],
        WLEN OFFSET(5) NUMBITS(2) [
            len5 = 0,
            len6 = 1,
            len7 = 2,
            len8= 3
        ],
        SPS OFFSET(7) NUMBITS(1) [],
    ],
    CONTROLL [
        ENABLE OFFSET(0) NUMBITS(1) [],
        RSV OFFSET(1) NUMBITS(7) [],
        TXE OFFSET(8) NUMBITS(1) [],
        RXE OFFSET(9) NUMBITS(1) [],
    ],
];

pub struct PhytiumUart {
    base: NonNull<PhytiumUartRegs>,
}

unsafe impl Send for PhytiumUart {}

impl PhytiumUart {
    pub const fn new(base: *mut u8) -> Self {
        Self {
            base: NonNull::new(base).unwrap().cast(),
        }
    }
    fn get_ti_tf(clock_hz: u32, baude_rate: u32) -> (u32, u32) {
        let baude_rate_16 = 16 * baude_rate;
        let ti = clock_hz / baude_rate_16;
        let tf = clock_hz % baude_rate_16;
        let tf = (tf * 64 + (baude_rate_16 >> 1)) / baude_rate_16;
        (ti, tf)
    }
    /// no irq, no fifo, 8bits data, 1 stop bit, no odd-even check
    pub fn init_no_irq(&mut self, clock_hz: u32, baude_rate: u32) {
        // disable reg
        let regs = self.regs();
        regs.cr_l.write(CONTROLL::ENABLE::CLEAR);

        // set bd rate
        let (ti, tf) = Self::get_ti_tf(clock_hz, baude_rate);
        regs.tibd.set(ti);
        regs.tfbd.set(tf);

        // width 8 , no check, stop bit 1
        regs.cr_h.write(CONTROLH::WLEN::len8);

        // no interrupt
        regs.imsc.set(0);

        // enable uart ,rx, tx
        regs.cr_l
            .write(CONTROLL::ENABLE::SET + CONTROLL::TXE::SET + CONTROLL::RXE::SET);
    }
    const fn regs(&self) -> &PhytiumUartRegs {
        unsafe { self.base.as_ref() }
    }

    pub fn read_byte_poll(&self) -> u8 {
        while self.regs().fr.read(FLAG::RXFE) != 0 {}
        (self.regs().dr.get() & 0xff) as u8
    }

    pub fn put_byte_poll(&mut self, b: u8) {
        while self.regs().fr.read(FLAG::BUSY) == 1 || self.regs().fr.read(FLAG::TXFF) == 1 {}
        self.regs().dr.set(b as u32);
    }
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_get_ti_tf() {
        let clock = 100_000_000;
        let bd_rate = 115200;
        let b16 = bd_rate * 16;
        let di = clock / b16;
        let df = clock % b16;
        let res = clock as f32 / b16 as f32;
        println!(
            "res = {res}, di = {di}, df={df},  df/b16 = {} , clock & (b16 -1)={}",
            df as f32 / b16 as f32,
            clock & (b16 - 1)
        );
        assert_eq!((54, 16), PhytiumUart::get_ti_tf(clock, bd_rate));
    }
}

use crate::mem::PhysAddr;
use crate::mem::phys_to_virt;
use kspin::SpinNoIrq;

/// we can only control uart 2
const UART2_BASE: PhysAddr = pa!(0x000_2800_E000);
pub static UART2: SpinNoIrq<PhytiumUart> =
   SpinNoIrq::new(PhytiumUart::new(phys_to_virt(UART2_BASE).as_mut_ptr()));

}
  • 将飞腾派uart暴露给上层应用: 飞腾派的uart0是通过 println!() 来暴露给 examples 下面的应用的。也就是说它们默认的stdio被输出到uart0的tx口, 这也是为什么我们接入串口调试线,就能看到应用层调用println!输出的字符。我们这里为了方便,将UART2这个设备直接暴露在hal层的misc模块下,你也可以为uart2定义为stderr的接口。
#![allow(unused)]
fn main() {
// modules/axhal/src/platform/aarch64_phytium_pi/uart.rs
use crate::mem::PhysAddr;
use crate::mem::phys_to_virt;
use kspin::SpinNoIrq;

/// we can only control uart 2
const UART2_BASE: PhysAddr = pa!(0x000_2800_E000);
pub static UART2: SpinNoIrq<PhytiumUart> =
    SpinNoIrq::new(PhytiumUart::new(phys_to_virt(UART2_BASE).as_mut_ptr()));
}
#![allow(unused)]
fn main() {
// modules/axhal/src/platform/aarch64_phytium_pi/mod.rs
...

pub mod misc {
    pub fn terminate() -> ! {
        info!("Shutting down...");
        loop {
            axcpu::asm::halt();
        }
    }
    pub use super::mio::*;
    pub use super::uart::*;
}
...
}
  • 在应用层代码中使用 UART2,进行初始化,读取接收数据,并将结果通过*println!*进行输出。
// examples/helloworld/src/main.rs
#![cfg_attr(feature = "axstd", no_std)]
#![cfg_attr(feature = "axstd", no_main)]

use core::time;

#[cfg(feature = "axstd")]
use axstd::println;

use axhal::misc::UART2;

#[cfg_attr(feature = "axstd", unsafe(no_mangle))]
fn main() {
    println!("Hello, world!");
    let mut uart = UART2.lock();
    uart.init_no_irq(100_000_000, 115200);
    let mut data = 0;
    loop {
        uart.put_byte_poll(data);
        println!("send data {data}");
        let read = uart.read_byte_poll();
        println!("read data {read}");
        println!("sleep 1s");
        data = data.wrapping_add(1);
        axstd::thread::sleep(time::Duration::from_secs(1));
    }
}
  • 烧入飞腾派开发板,运行得到如下日志。
运行结果 Starting kernel ...
    d8888                            .d88888b.   .d8888b.
    d88888                           d88P" "Y88b d88P  Y88b
    d88P888                           888     888 Y88b.
    d88P 888 888d888  .d8888b  .d88b.  888     888  "Y888b.
d88P  888 888P"   d88P"    d8P  Y8b 888     888     "Y88b.
d88P   888 888     888      88888888 888     888       "888
d8888888888 888     Y88b.    Y8b.     Y88b. .d88P Y88b  d88P
d88P     888 888      "Y8888P  "Y8888   "Y88888P"   "Y8888P"

arch = aarch64
platform = aarch64-phytium-pi
target = aarch64-unknown-none-softfloat
build_mode = release
log_level = trace
smp = 1

[ 13.466610 0 axruntime:130] Logging is enabled.
[ 13.472338 0 axruntime:131] Primary CPU 0 started, dtb = 0xf9c29000.
[ 13.479890 0 axruntime:133] Found physcial memory regions:
[ 13.486574 0 axruntime:135]   [PA:0x90000000, PA:0x90007000) .text (READ | EXECUTE | RESERVED)
[ 13.496382 0 axruntime:135]   [PA:0x90007000, PA:0x9000a000) .rodata (READ | RESERVED)
[ 13.505496 0 axruntime:135]   [PA:0x9000a000, PA:0x9000e000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
[ 13.516953 0 axruntime:135]   [PA:0x9000e000, PA:0x9004e000) boot stack (READ | WRITE | RESERVED)
[ 13.527022 0 axruntime:135]   [PA:0x9004e000, PA:0x90051000) .bss (READ | WRITE | RESERVED)
[ 13.536570 0 axruntime:135]   [PA:0x90051000, PA:0x100000000) free memory (READ | WRITE | FREE)
[ 13.546465 0 axruntime:135]   [PA:0x2800c000, PA:0x2800d000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.556794 0 axruntime:135]   [PA:0x2800d000, PA:0x2800e000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.567124 0 axruntime:135]   [PA:0x2800e000, PA:0x2800f000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.577453 0 axruntime:135]   [PA:0x2800f000, PA:0x28010000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.587782 0 axruntime:135]   [PA:0x30000000, PA:0x38000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.598111 0 axruntime:135]   [PA:0x40000000, PA:0x50000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.608440 0 axruntime:135]   [PA:0x58000000, PA:0x80000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.618770 0 axruntime:135]   [PA:0x28014000, PA:0x28016000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.629099 0 axruntime:135]   [PA:0x28016000, PA:0x28018000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.639428 0 axruntime:135]   [PA:0x28018000, PA:0x2801a000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.649757 0 axruntime:135]   [PA:0x2801a000, PA:0x2801c000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.660087 0 axruntime:135]   [PA:0x2801c000, PA:0x2801e000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.670416 0 axruntime:135]   [PA:0x28034000, PA:0x28035000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.680745 0 axruntime:135]   [PA:0x28035000, PA:0x28036000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.691074 0 axruntime:135]   [PA:0x28036000, PA:0x28037000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.701403 0 axruntime:135]   [PA:0x28037000, PA:0x28038000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.711732 0 axruntime:135]   [PA:0x28038000, PA:0x28039000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.722062 0 axruntime:135]   [PA:0x28039000, PA:0x2803a000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 13.732391 0 axruntime:150] Initialize platform devices...
[ 13.739074 0 axhal::platform::aarch64_common::gic:51] Initialize GICv2...
[ 13.747199 0 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
[ 13.755480 0 axhal::platform::aarch64_common::gic:27] GICD set enable: 116 true
[ 13.763986 0 axruntime:176] Initialize interrupt handlers...
[ 13.770843 0 axhal::platform::aarch64_common::gic:36] register handler irq 30
[ 13.779176 0 axhal::platform::aarch64_common::gic:27] GICD set enable: 30 true
[ 13.787596 0 axruntime:188] Primary CPU 0 init OK.
Hello, world!
send data 0
read data 0
sleep 1s
send data 1
read data 1
sleep 1s
send data 2
read data 2
sleep 1s
send data 3
read data 3
sleep 1s
send data 4
read data 4
sleep 1s
send data 5
read data 5
sleep 1s
send data 6
read data 6
sleep 1s
send data 7
read data 7
sleep 1s
send data 8
read data 8
sleep 1s
send data 9
read data 9
sleep 1s
send data 10
read data 10
sleep 1s
send data 11
read data 11
sleep 1s
send data 12
read data 12
sleep 1s
send data 13
read data 13
sleep 1s
send data 14
read data 14
sleep 1s
^C

实验结论

本实验验证了uart无中断,poll mode的通信模型,实现了相应的驱动代码。

实验代码

完整代码可以在这儿看到

qemu串口驱动实验

qemu模拟的qemu-virt机器使用串口为 pl011 模块,寄存器作用以及地址与飞腾派是一致的,并且默认已经被 arceos 的初始化(不然就看不到arceos的启动所打印的信息了)。具体实现代码可以看这个crate。这个crate默认采用了波特率为115200,8N1的通信格式。

参考资料

3.2 I2C驱动开发

I2C介绍

I2C是一种多主机、两线制、低速串行通信总线,广泛用于微控制器和各种外围设备之间的通信。它使用两条线路:串行数据线(SDA)和串行时钟线(SCL)进行双向传输。

核心特性:

  1. 两线制:I2C 通信只需两条线路:
    • SDA(Serial Data Line):数据线,用于传输数据。
    • SCL(Serial Clock Line):时钟线,由主设备控制,用于同步所有设备上的数据传输。
  2. 多主机和多从机:I2C 总线允许有多个主设备(可以发起通信)和多个从设备(响应主设备的请求)。这使得多个控制器可以管理不同的从设备,增加了系统的灵活性。
  3. 地址分配:每个 I2C 设备都通过一个唯一的地址进行识别。这些地址在设备通信时使用,确保数据包被正确发送到指定的设备。
  4. 简单的连接:由于通信线路数量少,I2C 设备通常易于安装和配置,减少了硬件布局的复杂性。
  5. 同步串行通信:数据传输是同步进行的,意味着数据传输由时钟信号控制,提高了数据传输的可靠性。

数据传输模式:

I2C 支持多种数据传输模式,包括标准模式(100kbps)、快速模式(400kbps)、快速模式加(1Mbps)和高速模式(3.4Mbps)。根据需要选择不同的速率,可以平衡通信速度和系统资源消耗。

应用场景:

I2C 协议在嵌入式系统中非常流行,适用于各种应用,如:

  • 传感器读取:温度、湿度、压力等传感器经常通过 I2C 与微控制器通信。
  • 设备控制:在许多小型设备或计算机系统中,如笔记本电脑,I2C 用于调节音量、亮度、电源管理等。
  • 存储设备:某些类型的 EEPROM 和其他存储设备通过 I2C 接口与主控制器通信。

在普通单片机上的I2C通信可以简化为如下步骤

  • SCL为高电平的时候,SDA由高电平变化到低电平,表示开始传输数据.
  • SCL变低电平,SDA开始不断高低电平表示逻辑0和1来发送数据,共8次.
  • SCL变高,从设备用SDA返回低电平0,则是ACK,表示发送成功,如果SDA是高电平1,则是NACK,表示没发送成功,
  • SCL又变低电平,SDA继续发送数据.
  • 当SCL变高电平的时候,SDA如果由低电平变高电平,则结束.

ArceOS 的 I2C 驱动实现

MIO是一个包含多种控制器功能的多路选择控制器,飞腾派的每个MIO均可单独当做UART/I2C。端口功能的选择,可以通过配置creg_mio_func_sel寄存器来实现,配置为00选择I2C,配置为01选择UART。

由MIO控制器来当作IIC来与设备通信,操作会比普通单片机中用GPIO口模拟iic时序要复杂

由MIO控制的I2C操作说明:

1.初始化

1.1 飞腾派 I/O 引脚初始化:

初始化I/Opad引脚寄存器,FIOPadConfig是一个配置实例,提供配置信息,包括其基地址和设备ID号,为MIO的初始化做铺垫,

#![allow(unused)]
#![no_std]
#![no_main]
fn main() {
use super::driver_mio::{mio, mio_g, mio_hw, mio_sinit};
use super::{i2c, i2c_hw, i2c_intr, i2c_master, i2c_sinit, io};
use core::ptr;
use core::ptr::write_volatile;
use __private_api::Value;
use log::*;

use crate::driver_iic::i2c::*;
use crate::driver_iic::i2c_hw::*;
use crate::driver_iic::i2c_intr::*;
use crate::driver_iic::i2c_master::*;
use crate::driver_iic::i2c_sinit::*;


use crate::driver_mio::mio::*;
use crate::driver_mio::mio_g::*;
use crate::driver_mio::mio_hw::*;
use crate::driver_mio::mio_sinit::*;

pub fn write_reg(addr: u32, value: u32) {
    //debug!("Writing value {:#X} to address {:#X}", value, addr);
    unsafe {
        *(addr as *mut u32) = value;
    }
}

pub fn read_reg(addr: u32) -> u32 {
    let value:u32;
    unsafe {
        value = *(addr as *const u32);
    }
    //debug!("Read value {:#X} from address {:#X}", value, addr);
    value
}

pub fn input_32(addr: u32, offset: usize) -> u32 {
    let address: u32 = addr + offset as u32;
    read_reg(address)
}

pub fn output_32(addr: u32, offset: usize, value: u32) {
    let address: u32 = addr + offset as u32;
    write_reg(address, value);
}

#[derive(Debug, Clone, Copy, Default)]
pub struct FIOPadConfig {
    pub instance_id: u32,    // 设备实例 ID
    pub base_address: usize, // 基地址
}

#[feature(const_trait_impl)]
#[derive(Debug, Clone, Copy, Default)]
pub struct FIOPadCtrl {
    pub config: FIOPadConfig, // 配置
    pub is_ready: u32,        // 设备是否准备好
}

pub static mut iopad_ctrl:FIOPadCtrl = FIOPadCtrl{
    config:FIOPadConfig{
        instance_id: 0,    
        base_address: 0, 
    },
    is_ready:0,
};


static FIO_PAD_CONFIG_TBL: [FIOPadConfig; 1] = [FIOPadConfig {
    instance_id: 0,
    base_address: 0x32B30000usize,
}];

pub fn FIOPadCfgInitialize(instance_p: &mut FIOPadCtrl, input_config_p: &FIOPadConfig) -> bool {
    assert!(Some(instance_p.clone()).is_some(), "instance_p should not be null");
    assert!(
        Some(input_config_p.clone()).is_some(),
        "input_config_p should not be null"
    );
    let mut ret: bool = true;
    if instance_p.is_ready == 0x11111111u32 {
        debug!("Device is already initialized.");
    }
    // Set default values and configuration data
    FIOPadDeInitialize(instance_p);
    instance_p.config = *input_config_p;
    instance_p.is_ready = 0x11111111u32;
    ret
}

pub fn FIOPadDeInitialize(instance_p: &mut FIOPadCtrl) -> bool {
    // 确保 `instance_p` 不为 null,类似于 C 中的 `FASSERT(instance_p)`
    if instance_p.is_ready == 0 {
        return true;
    }

    // 标记设备为未准备好
    instance_p.is_ready = 0;

    // 清空设备数据
    unsafe {
        core::ptr::write_bytes(instance_p as *mut FIOPadCtrl, 0, size_of::<FIOPadCtrl>());
    }

    true
}

pub fn FIOPadLookupConfig(instance_id: u32) -> Option<FIOPadConfig> {
    if instance_id as usize >= 1 {
        // 对应 C 代码中的 FASSERT 语句
        return None;
    }

    for config in FIO_PAD_CONFIG_TBL.iter() {
        if config.instance_id == instance_id {
            return Some(*config);
        }
    }

    None
}
}

1.2 MIO 控制器初始化

先对MIO进行初始化配置,包括功能寄存器地址,MIO寄存器地址以及中断编号 按照之前配置的I/Opad引脚寄存器,来设置 I2C 的 SCL 和 SDA 引脚功能。 设置MIO配置,包括 ID号、MIO基地址、中断号、频率、设备地址 和 传输速率。(这些由FMioLookupConfig来提供不同MIO的配置信息)

#![allow(unused)]
fn main() {
pub unsafe  fn FI2cMioMasterInit(address: u32, speed_rate: u32) -> bool {
    let mut input_cfg: FI2cConfig = FI2cConfig::default();
    let mut config_p: FI2cConfig = FI2cConfig::default();
    let mut status: bool = true;
    // MIO 初始化
    master_mio_ctrl.config = FMioLookupConfig(1).unwrap();
    status = FMioFuncInit(&mut master_mio_ctrl, 0b00);
    if status != true {
        debug!("MIO initialize error.");
        return false;
    }
    FIOPadSetFunc(&iopad_ctrl, 0x00D0u32, 5); /* scl */
    FIOPadSetFunc(&iopad_ctrl, 0x00D4u32, 5); /* sda */
    unsafe {
        core::ptr::write_bytes(&mut master_i2c_instance as *mut FI2c, 0, size_of::<FI2c>());
    }
    // 查找默认配置
    config_p = FI2cLookupConfig(1).unwrap(); // 获取 MIO 配置的默认引用
    if !Some(config_p).is_some() {
        debug!("Config of mio instance {} not found.", 1);
        return false;
    }
    // 修改配置
    input_cfg = config_p.clone();
    input_cfg.instance_id = 1;
    input_cfg.base_addr = FMioFuncGetAddress(&master_mio_ctrl, 0b00);
    input_cfg.irq_num = FMioFuncGetIrqNum(&master_mio_ctrl, 0b00);
    input_cfg.ref_clk_hz = 50000000;
    input_cfg.slave_addr = address;
    input_cfg.speed_rate = speed_rate;
    // 初始化
    status = FI2cCfgInitialize(&mut master_i2c_instance, &input_cfg);
    // 处理 FI2C_MASTER_INTR_EVT 中断的回调函数
    master_i2c_instance.master_evt_handlers[0 as usize] = None;
    master_i2c_instance.master_evt_handlers[1 as usize] = None;
    master_i2c_instance.master_evt_handlers[2 as usize] = None;
    if status != true {
        debug!("Init mio master failed, ret: {:?}", status);
        return status;
    }
    debug!(
        "Set target slave_addr: 0x{:x} with mio-{}",
        input_cfg.slave_addr, 1
    );
    status
}
}

1.3 初始化 I2C 配置

I2C配置中包含MIO配置,具体可以见结构体FI2c。首先检查设备是否已经初始化,防止重复初始化。如果设备没有初始化,它会进行去初始化操作,设置设备的配置数据,然后重置设备,最后将设备标记为已就绪状态

#![allow(unused)]
fn main() {
pub fn FI2cCfgInitialize(instance_p: &mut FI2c, input_config_p: &FI2cConfig) -> bool {
    assert!(Some(instance_p.clone()).is_some() && Some(input_config_p).is_some());

    let mut ret = true;

    // 如果设备已启动,禁止初始化并返回已启动状态,允许用户取消初始化设备并重新初始化,但防止用户无意中初始化
    if instance_p.is_ready == 0x11111111u32 {
        debug!("Device is already initialized!!!");
        return false;
    }

    // 设置默认值和配置数据,包括将回调处理程序设置为存根,以防应用程序未分配自己的回调而导致系统崩溃
    FI2cDeInitialize(instance_p);
    instance_p.config = *input_config_p;

    // 重置设备
    ret = FI2cReset(instance_p);
    if ret == true {
        instance_p.is_ready = 0x11111111u32;
    }

    ret
}
}

最后返回到MIO控制器初始化中,初始化I2C设备中的中断函数

2.收发数据

2.1 发送数据

#![allow(unused)]
fn main() {
pub unsafe fn FI2cMasterWrite(buf_p: &mut [u8], buf_len: u32, inchip_offset: u32) -> bool {
    let mut status: bool = true;

    if buf_len < 256 && inchip_offset < 256 {
        if (256 - inchip_offset) < buf_len {
            debug!("Write to eeprom failed, out of eeprom size.");
            return false;
        }
    } else {
        debug!("Write to eeprom failed, out of eeprom size.",);
        return false;
    }

    status = FI2cMasterWritePoll(&mut master_i2c_instance, inchip_offset, 1, buf_p, buf_len);
    //debug!("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
    if status != true {
        debug!("Write to eeprom failed");
    }

    status
}

}

这个发送数据函数接收一个buffer用来装要发送的数据,由于是[u8],只有一个u8大小的值,所以buf_len也是1(这份数据只发一次,不重复发)。还接收一个inchip_offset,指从设备内部的偏地址 先通过FI2c对象来判断是否准备好,工作模式是否为主模式 然后启动I2C主设备传输,可以看这里

#![allow(unused)]
fn main() {
pub fn FI2cMasterStartTrans(
    instance_p: &mut FI2c,
    mem_addr: u32,
    mem_byte_len: u8,
    flag: u16,
) -> bool {
    assert!(Some(instance_p.clone()).is_some());
    let base_addr = instance_p.config.base_addr;
    let mut addr_len: u32 = mem_byte_len as u32;
    let mut ret = true;

    ret = FI2cWaitBusBusy(base_addr.try_into().unwrap());
    if ret != true {
        return ret;
    }
    ret = FI2cSetTar(base_addr.try_into().unwrap(), instance_p.config.slave_addr);

    while addr_len > 0 {
        if FI2cWaitStatus(base_addr.try_into().unwrap(), (0x1 << 1)) != true {
            break;
        }
        if input_32(base_addr.try_into().unwrap(), 0x80) != 0 {
            return false;
        }
        if input_32(base_addr.try_into().unwrap(), 0x70) & (0x1 << 1) != 0 {
            addr_len -= 1;
            let value = (mem_addr >> (addr_len * 8)) & FI2C_DATA_MASK();
            if addr_len != 0 {
                output_32(base_addr.try_into().unwrap(), 0x10, value);
            } else {
                output_32(base_addr.try_into().unwrap(), 0x10, value + flag as u32);
            }
        }
    }
    ret
}
}

随后在FIFO不满的情况下,向0x10(IC_DATA_CMD)的bit[7:0]写数据,bit[8]写0表示写以外,向bit[9]写1表示停止。

2.2 接收数据

#![allow(unused)]
fn main() {
pub unsafe fn FI2cMasterRead(buf_p: &mut [u8], buf_len: u32, inchip_offset: u32) -> bool {
    let mut instance_p: FI2c = master_i2c_instance;
    let mut status: bool = true;

    assert!(buf_len != 0);

    for i in 0..buf_len as usize {
        buf_p[i] = 0;
    }

    status = FI2cMasterReadPoll(&mut instance_p, inchip_offset, 1, buf_p, buf_len);

    status
}
}

和发送数据函数一样,接收数据函数接收一个buffer用来装接收到的u8大小的数据,以及buf_len,inchip_offset 先通过FI2c对象来判断是否准备好,工作模式是否为主模式 然后启动I2C主设备传输。 发送读数据命令:向0x10(IC_DATA_CMD)bit[8]写1,表示命令为读操作. 在FIFO不空的情况,读取数据,读最后一个字节数据时要加上停止信号,即除了向0x10(IC_DATA_CMD)的bit[8]仍写1表示读以外,向bit[9]写1表示停止。 最后停止I2C传输,见

#![allow(unused)]
fn main() {
pub fn FI2cMasterStopTrans(instance_p: &mut FI2c) -> bool {
    assert!(Some(instance_p.clone()).is_some());
    let mut ret = true;
    let base_addr = instance_p.config.base_addr;
    let mut reg_val = 0;
    let mut timeout = 0;

    while true {
        if input_32(base_addr.try_into().unwrap(), 0x34) & (0x1 << 9) != 0 {
            reg_val = input_32(base_addr.try_into().unwrap(), 0x60);
            break;
        } else if 500 < timeout {
            break;
        }
        timeout += 1;
        busy_wait(Duration::from_millis(1));
    }

    ret = FI2cWaitBusBusy(base_addr.try_into().unwrap());
    if ret == true {
        ret = FI2cFlushRxFifo(base_addr.try_into().unwrap());
    }
    ret
}
}

飞腾派 I2C 寄存器(待整理):

寄存器偏移描述
IC_CON0x00I2C控制寄存器
IC_TAR0x04I2C主机地址寄存器
IC_SAR0x08I2C从机地址寄存器
IC_HS_MADDR0x0CI2C高速主机模式编码地址寄存器
IC_DATA_CMD0x10I2C数据寄存器
IC_SS_SCL_HCNT0x14标准模式I2C时钟信号SCL的高电平计数寄存器

飞腾派 MIO 寄存器基地址:

NameOffset
MIO00x000_2801_4000
MIO10x000_2801_6000
MIO20x000_2801_8000
MIO30x000_2801_A000
MIO40x000_2801_C000
MIO50x000_2801_E000
MIO60x000_2802_0000
MIO70x000_2802_2000
MIO80x000_2802_4000
MIO90x000_2802_6000
MIO100x000_2802_8000
MIO110x000_2802_A000
MIO120x000_2802_C000
MIO130x000_2802_E000
MIO140x000_2803_0000
MIO150x000_2803_2000

3.3 SPI驱动开发

SPI 基础

SPI(rial Peripheral interface)串行外围设备接口,从名称上就可以明白这个总线使用的还是串行通信,SPI一种主从模式高速全双工串行总线。 SPI接口一般使用四条信号线通信:MISO(数据输入),MOSI(数据输出),SCK(时钟),CS(片选)
MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
SCLK:串行时钟信号,由主设备产生。
CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。 单个spi设备连接 多个spi设备连接

常见的连接方法有上面两种,还有一种额外电路所能实现的片选方案。众所周知三个引脚其实是存在8种状态,所以实际上可以利用译码器实现三个CS/SS控制8个SPI设备。 3-8译码器

SPI 与一些常见的串行总线相比,有以下三个显著的区别:

① 无传输速率限定
SPI 没有严格的速度限制,一般实现通常能达到甚至超过 100Mbps,适合高速数据传输场景。

② 传输方式为数据交换
SPI 工作在主从模式下,但在每个时钟周期(Clock)内,主从设备都会同时发送和接收 1 bit 数据,实现真正的全双工数据交换。无论是主设备还是从设备,每个时钟周期都会有数据交换。 只不过是主机进行写操作时,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。 传输模型 ③ 没有明确的协议格式
SPI 没有指定的流控制,没有应答机制确认是否接收到数据,只要四根信号线连接正确,SPI模式相同,将CS/SS信号线拉低,即可以直接通信,一次一个字节的传输,读写数据同时操作。SPI并不关心物理接口的电气特性,例如信号的标准电压

飞腾派上SPI实验

第四章:RUST驱动开发教程

测试驱动开发

设计模式

注意事项

4.1 测试驱动开发

测试驱动开发(TDD)是一种软件开发方法,强调在编写实际代码之前先编写测试用例。它的主要步骤包括:

  1. 先写一个失败的单元测试

  2. 编写实现代码,使得该测试通过

  3. 重构代码,保持所有测试通过状态

优点:

  1. 改善设计:在编写测试的过程中,可以从使用者的角度去思考问题,这有助于提前发现设计上的问题,从而提高代码的质量。

  2. 降低风险:有了充分的单元测试,我们可以放心地进行重构,因为一旦我们的修改破坏了原有的功能,测试会立刻发现。

  3. 提高效率:虽然TDD需要在初期投入更多的时间,但是随着项目的推进,越来越多的测试将会大大减少因为错误和回归带来的时间损失。

实施

测试工具

  • qemu - 一个开源的虚拟机,可以模拟多种硬件平台。
  • cargo test
  • bare-test - 是一个轻量级的测试框架,专为裸机环境设计。它允许在没有操作系统支持的情况下运行测试(本身是一个小型操作系统)。
  • gdb-multiarch - GNU调试器,支持多种架构。

环境搭建

Linux

aptcargo 等工具直接安装

Windows

推荐

  • msys2 - 一个轻量级的Linux环境,可以在Windows上运行。可方便的安装qemugdb-multiarch等工具。

自定义测试原理

  1. Cargo test 默认如何工作?

    cargo test 命令会编译项目中的所有测试代码,libtest 收集 #[test] 标记的函数,成一个可执行文件,然后运行这个可执行文件来获取执行结果。

  2. 我们从哪里开始使用自定义测试框架?

    Cargo.toml 中添加

    [[test]]
    name = "test"
    path = "tests/test.rs"
    harness = false
    
  3. 定义测试函数声明规范

    ...

  4. 收集所有测试函数

    ...

  5. 构建no-std裸机程序

    ...

  6. 修改 Cargo runnerQemu

    ...

项目搭建

https://github.com/drivercraft/bare-test-template

点击 "Use this template" 按钮,创建一个新的仓库。

修改 Cargo.toml 文件:

[package]
edition = "2024"
name = "project-name"
version = "0.1.0"

project-name 替换为你的项目名称。

src/lib.rs 中构建你的驱动程序

tests/test.rs 中编写测试代码。

运行测试

安装 ostool

cargo install ostool

运行测试

cargo test --package project-name --test test -- tests --show-output
# 带uboot的开发板测试
cargo test --package project-name --test test -- tests --show-output --uboot 

完善测试工作流

github\gitee ci

CI步骤:

  1. Rust环境
  2. Qemu安装
  3. 代码质量检查
  4. 编译和测试

4.2 设计模式

驱动模型

可复用性(操作系统无关)

  • 硬件层:设计一个通用的驱动模型,使得驱动程序可以在不同的操作系统上复用。

    1. 只包括硬件相关的代码,避免与操作系统的特定实现绑定。
    2. 涉及修改的操作使用mut,将锁交由操作系统处理,避免引入锁。
    3. 暴露中断handler,将中断注册交由操作系统处理。
    4. 可用异步简化中断回调。
    5. 适度unsafe。
  • 操作系统连接层: 单元测试所在层级,基于操作系统的特性和API实现驱动程序的连接。

    1. 通过包裹系统的mutex等锁实现保证并发安全。
    2. 中断注册系统注册中断回调。
    3. work创建。
    4. 连接上层组件接口(fs、net等)。

寄存器定义

推荐使用 tock-registers 库来定义寄存器。

  1. 定义寄存器结构体。
  2. 定义字段bit

AI辅助

上下文提供手册,定义格式。

example: https://developer.arm.com/documentation/ddi0183/g/programmers-model/summary-of-registers

锁的实现

spinlock、atomic

中断

设备树中的中断信息

中断handler

基于中断的异步模式

原理概述

在传统的驱动开发中,中断处理通常使用回调函数或状态机来管理设备状态的变化。而基于中断的异步模式利用Rust的async/await语法,将中断驱动的状态变化转换为异步任务,使代码更加直观和易于维护。

核心实现机制

1. Future和Waker机制

2. 中断处理器与异步任务的桥接

中断处理器通过Waker唤醒等待的异步任务

3. 驱动异步接口

与传统状态机模式的对比

传统状态机模式

#![allow(unused)]
fn main() {
enum DeviceState {
    Idle,
    Reading,
    Writing,
    Error,
}

struct Device {
    state: DeviceState,
    callback: Option<Box<dyn Fn(Result<Vec<u8>, Error>)>>,
}

impl Device {
    fn start_read(&mut self, callback: Box<dyn Fn(Result<Vec<u8>, Error>)>) {
        self.state = DeviceState::Reading;
        self.callback = Some(callback);
        // 启动硬件操作
    }
    
    fn handle_interrupt(&mut self) {
        match self.state {
            DeviceState::Reading => {
                let data = self.get_data();
                if let Some(callback) = self.callback.take() {
                    callback(Ok(data));
                }
                self.state = DeviceState::Idle;
            }
            // 其他状态处理...
        }
    }
}
}

异步模式优势

  1. 代码可读性:异步模式使用线性的控制流,避免了状态机的复杂状态转换逻辑
  2. 错误处理:可以使用标准的?操作符和try-catch模式处理错误
  3. 组合性:异步函数可以轻松组合和链式调用
  4. 资源管理:自动的生命周期管理,减少内存泄漏风险

实际应用示例

复杂设备操作的异步实现

#![allow(unused)]
fn main() {
impl NetworkDevice {
    pub async fn send_packet(&mut self, packet: &[u8]) -> Result<(), NetworkError> {
        // 1. 检查设备状态
        self.wait_device_ready().await?;
        
        // 2. 设置DMA传输
        self.setup_dma_transfer(packet).await?;
        
        // 3. 启动传输
        self.start_transmission();
        
        // 4. 等待传输完成中断
        self.wait_for_tx_complete().await?;
        
        // 5. 清理资源
        self.cleanup_dma();
        
        Ok(())
    }
    
    async fn wait_device_ready(&mut self) -> Result<(), NetworkError> {
        loop {
            if self.is_ready() {
                break Ok(());
            }
            
            // 等待状态变化中断
            self.wait_for_status_interrupt().await?;
            
            // 检查是否出现错误状态
            if self.has_error() {
                break Err(NetworkError::DeviceError);
            }
        }
    }
}
}

同步调用异步模型

spin_on 等效于 spin_loop

性能考虑

零成本抽象

Rust的异步实现是零成本抽象,编译后的代码性能与手写状态机相当:

  • Future在编译时被展开为状态机
  • 无运行时开销的轮询机制
  • 内存使用效率高

中断延迟优化

#![allow(unused)]
fn main() {
// 快速中断处理器,最小化中断处理时间
fn fast_interrupt_handler() {
    // 仅做必要的硬件操作
    let status = read_interrupt_status();
    clear_interrupt_flag();
    
    // 唤醒等待的异步任务
    task_awake(pid);
}

## 最佳实践

1. **中断处理器保持简洁**:只做必要的硬件操作,复杂逻辑移至异步任务
2. **合理使用超时**:避免异步任务无限等待
3. **错误传播**:充分利用`?`操作符进行错误传播
4. **资源清理**:使用RAII模式确保资源正确释放

这种基于中断的异步模式为驱动开发提供了一种现代化的解决方案,既保持了性能优势,又显著提高了代码的可维护性和可读性。
}

4.3 注意事项

缓存一致性

高速缓存的基础知识

高速缓存的共享属性

高速缓存的维护指令

软件维护缓存一致性

利用 dma-api 简化缓存维护操作

内存屏障

cpu 乱序执行

内存屏障的类型

如何使用内存屏障

Rust 中的 fence

跨平台的内存屏障

第五章:高速传输类驱动

5.1 DMA驱动开发

5.2 PCI控制器驱动

5.3 PCIe互联驱动

第六章:网络通信类驱动

6.1 单元测试与调试

6.2 PCIe网卡驱动基础

6.3 IGB网卡驱动实现

6.4 GMAC以太网基础

6.5 YT8521驱动实现

6.6 net_device实现

第七章:存储驱动实现

7.1 Micro SD驱动

7.2 eMMC驱动

7.3 Flash驱动

第八章:多媒体方向

8.1 USB转串口驱动情境分析

USB转串口测试过程请阅读此文档:USB转串口测试流程

usb协议栈的初始化

  1. 把usb协议栈编译进内核空间。编译的命令是make A=apps/usb-hid ...,在usb-hid程序的配置文件Cargo.toml命令里指定了要依赖driver_usb,这样就决定了会把USB协议栈driver_usb编译进内核空间。

  2. 初始化XHCI控制器。在usb-hidmain函数会执行USBSystem::new().init(),这样就会进一步调用USBHostSystem::init()初始化XHCI控制器。执行流程如下:

    #![allow(unused)]
    fn main() {
    // host/data_structures/host_controllers/xhci/mod.rs
    fn init(&mut self) -> &mut Self {
        // 1. 硬件重置XHCI控制器
        self.chip_hardware_reset()
        // 2. 设置设备槽数量与内存映射
        .set_max_device_slots()
        .set_dcbaap()  // 设置设备上下文基地址
        // 3. 配置命令环与事件环
        .set_cmd_ring()
        .init_ir()      // 初始化中断
        // 4. 分配 scratchpad 缓冲区
        .setup_scratchpads()
        // 5. 启动控制器
        .start()
        .test_cmd()     // 测试命令环
        .reset_ports(); // 重置所有USB端口
        self
    }
    }
  3. XHCI控制器检测USB设备的接入。使用probe()方法扫描所有USB端口,检测物理连接的设备。执行流程如下:

    #![allow(unused)]
    fn main() {
    // host/data_structures/host_controllers/xhci/mod.rs
    fn probe(&mut self) -> Vec<usize> {
        let mut founded = Vec::new();
        let port_len = self.regs.port_register_set.len();
        // 1. 检查所有端口的连接状态
        for i in 0..port_len {
            let portsc = self.regs.port_register_set.read_volatile_at(i).portsc;
            if portsc.current_connect_status() {
                founded.push(i);
            }
        }
        // 2. 为每个连接的设备分配slot ID并初始化
        for port_idx in founded {
            let slot_id = self.device_slot_assignment();       // 分配slot ID
            self.dev_ctx.new_slot(slot_id as usize, 0, port_idx + 1, 32);
            self.address_device(slot_id, port_idx + 1);       // 分配设备地址
            self.set_ep0_packet_size(slot_id, self.control_fetch_control_point_packet_size(slot_id));
            founded.push(slot_id);
        }
        founded
    }
    }
  4. 获取USB设备的设备描述符

    #![allow(unused)]
    fn main() {
    // host/data_structures/host_controllers/xhci/mod.rs
    fn control_fetch_control_point_packet_size(&mut self, slot_id: usize) -> u8 {
        // 发送GET_DESCRIPTOR请求获取设备描述符
        let mut buffer = DMA::new_vec(0u8, 8, 64, self.config.lock().os.dma_alloc());
        self.control_transfer(
            slot_id,
            ControlTransfer {
                request_type: bmRequestType::new(
                    Direction::In,
                    DataTransferType::Standard,
                    Recipient::Device,
                ),
                request: bRequest::GetDescriptor,
                value: DescriptorType::Device.forLowBit(0).bits(),
                data: Some((buffer.addr() as usize, buffer.length_for_bytes())),
            },
        ).unwrap();
        // 解析描述符获取最大包大小
        let data = buffer.to_vec();
        data.last().copied().unwrap_or(8)
    }
    }
  5. USB驱动的匹配。在usb-hidmain函数会执行USBSystem::new().init(),这样也会进一步调用USBDriverSystem::init()注册所有的USB驱动模块。例如CdcSerialDriverModuleshould_active()方法,其主要功能是判断是否应该激活 CDC(通用串行总线控制设备类)串口驱动。具体步骤如下:

    1. 设备描述符检查:检查传入的独立设备实例的描述符是否已初始化。
    2. 设备类、厂商 ID 和产品 ID 检查:如果描述符已初始化,获取设备的类、厂商 ID 和产品 ID,判断是否为特定的厂商和产品(厂商 ID 为 0x1a86,产品 ID 为 0x7523,对应沁恒电子的 CH340 串口转换器)。
    3. 端点信息收集:若匹配成功,收集设备的端点信息。
    4. 驱动初始化:使用收集到的信息初始化 CdcSerialDriver 实例,并将其封装在 Option<Vec<Arc<SpinNoIrq<dyn USBSystemDriverModuleInstance<'a, O>>>> 中返回。
    5. 不匹配处理:若不匹配,则返回 None
  6. 驱动实例的创建。匹配成功后创建驱动实例,调用new_and_init方法初始化设备功能。例如CdcSerialDrivernew_and_init方法,其具体的初始化过程如下:

    1. 为读缓冲区分配空间
    2. 查找输入端点和输出端点
    3. 创建写缓冲区并对其进行初始化
    4. 创建控制缓冲区和状态缓冲区
    5. 创建CdcSerialDriver实例
  7. 驱动实例初始化设备。在usb-hidmain函数会执行USBSystem::new().init().init_probe(),驱动实例最终就可以调用prepare_for_drive方法通过 URB 请求配置设备接口、端点等。例如CdcSerialDriverprepare_for_drive方法,其具体的初始化过程如下:

    1. 初始化一个空向量todo_list以保存URB
    2. 设置配置描述符
    3. 厂商自定义配置流程。这些代码创建了多个控制传输的URB,包括输出和输入操作,涉及不同的命令(如CH341_CMD_C3, CH341_CMD_C1, CH341_CMD_W, CH341_CMD_R等)、索引和值,这些操作参考了Linux源码里的drivers/usb/serial/ch341.h中的ch341_configure函数以及wireshark抓包数据。
    4. 配置115200波特率。创建了一系列控制传输的URB,用于将设备的波特率配置为115200。
    5. 返回配置所需的URB列表。将todo_list封装在some中返回。
  8. 通过事件系统通知初始化完成。驱动初始化完成后通过事件总线通知上层系统:

    #![allow(unused)]
    fn main() {
    // usb系统主循环
    pub fn drive_all(mut self) -> Self {
        loop {
            let tick = self.usb_driver_layer.tick();  // 收集URB请求
            if tick.len() != 0 {
                self.host_driver_layer.tock(tick);  // 处理URB并获取UCB
            }
        }
    }
    }

用户向串口发送数据

  1. 用户把数据写入发送缓冲区。用户需要调用CdcSerialDriverwrite方法,把数据写入发送缓冲区。当前的测试流程,是在CdcSerialDrivernew_and_init方法里,把hello,world硬写入发送缓冲区。

  2. 生成URB并发送gather_urb方法会检查write_data_bnuffer队列,若队列中有数据,就生成用于批量传输的URB并发送。

    #![allow(unused)]
    fn main() {
        fn gather_urb(&mut self) -> Option<Vec<crate::usb::urb::URB<'a, O>>> {
            // 总的逻辑是遍历写入数据缓冲区,如果有数据就发送,如果没有数据就接收
            let mut todo_list = Vec::new();
    
            if self.write_data_buffer.len() > 0 {
                // 如果写入数据缓冲区有数据,就发送数据
                let write_buffer = if let Some(buffer) = self.write_data_buffer.front() {
                    buffer
                } else {
                    panic!("write_data_buffer is empty, but write_data_buffer.len() > 0");
                };
    
                todo_list.push(URB::new(
                    self.device_slot_id,
                    RequestedOperation::Bulk(BulkTransfer {
                        endpoint_id: self.out_endpoint as usize,
                        buffer_addr_len: write_buffer.lock().addr_len_tuple(),
                    }),
                ));
                self.driver_state_machine = StateMachine::Writing;
                Some(todo_list)
            } else {
                // ... 接收数据逻辑 ...
            }
        }
    }
  3. USB转串口转换器转换并发送数据。USB 转串口驱动把 URB 封装成符合 USB 协议的数据包,经 USB 总线传给 USB 转串口转换器。转换器解析数据包,将数据转为串口信号(如 UART 信号),再通过串口引脚发送出去。

串口数据传给用户

  1. USB 转串口转换器接收并转换信号。USB 转串口转换器检测到串口引脚的信号变化,按串口协议解码信号,提取原始数据,接着封装成 USB 数据包,通过 USB 总线传给计算机。

  2. 生成URB并发送gather_urb方法生成用于接收数据的URB。

    #![allow(unused)]
    fn main() {
        fn gather_urb(&mut self) -> Option<Vec<crate::usb::urb::URB<'a, O>>> {
            // ... 发送数据逻辑 ...
    
            } else {
                // 如果写入数据缓冲区没有数据,就接收数据
                todo_list.push(URB::new(
                    self.device_slot_id,
                    RequestedOperation::Bulk(BulkTransfer {
                        endpoint_id: self.in_endpoint as usize,
                        buffer_addr_len: self.read_data_buffer.as_ref().unwrap().lock().addr_len_tuple(),
                    }),
                ));
                self.driver_state_machine = StateMachine::Reading;
                Some(todo_list)
            }
        }
    }
  3. 处理接收完成事件。USB 转串口驱动收到数据后,触发接收完成事件,调用 CdcSerialDriverreceive_complete_event 方法。该方法把接收到的数据存储在accepted_date中。

  4. 用户应用程序接收数据。如果是进行测试,可以在Arceos的日志中看到accepted data: xxx,其中xxx为串口接收到的数据,这表明串口数据接收成功。如果用户需要串口数据,则应用程序应从accepted_data读取数据。

8.2 USB摄像头驱动

第九章:无线通讯方向

9.1 WiFi6驱动开发

9.2 蓝牙驱动开发

第十章:实时工业总线方向

10.1 CANFD驱动

10.2 EtherCAT驱动

附录:开发训练营资源

A.1 训练营课表

A.2 项目选题指南

A.3 优秀项目案例