Zynq SDK 驱动探求(二):外设,从初始化到干活

2020年12月4日 作者 火狐体育

Processor is ready. Configure programable logic.

在新专栏 Rapid TCP/IP on Zynq 中,将围绕 Xilinx Zynq 系列芯片,从 SDK 驱动,PS-PL 协同加速,嵌入式协议栈 LWIP 分析以及 TCP/IP 硬件加速等方面,一起探求可灵活配置,软件定义,硬件加速的 TCP/IP 协议栈的实现。

本文将以定时器为例,讨论定时器从初始化,配置到启动的全过程。
作者:李凡
来源:   https://zhuanlan.zhihu.com/p/58509731

外设:从初始化到搬砖

我们将先从 PS 的外设驱动开始研究外设的初始化和工作流程,这些驱动由 Xilinx 提供,位于软件工程的板级支持包(BSP)内。除了驱动本身以外,Xilinx 为大量的 PS 和 PL 标准外设都提供了文档和 Example 工程。

从 BSP 中的 system.mss 页面中的 Document 或者 Example 打开。

外设的初始化准备工作

所有的外设都有一套共用的初始化流程,这里以 PS 外设私有定时器为例。

XScuTimer_Config *ConfigPtr;
ConfigPtr = XScuTimer_LookupConfig(TIMER_DEVICE_ID);

其中的宏定义 TIMER\_DEVICE\_ID 为 SDK 为这个定时器分配的 ID 号,分配 ID 号的原因是因为同种类型的外设数量不止一个。比如我们上篇文章中说过:乐意的话完全可以配置 30 路串口,那么 SDK 就会为这 30 个串口从 0 开始分配 ID。在初始化和使用这些串口的过程中,都需要讲清楚要使用哪个 ID 所对应的驱动。

声明一个定时器配置结构体,其实虽然所有外设都有各自的配置结构体,但在主要内容上没有什么不同。

/**
 * This typedef contains configuration information for the device.
 */
typedef struct {
    u16 DeviceId;    /**< Unique ID of device */
    u32 BaseAddr;    /**< Base address of the device */
} XScuTimer_Config;

结构体中为 ID 号,和外设对应的寄存器在内存中的起始地址,这里首先断言:外设驱动函数操作外设的实质是对寄存器的读写操作,敲黑板,所以地址很重要,后面会展开讨论。

那么问题来了,外设 ID 是如何和寄存器基址一一对应的呢?

完成这项配对工作的是 XScuTimer\_LookupConfig 函数。

函数输入为独一无二的定时器外设 ID,独一无二是针对某类外设而言,不同外设间的 ID 是分别编号的,可能相同。

XScuTimer_Config *XScuTimer_LookupConfig(u16 DeviceId)
{
    XScuTimer_Config *CfgPtr = NULL;
    u32 Index;

    for (Index = 0U; Index < XPAR_XSCUTIMER_NUM_INSTANCES; Index++) {
        if (XScuTimer_ConfigTable[Index].DeviceId == DeviceId) {
            CfgPtr = &XScuTimer_ConfigTable[Index];
            break;
        }
    }

    return (XScuTimer_Config *)CfgPtr;
}

输出为定时器配置结构体,结构体来自于 XScuTimer\_ConfigTable ,在比较 ID 相同后,将该 ID 对应的配置表项返回。

那么 XScuTimer\_ConfigTable 又从何而来,归根结底来自于硬件平台的硬件配置,由 Vivado 根据我们的硬件配置自动生成。Zynq 设计流中,在 Vivado 中完成硬件配置,在 SDK 中完成软件代码的开发。之后会再讨论设计流的问题。

这个配置表位于 xscutimer\_g.c 该文件也是自动生成的。具体的宏定义声明于 xparameters.h ,xparameters.h 集中了所有外设的 ID 和地址,同样也是由软件自动生成的。

XScuTimer_Config XScuTimer_ConfigTable[XPAR_XSCUTIMER_NUM_INSTANCES] =
{
    {
        XPAR_PS7_SCUTIMER_0_DEVICE_ID,
        XPAR_PS7_SCUTIMER_0_BASEADDR
    }
};

总结一下,XScuTimer\_LookupConfig 函数通过 ID 完成了外设配置信息的读取。配置信息由硬件生成。虽然这里所谓的外设配置信息,对于私有定时器外设来说其实只有外设寄存器基址,但之后的操作都靠它了。

外设配置结构体的参数主要与底层硬件相关,结构体起到连接硬件与驱动的作用。在之后会看到的外设配置结构体中,比如 EmacPs,会有更多的参数。这些参数都是与底层硬件息息相关的。

初始化外设驱动

在将外设配置信息读取到配置结构体后,使用配置结构体作为参数,调用驱动初始化函数 XScuTimer\_CfgInitialize

static XScuTimer TimerInstance;
Status = XScuTimer_CfgInitialize(&TimerInstance, ConfigPtr,
            ConfigPtr->BaseAddr);
    if (Status != XST_SUCCESS) {

        xil_printf("In %s: Scutimer Cfg initialization failed...\r\n",
        __func__);
        return;
    }

驱动初始化函数还接收 XScuTimer 结构体指针作为参数。XScuTimer 结构体在后续的操作中将用于表示该定时器外设,所有针对定时器的操作,包括配置,启动,停止函数都需要传入 XScuTimer 结构体指针作为参数。

typedef struct {
    XScuTimer_Config Config; /**< Hardware Configuration */
    u32 IsReady;        /**< Device is initialized and ready */
    u32 IsStarted;        /**< Device timer is running */
} XScuTimer;

XScuTimer 结构体包括一个 XScuTimer\_Config 结构体,那也就包括了外设的 ID 以及寄存器基址。此外还包括两个状态变量:IsReady,IsStarted 分别表示外设是否完成初始化就绪以及启动。

s32 XScuTimer_CfgInitialize(XScuTimer *InstancePtr,
             XScuTimer_Config *ConfigPtr, u32 EffectiveAddress)
{
    s32 Status;
    Xil_AssertNonvoid(InstancePtr != NULL);
    Xil_AssertNonvoid(ConfigPtr != NULL);

    if (InstancePtr->IsStarted != XIL_COMPONENT_IS_STARTED) {
        InstancePtr->Config.DeviceId = ConfigPtr->DeviceId;

        InstancePtr->Config.BaseAddr = EffectiveAddress;

        InstancePtr->IsStarted = (u32)0;

        InstancePtr->IsReady = XIL_COMPONENT_IS_READY;

        Status =(s32)XST_SUCCESS;
    }
    else {
        Status = (s32)XST_DEVICE_IS_STARTED;
    }
    return Status;
}

XScuTimer\_CfgInitialize 函数中首先通过 Xil\_AssertNonvoid 函数判断传入的结构体参数是否为空。之后判断外设是否处于启动状态,如果仍处于启动状态,直接初始化未免不合适,这里返回 XST\_DEVICE\_IS\_STARTED 作为错误代码。上层函数根据错误代码进行判断,先停止外设再初始化为好。

正常的流程中,函数将配置结构体中的 ID 和基址参数赋给外设结构体,同时更改状态参数为就绪但未启动的状态。在后续的配置函数中,都会对外设的就绪状态变量进行检查。

至此完成了外设初始化的过程,整个过程更强调对于外设驱动的初始化,包括地址的配对,状态变量的初始化,其中 **ID 和外设基址的配对,**也就是与硬件层的交互是其中的核心任务。

外设的配置

在讨论 Zynq 的外设配置之前,这里我们就先和我们熟悉的 STM32 固件库外设配置操作做下对比。

在 STM32 中,外设的属性存放于外设配置结构体中,只需要修改外设结构体,再重新配置外设,传入结构体指针后,固件库函数就能完成配置的更新工作,屏蔽了寄存器的操作。比如下图就是串口外设的配置结构体。其中包括了缓冲区的指针,波特率等参数等等。

而我们之前已经认识了 Zynq 的私有定时器外设结构体,其中几乎不包括外设本身的信息,只有外设寄存器的基址,外设 ID 以及外设运行状态变量。那么定时器具体的参数是如何设置的呢?

我们以设定定时器自动装填为例,

#define XScuTimer_EnableAutoReload(InstancePtr)                \
    XScuTimer_WriteReg((InstancePtr)->Config.BaseAddr,        \
            XSCUTIMER_CONTROL_OFFSET,            \
            (XScuTimer_ReadReg((InstancePtr)->Config.BaseAddr, \
                XSCUTIMER_CONTROL_OFFSET) |         \
                XSCUTIMER_CONTROL_AUTO_RELOAD_MASK))

这是一个宏定义函数,让它变得可读一些:

void XScuTimer_EnableAutoReload(InstancePtr)        
{        
        XScuTimer_WriteReg((InstancePtr)->Config.BaseAddr,XSCUTIMER_CONTROL_OFFSET,            
            (XScuTimer_ReadReg((InstancePtr)->Config.BaseAddr,XSCUTIMER_CONTROL_OFFSET) 
                         |XSCUTIMER_CONTROL_AUTO_RELOAD_MASK)
                        )
}

XScuTimer\_EnableAutoReload 函数做了一件事,向定时器某个寄存器写入一个值。

这个寄存器是 XSCUTIMER\_CONTROL 寄存器,地址为 BaseAddr + XSCUTIMER\_CONTROL\_OFFSET

写入的值为读取 XSCUTIMER\_CONTROL 的值,或上XSCUTIMER\_CONTROL\_AUTO\_RELOAD\_MASK ,相当于将 MASK 对应的比特位置 1。

这实际上是一个经典的嵌入式寄存器操作,将寄存器某些比特位置 1 或者清 0 。只使用过 STM32 的开发者可能对此并不熟悉(比如笔者本人),但 STM32 库函数的背后,是同样的寄存器操作。固件库是 ST 对于寄存器操作的封装。

那么 Xilinx 的 SDK driver 也做了同样的事情,对寄存器读写配置操作进行了封装。当然,从私有定时器这一外设来说,相比 ST 的固件库的封装层数更小,恩,就显得简陋些。Xilinx 在 外设\_hw.h 文件中,提供了外设寄存器的偏移量,以及相应配置对应比特位 mask 的宏定义。

#define XSCUTIMER_LOAD_OFFSET        0x00U /**< Timer Load Register */
#define XSCUTIMER_COUNTER_OFFSET    0x04U /**< Timer Counter Register */
#define XSCUTIMER_CONTROL_OFFSET    0x08U /**< Timer Control Register */
#define XSCUTIMER_ISR_OFFSET        0x0CU /**< Timer Interrupt

外设的启动与停止

外设的启动函数的工作原理同上述的配置函数完全相同,但不是以宏定义的形式,原因在于要对参数进行检查。(开心的话,应该也能写成宏定义的形式)

void XScuTimer_Start(XScuTimer *InstancePtr)
{
    u32 Register;

    Xil_AssertVoid(InstancePtr != NULL);
    Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);

    /*
     * Read the contents of the Control register.
     */
    Register = XScuTimer_ReadReg(InstancePtr->Config.BaseAddr,
                  XSCUTIMER_CONTROL_OFFSET);

    /*
     * Set the 'timer enable' bit in the register.
     */
    Register |= XSCUTIMER_CONTROL_ENABLE_MASK;

    /*
     * Update the Control register with the new value.
     */
    XScuTimer_WriteReg(InstancePtr->Config.BaseAddr,
            XSCUTIMER_CONTROL_OFFSET, Register);

    /*
     * Indicate that the device is started.
     */
    InstancePtr->IsStarted = XIL_COMPONENT_IS_STARTED;
}

这里也提醒我们,在使用宏定义形式的配置函数之前,最好先对传入的参数进行检查。毕竟写错地址,写到 0 地址,可能是个大问题。

至此定时器就开始搬砖(jishi)了。

结语

本文我们以定时器为例,从树木见森林;以 SDK 的示例程序为纲,一个个函数走马观花。讨论了外设从初始化到搬砖的整个过程。

在后续的文章中,我们将继续深入探讨驱动的抽象层次,设计的意图,以及我不用这些个结构体和函数行不行的问题。另外,还将继续深入讨论定时器外设的使用,最后,将以定时器的驱动为例,赏析 Xilinx SDK 驱动的源文件层次设计。

推荐阅读

  • 尝·新:酷芯微 AI 推断加速硬件平台初体验
  • FPGA 系统中的处理器核们(一):全可编程与软硬兼备
  • [Demo愉快行]Xilinx ZCU102 LwIP Echo Demo 硬件平台搭建

关注此系列,请关注专栏FPGA的逻辑