嵌入式系统中的Board Support Package (BSP)详解:以Xilinx Zynq为例

嵌入式系统中的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这三个概念紧密相关,共同构成了嵌入式系统的软件基础。
他们之间的概念的关系,我们在下一篇博客中讨论