嵌入式系统中的Board Support Package (BSP)详解:以Xilinx Zynq为例
引言
在嵌入式系统开发中,硬件与软件的无缝集成至关重要。Board Support Package (BSP) 作为连接硬件和操作系统的桥梁,在这一过程中扮演着核心角色。本文将深入探讨BSP的概念、组成部分及其在Xilinx Zynq平台上的应用,特别聚焦设备树和硬件抽象层(HAL)这两个关键组件,帮助您理解BSP如何简化嵌入式系统开发并提高开发效率。
BSP的基本概念
什么是BSP?
Board Support Package (BSP)是一组软件组件的集合,它为特定硬件平台提供基础支持,使操作系统能够在该硬件上正常运行。BSP封装了硬件细节,提供标准化接口,允许上层软件(如操作系统和应用程序)与底层硬件交互而无需了解硬件的具体实现。
在嵌入式系统软件层次结构中,BSP的位置如下:
应用软件
↓
操作系统/中间件
↓
Board Support Package (BSP)
↓
硬件平台
BSP的组成部分
一个典型的BSP通常包含以下核心组件:
-
引导加载程序(Bootloader):
- 初始化关键硬件组件
- 加载操作系统内核
- 例如:FSBL (First Stage Boot Loader)、U-Boot等
-
设备驱动程序:
- 为各种硬件外设提供操作接口
- 包括UART、I2C、SPI、GPIO、以太网等驱动
- 允许操作系统控制和使用这些设备
-
硬件初始化代码:
- 配置时钟、电源管理和内存控制器
- 设置中断控制器
- 初始化关键系统组件
-
硬件抽象层(HAL):
- 提供硬件寄存器的抽象访问接口
- 简化应用程序对硬件的操作
- 提高代码可移植性
-
设备树:
- 描述硬件配置的数据结构
- 定义外设、内存映射和中断
- 配置内核如何与硬件交互
-
内存映射表:
- 定义硬件寄存器和内存区域的地址映射
- 配置内存控制器和缓存
- 设置内存保护单元
-
配置文件:
- 系统参数定义
- 编译和链接选项
- 硬件配置信息
BSP的功能与职责
BSP在嵌入式系统中执行以下关键功能:
- 硬件抽象:隐藏硬件细节,提供统一接口
- 设备支持:通过驱动程序支持各种硬件外设
- 启动与初始化:确保系统正确启动和初始化
- 中断管理:处理和分发硬件中断
- 电源管理:控制系统电源状态
- 内存管理:配置和管理系统内存
- 调试支持:提供调试接口和工具
Xilinx Zynq平台概述
在深入Zynq BSP之前,先了解一下Xilinx Zynq平台的基本架构。
Zynq架构特点
Xilinx Zynq是一种异构系统级芯片(SoC),集成了处理系统(PS)和可编程逻辑(PL):
-
处理系统(PS):
- 包含ARM Cortex-A9双核处理器(Zynq-7000系列)或Cortex-A53四核处理器(Zynq UltraScale+系列)
- 集成内存控制器、USB、以太网、UART等标准外设
- 提供高性能通用计算能力
-
可编程逻辑(PL):
- 基于FPGA技术的可编程硬件
- 可实现定制硬件加速器和接口
- 提供灵活的硬件定制能力
-
PS-PL接口:
- 通过AXI接口连接处理系统和可编程逻辑
- 支持高速数据传输
- 实现软硬件协同设计
这种异构架构使Zynq平台非常适合需要高性能处理和硬件加速的嵌入式应用。
Zynq启动流程
Zynq平台的启动过程涉及多个阶段,BSP在其中扮演关键角色:
-
BootROM:
- 芯片内置的只读程序
- 执行初始启动配置
- 加载FSBL
-
FSBL (First Stage Boot Loader):
- 初始化关键硬件(处理器、DDR、时钟等)
- 加载FPGA比特流(如果有)
- 加载第二阶段引导程序(通常是U-Boot)
-
U-Boot:
- 初始化更多硬件设备
- 提供命令行界面
- 加载操作系统内核和设备树
-
操作系统:
- 接管系统控制
- 初始化驱动程序
- 启动应用程序
Xilinx Zynq的BSP详解
Zynq BSP的类型
Xilinx为Zynq平台提供了两种主要的BSP实现方式:
-
独立式BSP (Standalone BSP):
- 用于裸机应用或实时操作系统
- 不依赖复杂的操作系统
- 提供基本的硬件抽象层
- 适合资源受限或实时要求高的应用
-
基于操作系统的BSP:
- 支持Linux、FreeRTOS等操作系统
- 提供完整的驱动程序和服务
- 包含设备树和内核配置
- 适合复杂应用和网络功能
Zynq Standalone BSP的组成
以Xilinx Vitis/SDK创建的Standalone BSP为例,其主要组件包括:
-
处理器初始化代码:
- 初始化ARM处理器
- 配置MMU、缓存和异常向量
- 设置栈和堆
-
外设驱动库:
- 提供访问UART、I2C、SPI等外设的API
- 支持中断和DMA操作
- 包含PS-PL接口驱动
-
系统库:
- 提供标准C库函数
- 包含数学函数和字符串处理
- 支持内存分配和管理
-
启动代码:
- 处理器复位后的入口点
- 执行硬件初始化
- 调用main函数
-
链接脚本:
- 定义内存布局
- 指定代码和数据段位置
- 配置堆栈大小
Zynq Linux BSP的组成
使用PetaLinux工具创建的Linux BSP主要包括:
-
FSBL:
- 初始化基本硬件
- 加载FPGA比特流
- 加载U-Boot
-
U-Boot:
- 第二阶段引导加载程序
- 提供环境变量和命令行界面
- 加载Linux内核和设备树
-
Linux内核:
- 定制的Linux内核
- 包含Zynq特定驱动程序
- 支持PS和PL部分集成
-
设备树:
- 描述硬件配置的数据结构
- 定义外设、内存映射和中断
- 配置内核如何与硬件交互
-
根文件系统:
- 基本的Linux文件系统
- 包含系统工具和库
- 可选的应用程序和服务
BSP创建与定制
使用Xilinx工具创建BSP
Xilinx提供了多种工具来创建和定制BSP:
-
使用Vitis/SDK创建Standalone BSP:
a. 创建硬件平台:
- 使用Vivado设计硬件系统
- 导出硬件描述到Vitis
b. 创建BSP项目:
- 在Vitis中创建新的应用项目
- 选择"创建新的平台"选项
- 导入硬件描述文件
- 选择处理器(如PS7_cortexa9_0)
c. 配置BSP设置:
- 选择操作系统(如"standalone")
- 配置BSP选项(如stdout设备、堆栈大小等)
- 选择需要的库和驱动程序
d. 生成BSP:
- Vitis自动生成BSP文件
- 生成的BSP包含所有必要的驱动和库
-
使用PetaLinux创建Linux BSP:
a. 创建PetaLinux项目:
petalinux-create --type project --template zynq --name my_project
b. 导入硬件描述:
cd my_project petalinux-config --get-hw-description=/path/to/hardware
c. 配置Linux内核:
petalinux-config -c kernel
d. 配置根文件系统:
petalinux-config -c rootfs
e. 构建BSP:
petalinux-build
f. 打包BSP映像:
petalinux-package --boot --format BIN --fsbl images/linux/zynq_fsbl.elf --u-boot
深入理解设备树(Device Tree)
设备树的基本概念
设备树是一种描述硬件配置的数据结构,它采用树状结构组织,包含节点和属性。在嵌入式Linux系统中,设备树已成为描述非x86架构硬件的标准方法,取代了早期的硬编码方式。
设备树的主要目的是将硬件描述与内核代码分离,使同一个内核镜像可以支持多种硬件配置,只需更换设备树文件即可。
设备树文件格式
设备树有三种主要文件格式:
-
DTS (Device Tree Source):
- 人类可读的文本格式
- 包含节点、属性和值
- 使用类似C语言的语法
-
DTB (Device Tree Blob):
- 编译后的二进制格式
- 由内核直接解析
- 通常由bootloader加载并传递给内核
-
DTSI (Device Tree Source Include):
- 包含公共定义的源文件
- 可被多个DTS文件包含
- 用于代码复用
设备树在Zynq中的应用
在Zynq平台上,设备树负责描述:
-
处理系统(PS)外设:
- 描述ARM核心、缓存、MMU等
- 定义内存控制器和DDR配置
- 配置UART、I2C、SPI、以太网等外设
-
可编程逻辑(PL)组件:
- 描述用户创建的IP核
- 定义AXI接口和中断映射
- 配置PL时钟和电源域
-
PS-PL接口:
- 定义AXI互连配置
- 映射PL中断到PS中断控制器
- 配置DMA通道
设备树示例(Zynq相关部分)
以下是Zynq平台设备树的简化示例:
/ {
compatible = "xlnx,zynq-7000";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
device_type = "cpu";
reg = <0>;
clocks = <&clkc 3>;
};
cpu@1 {
compatible = "arm,cortex-a9";
device_type = "cpu";
reg = <1>;
clocks = <&clkc 3>;
};
};
amba: amba {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@e0000000 {
compatible = "xlnx,xuartps";
reg = <0xe0000000 0x1000>;
interrupts = <0 27 4>;
clocks = <&clkc 23>, <&clkc 40>;
clock-names = "uart_clk", "pclk";
};
/* 其他PS外设节点... */
};
/* PL部分自定义IP */
my_custom_ip: my_custom_ip@43c00000 {
compatible = "vendor,my-custom-ip";
reg = <0x43c00000 0x10000>;
interrupts = <0 29 4>;
};
};
在这个例子中:
- 顶层节点定义了整个系统
cpus
节点描述了双核Cortex-A9处理器amba
节点包含了AMBA总线上的PS外设uart0
节点描述了UART外设的基地址、中断号和时钟源my_custom_ip
节点描述了PL中的自定义IP核
设备树与内核驱动的关系
设备树与内核驱动程序之间存在密切的关系:
-
设备-驱动匹配:
- 内核使用
compatible
属性匹配设备和驱动 - 驱动程序通过设备树获取硬件配置信息
- 无需修改驱动代码即可支持不同硬件配置
- 内核使用
-
资源获取:
- 驱动程序从设备树获取资源信息(地址、中断等)
- 使用标准API访问这些资源
- 例如:
of_iomap()
、of_irq_get()
等
-
设备属性配置:
- 通过设备树配置驱动行为
- 定义设备特定参数
- 支持运行时选项
硬件抽象层(HAL)详解
HAL的概念与目的
硬件抽象层(Hardware Abstraction Layer, HAL)是一种软件层,它隐藏了底层硬件的具体细节,提供标准化的API,使上层软件能够以一致的方式访问不同的硬件平台。
HAL的主要目的是:
- 提高代码可移植性
- 简化应用程序开发
- 隐藏硬件复杂性
- 标准化硬件访问接口
HAL的层次结构
HAL通常分为多个层次:
-
底层HAL:
- 直接与硬件寄存器交互
- 提供基本的读写操作
- 实现最低级别的硬件控制
-
中间层HAL:
- 提供设备级别的抽象
- 实现通用功能(如中断管理)
- 处理硬件特定的初始化和配置
-
上层HAL:
- 提供面向应用的API
- 实现高级功能(如DMA传输)
- 隐藏平台特定的细节
Zynq Standalone BSP中的HAL实现
在Zynq的Standalone BSP中,HAL主要包括:
- 低级硬件访问函数:
- 寄存器读写操作
- 内存屏障和同步原语
- 例如:
Xil_In32()
、Xil_Out32()
// 读取32位寄存器
static inline u32 Xil_In32(u32 Addr) {
return *(volatile u32 *) Addr;
}
// 写入32位寄存器
static inline void Xil_Out32(u32 Addr, u32 Value) {
*(volatile u32 *) Addr = Value;
}
- 外设驱动API:
- 设备初始化和配置
- 数据传输和处理
- 中断管理
// GPIO设备初始化
int XGpio_Initialize(XGpio *InstancePtr, u16 DeviceId) {
XGpio_Config *ConfigPtr;
// 查找设备配置
ConfigPtr = XGpio_LookupConfig(DeviceId);
if (ConfigPtr == NULL) {
return XST_DEVICE_NOT_FOUND;
}
// 设置基地址和其他参数
InstancePtr->BaseAddress = ConfigPtr->BaseAddress;
// ...其他初始化代码...
return XST_SUCCESS;
}
// 设置GPIO方向
void XGpio_SetDataDirection(XGpio *InstancePtr, unsigned Channel, u32 DirectionMask) {
// 计算寄存器地址
u32 RegOffset = (Channel == 1) ? XGPIO_TRI_OFFSET : XGPIO_TRI2_OFFSET;
// 写入方向寄存器
Xil_Out32(InstancePtr->BaseAddress + RegOffset, DirectionMask);
}
- 系统服务:
- 异常和中断处理
- 缓存和MMU管理
- 定时器和延迟函数
// 启用ARM处理器中断
void Xil_ExceptionEnable(void) {
// 修改CPSR寄存器,启用中断
asm volatile ("mrs r0, cpsr");
asm volatile ("bic r0, r0, #0x80");
asm volatile ("msr cpsr_c, r0");
}
Zynq Linux BSP中的HAL实现
在Linux BSP中,HAL的实现更加复杂,它通过内核的多层抽象实现:
-
内核硬件抽象:
- 体系结构特定代码(arch/arm/)
- 通用设备模型和驱动框架
- 资源管理和分配
-
设备驱动框架:
- 平台设备和驱动模型
- 总线抽象(如PCI、I2C、SPI)
- 通用子系统(如GPIO、DMA、时钟)
-
用户空间接口:
- 设备文件(/dev/)
- sysfs接口(/sys/)
- ioctl调用
在Linux内核中,HAL的一个重要部分是设备驱动模型,它提供了统一的框架来管理设备和驱动程序。例如,Zynq的UART驱动:
static struct platform_driver cdns_uart_platform_driver = {
.probe = cdns_uart_probe,
.remove = cdns_uart_remove,
.driver = {
.name = CDNS_UART_DRIVER_NAME,
.of_match_table = cdns_uart_of_match,
.pm = &cdns_uart_pm_ops,
},
};
static int cdns_uart_probe(struct platform_device *pdev)
{
struct resource *res;
struct uart_port *port;
int irq;
// 从设备树获取资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
// 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 分配端口结构
port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
if (!port)
return -ENOMEM;
// 设置端口参数
port->membase = devm_ioremap_resource(&pdev->dev, res);
port->irq = irq;
// ...其他初始化代码...
// 注册UART端口
return uart_add_one_port(&cdns_uart_uart_driver, port);
}
BSP、设备树和HAL的关系
BSP、设备树和HAL这三个概念紧密相关,共同构成了嵌入式系统的软件基础。
他们之间的概念的关系,我们在下一篇博客中讨论