概要

在 Zynq 裸机开发中,除了直接操作寄存器之外,还可以使用 Xilinx SDK 自动生成的硬件驱动库来完成外设控制。相比寄存器级编程,驱动库方式更直观、更易读、开发效率更高,也更适合初学者快速上手。本文以 GPIO 控制 LED 为例,介绍什么是 SDK 硬件驱动库、为什么要使用驱动库、驱动库方式与寄存器方式的区别,以及基于 XGpioPs 驱动库实现 LED 闪烁和按键控制 LED 的基本方法。

关键词
Zynq;SDK;硬件驱动库;GPIO;XGpioPs;裸机开发;LED

一、前言

在前一篇关于 GPIO 硬件编程原理的内容中,我们已经从底层寄存器的角度,分析了如何通过读写寄存器控制 GPIO,并最终实现 LED 闪烁。

这种方法有一个很明显的优点:

代码尺寸小,执行效率高,控制粒度细。

但与此同时,它也有几个非常现实的问题:

  • 对开发者的底层理解要求很高;
  • 代码中常常需要大量移位、按位与、按位或等操作;
  • 可读性相对较差;
  • 针对具体硬件写死的代码较多,移植性不够好。

对于初学者来说,如果对 Zynq 各个外设控制器的寄存器结构还不够熟悉,那么完全依赖寄存器级编程,往往会让开发和调试过程变得很吃力。

所以,在很多对性能要求没有那么苛刻的场合,更常见的一种方式其实是:

使用 SDK 提供的硬件驱动库进行编程。

这也是 Zynq 裸机开发中非常重要的一种方法。本文就围绕这种方式展开。

二、什么是 SDK 硬件驱动库

Zynq SoC FPGA 芯片和对应的软件开发环境 SDK 都是 Xilinx 提供的。因此,Xilinx 原厂已经为 Zynq 的各类常用外设编写好了配套驱动库,并且这些驱动库可以和 Vivado 导出的硬件平台信息自动匹配。

当我们在 Vivado 中创建好了包含 Zynq PS 的硬件系统,并将其导出到 SDK 之后,SDK 会根据当前硬件平台自动生成对应的 BSP,而在 BSP 中,就会包含当前硬件系统所启用外设的驱动程序和相关头文件。

这意味着,用户在编写应用程序时,并不一定要自己一条条读写寄存器,而是可以通过调用这些已经写好的驱动函数,来实现对外设的控制。

可以把它理解成:

  • 寄存器编程:自己直接和外设寄存器打交道
  • 驱动库编程:调用原厂提供的函数,由驱动底层帮你完成寄存器操作

所以,SDK 硬件驱动库本质上就是:

原厂封装好的外设控制接口。

三、为什么要使用驱动库

  1. 驱动库让代码更直观

使用寄存器直接编程时,往往会看到很多类似下面的操作:

reg_val = Xil_In32(base + offset);
reg_val |= (1 << 7);
Xil_Out32(base + offset, reg_val);

这种写法虽然高效,但需要开发者非常清楚每一个寄存器、每一位的功能,否则很容易出错。
而使用驱动库时,代码通常会更接近“功能描述”:

XGpioPs_SetDirectionPin(&Gpio, 7, 1);
XGpioPs_SetOutputEnablePin(&Gpio, 7, 1);
XGpioPs_WritePin(&Gpio, 7, 1);

即使不看底层实现,也大致能猜出这些函数在做什么。

  1. 驱动库降低出错概率

这些函数由 Xilinx 原厂编写,并经过大量实际项目和用户验证,因此可靠性通常比较高。使用这些函数,既能减少自己手写底层代码的数量,也能降低因为位操作写错、地址写错而导致的 bug。

  1. 驱动库更利于移植和维护

如果直接使用寄存器方式,程序往往会绑定到某个具体硬件配置。例如之前用 MIO7 控制 LED 时,可能直接使用了 GPIO 的某个基地址和某个特定寄存器偏移;如果换成其他 GPIO 引脚,比如 MIO50,那么相关代码往往也要跟着改。

而驱动库函数通常把这些复杂细节封装起来了,应用层只需要传入 Pin 编号和状态即可,因此程序的可移植性和可维护性都会更好。

四、驱动库方式和寄存器方式的区别

这两种方式并不是谁绝对更好,而是各有适用场景。

  1. 寄存器方式的特点

优点:

  • 执行效率高
  • 代码体积小
  • 控制更底层、更直接
  • 适合高实时性场景

缺点:

  • 可读性差
  • 学习门槛高
  • 对寄存器功能理解要求高
  • 移植性较差
  1. 驱动库方式的特点

优点:

  • 写法简单
  • 可读性强
  • 原厂支持,稳定性较高
  • 开发效率更高
  • 更适合初学者和一般应用

缺点:

  • 运行效率相对较低
  • 函数调用过程会引入额外开销
  • 代码尺寸通常更大

所以可以这样概括:

对效率要求极高的底层场合,优先考虑寄存器方式;
对开发效率和代码可读性更看重的场合,优先考虑驱动库方式。

五、一个最简单的例子:用驱动库点亮 LED

资料中给出了一个基于 XGpioPs 驱动库的例子,用来控制 ACZ702 开发板上的 PS 端 LED 闪烁。

核心代码如下:

#include "xgpiops.h"
#include "unistd.h"

XGpioPs Gpio;
XGpioPs_Config *ConfigPtr;

int main(void)
{
    ConfigPtr = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID);
    XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);

    XGpioPs_SetDirectionPin(&Gpio, 7, 1);
    XGpioPs_SetOutputEnablePin(&Gpio, 7, 1);

    while(1)
    {
        XGpioPs_WritePin(&Gpio, 7, 0x1);
        usleep(500000);

        XGpioPs_WritePin(&Gpio, 7, 0x0);
        usleep(500000);
    }

    return 0;
}

这段程序的功能很直接:

  • 查找 GPIO 设备配置;
  • 初始化 GPIO 驱动;
  • 把 MIO7 设置为输出;
  • 打开 MIO7 的输出使能;
  • 在循环中不断输出高低电平,让 LED 闪烁。

和前面寄存器方式实现的 LED 闪烁相比,它最大的特点就是:

不再需要自己去计算 LSW、MSW、MASK_DATA 等寄存器细节,直接调用函数即可。

六、程序中几个核心函数的作用

在这个例程中,最关键的是以下几个函数。

  1. XGpioPs_LookupConfig

这个函数用于查找指定 GPIO 设备的配置信息。
它根据设备 ID,从系统中找到对应 GPIO 控制器的配置结构体。也就是说,这是获取 GPIO 驱动初始化所需参数的第一步。

  1. XGpioPs_CfgInitialize

这个函数用于完成 GPIO 驱动初始化。
调用之后,驱动对象 Gpio 就和具体硬件建立了关联,后续所有 GPIO 操作都会基于这个驱动对象展开。

  1. XGpioPs_SetDirectionPin

这个函数用于设置指定 Pin 的方向。
对于 LED 控制来说,MIO7 是输出,所以这里传入 1,表示设置为输出。

  1. XGpioPs_SetOutputEnablePin

这个函数用于设置指定 Pin 的输出使能。
在 GPIO 中,方向设置为输出还不够,通常还要显式使能输出驱动,这一步完成后,对应引脚才能真正输出高低电平。

  1. XGpioPs_WritePin

这个函数用于写入指定 Pin 的状态。
如果传入 1,则输出高电平;如果传入 0,则输出低电平。对于连接方式为高电平点亮的 LED,就可以通过这个函数直接控制亮灭。

七、为什么驱动库效率会低一些

很多初学者在第一次接触驱动库时,会有一个误区:
既然驱动库这么方便,是不是以后都没必要学寄存器编程了?

其实并不是。

驱动库之所以方便,是因为它在底层做了很多额外工作。以 XGpioPs_WritePin 为例,资料中分析了它的内部实现过程,大致包括:

  • 定义临时变量;
  • 检查传入参数是否合法;
  • 判断驱动实例是否已经正确初始化;
  • 根据 Pin 编号确定对应的 GPIO Bank;
  • 判断该 Pin 位于低 16 位还是高 16 位;
  • 选择对应的 MASK_DATA_LSW 或 MASK_DATA_MSW;
  • 再完成与寄存器方式类似的移位、取反、拼接和写寄存器过程。

也就是说,驱动库并不是“凭空控制硬件”,而是:

在底层替你做了更多判断和封装。

正因为多了这些步骤,所以相比直接写寄存器,效率会低一些。资料中提到,这种效率大约只有直接寄存器方式的三分之一左右。

八、这种效率损失会不会很严重

答案是:看场景。

  1. 对 LED 闪烁这类应用,影响几乎可以忽略

LED 闪烁本身就是一个很低速的动作,哪怕驱动函数比寄存器方式慢一些,也完全不会影响实验效果。因此在这类场景中,驱动库方式完全可以放心使用。

  1. 对高频 IO 翻转场景,影响会更明显

如果使用 GPIO 去模拟一些串行协议,比如:

  • 模拟 SPI
  • 模拟 I2C
  • 模拟 UART
    这类场景中 GPIO 翻转频率会高很多,而且操作可能持续很长时间,这时驱动库带来的额外开销就会更加明显。
  1. 在中断服务函数中,也更适合直接操作寄存器

如果某个 GPIO 操作位于中断服务程序中,那么每多消耗一点时间,都可能影响整个系统的实时性。因此在这类对时钟周期极为敏感的场景下,直接寄存器方式通常更合适。

所以,真正正确的理解应该是:

驱动库不是不能用,而是要分场合用。

九、按键控制 LED 的扩展示例

资料中还给出了一个更完整的例子:
除了用 MIO7 控制 LED,还把 MIO47 配置为输入,用来读取按键状态。

对应程序的核心思路是:

  • 初始化 MIO7 为输出;
  • 初始化 MIO47 为输入;
  • 当按键按下时,LED 周期闪烁;
  • 当按键释放后,LED 熄灭。

代码示例大致如下:

#include "xgpiops.h"
#include "unistd.h"

XGpioPs Gpio;
XGpioPs_Config *ConfigPtr;

int main(void)
{
    ConfigPtr = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID);
    XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);

    XGpioPs_SetDirectionPin(&Gpio, 7, 1);
    XGpioPs_SetOutputEnablePin(&Gpio, 7, 1);

    XGpioPs_SetDirectionPin(&Gpio, 47, 0);
    XGpioPs_SetOutputEnablePin(&Gpio, 47, 0);

    while(1)
    {
        while(!XGpioPs_ReadPin(&Gpio, 47))
        {
            XGpioPs_WritePin(&Gpio, 7, 0x1);
            usleep(500000);

            XGpioPs_WritePin(&Gpio, 7, 0x0);
            usleep(500000);
        }

        XGpioPs_WritePin(&Gpio, 7, 0x0);
    }

    return 0;
}

这个例子非常有代表性,因为它同时演示了:

  • GPIO 输出配置
  • GPIO 输入配置
  • 使用 XGpioPs_ReadPin 读取按键状态
  • 根据输入状态控制 LED 行为

也就是说,驱动库方式不仅适合做单纯的 LED 点亮,也同样适合完成更完整的输入输出联合实验。

十、如何知道这些驱动库怎么用

很多人第一次看到这些函数时,会有一个自然的问题:

  • 为什么初始化时要用这几个函数?
  • 参数为什么这样传?
  • 我怎么知道该用哪个函数?
  • 如果要控制 UART、I2C、SPI,又该去哪里查?

这其实是一个非常关键的问题。

答案并不复杂:

最直接的方法,就是看 SDK 自带的驱动文档和例程。

Xilinx 在 SDK 中,不仅提供了驱动库本身,也通常会提供对应外设的示例程序。对初学者来说,学会去 SDK 里查找官方例程,是非常重要的一种能力。

因为官方例程通常已经把最基础的初始化顺序、函数调用关系和参数用法都写出来了,你只需要结合自己的硬件需求进行修改,就能快速完成程序编写。

这其实也是很多工程开发中的常见方法:

  • 先找官方例程;
  • 再结合硬件平台修改;
  • 最后按自己的应用需求扩展。

十一、这一篇真正要掌握的核心思路

这篇内容最重要的,不只是记住几个 XGpioPs_ 函数名,而是理解下面这几件事。

  1. 同一个外设,往往有两种控制方式
    一种是寄存器级方式
    一种是驱动库方式

这两种方法不是互相替代,而是互相补充。

  1. 驱动库方式更适合快速开发

如果当前项目对性能没有极致要求,那么直接使用驱动库,可以大大节省开发时间,提高代码可读性。

  1. 寄存器方式依然很重要

即使以后经常用驱动库,也不代表底层原理可以不学。因为只有理解寄存器方式,才能真正明白这些驱动函数在底层到底做了什么。

  1. 学会查例程,比死记函数更重要

面对不同外设时,不可能把所有函数全背下来。真正重要的是知道去哪里找资料、怎么看例程、怎么改造成适合自己的程序。

十二、知识小结

本文围绕 Zynq 裸机开发中的 SDK 硬件驱动库,主要介绍了以下内容:

  • SDK 会根据 Vivado 导出的硬件平台,自动生成对应外设的驱动库和 BSP。
  • 使用驱动库编程时,开发者可以直接调用功能型接口,而不必手写底层寄存器操作。
  • 与寄存器编程相比,驱动库方式具有更好的可读性、开发效率和可维护性。
  • XGpioPs_LookupConfig、XGpioPs_CfgInitialize、XGpioPs_SetDirectionPin
  • XGpioPs_SetOutputEnablePin 和 XGpioPs_WritePin 是 GPIO 驱动中最常用的几个基础函数。
  • 驱动库在底层做了更多参数检查和功能封装,因此执行效率会低于直接寄存器方式。
  • 对 LED 闪烁这类低速应用,驱动库方式非常适合;对高频 IO 翻转和高实时性场景,寄存器方式更有优势。
  • 使用 XGpioPs_ReadPin 可以方便地完成按键输入读取,并实现输入控制输出的扩展实验。
  • 学习驱动库的最好方法之一,就是阅读 SDK 自带的官方例程。
    十三、结束语

从这篇开始,你可以明显感觉到,Zynq 裸机开发已经不再只是“硬着头皮看寄存器表”了,而是开始进入一种更加工程化、更贴近实际项目开发的方式。

对于初学者来说,驱动库方式的价值非常大。它能帮助你先把功能做出来,再逐步回过头去理解底层寄存器细节。这样学习路径会更顺,也更容易建立信心。

当然,这并不意味着寄存器编程不重要。恰恰相反,驱动库只是帮你“把难的部分封装起来”,而不是让底层原理消失。所以比较理想的学习顺序应该是:

先理解寄存器原理,再学会使用驱动库,最后根据场景选择合适的方法。

这样,你就不仅会“调用函数”,也真正明白函数背后的硬件控制逻辑。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐