前言
本教材仍在撰写和调整中
第零章:环境配置与预备知识
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 运行环境
编译环境
编译依赖ubuntu操作系统,使用的winodws操作系统的同学可以通过安装WSL(linux 子系统)或者VMwork虚拟机来安装ubuntu。 安装好ubuntu 后需要安装必要的一些组件可直接执行如下指令
# 安装git 拉取代码
sudo apt install git
# 输入自己github的邮箱用户名
git config --global user.name "runoob"
git config --global user.email test@runoob.com
# 生成ssh密钥
ssh-keygen -t rsa -b 4096 -C "your.email@example.com"
# 安装编译qemu所需的依赖包
sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \
gawk build-essential bison flex texinfo gperf libtool patchutils bc \
zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev libsdl2-dev \
git tmux python3 python3-pip ninja-build
vscode使用
由于是使用的wsl(inux子系统)作为基础编译环境,默认提供的是命令行搭配vim的方式来编辑文件。这可能并不是大部分人喜欢的开发环境,所以推荐使用vscode搭配remote-ssh插件来进行开发,并且该插件对于wsl是有很好的兼容性的,安装插件后直接选择连接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
飞腾派资料包(提取码: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启动会禁用风扇请注意,过热可能会损坏飞腾派。
启动后连接串口,可以看到如下打印即说明系统成功启动,
账号: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
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驱动开发
1. 硬件工作原理
1.1 模块概述
- 功能描述
GPIO 是 General Purpose Input Output(通用输入/输出) 的缩写,也就意味着这种类型的外设可以配置为多种输入/输出类型。单根GPIO的模型可以简单理解为一根导线。 导线的一端留给硬件工程师,他们可以将这一端任意的连接到他们想要的地方,然后告诉驱动工程师,他们想要 ”这根线“ 起到什么作用;导线的一端连接到cpu核心,驱动工程师通过cpu配置这个模块为指定的功能。
一般来说,GPIO可以用于获取某个的高低电平,作为cpu中断触发源等等。
-
应用场景
- 外部中断源输出。连接到特定的开关上去,当这个开关被按下时,电压发生跳变,由此执行相应逻辑。
- 检测外部输入,用于检查系统关键位置是否满足要求。
- 输出恒定电压,用于点亮LED等。
-
核心特性
- 开漏输出:可以实现电平转换,输出电平取决于上拉电阻电源;可以实现io的线与逻辑。
- 推挽输出:通过控制两个晶体管的开关来控制电平状态,优点是驱动能力较强,输出IO口驱动电流最大可到20ma。
- 浮空输入:浮空输入是指将输入引脚未连接到任何外部信号源或电路,使其处于未定义的状态。
- 下拉输入:芯片输入引脚通过电阻接到电源电压。
- 上拉输入:芯片输入引脚通过电阻借到参考0电平。
1.2 硬件接口介绍
1.3 时序图
2.接口表
pl061 接口表
- 表格格式:
API函数 | 描述 | 参数 | 返回值 |
---|---|---|---|
Pl0611::new | 创建gpio实例 | base_addr: Gpio控制器的基地址 | 初始化的gpio控制器 |
Pl0611::set_func | 设置gpio引脚功能 | self,ch:通道号,func: Gpio功能 | Result<(),IoError>,成功Ok(()), 失败:无效的通道号 |
Pl0611::int_when | 设置gpio中断条件 | self, ch:通道号,cond:什么时候触发中断,必须先配置为中断模式 | Result<(),IoError> 成功Ok(()), 失败:无效的通道号 |
Pl0611::set_output | 设置gpio输出值 | self, ch:通道号, val:引脚值 | Result<(),IoError> 成功Ok(()), 失败:无效的通道号(没有配置为输出) |
Pl0611::get_input | 设置gpio输出值 | self, ch:通道号, | Result<bool,IoError> 成功Ok(value), 失败:无效的通道号 |
飞腾派 GPIO1 接口表
- 表格格式:
API函数 | 描述 | 参数 | 返回值 |
---|---|---|---|
PhytiumGpio::new | 创建gpio实例 | base_addr: Gpio控制器的基地址 | 初始化的gpio控制器 |
PhytiumGpio::set_func | 设置gpio引脚功能 | self,ch:通道号,func: Gpio功能 | Result<(),IoError>,成功Ok(()), 失败:无效的通道号 |
PhytiumGpio::set_output | 设置gpio输出值 | self, ch:通道号, val:引脚值 | Result<(),IoError> 成功Ok(()), 失败:无效的通道号(没有配置为输出) |
PhytiumGpio::get_input | 设置gpio输出值 | self, ch:通道号, | Result<bool,IoError> 成功Ok(value), 失败:无效的通道号 |
-
调用顺序
- 中断模式
- 初始化
- 配置中断模式
- 配置中断条件
- 定义自己的中断函数并注册
- 输入/输出模式
- 初始化
- 配置输入/输出功能
- 输入/输出值
- 中断模式
-
错误处理 IoError::InvChn 不合法的通道
3. 寄存器结构
pl061模块
-
基地址 对于嵌入式平台,device tree是一种常用的方法。这次实现也需要通过设备树的方法获取基地址。不幸的是 qemu 没有直接提供他的设备树,但是启动的时候确实会传递一个默认的设备树。我们通过导出qemu设备树的方法来获取设备树。获得了dts之后,我们在这个dts中搜索 "pl061",可以看到这个:
pl061@9030000说明基地址为 0x9030000。
-
寄存器表
寄存器名称 | 偏移 | 寄存器定义 |
---|---|---|
GPIODIR | 0x400 | 设置GPIO引脚的输入输出功能,1代表该引脚为输出模式,0代表该引脚为输入模式。 |
GPIOIS | 0x404 | 中断的触发方式p1,0代表检测电压,1代表检测边缘 |
GPIOIBE | 0x408 | 中断的触发方式p2,0代表中断通过GPIOIEV来控制,1代表上升沿和下降沿都能触发中断 |
GPIOIEV | 0x40c | 中断的触发方式设置p3,如果中断设置为边沿触发,0代表设置为下降沿触发,1代表上升沿触发;如果中断设置为电平触发, 0代表低电平,1代表高电平 |
GPIOIE | 0x410 | 1是使能中断,0是去使能中断 |
飞腾派GPIO1模块
-
基地址 通过数据手册可以查出,GPIO1模块的基地址为0x000_2803_5000。
-
寄存器表
寄存器名称 | 偏移 | 寄存器定义 |
---|---|---|
GPIO_SWPORT_DR | 0x00 | 每一bit定义了对应引脚的输出值(当配置为输出模式时)。如果该引脚被配置为输出模式,写入这个寄存器的值将会被输出。1对应高电平,0对应低电平。 |
GPIO_SWPORT_DDR | 0x04 | 每一bit定义了对应引脚的in/out属性。1代表该引脚为输出模式,0代表该引脚为输入模式。 |
GPIO_EXT_PORT | 0x08 | 每一bit定义了对应引脚的引脚值。1代表对应引脚输出为高,0代表输出为低。 |
4 具体实现讲解
qemu平台关机实验
对于一些简单的设备,qemu能够很好的进行模拟。因此,对于部分没有开发板而想尝试进行驱动开发学习的同学,我们提供了基于qemu的部分实验。
-
在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。
当然,如果你愿意,飞腾派开发板提供了很多GPIO的拓展线。你可以自己实现一套电路来点亮外接的led灯。
我们最终要实现led灯的"心跳"效果,即 亮1秒,暗1秒,如此往复循环。
- 编写驱动代码,实现 set_dir 和 set_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
5. 开发注意
5.1 优化代码
- 目前驱动代码位于
examples/helloworld/main.c
中,这不是一种正确的做法。参考modules/axhal/src/platform/aarch64_common/pl011.rs
的实现,在同级目录下实现 pl061.rs。 rust 提供了如tock_registers
这样的可以用来定义寄存器的crate,用起来! - 关机函数实际上是触发了一个异常而导致的关机,当把上一步完成后,换成
axhal::misc::terminate
来优雅的关机! - 实验2的完整代码在https://github.com/arceos-org/arceos/commit/2e7837a786d13b0a77804d15a10f614ef715150d。
6. 参考资料
pl061_datasheet:
https://github.com/elliott10/dev-hw-driver/blob/main/docs/GPIO-controller-pl061-DDI0190.pdf
导出qemu设备树:
https://blog.51cto.com/u_15072780/3818667
飞腾派硬件原理图:
飞腾派软件开发手册
https://github.com/elliott10/dev-hw-driver/blob/main/phytiumpi/docs/飞腾派软件编程手册V1.0.pdf
1.2 PWM驱动开发
PWM介绍
PWM(Pulse Width Modulation) 是一种通过调节脉冲宽度(占空比)来模拟不同模拟量输出的数字控制技术。它利用数字信号(高/低电平)控制模拟电路,广泛应用于电机调速、电源转换、LED调光等领域。其核心是通过调整脉冲的“有效时间比例”实现连续可调的电压、功率或信号输出
PWM 最关键的两个参数:频率和占空比。
频率是指单位时间内脉冲信号的周期数。比如开关灯,开关一次算一次周期,在 1s 进行多少次开关(开关一次为一个周期)。
占空比是指一个周期内高电平时间和低电平时间的比例。也拿开关当作例子,总共 100s,开了 50s 灯(高电平),关了 50s 灯(低电平),这时候的占空比就为 50%(比例)。
PWM核心特性
1.占空比可变
- 占空比越大,等效输出电压越高(例:占空比50% ≈ 最大电压的50%)
2.数字模拟转换能力
- 微控制器通过输出高频方波(如10kHz),配合滤波电路,可生成平滑的模拟电压(如0-5V连续可调)
3.控制灵活性强
- 频率可调:适应不同负载需求(电机控制常用6-16kHz,LED调光>80Hz避频闪)
- 动态响应快:占空比可实时调整(如根据传感器反馈调节电机转速)
PWM控制原理和工作过程
关键参数
- 周期(T):一个完整脉冲的时间(单位:秒)。
- 频率(f):周期的倒数(f=1/T),决定信号切换速度。
- 脉宽时间(tW):高电平持续时间,直接决定占空比
PWM波形生成过程
1.周期设定
- 通过定时器计数器设定周期值(PWM定时器的工作方式有点像一个精准的节拍器。它的核心是一个计数器,从0开始计数,数到某个设定值(称为模数)后清零,循环往复。这个模数决定了PWM信号的周期)
- 举例:假设模数设为9,计数器会从0数到9,总共10个状态,构成一个完整的周期。
- PWM定时器通常会有个预分频器,用来把主时钟频率降低,方便控制计数器的速度,这里我们假设主时钟频率24 MHz,预分频器可选1、2、4、8、16、32、64、128
- 预分频器的值决定了计数器的时钟频率,计数器的时钟频率 = 主时钟频率 / 预分频器的值
- 当预分频器设为8,计数器的时钟频率为24 MHz / 8 = 3 MHz
2.脉宽调制
- 设置“宽度寄存器”值W控制高电平时间,这里我们还是以上面的例子继续讲解:
- 我们需求电机静止脉宽:1.5毫秒,最大顺时针速度脉宽:1毫秒,最大逆时针速度脉宽:2毫秒
- 脉宽时间tW = W × 0.333微秒,W是宽度寄存器的值,由此
- 电机静止:1.5毫秒 ÷ 0.333微秒 ≈ 4500
- 最大顺时针速度:1毫秒 ÷ 0.333微秒 ≈ 3000
- 最大逆时针速度:2毫秒 ÷ 0.333微秒 ≈ 6000
飞腾派PWM硬件实现
飞腾派集成的 PWM 控制器支持典型的 PWM 功能,有 2 个完全独立的 compare 输出通道。使用 PWM 功能前,需要先配置相关 PAD 复用寄存器,将对应 PAD 配置到对应功能上,即可使用 PWM 功能。
飞腾派PWM硬件模块
模块 | 功能 |
---|---|
PWM控制器核心模块(处理器内置) | 支持compare输出模式,提供寄存器、FIFO双模式驱动,并支持中断控制:计数器溢出、比较匹配、FIFO空中断 |
死区生成器 | 防短路保护,并提供了Bypass(原始信号直通)、FallEdgeOnly(只添加下降沿延迟)、RiseEdgeOnly(只添加上升沿延迟)、FullDeadband(双边延迟)四种工作模式(由DBDLY和DBCTRL控制) |
飞腾派PWM驱动API调用表
API函数 | 描述 | 参数 | 返回值 |
---|---|---|---|
PwmDriver::new | 创建 PWM 驱动实例并映射硬件寄存器。 | base_addr: PWM 控制器的物理基地址 | 初始化的 PwmDriver 对象 |
configure_channel | 配置 PWM 通道参数 | channel: PWM 通道号 (0-7)、config: PwmConfig 结构体,包含:frequency: PWM 频率 (Hz)、duty_cycle: 占空比 (0.0-1.0)、counting_mode: 计数模式 (Modulo/UpAndDown)、deadtime_ns: 死区时间 (纳秒)、use_fifo: 是否使用 FIFO 模式 | Option:成功:Ok(());失败:错误信息(如无效通道、占空比越界等) |
init_fifo_mode | 初始化 FIFO 模式 | channel: PWM 通道号、 initial_duty: 初始占空比值 | Option:成功:Ok(());失败:错误信息 |
push_fifo_data | 向 FIFO 推送占空比数据 | channel: PWM 通道号;duty_value: 16 位占空比值 | Option:成功:Ok(());失败:错误信息 |
enable_channel | 启用 PWM 通道输出 | channel: PWM 通道号 | 无 |
safe_stop_channel | 安全停止 PWM 输出(防电源瞬变) | channel: PWM 通道号 | 无 |
enable_multiple_channels | 同时启用多个 PWM 通道 | mask: 通道掩码(bit0=通道0, bit1=通道1, ...) | 无 |
handle_interrupt | 处理 PWM 中断 | 无 | 无 |
pwm_init | 初始化 PWM 控制器(高级封装) | base_addr: PWM 控制器物理基地址 | 初始化的 PwmDriver 对象 |
1.3 复位与引脚复用驱动
硬件原理
复位
复位是嵌入式系统中将硬件模块或整个系统恢复到已知初始状态的过程,用于初始化硬件、清除错误状态或恢复系统运行。复位机制通常包括以下类型:
- 上电复位(Power-on Reset, POR):系统上电时由硬件自动触发,初始化所有寄存器和模块到默认状态。
- 软件复位(Software Reset):通过软件写入特定控制寄存器触发,针对系统或特定模块。
- 看门狗复位(Watchdog Reset):由看门狗定时器在检测到系统超时或故障时触发,重启系统。
- 外部复位:通过外部引脚输入信号(如低电平脉冲)触发,通常用于调试或手动重置。
复位过程通常涉及以下步骤:
- 触发信号:硬件或软件生成复位信号(如寄存器写入或外部引脚电平变化)。
- 寄存器清零:相关寄存器(如计数器、状态寄存器)被设置为默认值(通常为0x0)。
- 模块初始化:硬件模块(如CPU、定时器、外设)恢复到初始状态,准备重新运行。
复位类型比较
复位类型 | 触发方式 | 作用范围 | 典型应用场景 |
---|---|---|---|
上电复位 | 硬件上电自动触发 | 整个系统 | 系统启动初始化 |
软件复位 | 写入控制寄存器 | 系统或特定模块 | 模块错误恢复 |
看门狗复位 | 定时器超时 | 整个系统 | 系统故障恢复 |
外部复位 | 外部引脚信号 | 整个系统或模块 | 调试或手动重置 |
sequenceDiagram participant S as 软件 participant R as 复位寄存器 participant M as 硬件模块 S->>R: 写入复位控制位(如write_load=1) R->>M: 发送复位信号 M-->>M: 清零寄存器,重置状态 M->>R: 完成复位 R->>S: 返回复位完成状态
引脚复用
引脚复用(Pin Multiplexing)是嵌入式系统中优化引脚资源的技术,允许一个物理引脚支持多种功能(如GPIO、UART、SPI、I2C等)。由于SoC引脚数量有限,引脚复用通过配置寄存器动态选择引脚的功能和电气特性(如驱动强度、上下拉电阻、延迟)。
引脚复用机制的工作原理:
- 功能选择:通过控制寄存器(如功能选择寄存器)的位字段选择引脚功能。例如,3位字段可支持8种功能(如000=GPIO,001=UART)。
- 电气特性配置:调整驱动强度(mA)、上下拉电阻(上拉/下拉/无)以及输入/输出延迟,以适配不同外设的信号要求。
- 设备树映射:在嵌入式系统中,引脚复用通常通过设备树(Device Tree)定义,映射到具体寄存器配置。
引脚复用功能示例
功能选择位 | 功能 | 描述 |
---|---|---|
000 | GPIO | 通用输入输出 |
001 | UART | 串口通信 |
010 | SPI | 串行外设接口 |
011 | I2C | 两线串行总线 |
sequenceDiagram participant D as 设备驱动 participant P as 引脚控制器 participant R as PAD寄存器 D->>P: 请求引脚功能(如UART) P->>R: 写入功能选择位(如001) R-->>R: 配置驱动强度、上下拉 R->>P: 确认配置完成 P->>D: 返回配置成功
飞腾派复位与引脚复用系统
飞腾派开发板基于飞腾E2000处理器(ARMv8架构,2个FTC664核@1.8GHz和2个FTC310核@1.5GHz),其复位和引脚复用系统由Timer、RAS和PAD模块支持。以下详细介绍飞腾派的实现。
飞腾派复位系统
飞腾派的复位系统由Timer和RAS模块实现,支持系统级和模块级复位。
Timer模块
Timer模块用于定时功能,同时支持复位操作。其关键特性包括:
- 全局恢复位:通过控制寄存器(如ctrl_reg)的write_load位,重置计数器到默认值(通常0x0)。
- 重启/自由运行模式:复位后可选择重新计数或进入自由运行状态。
- 应用场景:初始化Timer模块或触发系统软复位。
Timer模块复位功能
功能 | 描述 | 触发方式 |
---|---|---|
计数器复位 | 将计数器寄存器清零 | 写入ctrl_reg的write_load位 |
模块初始化 | 重置Timer模块到初始状态 | 软件控制或上电复位 |
RAS模块
RAS(可靠性、可用性和可服务性)模块通过错误重置寄存器管理错误状态恢复,增强系统可靠性。其关键特性包括:
- 错误重置:通过err_reset寄存器(偏移0x0D0,32位)清除错误状态或计数器。
- 安全属性配置:通过err_reset_set寄存器(偏移0x0B0,64位)设置安全状态(1为安全,0为非安全)。
- 错误注入:通过err_inject_num寄存器模拟错误状态,用于调试。
RAS模块复位功能
功能 | 描述 | 触发方式 |
---|---|---|
错误状态清除 | 清除错误计数器或状态 | 写入err_reset |
安全属性配置 | 设置复位操作的安全性 | 写入err_reset_set |
错误注入 | 模拟错误触发复位 | 写入err_inject_num |
sequenceDiagram participant S as 软件驱动 participant R as err_reset寄存器 participant M as RAS模块 S->>R: 写入err_reset(0x0D0,置1) R->>M: 清除错误状态 M-->>M: 重置计数器,恢复运行 M->>R: 返回复位完成 R->>S: 通知驱动复位成功
看门狗支持
飞腾派支持看门狗定时器,通过 watchdog_ctrl 寄存器配置超时时间,触发系统级复位,适用于故障恢复场景。
飞腾派引脚复用系统
飞腾派的引脚复用由PAD模块管理,支持10个复用引脚,每个引脚通过两个寄存器(x_reg0和x_reg1)配置,基地址推测为0x32830000。每个引脚可动态切换功能(如GPIO、UART、SPI、I2C)和电气特性。
PAD模块功能
- 功能选择:x_reg0的位[2:0]选择引脚功能(如000=GPIO,001=UART)。
- 驱动强度:x_reg0的位[7:4]设置驱动电流(默认4mA)。
- 上下拉电阻:x_reg0的位[9:8]配置上拉、下拉或无电阻。
- 输入/输出延迟:x_reg1配置输入延迟(位[7:0],粒度100ps)和输出延迟(位[15:8],粒度366ps,最大3.7ns)。
飞腾派引脚复用配置
配置项 | 寄存器 | 位字段 | 功能描述 |
---|---|---|---|
功能选择 | x_reg0 | [2:0] | 选择引脚功能(如GPIO、UART) |
驱动强度 | x_reg0 | [7:4] | 设置驱动电流(默认4mA) |
上下拉电阻 | x_reg0 | [9:8] | 配置上拉/下拉/无电阻 |
输入延迟 | x_reg1 | [7:0] | 启用延迟,粒度100ps |
输出延迟 | x_reg1 | [15:8] | 启用延迟,粒度366ps,最大3.7ns |
飞腾派引脚复用配置
sequenceDiagram participant D as 设备驱动 participant P as PAD控制器 participant R as x_reg0/x_reg1 D->>P: 请求配置引脚AN59为UART P->>R: 写入x_reg0[2:0]=001 P->>R: 写入x_reg0[7:4]=4mA P->>R: 写入x_reg1[7:0]=延迟级别 R-->>R: 更新引脚配置 R->>P: 配置完成 P->>D: 返回成功
引脚复用示例
飞腾派40Pin接口支持多种功能(如GPIO、UART、I2C),通过设备树定义引脚组。例如,引脚AN59可配置为:
- GPIO:x_reg0[2:0]=000,用于通用输入输出。
- UART:x_reg0[2:0]=001,用于串口通信。
- I2C:x_reg0[2:0]=011,用于两线总线。
配置时需确保功能有效,并根据外设需求调整电气特性。
驱动 API 调用表
TODO
寄存器基地址表
TODO
驱动实现解析
TODO
第二章:时钟管理类驱动
2.2 时钟源驱动
时钟是嵌入式系统的"心跳",它驱动着系统的运行和数据的流动,同时影响着系统的能耗.因此,外设的时钟以一种树状的形式在SoC中被组织起来,我们称之为"时钟树",每个"节点"都代表着具有不同频率或性质的时钟源.
根据时钟源的性质,除了某些特定的时钟源,大部分设备的时钟源在Linux内核中可以被分成6大类:
- fixed-clock: 表示频率固定,不可更改的时钟源.在设备树或驱动中,其只需指定频率,无需动态调整.
- fixed-factor-clock: 表示由一个父时钟通过固定的倍频(乘法因子)和分频(除法因子)得到的时钟源。它的频率由父时钟频率乘以一个固定的分子再除以一个固定的分母,不能动态调整,只能在设备树或驱动中静态配置.
- divider-clock: 表示通过对父时钟进行分频(除法因子)得到的时钟源。它的输出频率等于父时钟频率除以一个可配置的分频值。通常用于降低时钟频率以满足外设需求,分频值可以在驱动或设备树中配置,有些情况下支持动态调整.
- gate-clock: 表示可以通过使能或关闭来控制时钟信号输出的时钟源。它通常用于控制外设的时钟开关,实现节能或功能管理。
gate-clock
只负责时钟的开关,不改变时钟频率,相关配置可在驱动或设备树中完成. - mux-clock: 表示可以在多个父时钟之间选择一个作为输出源的时钟节点。它通过切换父时钟,实现时钟源的灵活选择,适用于需要根据不同工作模式或性能需求动态切换时钟源的场景。相关配置可在驱动或设备树中完成.
- composite-clock:
composite-clock
是 Linux 时钟框架中的一种复合型时钟节点,集成了分频(divider)、选择(mux)和开关(gate)等多种功能。它可以灵活地组合这些功能,实现复杂的时钟控制需求。composite-clock
适用于需要同时支持时钟源切换、分频和使能控制的场景,相关配置可在驱动或设备树中完成.
在linux中,时钟的管理依赖于Common Clk Framework.CCF将这些时钟设备的共性抽象出来,使用struct clk_hw
来表示
#![allow(unused)] fn main() { /** * struct clk_hw - handle for traversing from a struct clk to its corresponding * hardware-specific structure. struct clk_hw should be declared within struct * clk_foo and then referenced by the struct clk instance that uses struct * clk_foo's clk_ops * * @core: pointer to the struct clk_core instance that points back to this * struct clk_hw instance * * @clk: pointer to the per-user struct clk instance that can be used to call * into the clk API * * @init: pointer to struct clk_init_data that contains the init data shared * with the common clock framework. This pointer will be set to NULL once * a clk_register() variant is called on this clk_hw pointer. */ struct clk_hw { struct clk_core *core; struct clk *clk; const struct clk_init_data *init; }; }
其中,init
字段,是驱动要关心的,在驱动初始化过程中,需要调用clk_register()
结构注册时钟硬件.而在此之前,驱动需要填充init
字段.struct clk_core
的定义如下
struct clk_init_data {
const char *name;
const struct clk_ops *ops;
const char* const *parent_names;
u8 num_parents;
unsigned long flags;
};
ops
是一组与时钟相关的函数,它决定了时钟能提供的功能,上层驱动通过一组clk_*
的API可以调用它们.其定义在include/linux/clk-provider.h
中
/**
* struct clk_ops - Callback operations for hardware clocks; these are to
* be provided by the clock implementation, and will be called by drivers
* through the clk_* api.
*
* @prepare: Prepare the clock for enabling. This must not return until
* the clock is fully prepared, and it's safe to call clk_enable.
* This callback is intended to allow clock implementations to
* do any initialisation that may sleep. Called with
* prepare_lock held.
*
* @unprepare: Release the clock from its prepared state. This will typically
* undo any work done in the @prepare callback. Called with
* prepare_lock held.
*
* @is_prepared: Queries the hardware to determine if the clock is prepared.
* This function is allowed to sleep. Optional, if this op is not
* set then the prepare count will be used.
*
* @unprepare_unused: Unprepare the clock atomically. Only called from
* clk_disable_unused for prepare clocks with special needs.
* Called with prepare mutex held. This function may sleep.
*
* @enable: Enable the clock atomically. This must not return until the
* clock is generating a valid clock signal, usable by consumer
* devices. Called with enable_lock held. This function must not
* sleep.
*
* @disable: Disable the clock atomically. Called with enable_lock held.
* This function must not sleep.
*
* @is_enabled: Queries the hardware to determine if the clock is enabled.
* This function must not sleep. Optional, if this op is not
* set then the enable count will be used.
*
* @disable_unused: Disable the clock atomically. Only called from
* clk_disable_unused for gate clocks with special needs.
* Called with enable_lock held. This function must not
* sleep.
*
* @save_context: Save the context of the clock in prepration for poweroff.
*
* @restore_context: Restore the context of the clock after a restoration
* of power.
*
* @recalc_rate: Recalculate the rate of this clock, by querying hardware. The
* parent rate is an input parameter. It is up to the caller to
* ensure that the prepare_mutex is held across this call. If the
* driver cannot figure out a rate for this clock, it must return
* 0. Returns the calculated rate. Optional, but recommended - if
* this op is not set then clock rate will be initialized to 0.
*
* @round_rate: Given a target rate as input, returns the closest rate actually
* supported by the clock. The parent rate is an input/output
* parameter.
*
* @determine_rate: Given a target rate as input, returns the closest rate
* actually supported by the clock, and optionally the parent clock
* that should be used to provide the clock rate.
*
* @set_parent: Change the input source of this clock; for clocks with multiple
* possible parents specify a new parent by passing in the index
* as a u8 corresponding to the parent in either the .parent_names
* or .parents arrays. This function in affect translates an
* array index into the value programmed into the hardware.
* Returns 0 on success, -EERROR otherwise.
*
* @get_parent: Queries the hardware to determine the parent of a clock. The
* return value is a u8 which specifies the index corresponding to
* the parent clock. This index can be applied to either the
* .parent_names or .parents arrays. In short, this function
* translates the parent value read from hardware into an array
* index. Currently only called when the clock is initialized by
* __clk_init. This callback is mandatory for clocks with
* multiple parents. It is optional (and unnecessary) for clocks
* with 0 or 1 parents.
*
* @set_rate: Change the rate of this clock. The requested rate is specified
* by the second argument, which should typically be the return
* of .round_rate call. The third argument gives the parent rate
* which is likely helpful for most .set_rate implementation.
* Returns 0 on success, -EERROR otherwise.
*
* @set_rate_and_parent: Change the rate and the parent of this clock. The
* requested rate is specified by the second argument, which
* should typically be the return of .round_rate call. The
* third argument gives the parent rate which is likely helpful
* for most .set_rate_and_parent implementation. The fourth
* argument gives the parent index. This callback is optional (and
* unnecessary) for clocks with 0 or 1 parents as well as
* for clocks that can tolerate switching the rate and the parent
* separately via calls to .set_parent and .set_rate.
* Returns 0 on success, -EERROR otherwise.
*
* @recalc_accuracy: Recalculate the accuracy of this clock. The clock accuracy
* is expressed in ppb (parts per billion). The parent accuracy is
* an input parameter.
* Returns the calculated accuracy. Optional - if this op is not
* set then clock accuracy will be initialized to parent accuracy
* or 0 (perfect clock) if clock has no parent.
*
* @get_phase: Queries the hardware to get the current phase of a clock.
* Returned values are 0-359 degrees on success, negative
* error codes on failure.
*
* @set_phase: Shift the phase this clock signal in degrees specified
* by the second argument. Valid values for degrees are
* 0-359. Return 0 on success, otherwise -EERROR.
*
* @get_duty_cycle: Queries the hardware to get the current duty cycle ratio
* of a clock. Returned values denominator cannot be 0 and must be
* superior or equal to the numerator.
*
* @set_duty_cycle: Apply the duty cycle ratio to this clock signal specified by
* the numerator (2nd argurment) and denominator (3rd argument).
* Argument must be a valid ratio (denominator > 0
* and >= numerator) Return 0 on success, otherwise -EERROR.
*
* @init: Perform platform-specific initialization magic.
* This is not used by any of the basic clock types.
* This callback exist for HW which needs to perform some
* initialisation magic for CCF to get an accurate view of the
* clock. It may also be used dynamic resource allocation is
* required. It shall not used to deal with clock parameters,
* such as rate or parents.
* Returns 0 on success, -EERROR otherwise.
*
* @terminate: Free any resource allocated by init.
*
* @debug_init: Set up type-specific debugfs entries for this clock. This
* is called once, after the debugfs directory entry for this
* clock has been created. The dentry pointer representing that
* directory is provided as an argument. Called with
* prepare_lock held. Returns 0 on success, -EERROR otherwise.
*
*
* The clk_enable/clk_disable and clk_prepare/clk_unprepare pairs allow
* implementations to split any work between atomic (enable) and sleepable
* (prepare) contexts. If enabling a clock requires code that might sleep,
* this must be done in clk_prepare. Clock enable code that will never be
* called in a sleepable context may be implemented in clk_enable.
*
* Typically, drivers will call clk_prepare when a clock may be needed later
* (eg. when a device is opened), and clk_enable when the clock is actually
* required (eg. from an interrupt). Note that clk_prepare MUST have been
* called before clk_enable.
*/
struct clk_ops {
int (*prepare)(struct clk_hw *hw);
void (*unprepare)(struct clk_hw *hw);
int (*is_prepared)(struct clk_hw *hw);
void (*unprepare_unused)(struct clk_hw *hw);
int (*enable)(struct clk_hw *hw);
void (*disable)(struct clk_hw *hw);
int (*is_enabled)(struct clk_hw *hw);
void (*disable_unused)(struct clk_hw *hw);
int (*save_context)(struct clk_hw *hw);
void (*restore_context)(struct clk_hw *hw);
unsigned long (*recalc_rate)(struct clk_hw *hw,
unsigned long parent_rate);
long (*round_rate)(struct clk_hw *hw, unsigned long rate,
unsigned long *parent_rate);
int (*determine_rate)(struct clk_hw *hw,
struct clk_rate_request *req);
int (*set_parent)(struct clk_hw *hw, u8 index);
u8 (*get_parent)(struct clk_hw *hw);
int (*set_rate)(struct clk_hw *hw, unsigned long rate,
unsigned long parent_rate);
int (*set_rate_and_parent)(struct clk_hw *hw,
unsigned long rate,
unsigned long parent_rate, u8 index);
unsigned long (*recalc_accuracy)(struct clk_hw *hw,
unsigned long parent_accuracy);
int (*get_phase)(struct clk_hw *hw);
int (*set_phase)(struct clk_hw *hw, int degrees);
int (*get_duty_cycle)(struct clk_hw *hw,
struct clk_duty *duty);
int (*set_duty_cycle)(struct clk_hw *hw,
struct clk_duty *duty);
int (*init)(struct clk_hw *hw);
void (*terminate)(struct clk_hw *hw);
void (*debug_init)(struct clk_hw *hw, struct dentry *dentry);
};
这些函数的实现是可选择的,一些是强制性的,这取决于时钟的类型.以fixed-rate类型为例,这也是飞腾派设备树中定义的唯一时钟类型,该类型对应的ops在内核中有定义
static unsigned long clk_fixed_rate_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
return to_clk_fixed_rate(hw)->fixed_rate;
}
static unsigned long clk_fixed_rate_recalc_accuracy(struct clk_hw *hw,
unsigned long parent_accuracy)
{
struct clk_fixed_rate *fixed = to_clk_fixed_rate(hw);
if (fixed->flags & CLK_FIXED_RATE_PARENT_ACCURACY)
return parent_accuracy;
return fixed->fixed_accuracy;
}
const struct clk_ops clk_fixed_rate_ops = {
.recalc_rate = clk_fixed_rate_recalc_rate,
.recalc_accuracy = clk_fixed_rate_recalc_accuracy,
};
而其它内核模块,可以通过
/**
* clk_get_rate - return the rate of clk
* @clk: the clk whose rate is being returned
*
* Simply returns the cached rate of the clk, unless CLK_GET_RATE_NOCACHE flag
* is set, which means a recalc_rate will be issued. Can be called regardless of
* the clock enabledness. If clk is NULL, or if an error occurred, then returns
* 0.
*/
unsigned long clk_get_rate(struct clk *clk)
{
unsigned long rate;
if (!clk)
return 0;
clk_prepare_lock();
rate = clk_core_get_rate_recalc(clk->core);
clk_prepare_unlock();
return rate;
}
EXPORT_SYMBOL_GPL(clk_get_rate);
调用recalc_rate
获取到时钟的频率.
那么外设又是如何获取到对应的时钟呢?依然是通过设备树,以飞腾派的串口设备树定义为例
uart1: uart@2800d000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x0 0x2800d000 0x0 0x1000>;
interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&sysclk_100mhz &sysclk_100mhz>;
clock-names = "uartclk", "apb_pclk";
status = "disabled";
};
时钟是通过clocks
属性分配给使用者的,该节点定义了串口依赖两个时钟,分别名为uartclk
和apb_pclk
,我们可以通过devm_clk_get
函数获取到对应的时钟设备接口.
在drivers/tty/serial/phytium-uart-v2.c
可以看到这样一行逻辑
pup->clk = devm_clk_get(&pdev->dev, "uartclk");
即获取外设对应的clk.
2.2 Timer 驱动
noc介绍
noc 是 network on chip 的缩写。简单的来说,它是arm公司设计的一种总线。 总线按照根据不同拓扑来分,分为:
- NIC/CCI系列,交叉矩阵
- CCN系列,环状总线的(Ring)
- NoC系列,网状总线(Mesh)
noc的示意图可以如下:
每个节点都被视为是一种小型路由,他们之间通过发送异步的包进行通信,就好像是一个局域网。中间的noc总线,则可以视为是接入交换机。 这样,就不必维持路由和路由之间很大数量的连线,从而提高频率,也能支持更多的设备
功能和作用
- NOC设计用于解决片上系统(SoC)内部不同组件之间的通信需求。
- 提供片上处理器核心、存储器、外设、专用加速单元(如GPU、AI加速器等)之间的高效数据传输通道。
- 使用网络路由技术(类似于计算机网络中使用的技术)在芯片内部形成一个网状或其他拓扑结构,以提高带宽和降低延迟。
- 多核处理器和复杂SoC中,以有效管理和调度大量的数据流。
- 高性能计算、移动设备、嵌入式系统、数据中心加速卡等。
- 取代传统总线架构,解决总线传输瓶颈,提高可扩展性和并发性能。
generic timer介绍
generic timer arm公司提出的一种硬件上的设计框架。在早期单核时代,每个SOC vendor厂商购买arm core的IP,然后自己设计soc上的timers。这就导致了对于每一种soc,驱动代码都会不同。此外,linux对硬件timer也有一定的要求,有些硬件厂商设计的timer支持的不是很好。在进入多核时代之后,核与核之间有同步的需求,为单核设计timer的方案会变的更加复杂。由此,arm公司提供了timer的硬件设计,集成在了自己的多核结构中。也就是generic_timer.
对于软件开发者来说,总体框架如下:
非虚拟化:
虚拟化:
每一个核有单独的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 -nographicd8888 .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。
飞腾派外设timer
沸腾派共有38个外设timer,其中0-15支持基础的 timer, tacho_meter 以及单纯的 capture 功能, 16-37只有timer功能。由于已经有 generic timer 来为每个cpu提供可靠的timer中断,通常来说,飞腾派的这些外设timer会主要被用于提供tachometer功能。它们的时钟频率都是50MHz。
在飞腾派的设备树中可以找到对这些设备的定义。关键字:tacho。
核心特性
- timer功能
与generic timer类似,计数器当达到设定值之后,产生一个中断给cpu核。不过需要注意的是,generic timer产生的中断是每个cpu独立的,而外设timer产生的中断会通过gic,最终通知到某一个cpu上,具体通知策略得看gic的配置。
常见的配置方式如下(具体参照飞腾派软件开发手册的5.25.2.6章节):
- 设置对应timer外设功能为 timer
- 使能相关中断
- 配置定时计数值,超过这个值并且配置了中断,系统将能收到中断
- 使能计数器,开始计数
- 支持 free_run 等配置
肯定的是,外设timer作为多核场景的调度源是不合适的,这种timer应当被用于对外设的计数等场景。
- tachometer 功能
翻译过来就是"转速表"或"转速计,通过与电机的协作来完成对电机的转速计数:每当电机转动一圈,则电机通过tacho线触发一次信号(通常通过光电编码器或者霍尔传感器)。 tachometer在一次周期里对这个信号的数量进行技术,就能够获取外部某个设备的转速,比如说风扇。
常常与pwm结合,通过pid等控制算法来控制电机转速。
基础工作原理如下(具体参照飞腾派软件开发手册的5.25.2.7章节):
-
设置对应timer外设功能为 tachometer,并配置外部触发方式(边沿 or 高/低电平)
-
配置定时计数器值,这个值*50MHz 就是一次计数周期
-
配置高转数值和低转速值,转速过高或者过低会尝试触发一次中断
-
使能相关中断,比如过快中断,或者过慢中断
-
中断触发时软件进行可以读取转速值,并相应的调整pwm占空比来调整转速。
-
capture 功能
个人认为是 tachometer 的简化版本:对一段时间内发生的脉冲数量进行计数,当计数达到阈值之后,产生一次相应的中断。
基础工作原理如下(具体参照飞腾派软件开发手册的5.25.2.8章节):
- 设置对应timer外设功能为 capture, 并配置外部触发方式(边沿 or 高/低电平)
- 配置cnt值,7bits
- 使能相关中断
- 当秒冲数量达到cnt值之后,会触发一次中断,然后重新计数。
fan controll实验
note: 本实验需要具有 1_2_pwd_driver的前置知识。
参考资料
arm_generic_timer generic_timer_in_linux
2.3 看门狗驱动
看门狗工作原理
看门狗是为了保障系统在因为某些错误导致挂起时,用来恢复系统所设计的一个硬件.其内部有个定时器,如果在定时器超时时间内,没有周期性地去"喂"(通常以设置某个寄存器,或者拉升/低某个引脚的形式)看门狗,则会导致看门狗重启整个系统.所以,从用户使用的角度来看,用户通常是会设计一个周期性"喂"看门狗的程序,如果内核崩溃恐慌导致系统挂起,则该程序无法继续"喂"狗,看门狗超时导致系统重启.
飞腾派Linux源码中的看门狗操作接口实现
如上所述,一个看门狗驱动,至少需要提供让用户可以设置超时,启动/关闭看门狗,"喂"狗等等的接口.因此,在飞腾派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下,一个看门狗设备需要提供的操作接口.而这些,由驱动实现.本节我们只会分析其中的start
,set_timeout
和ping
接口,其它接口的说明,超出了本节范围,其中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_start
,sbsa_gwdt_set_timeout
和sbsa_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_WOR
,SBSA_GWDT_WRR
和SBSA_GWDT_WCS
三个寄存器的操作.关于这些寄存器的描述,我们可以在飞腾派软件软件编程手册V1.0上找到.
WDT_WRR (0x0000)
域 | 位 | 读写 | 复位值 | 描述 |
---|---|---|---|---|
WDT_WRR | 31:0 | RW | 0x0 | Watchdog更新寄存器.写操作会重新开始看门狗计数,读返回0 |
WDT_WOR (0x1008)
域 | 位 | 读写 | 复位值 | 描述 |
---|---|---|---|---|
WDT_WOR | 31:0 | RO | 0x3000000 | Watchdog清除寄存器 |
WDT_WCS (0x1000)
域 | 位 | 读写 | 复位值 | 描述 |
---|---|---|---|---|
reserved | 31:3 | RO | 0x0 | 保留 |
Ws1 | 2 | RO | 0x0 | 二次超时,读返回当前ws1的值 |
Ws0 | 1 | RO | 0x0 | 一次超时,读返回当期ws0的值 |
Wdt_en | 0 | RW | 0x0 | Watchdog使能信号,高有效,常规复位和热保护都会清0. |
对该三个寄存器的写入,会直接导致sys_cnt+WOR寄存器储存的值被更新到WCV寄存器中.而sys_cnt的计数值大于当前WCV寄存器存储的比较值则会导致看门狗超时.
看门狗驱动的Rust实现
为了让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源码中的实现,来完成看门狗使能,设置超时和关闭看门狗的功能,因此,我们需要实现如下接口:
接口名称 | 参数 | 调用范例 | 简要功能说明 |
---|---|---|---|
start_watchdog | 无 | start_watchdog() | 启动看门狗 |
set_watchdog_timeout | timeout | set_watchdog_timeout(6) | 设置看门狗超时,参数单位为秒 |
stop_watchdog | 无 | stop_watchdog() | 停止看门狗 |
ping_watchdog | 无 | ping_watchdog() | 喂看门狗,重置计数器 |
#![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 介绍
1.1 模块概述
uart: universal asynchronous receiver/transmitter,通用异步发送/接受器,是嵌入式设备中最常用全双工通信协议之一。关于它的详细介绍,可以参考 这篇文章。对于软件工程师来说,以下几个概念是必须要掌握的。
-
波特率
波特率就是一秒传递多少bit。由于uart里面并没有时钟线,这也就意味着通信双方需要预先约定好具体的通信速率是多少。使用过类似于xshell这样的串口工具的同学应该就知道,在配置串口的时候,这个值配置不对就会导致屏幕上出现乱码。常用的波特率有115200,9600等。
-
数据宽度
一次传输的数据宽度,可以是5-8的一个值,常见是8bit,因为这对软件开发者来说最友好。毕竟,你也不想把一个u8拆成5bits和3bits来发送吧~
-
停止位
一个特殊的bit,用来告诉对方这一次传输结束了。可以配置为1位或者2位。
-
奇偶校验
在数据传输时增加额外一个1bit,用来使得这一个bit+数据的每一位为1的总数为奇数或者偶数。发送端在发送时,计算出奇偶校验位应该为1或者为0,接收端在接受时,就可以根据这一位校验出数据传输是否出错。当然,这个校验非常不准确,一般不开。
-
8N1
这是一种缩写,意味着8个数据位,没有奇偶校验,1位停止位。这是最常见的串口配置。
-
调制/解调
一些高级的uart芯片集成了调制/解调的功能,这涉及到了通信原理的一些知识,其配置也对应的复杂更多。飞腾派上搭载9线串口就具有这个功能,不过幸运的是本次实验不涉及这块内容,所以在看寄存器定义时,也不需要对这部分有深入理解。
1.2 硬件接口介绍
- 3线串口连接图 一个典型的三线串口连接图如下:
1.3 时序图
notice: 这里的时钟不会体现在传输线上,展示设备内部接受/发送时序。
这个图展示了一帧数据的可能结构。
2. 接口表
完整代码在这儿: https://github.com/chenlongos/appd/commit/897c85c5952a123fa27f8612a4a5c86d37d679e3
- 接口表
API函数 | 描述 | 参数 | 返回值 |
---|---|---|---|
PhytiumUart::new | 新的飞腾派uart实例 | base:uart控制器基地址 | uart控制器 |
PhytiumUart::init_no_irq | 创建非irq模式的uart控制器 | self, clock_hz: uart时钟频率,baude_rate: uart波特率 | none |
PhytiumUart::read_byte_poll | 轮询的方式读取一个字节 | self | u8,一个读取的字节 |
PhytiumUart::put_byte_poll | 轮询的方式输出一个字节 | self, byte:输出的字节 | none |
- 调用顺序 首先进行 new -> init_no_irq, 之后可以按照需求进行读取或者写入字节。
3. 寄存器结构
我们主要关注下面几个寄存器,这些寄存器的定义可以在飞腾派软件开发手册里找到。
- UARTDR: 数据寄存器,用来传输/接受最大8位的数据
- UARTRSR/UARTECR: 接受状态寄存器,用来查看接收过程中是否产生了错误,写这个寄存器可以清除错误。
- UARTFR:标志寄存器,存了各种各样的标志,包括设备忙,发送队列是否为空等等。发送接收都需要查看这个寄存器。
- UARTIBRD:存储波特率经过算法转变后的整数部分
- UARTFBRD:存储波特率经过算法转变后的小数部分
- UARTLCR_H:线控寄存器,控制uart的一些行为。
- UARTLCR:控制寄存器,控制uart的一些行为。
- UARTIMSC:控制是否打开中断。
寄存器名称 | 偏移 | 寄存器定义 |
---|---|---|
UARTDR | 0x000 | 数据寄存器 |
UARTRSR/UARTECR | 0x004 | 接收状态寄存器/错误清除寄存器 |
UARTFR | 0x018 | 标志寄存器 |
UARTILPR | 0x020 | 低功耗计数寄存器 |
UARTIBRD | 0x024 | 波特率整数值配置寄存器 |
UARTFBRD | 0x028 | 波特率小数值配置寄存器 |
UARTLCR_H | 0x02C | 线控寄存器 |
UARTCR | 0x030 | 控制寄存器 |
UARTIFLS | 0x034 | FIFO 阈值选择寄存器 |
UARTIMSC | 0x038 | 中断屏蔽选择/清除寄存器 |
UARTRIS | 0x03C | 中断状态寄存器 |
UARTMIS | 0x040 | 中断屏蔽状态寄存器 |
UARTICR | 0x044 | 中断清除寄存器 |
UARTDMACR | 0x048 | DMA 控制寄存器 |
uart初始化步骤
参考 飞腾派软件开发手册 的 5.22.1.1章节。
发送数据操作流程
参考 飞腾派软件开发手册 的 5.22.1.2章节。
接收数据操作流程
参考 飞腾派软件开发手册 的 5.22.1.3章节。
4. 飞腾派 UART 串口驱动实验
4.1 实验目标
- 编写代码实现串口驱动
- 初始化为8N1模式,波特率为115200,非中断模式
- 没有tx/rx FIFO队列,单次接收/发送1一个byte。
- 验证串口通信的基本原理
4.2 实验原理
根据原理图,我们使用飞腾派的UART2串口,它被连线到J1端子板的8(tx),10(rx)口上。用杜邦线将8口与10口相连接,uart2 tx发出的数据会被uart rx口被收到。
4.3 实验步骤
- 根据数据手册,先使用tock-register库将寄存器定义好,将初始化函数实现。使用者通过传入一个 基地址 指向uart寄存器,返回一个不可并发的uart。
#![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(), } } } }
- 根据数据手册,实现初始化函数。值的注意的是,计算波特率的小数部分的值有一个小trick:很多低端嵌入式设备可能不支持浮点运算,为了驱动的通用性,你需要实现一种不用浮点的方法来计算出小数部分的divider的方法
#![allow(unused)] fn main() { impl PhytiumUart { 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() } } } // 加一些简单的测试,会使得你的代码更加可靠。 #[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)); } } }
- 根据数据手册,实现通过轮询的方式发送/接收串口中的数据。发送时检查flag寄存器的 txfifo 是否为满,如果为满,那么cpu死等;接收时检查flag寄存器的 rxfifo 是否为空,如果为空,那么cpu死等。
#![allow(unused)] fn main() { impl PhytiumUart { pub fn read_byte_poll(&self) -> u8 { // 检查flag寄存器的 rxfifo 是否为空,如果为空,那么cpu死等 while self.regs().fr.read(FLAG::RXFE) != 0 {} (self.regs().dr.get() & 0xff) as u8 } pub fn put_byte_poll(&mut self, b: u8) { // 检查flag寄存器的txfifo是否为满,如果为满,那么cpu死等。 while self.regs().fr.read(FLAG::TXFF) == 1 {} self.regs().dr.set(b as u32); } } }
- 将飞腾派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
4.4 实验结论
本实验验证了uart无中断,poll mode的通信模型,实现了相应的驱动代码。
4.5 实验代码
完整代码可以在这儿看到
5. qemu串口驱动实验
qemu模拟的qemu-virt机器使用串口为 pl011 模块,寄存器作用以及地址与飞腾派是一致的,并且默认已经被 arceos 的初始化(不然就看不到arceos的启动所打印的信息了)。具体实现代码可以看这个crate。这个crate默认采用了波特率为115200,8N1的通信格式。
6. 参考资料
pl011
https://developer.arm.com/documentation/ddi0183/g/programmers-model
uart协议
飞腾派软件开发手册
https://github.com/elliott10/dev-hw-driver/blob/main/phytiumpi/docs/飞腾派软件编程手册V1.0.pdf
飞腾派硬件原理图
3.2 I2C驱动开发
I2C介绍
I2C(Inter-Integrated Circuit)是一种两线制、低速串行通信协议,广泛用于嵌入式系统中微控制器与外设(如传感器、EEPROM)的通信,使用串行数据线(SDA)和串行时钟线(SCL)实现双向传输。
核心特性
- 两线制通信:SDA传输数据,SCL提供时钟同步。
- 多主机多从机:支持多个主设备发起通信,多个从设备通过唯一地址(7位或10位)响应。
- 传输模式:支持标准模式(100kbps)、快速模式(400kbps)、快速模式加(1Mbps)和高速模式(3.4Mbps)。
- 简单连接:仅需两根信号线,适合小型系统。
- 同步通信:数据传输由SCL时钟控制,确保可靠性。
数据传输过程
I2C通信包括以下步骤:
- 起始信号:SCL高电平时,SDA从高到低跳变。
- 地址和数据传输:主设备发送7位/10位从设备地址和读/写位,SCL低电平时SDA传输8位数据+1位ACK/NACK。
- 应答信号:从设备在第9个SCL脉冲返回ACK(SDA低)或NACK(SDA高)。
- 数据传输:继续传输数据字节,每字节后有ACK/NACK。
- 停止信号:SCL高电平时,SDA从低到高跳变。
I2C 数据传输过程
sequenceDiagram participant M as 主设备 participant S as 从设备 M->>S: SCL高,SDA高->低(起始信号) M->>S: 发送7位地址+读/写位 S->>M: ACK(SDA低) M->>S: 发送/接收数据字节 S->>M: ACK M->>S: SCL高,SDA低->高(停止信号)
I2C传输模式
模式 | 速率 | 应用场景 |
---|---|---|
标准模式 | 100kbps | 低速传感器、EEPROM |
快速模式 | 400kbps | 中速外设通信 |
快速模式加 | 1Mbps | 较高性能需求 |
高速模式 | 3.4Mbps | 高性能嵌入式设备 |
飞腾派I2C硬件实现
飞腾派的I2C功能由MIO(多路输入输出)控制器实现,每个MIO可配置为I2C或UART,通过creg_mio_func_sel寄存器选择功能(00=I2C,01=UART)。I2C通信依赖PAD模块配置SCL和SDA引脚的电气特性。
MIO控制器
- 功能:支持I2C主模式通信,管理数据传输和中断。
- 基地址:MIO0(0x28014000)至MIO15(0x28032000)。
- 配置:creg_mio_func_sel设置功能,偏移地址需参考硬件手册。
- 中断:每个MIO提供中断编号,处理传输完成或错误事件。
PAD模块
- 功能:配置引脚功能(如I2C)和电气特性(驱动强度、上下拉、延迟)。
- 基地址:0x32B30000。
- 引脚:SCL(偏移0x00D0)和SDA(偏移0x00D4)配置为I2C功能(值5)。
飞腾派I2C硬件模块
模块 | 功能 | 基地址 |
---|---|---|
MIO | I2C/UART功能选择、数据传输 | 0x28014000–0x28032000 |
PAD | 引脚功能和电气特性配置 | 0x32B30000 |
飞腾派I2C驱动API调用表
API函数 | 描述 | 参数 | 返回值 |
---|---|---|---|
FIOPadCfgInitialize | 初始化PAD控制器,设置引脚基地址和设备ID,为SCL/SDA引脚配置做准备。 | instance_p: &mut FIOPadCtrl(PAD控制器实例) input_config_p: &FIOPadConfig(配置参数,包含基地址和ID) | bool:true(成功),false(已初始化或参数错误) |
FIOPadDeInitialize | 去初始化PAD控制器,清除设备状态,标记未就绪。 | instance_p: &mut FIOPadCtrl(PAD控制器实例) | bool:true(成功) |
FIOPadLookupConfig | 根据设备ID查找PAD配置,返回基地址等信息。 | instance_id: u32(设备ID) | Option |
FI2cMioMasterInit | 初始化MIO和I2C控制器,配置MIO为I2C模式,设置SCL/SDA引脚、基地址、中断编号、时钟频率、从设备地址和速率。 | address: u32(从设备地址) speed_rate: u32(传输速率,如100kbps) | bool:true(成功),false(初始化失败) |
FI2cCfgInitialize | 初始化I2C控制器,设置配置参数(如基地址、中断号),重置设备并标记就绪。 | instance_p: &mut FI2c(I2C实例) input_config_p: &FI2cConfig(配置参数) | bool:true(成功),false(已初始化或重置失败) |
FI2cMasterWrite | 发送数据到从设备(如EEPROM),检查长度和偏移限制,执行轮询写操作。 | buf_p: &mut [u8](数据缓冲区) buf_len: u32(数据长度) inchip_offset: u32(从设备内部偏移) | bool:true(成功),false(长度超限或写入失败) |
FI2cMasterStartTrans | 启动I2C主设备传输,设置从设备地址,向IC_DATA_CMD写入数据,配置读/写和停止标志。 | instance_p: &mut FI2c(I2C实例) mem_addr: u32(从设备内存地址) mem_byte_len: u8(地址字节数) flag: u16(控制标志,如停止位) | bool:true(成功),false(总线忙或FIFO错误) |
FI2cMasterRead | 从设备读取数据,初始化缓冲区,执行轮询读操作。 | buf_p: &mut [u8](接收缓冲区) buf_len: u32(数据长度) inchip_offset: u32(从设备内部偏移) | bool:true(成功),false(长度无效或读取失败) |
FI2cMasterStopTrans | 停止I2C传输,检查停止条件,清空接收FIFO。 | instance_p: &mut FI2c(I2C实例) | bool:true(成功),false(超时或总线忙) |
说明
- 调用顺序:
- 调用FIOPadCfgInitialize和FIOPadLookupConfig初始化PAD控制器,设置SCL/SDA引脚(偏移0x00D0/0x00D4,值5)。
- 调用FI2cMioMasterInit配置MIO为I2C模式,初始化I2C控制器。
- 调用FI2cCfgInitialize完成I2C配置,确保设备就绪。
- 使用FI2cMasterWrite或FI2cMasterRead进行数据传输。
- 调用FI2cMasterStopTrans结束传输。
- 硬件依赖:
- PAD基地址:0x32B30000。
- MIO基地址:0x28014000(MIO0)至0x28032000(MIO15)。
- I2C寄存器(如IC_DATA_CMD)用于数据和命令控制。
- 注意事项:
- 确保数据长度和偏移不超过从设备限制(如EEPROM的256字节)。
- 检查总线状态(FI2cWaitBusBusy)和FIFO状态(IC_TXFLR/IC_RXFLR)。
- 中断处理需开发者根据应用注册master_evt_handlers。
ArceOS 的 I2C 驱动实现
MIO是一个包含多种控制器功能的多路选择控制器,飞腾派的每个MIO均可单独当做UART/I2C。端口功能的选择,可以通过配置creg_mio_func_sel寄存器来实现,配置为00选择I2C,配置为01选择UART。
由MIO控制器来当作IIC来与设备通信,操作会比普通单片机中用GPIO口模拟iic时序要复杂
由MIO控制的I2C操作说明:
初始化
飞腾派 PAD 引脚初始化:
配置PAD控制器,设置SCL和SDA引脚为I2C功能,确保引脚电气特性适配外设。
#![allow(unused)] fn main() { // 定义PAD配置结构体 #[derive(Debug, Clone, Copy, Default)] pub struct FIOPadConfig { pub instance_id: u32, // 设备实例ID pub base_address: usize, // PAD基地址 } // 定义PAD控制器结构体 #[feature(const_trait_impl)] #[derive(Debug, Clone, Copy, Default)] pub struct FIOPadCtrl { pub config: FIOPadConfig, // 配置信息 pub is_ready: u32, // 设备就绪状态 } // 全局PAD控制器实例 pub static mut iopad_ctrl: FIOPadCtrl = FIOPadCtrl { config: FIOPadConfig { instance_id: 0, base_address: 0, }, is_ready: 0, }; // PAD配置表 static FIO_PAD_CONFIG_TBL: [FIOPadConfig; 1] = [FIOPadConfig { instance_id: 0, base_address: 0x32B30000usize, }]; // 初始化PAD控制器 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."); } // 去初始化并设置配置 FIOPadDeInitialize(instance_p); instance_p.config = *input_config_p; instance_p.is_ready = 0x11111111u32; ret } // 去初始化PAD控制器 pub fn FIOPadDeInitialize(instance_p: &mut FIOPadCtrl) -> bool { 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 } // 查找PAD配置 pub fn FIOPadLookupConfig(instance_id: u32) -> Option<FIOPadConfig> { if instance_id as usize >= 1 { return None; } for config in FIO_PAD_CONFIG_TBL.iter() { if config.instance_id == instance_id { return Some(*config); } } None } }
说明:
- FIOPadCfgInitialize:设置PAD基地址(0x32B30000),标记设备就绪(is_ready=0x11111111)。
- FIOPadDeInitialize:清除设备状态,防止重复初始化。
- FIOPadLookupConfig:根据实例ID查找配置,确保正确映射硬件资源。
- SCL(偏移0x00D0)和SDA(偏移0x00D4)通过FIOPadSetFunc配置为I2C功能(值5)。
MIO和I2C控制器初始化
配置MIO为I2C模式,初始化I2C控制器,包括基地址、中断编号、时钟频率等。
#![allow(unused)] fn main() { // 初始化MIO和I2C控制器 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); // 设置为I2C模式 if status != true { debug!("MIO initialize error."); return false; } // 配置SCL和SDA引脚 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>()); } // 查找I2C默认配置 config_p = FI2cLookupConfig(1).unwrap(); if !Some(config_p).is_some() { debug!("Config of mio instance {} not found.", 1); return false; } // 设置I2C配置 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; // 初始化I2C控制器 status = FI2cCfgInitialize(&mut master_i2c_instance, &input_cfg); // 设置中断处理函数 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 } // 初始化I2C配置 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 } }
说明:
- FI2cMioMasterInit:设置MIO为I2C模式(creg_mio_func_sel=00),配置SCL/SDA引脚,初始化I2C控制器(基地址、中断号、50MHz时钟、从设备地址、速率)。
- FI2cCfgInitialize:检查设备状态,设置配置,重置I2C控制器,标记就绪。
- 中断处理函数初始化为空,开发者可根据需求注册。
收发数据
数据发送
发送数据到从设备(如EEPROM),确保数据长度和偏移地址符合限制。
#![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); if status != true { debug!("Write to eeprom failed"); } status } 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 } }
说明:
- FI2cMasterWrite:检查数据长度和偏移(<256字节),调用FI2cMasterWritePoll执行轮询写操作。
- FI2cMasterStartTrans:检查总线空闲(FI2cWaitBusBusy),设置从设备地址(IC_TAR),向IC_DATA_CMD(偏移0x10)写入数据(bit[8]=0表示写,bit[9]=1表示停止)。
数据接收
#![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 } 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 } }
说明:
- FI2cMasterRead:初始化接收缓冲区,调用FI2cMasterReadPoll执行轮询读操作。
- FI2cMasterStopTrans:检查停止条件(IC_RAW_INTR_STAT的bit[9]),清空接收FIFO(FI2cFlushRxFifo),确保传输结束。
飞腾派I2C数据发送
sequenceDiagram participant D as 驱动 participant I as I2C控制器 participant R as IC_DATA_CMD D->>I: 检查总线空闲(FI2cWaitBusBusy) I->>D: 空闲确认 D->>I: 设置从设备地址(IC_TAR) D->>R: 写入数据,bit[8]=0,bit[9]=1(停止) R->>I: 传输数据到从设备 I->>D: 返回传输状态
飞腾派 I2C 和 MIO 寄存器信息
I2C寄存器
寄存器 | 偏移 | 描述 |
---|---|---|
IC_CON | 0x00 | 控制寄存器,配置主/从模式、速率等 |
IC_TAR | 0x04 | 主机目标地址,设置从设备地址 |
IC_SAR | 0x08 | 从机地址寄存器,配置本设备地址 |
IC_HS_MADDR | 0x0C | 高速模式地址编码 |
IC_DATA_CMD | 0x10 | 数据和命令,bit[7:0]=数据,bit[8]=读/写,bit[9]=停止 |
IC_SS_SCL_HCNT | 0x14 | 标准模式SCL高电平计数 |
IC_SS_SCL_LCNT | 0x18 | 标准模式SCL低电平计数 |
IC_FS_SCL_HCNT | 0x1C | 快速模式SCL高电平计数 |
IC_FS_SCL_LCNT | 0x20 | 快速模式SCL低电平计数 |
IC_INTR_STAT | 0x2C | 中断状态寄存器 |
IC_INTR_MASK | 0x30 | 中断屏蔽寄存器 |
IC_RAW_INTR_STAT | 0x34 | 原始中断状态,bit[9]=停止检测 |
IC_CLR_INTR | 0x40 | 清除中断寄存器 |
IC_STATUS | 0x70 | 状态寄存器,检查总线状态 |
IC_TXFLR | 0x74 | 发送FIFO级别 |
IC_RXFLR | 0x78 | 接收FIFO级别 |
IC_TX_ABRT_SRC | 0x80 | 发送中止源,检查错误状态 |
MIO寄存器
Name | Offset |
---|---|
MIO0 | 0x000_2801_4000 |
MIO1 | 0x000_2801_6000 |
MIO2 | 0x000_2801_8000 |
MIO3 | 0x000_2801_A000 |
MIO4 | 0x000_2801_C000 |
MIO5 | 0x000_2801_E000 |
MIO6 | 0x000_2802_0000 |
MIO7 | 0x000_2802_2000 |
MIO8 | 0x000_2802_4000 |
MIO9 | 0x000_2802_6000 |
MIO10 | 0x000_2802_8000 |
MIO11 | 0x000_2802_A000 |
MIO12 | 0x000_2802_C000 |
MIO13 | 0x000_2802_E000 |
MIO14 | 0x000_2803_0000 |
MIO15 | 0x000_2803_2000 |
PAD寄存器
基地址:0x32B30000
寄存器 | 偏移 | 描述 |
---|---|---|
x_reg0 | 0x00D0 (SCL), 0x00D4 (SDA) | 功能选择(值5=I2C),驱动强度,上下拉 |
x_reg1 | 0x00D0 (SCL), 0x00D4 (SDA) | 输入/输出延迟,粒度100ps/366ps |
3.3 SPI驱动开发
SPI介绍
SPI(Serial Peripheral Interface)串行外围设备接口是一种单主多从模式、高速、全双工同步串行总线,通常用在一些 flash、LCD 屏幕等设备的通信上。
核心特性:
1. 四线制:SPI通信需要四根线:
- MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
- MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
- SCK:串行时钟信号,由主设备产生。
- CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。


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

2. 传输方式:数据交换
SPI 工作在主从模式下,但在每个时钟周期(Clock)内,主从设备都会同时发送和接收 1 bit 数据,实现真正的全双工数据交换。无论是主设备还是从设备,每个时钟周期都会有数据交换。
主机进行写操作时,只需忽略接收到的字节;主机要读取从机的一个字节时,必须发送一个空字节来引发从机的传输。

3. 传输规范:无速率限制无格式要求
SPI 没有严格的速度限制,一般实现通常能达到甚至超过 100Mbps,适合高速数据传输场景。
SPI 没有指定的流控制,没有应答机制确认是否接收到数据。不像其他协议会有协议头、设备地址以及各种指令。只要四根信号线连接正确,SPI模式相同,将CS/SS信号线拉低,即可以直接通信。一次一个字节的传输,读写数据同时操作。
所以 SPI 设备更适合一些适用流数据的设备,比如 LCD 屏幕、AD 转换器等。
4. 4种传输模式:SCK采样
SPI 有四种模式简单理解就是上升沿下降沿采样和空闲时高电平与低电平,两两组合而出现的四种模式
- 模式0:CPOL= 0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样
- 模式1:CPOL= 0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样
- 模式2:CPOL= 1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样
- 模式3:CPOL= 1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样
以模式0为例子,下图为模式0下的SPI时序图

在普通单片机上的SPI通信可以简化为如下步骤
- 首先设置好SPI主机的模式
- 拉低SPI设备对应的片选信号CS/SS信号先
- SPI主机主动发送时钟SCK信号,表示数据交换开始
飞腾派上SPI实验
第四章:中断
4.1 gic驱动
4.2 中断方式实现 i2c 驱动
4.3 中断方式实现 spi 驱动
第五章:高速传输类驱动
5.1 DMA驱动开发
5.2 PCI控制器驱动
5.3 PCIe互联驱动
第六章:网络通信类驱动
6.1 单元测试与调试
PCIe基础知识
课程概述
本课程将介绍PCIe(Peripheral Component Interconnect Express)的基本知识,包括配置空间、地址空间、深度优先遍历等核心概念。
1. PCIe简介
1.1 什么是PCIe
- PCIe是一种高速串行总线标准
- 替代了传统的PCI总线
- 采用点对点连接,支持全双工通信
- 具有更高的带宽和更好的扩展性
1.2 PCIe整体框图
PCIe的架构主要由五个部分组成:Root Complex,PCIe Bus,Endpoint,Port and Bridge,Switch
BDF(Bus Number, Device Number, Function Number)
PCIe上所有的设备,无论是Type 0还是Type 1,在系统启动的时候,都会被分配一个唯一的地址,它有三个部分组成:
Bus Number:8 bits,也就是最多256条总线 Device Number:5 bits,也就是最多32个设备 Function Number:3 bits,也就是最多8个功能
lspci -t -v
-[0000:00]-+-00.0 Intel Corporation Device 9b43
+-01.0-[01]--+-00.0 NVIDIA Corporation GK208B [GeForce GT 730]
| \-00.1 NVIDIA Corporation GK208 HDMI/DP Audio Controller
+-02.0 Intel Corporation CometLake-S GT2 [UHD Graphics 630]
+-08.0 Intel Corporation Xeon E3-1200 v5/v6 / E3-1500 v5 / 6th/7th/8th Gen Core Processor Gaussian Mixture Model
+-14.0 Intel Corporation Comet Lake PCH-V USB Controller
+-14.2 Intel Corporation Comet Lake PCH-V Thermal Subsystem
+-16.0 Intel Corporation Comet Lake PCH-V HECI Controller
+-17.0 Intel Corporation 400 Series Chipset Family SATA AHCI Controller
+-1b.0-[02]----00.0 Yangtze Memory Technologies Co.,Ltd Device 0071
+-1c.0-[03]----00.0 Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
+-1d.0-[04-05]----00.0-[05]--
+-1f.0 Intel Corporation B460 Chipset LPC/eSPI Controller
+-1f.2 Intel Corporation Memory controller
+-1f.3 Intel Corporation Comet Lake PCH-V cAVS
\-1f.4 Intel Corporation Comet Lake PCH-V SMBus Host Controller
Intel Corporation Comet Lake PCH-V USB Controller 的 BDF 是:0000:14.0
2. PCIe配置空间
2.1 配置空间概述
PCIe配置空间是用于存储设备配置信息的内存区域,每个PCIe设备都有自己的配置空间。
2.2 配置空间结构
配置空间大小:4KB(4096字节)
标准配置头
2.4 配置空间访问方法
- 端口访问
- 内存映射访问
- ECAM(Enhanced Configuration Access Mechanism)
如果某个设备的BDF是46:00.1
,ECAM基址是0xE0000000
,那么其配置空间起始地址就是:0xE0000000 + (0x46 << 20) | (0x00 << 15) | (0x01 << 12) = 0xE46001000
。
Type 0设备
Type 1设备
4. PCIe深度优先遍历
4.1 PCIe拓扑结构
4.2 总线编号机制
- Primary Bus Number(Pri):这个Bridge所在的Bus Number,也就是它的上游连接的Bus Number
- Secondary Bus Number(Sec):这个Bridge所连接的下一个Bridge的Bus Number
- Subordinate Bus Number(Sub):这个Bridge所连接的下游所有的Bus的最大的Bus Number
4.3 深度优先遍历算法
4.4 遍历过程详解
4.4.1 遍历步骤
- 从根总线开始:通常从总线0开始
- 扫描设备:遍历每个可能的设备号(0-31)
- 检查功能:对每个设备检查所有功能(0-7)
- 识别桥设备:通过Header Type识别桥设备
- 递归遍历:对桥设备的下游总线进行递归遍历
4.4.2 设备识别流程
读取Vendor ID → 检查是否为0xFFFF → 存在则处理设备 → 检查Header Type → 如果是桥设备则递归遍历子总线
IGB 网卡驱动
资料
硬件架构
CPU集成MAC:
网卡包含MAC和PHY:
IGB网卡架构:
MAC(Media Access Control)
PHY(Physical Layer)
MII(Media Independent Interface)
-
发送数据接口
-
接收数据接口
-
MDIO:配置PHY芯片状态、读取寄存器、获取LINK状态等操作
RMII、GMII、RGMII、SGMII等都是MII的变种,主要区别在于数据传输速率和引脚数量。
驱动分析
OSAL 操作系统抽象层
trait-ffi 使用trait
生成和安全使用外部函数。
驱动初始化
PCIe 枚举
手册阅读、寄存器定义
MAC定义
PHY定义
PHY寄存器读写
Smoltcp
网络栈验证
Request/Response 模型
收发数据
Ring
-
Head
-
Tail
Descriptor
-
Read
-
Write-Back
-
Buffer
Reqeust
生命周期
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协议栈的初始化
-
把usb协议栈编译进内核空间。编译的命令是
make A=apps/usb-hid ...
,在usb-hid
程序的配置文件Cargo.toml
命令里指定了要依赖driver_usb
,这样就决定了会把USB协议栈driver_usb
编译进内核空间。 -
初始化XHCI控制器。在
usb-hid
的main
函数会执行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 } }
-
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 } }
-
获取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) } }
-
USB驱动的匹配。在
usb-hid
的main
函数会执行USBSystem::new().init()
,这样也会进一步调用USBDriverSystem::init()
注册所有的USB驱动模块。例如CdcSerialDriverModule
的should_active()
方法,其主要功能是判断是否应该激活 CDC(通用串行总线控制设备类)串口驱动。具体步骤如下:- 设备描述符检查:检查传入的独立设备实例的描述符是否已初始化。
- 设备类、厂商 ID 和产品 ID 检查:如果描述符已初始化,获取设备的类、厂商 ID 和产品 ID,判断是否为特定的厂商和产品(厂商 ID 为
0x1a86
,产品 ID 为0x7523
,对应沁恒电子的 CH340 串口转换器)。 - 端点信息收集:若匹配成功,收集设备的端点信息。
- 驱动初始化:使用收集到的信息初始化
CdcSerialDriver
实例,并将其封装在Option<Vec<Arc<SpinNoIrq<dyn USBSystemDriverModuleInstance<'a, O>>>>
中返回。 - 不匹配处理:若不匹配,则返回
None
。
-
驱动实例的创建。匹配成功后创建驱动实例,调用
new_and_init
方法初始化设备功能。例如CdcSerialDriver
的new_and_init
方法,其具体的初始化过程如下:- 为读缓冲区分配空间。
- 查找输入端点和输出端点。
- 创建写缓冲区并对其进行初始化。
- 创建控制缓冲区和状态缓冲区。
- 创建CdcSerialDriver实例。
-
驱动实例初始化设备。在
usb-hid
的main
函数会执行USBSystem::new().init().init_probe()
,驱动实例最终就可以调用prepare_for_drive
方法通过 URB 请求配置设备接口、端点等。例如CdcSerialDriver
的prepare_for_drive
方法,其具体的初始化过程如下:- 初始化一个空向量todo_list以保存URB。
- 设置配置描述符。
- 厂商自定义配置流程。这些代码创建了多个控制传输的URB,包括输出和输入操作,涉及不同的命令(如CH341_CMD_C3, CH341_CMD_C1, CH341_CMD_W, CH341_CMD_R等)、索引和值,这些操作参考了Linux源码里的drivers/usb/serial/ch341.h中的
ch341_configure
函数以及wireshark抓包数据。 - 配置115200波特率。创建了一系列控制传输的URB,用于将设备的波特率配置为115200。
- 返回配置所需的URB列表。将todo_list封装在some中返回。
-
通过事件系统通知初始化完成。驱动初始化完成后通过事件总线通知上层系统:
#![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 } } } }
用户向串口发送数据
-
用户把数据写入发送缓冲区。用户需要调用
CdcSerialDriver
的write
方法,把数据写入发送缓冲区。当前的测试流程,是在CdcSerialDriver
的new_and_init
方法里,把hello,world
硬写入发送缓冲区。 -
生成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 { // ... 接收数据逻辑 ... } } }
-
USB转串口转换器转换并发送数据。USB 转串口驱动把 URB 封装成符合 USB 协议的数据包,经 USB 总线传给 USB 转串口转换器。转换器解析数据包,将数据转为串口信号(如 UART 信号),再通过串口引脚发送出去。
串口数据传给用户
-
USB 转串口转换器接收并转换信号。USB 转串口转换器检测到串口引脚的信号变化,按串口协议解码信号,提取原始数据,接着封装成 USB 数据包,通过 USB 总线传给计算机。
-
生成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) } } }
-
处理接收完成事件。USB 转串口驱动收到数据后,触发接收完成事件,调用
CdcSerialDriver
的receive_complete_event
方法。该方法把接收到的数据存储在accepted_date
中。 -
用户应用程序接收数据。如果是进行测试,可以在Arceos的日志中看到
accepted data: xxx
,其中xxx
为串口接收到的数据,这表明串口数据接收成功。如果用户需要串口数据,则应用程序应从accepted_data
读取数据。
8.2 USB摄像头驱动
第九章:无线通讯方向
9.1 WiFi6驱动开发
9.2 蓝牙驱动开发
第十章:实时工业总线方向
10.1 CANFD驱动
10.2 EtherCAT驱动
附录:开发训练营资源
测试驱动开发
测试驱动开发(TDD)是一种软件开发方法,强调在编写实际代码之前先编写测试用例。它的主要步骤包括:
-
先写一个失败的单元测试
-
编写实现代码,使得该测试通过
-
重构代码,保持所有测试通过状态
优点:
-
改善设计:在编写测试的过程中,可以从使用者的角度去思考问题,这有助于提前发现设计上的问题,从而提高代码的质量。
-
降低风险:有了充分的单元测试,我们可以放心地进行重构,因为一旦我们的修改破坏了原有的功能,测试会立刻发现。
-
提高效率:虽然TDD需要在初期投入更多的时间,但是随着项目的推进,越来越多的测试将会大大减少因为错误和回归带来的时间损失。
实施
测试工具
- qemu - 一个开源的虚拟机,可以模拟多种硬件平台。
- cargo test
- bare-test - 是一个轻量级的测试框架,专为裸机环境设计。它允许在没有操作系统支持的情况下运行测试(本身是一个小型操作系统)。
- gdb-multiarch - GNU调试器,支持多种架构。
环境搭建
Linux
apt
、cargo
等工具直接安装
Windows
推荐
- msys2 - 一个轻量级的Linux环境,可以在Windows上运行。可方便的安装
qemu
、gdb-multiarch
等工具。
自定义测试原理
-
Cargo test
默认如何工作?cargo test
命令会编译项目中的所有测试代码,libtest
收集#[test]
标记的函数,成一个可执行文件,然后运行这个可执行文件来获取执行结果。 -
我们从哪里开始使用自定义测试框架?
在
Cargo.toml
中添加[[test]] name = "test" path = "tests/test.rs" harness = false
-
定义测试函数声明规范
...
-
收集所有测试函数
...
-
构建
no-std
裸机程序...
-
修改
Cargo runner
为Qemu
...
项目搭建
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步骤:
Rust
环境Qemu
安装- 代码质量检查
- 编译和测试
设计模式
驱动模型
可复用性(操作系统无关)
-
硬件层:设计一个通用的驱动模型,使得驱动程序可以在不同的操作系统上复用。
- 只包括硬件相关的代码,避免与操作系统的特定实现绑定。
- 涉及修改的操作使用mut,将锁交由操作系统处理,避免引入锁。
- 暴露中断handler,将中断注册交由操作系统处理。
- 可用异步简化中断回调。
- 适度unsafe。
-
操作系统连接层: 单元测试所在层级,基于操作系统的特性和API实现驱动程序的连接。
- 通过包裹系统的mutex等锁实现保证并发安全。
- 中断注册系统注册中断回调。
- work创建。
- 连接上层组件接口(fs、net等)。
寄存器定义
推荐使用 tock-registers 库来定义寄存器。
- 定义寄存器结构体。
- 定义字段
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; } // 其他状态处理... } } } }
异步模式优势
- 代码可读性:异步模式使用线性的控制流,避免了状态机的复杂状态转换逻辑
- 错误处理:可以使用标准的
?
操作符和try-catch模式处理错误 - 组合性:异步函数可以轻松组合和链式调用
- 资源管理:自动的生命周期管理,减少内存泄漏风险
实际应用示例
复杂设备操作的异步实现
#![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模式确保资源正确释放 这种基于中断的异步模式为驱动开发提供了一种现代化的解决方案,既保持了性能优势,又显著提高了代码的可维护性和可读性。 }
注意事项
在驱动开发中,缓存一致性和内存屏障是两个至关重要的概念。正确理解和处理这些问题对于编写稳定、高性能的驱动程序至关重要。
缓存一致性
高速缓存的基础知识
高速缓存(Cache)是位于CPU和主内存之间的高速存储器,用于缓解CPU和内存之间的速度差异。
缓存层次结构
现代处理器通常具有多级缓存:
- L1 缓存:最接近CPU核心,容量小但速度最快(通常几十KB)
- L2 缓存:中等容量和速度(通常几百KB到几MB)
- L3 缓存:容量最大但相对较慢(通常几MB到几十MB)
缓存行(Cache Line)
#![allow(unused)] fn main() { // 缓存行通常是 64 字节 const CACHE_LINE_SIZE: usize = 64; // 结构体对齐到缓存行边界 #[repr(align(64))] struct CacheAligned { data: [u8; 64], } }
缓存映射方式
- 直接映射:每个内存地址只能映射到特定的缓存行
- 全相联映射:任何内存地址可以映射到任何缓存行
- 组相联映射:直接映射和全相联映射的折中
高速缓存的共享属性
在多核系统中,缓存共享带来了复杂性
Inner share
Outer share
伪共享(False Sharing)
当两个无关的变量位于同一缓存行时,会导致性能问题:
#![allow(unused)] fn main() { // 错误示例 - 可能导致伪共享 struct BadLayout { counter1: AtomicU64, // 可能与 counter2 在同一缓存行 counter2: AtomicU64, } // 正确示例 - 避免伪共享 #[repr(align(64))] struct GoodLayout { counter1: AtomicU64, _pad1: [u8; 56], // 填充到缓存行边界 counter2: AtomicU64, _pad2: [u8; 56], } }
高速缓存的维护指令
常见的缓存维护指令
- 刷新(Flush):将缓存行写回内存并标记为无效
- 无效化(Invalidate):标记缓存行为无效
- 清理(Clean):将脏缓存行写回内存但保持有效
#![allow(unused)] fn main() { // 在 Rust 中使用内联汇编进行缓存操作 #[cfg(target_arch = "aarch64")] unsafe fn flush_cache_line(addr: *const u8) { asm!("dc civac, {}", in(reg) addr, options(nostack)); } }
缓存维护的时机
#![allow(unused)] fn main() { // DMA 操作前后的缓存维护 unsafe fn dma_coherent_write(buffer: &mut [u8], device_addr: u64) { // 1. 清理缓存,确保数据写入内存 for chunk in buffer.chunks(CACHE_LINE_SIZE) { flush_cache_line(chunk.as_ptr()); } // 2. 启动 DMA 传输 start_dma_transfer(buffer, device_addr); // 3. 等待传输完成 wait_dma_complete(); // 4. 无效化缓存,确保读取到最新数据 for chunk in buffer.chunks(CACHE_LINE_SIZE) { invalidate_cache_line(chunk.as_ptr()); } } }
软件维护缓存一致性
显式缓存管理
#![allow(unused)] fn main() { pub struct CacheManager; impl CacheManager { /// 为 DMA 操作准备缓冲区 pub unsafe fn prepare_for_dma_to_device(buffer: &[u8]) { // 清理缓存,确保数据同步到内存 Self::clean_dcache_range(buffer.as_ptr(), buffer.len()); } /// DMA 从设备读取后的处理 pub unsafe fn finish_dma_from_device(buffer: &mut [u8]) { // 无效化缓存,确保读取到设备写入的数据 Self::invalidate_dcache_range(buffer.as_ptr(), buffer.len()); } /// 双向 DMA 操作 pub unsafe fn prepare_for_bidirectional_dma(buffer: &mut [u8]) { // 刷新缓存(清理 + 无效化) Self::flush_dcache_range(buffer.as_ptr(), buffer.len()); } #[cfg(target_arch = "aarch64")] unsafe fn clean_dcache_range(start: *const u8, len: usize) { let end = start.add(len); let mut addr = (start as usize) & !(CACHE_LINE_SIZE - 1); while addr < end as usize { asm!("dc cvac, {}", in(reg) addr); addr += CACHE_LINE_SIZE; } asm!("dsb sy"); } #[cfg(target_arch = "aarch64")] unsafe fn invalidate_dcache_range(start: *const u8, len: usize) { let end = start.add(len); let mut addr = (start as usize) & !(CACHE_LINE_SIZE - 1); while addr < end as usize { asm!("dc ivac, {}", in(reg) addr); addr += CACHE_LINE_SIZE; } asm!("dsb sy"); } #[cfg(target_arch = "aarch64")] unsafe fn flush_dcache_range(start: *const u8, len: usize) { let end = start.add(len); let mut addr = (start as usize) & !(CACHE_LINE_SIZE - 1); while addr < end as usize { asm!("dc civac, {}", in(reg) addr); addr += CACHE_LINE_SIZE; } asm!("dsb sy"); } } }
利用 dma-api
简化缓存维护操作
一致性 DMA 内存分配
使用示例
内存屏障
CPU 乱序执行
现代 CPU 为了提高性能,会对指令进行乱序执行,这可能导致内存访问顺序与程序代码顺序不一致。
乱序执行的类型
- 编译器重排序:编译器优化可能改变指令顺序
- CPU 重排序:CPU 在执行时可能调整指令顺序
- 内存系统重排序:缓存和内存控制器可能影响访问顺序
#![allow(unused)] fn main() { // 示例:可能被重排序的代码 static mut FLAG: bool = false; static mut DATA: u32 = 0; // 线程 1 unsafe fn producer() { DATA = 42; // 可能被重排到 FLAG = true 之后 FLAG = true; } // 线程 2 unsafe fn consumer() { while !FLAG {} // 可能读取到 FLAG = true let value = DATA; // 但 DATA 可能还是 0 } }
重排序规则
不同架构有不同的重排序规则:
- x86/x64:相对较强的内存模型,主要是 store-load 重排序
- ARM/AArch64:较弱的内存模型,允许更多重排序
- RISC-V:弱内存模型,类似 ARM
内存屏障的类型
全屏障(Full Barrier)
mb
#![allow(unused)] fn main() { use std::sync::atomic::Ordering; // 防止所有重排序 std::sync::atomic::fence(Ordering::SeqCst); }
获取屏障(Acquire Barrier)
rmb
#![allow(unused)] fn main() { // 防止后续操作重排到屏障之前 std::sync::atomic::fence(Ordering::Acquire); }
释放屏障(Release Barrier)
wmb
#![allow(unused)] fn main() { // 防止前面的操作重排到屏障之后 std::sync::atomic::fence(Ordering::Release); }
编译器屏障
#![allow(unused)] fn main() { // 只防止编译器重排序,不影响 CPU std::sync::atomic::compiler_fence(Ordering::SeqCst); }
如何使用内存屏障
生产者-消费者模式
#![allow(unused)] fn main() { // 生产者-消费者模式 fn producer_consumer_example() { // 生产者 unsafe { // 写入数据 core::ptr::write_volatile(data_ptr, 42); // 写屏障确保数据写入完成 wmb(); // 设置标志 core::ptr::write_volatile(flag_ptr, true); } // 消费者 unsafe { // 读取标志 if core::ptr::read_volatile(flag_ptr) { // 读屏障确保标志读取完成 rmb(); // 读取数据 let value = core::ptr::read_volatile(data_ptr); } } } }
DMA 一致性保证
#![allow(unused)] fn main() { struct DmaDescriptor { addr: AtomicU64, length: AtomicU32, flags: AtomicU32, } impl DmaDescriptor { fn setup_transfer(&self, buffer_addr: u64, size: u32) { // 设置传输参数 self.addr.write(buffer_addr); self.length.write(size); // 释放屏障:确保参数设置完成 wmb(); // 设置有效标志 self.flags.write(0x80000000); } fn is_complete(&self) -> bool { // 获取屏障:确保读取到最新状态 rmb(); let flags = self.flags.read(); flags & 0x40000000 != 0 } } }