Petalinux DMA驱动

计划先整理下驱动的基础知识,再介绍下设备树相关dts文件的写法。

最简单的内核驱动代码示例

首先以最简单的内核驱动代码为例,介绍如何编写以及编译驱动代码。

内核驱动代码至少需要包含两个函数,init函数(insmod时被调用)、exit函数(rmmod时被调用)。

简单的内核驱动代码示例如下所示:

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
static int __init init_mod ( void ) /* Constructor */
{
printk ( KERN_INFO " Module1 start\ n ");
return 0;
}
static void __exit end_mod ( void ) /* Destructor */
{
printk ( KERN_INFO " Module1 end\ n ");
}
module_init ( init_mod );
module_exit ( end_mod );
MODULE_LICENSE (" GPL ");
MODULE_AUTHOR (" SUIKAMMD");
MODULE_DESCRIPTION (" Driver Module ");

编译驱动需要相应的kernel headers,可以通过下面的指令安装系统的kernel header,kernel header的安装位置在/usr/src/linux。

sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)

下面是Makefile的示例,如下所示:

ifneq (${KERNELRELEASE},)
obj-m := module.o
else
KERNEL_SOURCE := /usr/src/$(header version)
PWD := $(shell pwd)
default:
${MAKE} -C ${KERNEL_SOURCE} M=${PWD} modules
clean :
${MAKE} -C ${KERNEL_SOURCE} M=${PWD} clean
endif

与编译非内核驱动代码的Makefile对比,有两点让人疑惑的地方:

  1. KERNELRELEASE是什么?
  2. obj-m := module何时被执行?

其实在这里,make执行了两次,一次是执行当前目录下的Makefile,一次是在/usr/src/linux下的Makefile。在当前目录下KERNELRELEASE没有被定义,因此会执行else下的语句,执行make不加参数的默认情况下会再执行default中的内容,在default这条语句中,通过make -C进行目录跳转至内核源码目录读取那里的Makefile。M=${PWD}表明返回当前目录从头重新开始读取当前目录的Makefile,编译内核源码文件生成对应的.o文件。从内核源码目录返回时, KERNELRELEASE已被定义,make将读取else之前的内容,联合中间文件生成.ko文件。编译完之后执行insmod module.ko 加载内核模块,通过rm module删除模块,通过 dmesg查看内核信息判断模块是否成功加载或删除。

在当前目录执行make之后,生成一系列文件,device.ko device.mod.c device.mod.o device.o modules.order Module.symvers,再执行sudo insmod device.ko将模块加载到内核中,通过dmesg tail显示最近的linux内核信息,如果输出Module start说明模块成功加载。之后使用sudo rmmod device删除该模块, 通过dmesg tail显示最近的linux内核信息,如果输出Module end说明模块成功删除。

稍微复杂的内核驱动代码示例

上面只是一个非常简单的示例,甚至没有将该驱动与设备进行绑定,下面给出一个完成的驱动代码示例。在编写驱动代码前,使用这条指令mknod -m 0666 /dev/mydev 240 0创建mydev设备,对应的major是240,minor是0。驱动代码如下所示:

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>

static int mydev_open(struct inode *inode, struct file *filep)
{
printk("Device opened\n");
return 0;
}

static int mydev_release(struct inode *inode, struct file *filep)
{
printk("Device release\n");
return 0;
}

static ssize_t mydev_read(struct file *filep, char *buf, size_t count, loff_t *f_pos)
{
printk("Writing to the device...\n");
return 1;
}

static ssize_t mydev_write(struct file *filep, const char *buf, size_t count, loff_t *f_pos)
{
printk("Writing to the device...\n");
return 1;
}

struct file_operations mydev_fops =
{
owner: THIS_MODULE,
open: mydev_open,
release: mydev_release,
read: mydev_read,
write: mydev_write,
};

static int __init init_mydevDriver(void)
{
int result;
if ((result = register_chrdev(240, "mydev", &mydev_fops)) < 0)
goto fail;
printk("my device driver loaded...\n");
return 0;
fail:
return -1;
}

static void __exit exit_mydevDriver(void)
{
unregister_chrdev(240, "mydev");
printk("mydev driver unloaded...\n");
}

module_init(init_mydevDriver);
module_exit(exit_mydevDriver);

MODULE_LICENSE(" GPL ");
MODULE_AUTHOR(" SUIKA ");
MODULE_DESCRIPTION(" Test Driver Module ");

Makefile与上文示例几乎一致,下面给出测试代码:

#include <fcntl.h>
#include <stdio.h>

int main() {
char buffer[1];
int fd;
fd=open("/dev/mydev", O_RDWR);
buffer[0]=0x00;
write(fd, buffer, 1);
read(fd, buffer);
printf("Value : 0x%02x\n", buffer[0]);
close(fd);
}

上面介绍了驱动代码的编写和编译,接下来介绍下xilinx的DMAENGINE和DMA的基础知识。

DMA基础知识

dma一般是内核空间下使用的驱动,用户空间比较复杂。

DMA有两种,Slave-DMAAsync TX

  1. Slave-DMA:memory->device, device->memory, device->device

    slave的含义是指参与DMA传输的设备,对应的”master”是DMA controller本身

  2. Async TX:memory->memory

这里我们主要介绍slave DMA的使用方法,包括如下5个步骤,详细可以参考xilinx dma slave api

  1. 申请一个DMA channel

    DMA channel(结构:struct dam_chan),由DMA controller(provider)提供,被consumer(client,driver,slave driver)使用,client通过struct dma_chan *dma_request_chan(struct device *dev, const char *name)API申请channel,通过void dma_release_channel(struct dma_chan *chan)API释放channle。

  2. 配置DMA channel参数

    driver申请到DMA channel后根据实际情况及DMA controller的能力配置channel,通过APIint dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)struct dma_slave_config告知DMA controller。

    #struct dma_slave_config
    struct dma_slave_config {
    enum dma_transfer_direction direction;
    phys_addr_t src_addr;
    phys_addr_t dst_addr;
    enum dma_slave_buswidth src_addr_width;
    enum dma_slave_buswidth dst_addr_width;
    u32 src_maxburst;
    u32 dst_maxburst;
    bool device_fc;
    unsigned int slave_id;
    };

    代码中参数介绍:

    direction:指明传输方向 memory->device, device->memory, device->device, memory->memory

    src_addr:读取数据位置(src为mem不需要配置)

    dst_addr:写入数据位置(dst为mem不需要配置)

    src_addr_width、dst_addr_width:src/dst地址的宽度

    src_maxburst、dst_maxburst:src/dst最大可传输的burst size(DMA控制的可缓存数据量大小)

    device_fc:决定DMA传输是否结束的模块

    slave_id:slave通过slave_id告诉dma controller自己的身份,一般情况下非必要,只要知道src、dst、len即可。

  3. 获取传输描述

    通过APIdmaengine_prep_slave_single()获得描述符dma_async_tx_descriptor

    struct dma_async_tx_descriptor { 
    dma_cookie_t cookie;
    enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */
    dma_addr_t phys;
    struct dma_chan *chan;
    dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
    int (*desc_free)(struct dma_async_tx_descriptor *tx);
    dma_async_tx_callback callback;
    void *callback_param;
    struct dmaengine_unmap_data *unmap;
    #ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
    struct dma_async_tx_descriptor *next;
    struct dma_async_tx_descriptor *parent;
    spinlock_t lock;
    #endif
    };
  4. 启动传输

    获取传输描述符之后,通过dmaengine_submit API将描述符放到传输队列,返回唯一识别该标志符的cookie,再调用dma_async_issue_pendingAPI启动传输。

  5. 等待传输结束

    通过APIdma_async_is_tx_complete()判断DMA事务是否完成。

基于Petalinux编译DMA驱动

请根据基于Xilinx zcu104版的petalinux移植linux(ffmpeg)这边文章认真配置环境

  • Pre-requisites

    1. A zero-copy Linux driver and a userspace interface library for Xilinx’s AXI DMA and VDMA IP blocks. These serve as bridges for communication between the processing system and FPGA programmable logic fabric, through one of the DMA ports on the Zynq processing system.

      git clone https://github.com/bperez77/xilinx_axidma.git

  • Steps

    1. add hdf to ur petalinux project(这个hdf文件仅仅做了loopback,以下基于此loopback进行介绍)

    2. petalinux-config --get-hw-description $(UR-HDF-PATH)

      Image Packaging Configurations -> Root filesystem type -> SD card

      取消勾选DTG Settings->Kernel Bootargs->generate boot args automatically,set bootargs to earlycon console=ttyPS0,115200 clk_ignore_unused root=/dev/mmcblk0p2 rw rootwait

    3. petalinux-build

      在编译过程中,如果卡在fsbl的编译,请执行以下两条指令

      sudo apt install libgtk-3-dev
      sudo apt install libgtk2.0-dev
    4. create module

      • petalinux-create -t modules -n xilinx-axidma --enable

      • 进入到git clone的文件夹路径,将所需文件拷贝到petalinux module的路径。这里涉及到三步,第一步拷贝文件,第二步删除不必要的文件,第三步是修改petalinux module文件夹的下的Makefile和.bb文件。

        (1)将git clone下来的文件夹里的文件拷贝到petalinux module的路径<path/to/PetaLinux/project>/project-spec/meta-user/recipes-modules/xilinx-axidma/files

        cp -a driver/*.c driver/*.h include/axidma_ioctl.h <path/to/PetaLinux/project>/project-spec/meta-user/recipes-modules/xilinx-axidma/files

        (2) 删除xilinx-axidma.c

        rm <path/to/PetaLinux/project>/project-spec/meta-user/recipes-modules/xilinx-axidma/files/xilinx-axidma.c

        (3) 修改<path/to/PetaLinux/project>/project-spec/meta-user/recipes-modules/xilinx-axidma/files/Makefile

        删除Makefile第一行,取代为下面三行代码

        DRIVER_NAME = xilinx-axidma
        $(DRIVER_NAME)-objs = axi_dma.o axidma_chrdev.o axidma_dma.o axidma_of.o
        obj-m := $(DRIVER_NAME).o

        (4) 修改<path/to/PetaLinux/project>/project-spec/meta-user/recipes-modules/xilinx-axidma/xilinx-axidma.bb

        SRC_URI = "file://Makefile \ 
        file://axi_dma.c \
        file://axidma_chrdev.c \
        file://axidma_dma.c \
        file://axidma_of.c \
        file://axidma.h \
        file://axidma_ioctl.h \
        file://COPYING \
        "
    5. create app

      这里的app实现的功能是,将从千兆拿到的视频i帧转为yuv格式(基于ffmpeg)送到pl端,再将从pl端返回的结果送回内网的服务器。

    6. change the system-user.dtsi according the pl.dtsi.

      (1) 根据之前petalinux-build生成的pl.dtsi,将其中的内容全部复制到system-conf.dtsi,这两个文件的地址均在<path/to/PetaLinux/project>/components/plnx_workspace/device-tree/device-tree,按理说这部分复制进system-user.dtsi也可以,<path/to/PetaLinux/project>/components/plnx_workspace/device-tree/device-tree这个目录下的文件一般是自动生成的。

      (2) 同时删除system-top.dtsi最后一行#include system-user.dtsi

      (3) 修改system-user.dtsi(位置/project-spec/meta-user/recipes-bsp/device-tree/files/”),加入以下内容

      / {
      axidma_chrdev: axidma_chrdev@0 {
      compatible = "xlnx,axidma-chrdev";
      dmas = <&axi_dma_0 0 &axi_dma_0 1>;
      dma-names = "tx_channel", "rx_channel";
      };
    7. petalinux-build

    8. petalinux-package --boot --fsbl ./images/linux/zynqmp_fsbl.elf --fpga ./images/linux/system.bit --pmufw ./images/linux/pmufw.elf --u-boot,生成的文件BOOT.bin、image.ub位于<path/to/PetaLinux/project>/images/linux/

Author: suikammd
Link: https://suikammd.github.io/2019/05/12/write-linux-dma-driver/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.