边缘AI推理计算 - StarryOS RK3588 边缘AI系统架构深度解析(四):设备树解析与硬件抽象层

37 阅读6分钟

仓库地址: github.com/kaori-seaso…


引言

在前面的文章中,我们深入探讨了StarryOS RK3588系统的启动流程、内存管理、中断控制器和多核调度机制。本文将聚焦于另一个关键组件——设备树解析与硬件抽象层(HAL)。

在现代嵌入式系统中,硬件平台的多样性要求软件具有良好的可移植性。设备树(Device Tree)作为一种描述硬件平台信息的标准格式,使得操作系统能够在运行时动态获取硬件配置信息,而无需在代码中硬编码具体的硬件参数。

设备树基础概念

什么是设备树?

设备树是一种数据结构,用于描述硬件平台的设备信息。它最初由Open Firmware使用,后来被Linux内核广泛采用。设备树以树状结构组织,每个节点代表一个设备或设备组,节点的属性描述了设备的具体信息。

在RK3588平台上,设备树包含了以下关键信息:

  • CPU拓扑结构
  • 内存布局
  • 外设寄存器基地址
  • 中断号映射
  • 时钟配置
  • 引脚复用配置

FDT与DTB

设备树有两种表示形式:

  1. DTS (Device Tree Source) - 源代码格式,人类可读
  2. DTB (Device Tree Blob) - 二进制格式,供系统使用

在StarryOS中,bootloader(如U-Boot)会将DTB传递给内核,内核需要解析这个二进制文件来获取硬件信息。

StarryOS中的FDT解析器实现

FDT解析器架构

StarryOS使用fdt-parser crate来解析设备树。让我们看看核心实现:

/// 外设信息结构
#[derive(Debug, Clone)]
pub struct DeviceInfo {
    pub name: String,
    pub device_type: String,
    pub reg_addr: u64,
    pub reg_size: u64,
    pub interrupts: Vec<u32>,
    pub status: String,
}

这个结构体用于存储从设备树中提取的外设信息,包括设备名称、类型、寄存器地址、中断号等。

解析流程

FDT解析器的初始化流程如下:

/// 初始化FDT解析
pub fn fdt_init(dtb_ptr: u64) -> Result<(), &'static str> {
    let parser = FdtParser::parse(dtb_ptr)?;
    
    // 存储解析器实例
    let mut fdt = FDT.lock();
    *fdt = Some(parser);
    
    Ok(())
}

在系统启动时,main函数会调用fdt_init函数来解析bootloader传递的DTB:

// src/main.rs
#[no_mangle]
pub extern "C" fn main(dtb_ptr: u64) -> ! {
    // ... 其他初始化代码
    
    // 4. 解析设备树 (获取外设基地址和中断配置)
    println!("[StarryOS] Parsing device tree...");
    match fdt_init(dtb_ptr) {
        Ok(_) => println!("[StarryOS] Device tree parsed successfully"),
        Err(e) => {
            println!("[StarryOS] FDT parse error: {}", e);
            panic!("Failed to parse device tree");
        }
    }
    
    // ... 后续初始化代码
}

设备信息查询

解析完成后,系统可以通过FDT解析器查询特定设备的信息:

/// 获取UART设备信息 (用于调试输出)
pub fn get_uart_device() -> Option<u64> {
    FDT.lock()
        .as_ref()
        .and_then(|fdt| {
            fdt.find_by_name("uart")
                .map(|dev| dev.reg_addr)
        })
}

/// 获取I2C设备信息
pub fn get_i2c_devices() -> Vec<(u64, u32)> {
    FDT.lock()
        .as_ref()
        .map(|fdt| {
            let devices = fdt.find_by_type("i2c");
            devices
                .iter()
                .map(|dev| {
                    let irq = dev.interrupts.first().copied().unwrap_or(0);
                    (dev.reg_addr, irq)
                })
                .collect()
        })
        .unwrap_or_default()
}

这种设计使得驱动程序可以在运行时动态获取所需硬件的配置信息,大大提高了系统的可移植性。

硬件抽象层(HAL)设计

HAL的作用

硬件抽象层是连接操作系统内核和具体硬件的桥梁。它的主要作用包括:

  1. 提供统一的硬件访问接口
  2. 封装硬件细节,简化上层开发
  3. 提供类型安全的寄存器访问
  4. 实现平台无关性

RK3588 HAL实现

StarryOS为RK3588芯片实现了一个专门的硬件抽象层,主要包括以下几个组件:

  1. GIC-500中断控制器驱动
  2. FDT解析器
  3. 各种外设驱动

让我们重点看一下I2C驱动如何实现embedded-hal规范:

/// I2C基地址 (RK3588有9个I2C控制器)
pub const I2C0_BASE: u64 = 0xfea80000;
pub const I2C1_BASE: u64 = 0xfea90000;
// ... 其他I2C控制器基地址

/// I2C控制器结构体
pub struct I2c {
    base: u64,
    freq_khz: u32,
}

embedded-hal规范实现

embedded-hal是Rust嵌入式生态系统中的一个重要规范,它定义了一套通用的硬件抽象接口。StarryOS的I2C驱动实现了这一规范:

impl I2c {
    /// 创建新的I2C实例
    pub fn new(base: u64, freq_khz: u32) -> Self {
        I2c { base, freq_khz }
    }
    
    /// 初始化I2C控制器
    pub fn init(&self, apb_freq_mhz: u32) -> Result<(), I2cError> {
        unsafe {
            // 1. 禁用I2C
            write_volatile((self.base + I2C_CON) as *mut u32, 0);
            
            // 2. 计算时钟分频
            let div = ((apb_freq_mhz * 1000) / (2 * self.freq_khz)) - 1;
            
            if div > 0xFFFF {
                return Err(I2cError::InvalidAddr);
            }
            
            write_volatile((self.base + I2C_CLKDIV) as *mut u32, div as u32);
            
            // 3. 启用I2C控制器
            write_volatile((self.base + I2C_CON) as *mut u32, I2C_CON_EN);
            
            Ok(())
        }
    }
    
    /// 写入数据
    pub fn write(&mut self, addr: u8, data: &[u8]) -> Result<(), I2cError> {
        // 实现I2C写操作
        // ...
        Ok(())
    }
    
    /// 读取数据
    pub fn read(&mut self, addr: u8, len: u8) -> Result<Vec<u8>, I2cError> {
        // 实现I2C读操作
        // ...
        Ok(Vec::new())
    }
}

通过实现embedded-hal规范,StarryOS的驱动可以与其他遵循相同规范的Rust嵌入式库兼容,提高了代码的复用性。

全局实例管理

为了方便使用,StarryOS为每个I2C控制器创建了全局实例:

/// 全局I2C实例
use lazy_static::lazy_static;

lazy_static! {
    pub static ref I2C0: spin::Mutex<I2c> = spin::Mutex::new(I2c::new(I2C0_BASE, 100));
    pub static ref I2C1: spin::Mutex<I2c> = spin::Mutex::new(I2c::new(I2C1_BASE, 100));
    // ... 其他I2C控制器
}

这种设计使得在系统任何地方都可以方便地访问I2C控制器,而无需手动传递实例。

系统集成

在系统初始化阶段,所有I2C控制器都会被初始化:

/// 初始化所有I2C控制器
pub fn i2c_init_all(apb_freq_mhz: u32) {
    for i2c in [
        I2C0.lock(),
        I2C1.lock(),
        // ... 其他I2C控制器
    ] {
        let _ = i2c.init(apb_freq_mhz);
    }
}

这种集中初始化的方式确保了所有外设在系统启动时都处于正确的状态。

总结

本文深入解析了StarryOS RK3588系统中的设备树解析和硬件抽象层实现。通过FDT解析器,系统能够动态获取硬件配置信息,提高了可移植性。通过实现embedded-hal规范,驱动程序获得了更好的兼容性和复用性。

设备树解析和硬件抽象层是现代嵌入式系统的重要组成部分,它们使得操作系统能够更好地适应不同的硬件平台。在下一文中,将探讨I2C驱动的具体实现细节和使用方法。