[干货]手把手教你用Zedboard学习Linux移植和驱动开发

作者:殷建飞

本文是昨天发的文章《龙芯杯CPU设计竞赛与ZYNQ设计流程介绍》接续部分。重点介绍传统方式的Linux移植和Xilinx的Petalinux的快速移植开发两种。

部分硬件设计中需要CPU完成对电路寄存器的配置,为了完成Zedboard对FPGA上部分寄存器的配置功能,可以在PS单元(处理器系统)上运行裸机程序(无操作系统支持)完成和PL单元(FPGA部分)的数据交互功能,此时PS单元更像单片机开发;另一种方法是PS单元运行Linux操作系统,通过驱动程序和应用程序完成对硬件寄存器的读写操作,并且Linux有着完整的网络协议栈支持,后续可拓展性更强,可以更好的发挥ZYNQ这种异构架构芯片的性能。主要分为两部分,分别阐述Zedboard中FPGA和处理器互联总线与硬件设计和Zedboard处理器系统上嵌入式Linux的移植与通过驱动和应用程序简单配置FPGA寄存器的实现。上次介绍了没有操作系统下的驱动和应用程序开发,本文介绍带操作系统的驱动和应用程序开发。

1、传统方式移植Linux

Zedboard上电后会首先启动BootRom,bootrom中固化了最初启动需要的初始代码,并根据板卡上的跳线决定从flash或者sd卡或者jtag启动。这里选择从SD卡启动,bootrom中的代码会将SD卡中的启动文件拷贝到RAM或者片上共享缓存中去,为下一步启动做准备。

下一阶段的启动文件负责初始化FPGA的比特流文件和初始化ARM处理器的FSBL文件(VIVADO生成),在PL和PS单元完成最基本的初始化操作后,就需要启动BootLoader来引导后面发linux内核,XIlixn的解决方案中可以将二进制比特流文件和fsbl以及uboot打包成BOOT.bin文件,BOOT.bin中的uboot可以加载内核到内存,并从0x00080000位置启动内核。另外,内核启动还需要设备树和根文件系统。

(1)交叉编译链和开发环境搭建

为了得到能够在嵌入式平台上运行的代码,需要在linux主机上交叉编译需要运行的代码,交叉编译工具链就是提供交叉编译的一套工具集。开发主机选择Ubuntu1604LTS系统,安装VIVADO17.4版本,安装完成后 VIVADO SDK

用时已经自动安装了交叉编译链arm-linux-gnueabihf- ,使用命令
source/opt/Xilinx/SDK/2017.4/setting64.sh

添加引用1后即可使用交叉编译链。Xilinx在较早的VIVADO SDK版本中提供了arm-xilinx-linux-gnueabi-编译链,区别在于arm-linux-gnueabihf-使用硬件加速浮点数运算,而arm-xilinx-linux-gnueabi-使用软件计算。通过查询资料,发现17.4版本的SDK中包含arm-xilinx-linux-gnueabi-编译链的引用,但是软件安装时没有成功安装,这应该是17.4版本的一个BUG,我们在另一台安装15.4版本VIVADO SDK的Ubuntu主机下,找到/opt/Xilinx/SDK/2015/gnu/arm文件夹,将其拷贝到17.4版本对应的目录,发现可以成功引用,输入(交叉编译链)gcc-v查看:


gcc版本为4.9.2。需要注意的是,使用两条编译链中的任意一条都可以用于交叉编译,但是两者之前不兼容,因此使用其中一条交叉编译链即可。17.4自带的gcc编译器版本更高,是6.2.1版本。


为了支持32 位工具,需要预先安装 32 位支持工具包。使用sudo命令获取root权限,apt-get install lib32z1 lib32ncurses5lib32bz2-1.0 lib32stdc++6安装上述工具包。(PS,可以修改Ubuntu镜像源为西电开源社区镜像,实测速度在5MB左右)。安装上述包后还需要安装Openssl库来实现网络保密性,在编译u-boot时会用到,使用命令apt-get install libssl-dev安装。

为了提高工作效率,嵌入式开发通常可以在Windows下使用SourceInsight等内核源码阅读工具来开发驱动和应用程序,而交叉编译环境则往往在linux主机上,因此我们可以使用ssh登陆linux服务器,完成命令控制和编译文件,使用ftp文件传输服务在Windows和linux主机之间传递文件,编译完成的驱动可以以NFS挂载的方式直接在嵌入式开发板运行。搭建工作环境不是本文的重点,因此不再这里详细说明。

(2)U-boot编译

Xilinx官方提供了u-boot的源码,位于https://github.com/Xilinx/u-boot-xlnx/releases,我们按照自己需要的版本进行下载和使用。

将下载好的u-boot-xlnx-xilinx-v2017.1.zip文件上传到Ubuntu服务器,使用命令unzip解压缩后进入u-boot-xlnx-xilinx-v2017.1目录,在 u-boot 的文件夹下有很多子文件夹构成,其中每个文件夹都实现一个对应的功能。

1) api:相关的api函数,如输出字符函数。

2) arch: 与特定的 CPU 构架相关。在该目录下,有u-boot 所支持的各种架构的cpu,并且有一个单独的子目录对应。典型的,arch 文件夹下名字为 arm 的子目录就是 Zynq-7000 SOC所对应使用的 CPU 构架目录。

3)board: 和一些已有开发板有关的文件。每一个开发板都有一个子目录出现在当前目录下

4)common: 实现u-boot 命令行下所支持的命令。在该目录下,每条命令对应一个独立的文件夹。

5)disk: 提供对磁盘的支持。

6)doc:文档说明

7)drivers: 在该目录下保存着 u-boot 所支持的设备驱劢程序。典型的如各种网卡、支持的CFI 癿 Flash 存储器、串口和 USB 等。

8)fs:支持的文件系统

9)include:该目录下保存着 u-boot 所使用的头文件,对各种硬件平台支持的汇编文件、系统的配置文件以及对文件系统支持的文件。该目录下configs 目录有开发板相关的配置头文件,如 zynq_common.h 是与 zynq 开发板相关的配置文件。

10)lib: 该目录下保存着体系结构相关的库文件。

11)net: 该目录下保存着网络协议相关的代码。比如BOOTP 协议、 TFTP 协议、RARP 协议和 NFS 文件系统的实现

12)tools: 该目录下保存着用于生成 u-boot 癿工具,包括 mkimage、 crc、 Makefile 和boards.cfg配置文件。


下面开始进行u-boot的编译,编译u-boot需要扁平化设备树的支持,首先输入命令apt-get installdevice-tree-compiler安装设备树编译工具。安装完成dtc工具后就可以进行u-boot的编译了。

在configs文件下保存有各个开发板的默认配置,我们搜索zynq有关的配置文件,发现zynq_zed_defconfig文件,这个就是Zedboard默认的配置选项。而ax70**系列的则是黑金开发板的默认配置文件。


在编译u-boot之前,需要先将配置选项写入.config配置文件中,输入命令make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zynq_zed_defconfig进行配置。注意,你需要先source /opt/Xilinx/SDK/2017/setting64.sh添加相关引用才能使用,当然也可以把上述命令写入/etc/profile这样就可以开机使用。


当出现written to .configs时,表明配置选项写入成功,接下来我们就可以进行编译u-boot了。


使用命令make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-编译u-boot,经过一段时间的编译后,在u-boot根目录下会出现u-boot文件,我们将其下载到Windows下并重命名为u-boot.elf,等待下一步使用。



(3)生成BOOT.bin启动文件

BOOT.bin需要VIVADO SDK生成fsbl,然后将u-boot和VIVADO生成的比特流打包。

启动VIVADO SDK,选择File ->New -> Application Project创建一个新的SDK工程,工程命名为fsbl,其余保持默认不变。



点击next,选择ZYNQ FSBL模板,点击Finish完成工程的创建,SDK会自动创建一个名为fsbl的工程和fsbl_bsp板级支持包。选中fsbl工程,右键选项选择Create Boot Image,在弹出的选项卡中可以发现SDK已经问我们添加了刚才生成的fsbl和有VIVADO导入SDK中的比特流文件,我们只需要再添加编译好的u-boot即可。


点击右侧的Add可以添加新的文件,Delete可以删除选中的文件,Edit可以编辑文件的类型。我们选择Add添加u-boot.elf文件。

在新的选项卡中填入uboot.elf的路径,这里一定要注意类型为Datafile类型,否则无法正常启动。


点击OK确认后退回到上次层选项卡,选择Create Image选项,在SDK目录下就会生成对应的BOOT.bin文件。


将BOOT.bin拷贝到Zedboard的SD卡,连接串口,开机观察串口提示,发现u-boot已经可以正常启动了,并且此时FPGA也已经按照VIVADO的网表文件初始化完成,但是u-boot提示无法读取内核镜像,我们将在下一步中生成。

(4)内核编译

Xilinx官方提供了linux的源码,供开发者下载和使用,我们打开Xilinx官网链接:https://github.com/Xilinx/linux-xlnx/releases;选择17.4版本下载并解压。

Linux解压命令为 :

tar zxvf linux-xlnx-xilinx-v2017.4.tar.gz

解压后进入该目录,这里对关键目录进行说明:

1)include/---- 内核头文件,需要提供给外部模块使用

2) kernel/---- Linux 内核癿核心代码,包扩进程调度子系统,以及进程调度相关的模块。


3)arch/---- 体系结构相关的代码,例如 arm, x86 等等,我们使用的ARM A9处理器就在arch/arm/目录下。

arch/mach包含了具体开发板有关的代码

arch/boot/dts 包含了设备树文件

arch/arm/configs目录下包含了arm架构处理器和开发板的一些内核默认配置文件,Zedboard的默认配置文件也在此目录下。

4)driver目录则存放了可用的驱动程序,你可以将自己的驱动放入此目录,在后面选择编译进内核。

5)scripts目录下包含了设备树编译器dtc和解释内核配置选项相关的文件和目录。

其余目录则不是本文介绍的重点,当开发平台启动BootLoader后,需要读取内核镜像,并依赖设备树文件传入的一些启动参数才能启动。当然还需要文件系统的支持。Linux内核有Imange、zImage和uImage等格式,Image就是正常编译出的linux内核,但是鉴于嵌入式资源有限,我们可以将内核和一段自解压程序进行压缩,这样启动时BootLoader先调用zImage的解压接口进行解压缩,而后在调用内核接口启动内核,相比于Image,zImage启动更慢一些。uImage就是在头部加入了一些u-boot相关代码的压缩Linux内核镜像,便于u-boot启动内核镜像。因此,我们最终要生成的就是uImage内核镜像。

上面说过在arch/arm中存放了我们需要的arm A9处理器的代码和文件,进入arch/arm/configs,搜索zynq相关的配置,发现xilinxz_zynq_defconfig配置文件,这就是Zedboard可用的默认配置文件。和u-boot类似,我们也需要先写入默认配置到.config文件才能编译内核。

使用命令make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xilixn_zynq_defconfig进行内核配置文件的写入。写入完成后提示written to .config。可用使用make menuconfig配置内核选项:


这里保持默认,无需修改,如果需要将自己的驱动编译进内核,可以在这里选中,但是这样不利于调试驱动。


使用命令make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 编译内核,内核编译需要较长的时间。如果配置过程中需要重新修改或者发生错误,可以使用make distclean命令使内核恢复最初的状态,然后重新编译。

我们使用make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- uImageLOADADDR=0x00008000重新生成内核,指定内核镜像为uImage,内核入口地址为0x00008000。由于内核已经编译过一次,这次可以很快生成。


生成的内核位于arch/arm/boot目录下。拷贝uImage到Zedboard的SD卡准备启动时使用。

(5)生成设备树文件

设备树是一种设备节点的描述,它告诉内核板卡上有哪些外设以及外设占用的资源,比如寄存器映射空间和中断号等信息。另外设备树还可以修改内核启动参数,如串口选择、波特率设置和根文件系统的选择。

通过VIVADO SDK可以生成设备树描述文件,这样便于我们开发,而不需要完全手动创建。VIVIADO安装时并没有安装设备树生成器,所以需要我们手动安装。我们首先下载xilinx提供的device tree generator,并安装到SDK。访问https://github.com/Xilinx/device-tree-xlnx/releases获取对应版本的设备树生成器。下载并放到VIVADO安装目录下的SDK\2017.4\data\embeddedsw\devicetree\bsp\目录下,重命名为device-tree-xlnx_v2017_4(我的VIVADO版本为17.4)。打开SDK,在SDK中操作点击菜单: Xilinx Tools -> Repositories,然后在LocalRepositories中添加我们刚才下载的SDK\2017.4\data\embeddedsw\devicetree\bsp\device-tree-xlnx_v2017_4路径并点击OK。


添加成功后如上图。点击菜单File -> New -> Board Support Package。弹出选项卡New Board Support Packet Project,选择device_tree,如果你上一步配置不成功,则不会出现device_tree选项,此时需要检查上一个步骤的问题。添加成功后点击Finish选项,VIVADO SDK会自动生成设备树描述文件dts。

稍后,VIVADO弹出BoardSupport Packet Setting选项卡,在bootargs中填入console=ttyPS0,115200 root=/dev/ram rw initrd=0x800000,8Mearlyprintk rootfstype=ext4 rootwait devtmpfs.mount=0。“bootargs”参数用于指定启动时传递给内核的参数。“console device”参数用于指定所使用的串口输出设备。


在SDK目录下的device_tree目录下可以看到很多dts文件,system-top.dts就是我们需要编译的设备树描述文件。它引用了zynq-7000.dtsi等对于zynq芯片通用的部分文件。将SDK目录下的整个device_tree目录上传到Ubuntu服务器,使用dtc编译器编译。编译命令如下:

./scripts/dtc/dtc-I dts -O dtb -o device.dtb ./device_tree/system-top.dts


6)文件系统

根文件系统使用uramdisk.image.gz根文件系统,ramdisk.image.gz根文件系统其格式与uboot不同,启动时uboot会提示ramdisk格式错误,若要让uboot能够识别ramdisk.image.gz根文件系统,需要利用mkimage给ramdisk.image.gz添加一些头部信息,生成uramdisk.image.gz。可以直接使用网络上的uramdisk.image.gz来作为根文件系统,一般来讲,根文件系统不需要做出修改。

另一种广泛应用的根文件系统是LINARO_FS,Linaro文件系统也可从网络上获取,因为我们的设备树中指定了从uramdisk.image.gz文件系统启动,因此这里不再介绍从Linaro文件系统启动。

现在,我们已经得到BOOT.bin文件,设备树device_tree.dtb文件和根文件系统uramdisk.image.gz根文件系统。将这三个文件放入Zedboard的SD卡,上电启动就可以使用Linux操作系统了。


7)驱动程序和应用程序测试

Linux驱动程序有静态编译进内核和动态模块加载两种,这里选择动态模块加载的方式,便于进行调试。在前面的硬件设计中,我们将AXI-Lite Slave的四个寄存器挂载到基地址为0x43c00000的位置,而Zedboard板卡上的8位LED灯连接到了寄存器0的低8位,因此我们写寄存器0的低八位就能很容易的通过LED的状态来判断写入是否成功。

驱动程序的入口和出口分别是init和exit,需要使用宏进行修饰如下:

// 注册初始化Linux驱动的函数
module_init( leds_drv_init);
// 注册卸载Linux驱动的函数
module_exit( leds_drv_exit);

linux操作系统中无法直接读写物理地址,因此入口函数中,我们需要映射物理地址,使用ioremup函数映射物理地址。注意这里物理地址和硬件设计中保持一致。

leds= ioremap(0x43c00000, sizeof(LEDS_T))

Led灯只是一个简单的字符设备,但是这里我们使用该设备来注册设备驱动。

ret= misc_register(&misc);

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的include/linux目录下有Miscdevice.h文件,要把自己定义的misc device从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。LDD3中led设备也是用misc_register函数注册为杂设备,这说明led设备是作为杂项设备出现在内核中的,在内核中,misc杂项设备驱动接口是对一些字符设备的简单封装,他们共享一个主设备号,有不同的次设备号,共享一个open调用,其他的操作函数在打开后运用linux驱动程序的方法重载进行装载。

驱动代码:

#define DEVICE_NAME "leds"
#define LEDS_BASE_ADDR (0x43c00000)
typedef struct{
volatile unsigned int ADDR0;
volatile unsigned int ADDR1;
volatile unsigned int ADDR2;
volatile unsigned int ADDR3;
}LEDS_ADDR;

LEDS_ADDR* leds;

static int leds_drv_open(struct inode *Inode, struct file *File)
{
leds->ADDR0 = 0xffffffff;
leds->ADDR1 = 0xffffffff;
leds->ADDR2 = 0xffffffff;
leds->ADDR3 = 0xffffffff;
return 0;
}

static ssize_t leds_drv_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
return 0;
}
static ssize_t leds_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
unsigned int ret = 0;
unsigned int tmp_val;
u32 pos = *offset;
ret = copy_from_user(&tmp_val, buf, count);
leds->ADDR0 = tmp_val;
//默认写入寄存器0
return ret;
}
// 描述与设备文件触发的事件对应的回调函数指针
static struct file_operations dev_fops =
{
.owner = THIS_MODULE,
.open = leds_drv_open,
.read = leds_drv_read,
.write = leds_drv_write,
};
// 描述设备文件的信息
static struct miscdevice misc =
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops
};
// 初始化Linux驱动
static int __init leds_drv_init(void)
{
int ret;
leds = ioremap(LEDS_BASE_ADDR, sizeof(LEDS_T));
// 建立设备文件
ret = misc_register(&misc);
// 输出日志信息
if(ret)
{
printk("leds_drv_init faiitrt!\n");
}
else
{
printk("leds_drv_init success!\n");
}
return ret;
}

// 卸载Linux驱动
static void __exit leds_drv_exit(void)
{
iounmap(leds);
// 删除设备文件
misc_deregister(&misc);
// 输出日志信息
printk("leds_drv_exit success!\n");
}
// 注册初始化Linux驱动的函数
module_init( leds_drv_init);
// 注册卸载Linux驱动的函数
module_exit( leds_drv_exit);

MODULE_LICENSE("Dual BSD/GPL");

应用程序调用驱动程序接口,从控制台读取一个数字,写入到寄存器0,寄存器0的低八位就可以在led灯上显示出来。

应用程序源码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char** argv)
{
int fd;
fd = open("/dev/leds", O_RDWR);
if(fd < 0)
{
printf("fd = %d open fialed!\n", fd);
}

//unsigned int leds = 0;
int leds = 0;
char ch[4];

printf("input your strings:\n");
int i = 0;
while( 1 )
{
ch[i] = getchar();
if (ch[i] == '\n')
break;
else
i++;
}
if (i == 3)
{
leds = (ch[0]-48)*100 + (ch[1]-48)*10 + ch[2]-48;
}
if (i == 2)
{
leds = (ch[0]-48)*10 + ch[1]-48;
}
if (i == 1)
{
leds = ch[0]-48;
}

printf("leds = %d,i = %d\n",leds,i);

write(fd, &leds, 8);

printf("end\n");
close(fd);
return 0;
}

将驱动和应用程序上传到Ubuntu,使用arm-linux-gnueabihf-交叉编译链编译。驱动程序需要编写makefile,Makefile中需要指定一个已编译的内核源码树。编译完成后再Zedboard板测试如下:

1> 挂载Ubuntu的NFS服务,(需要Ubuntu开启NFS服务并指定路径)首先查询Ubuntu IP地址为192.168.1.103,在Zedboard控制台输入:

mount-t nfs -o nolock 192.168.1.103:/home/zed/work /mnt

将Ubuntu上的work目录挂载到Zedboard板


挂载成功后使用ssh登陆Ubuntu并转到Ubuntu的驱动目录,执行make,得到驱动.ko文件。



Make命令执行成功,目录下生成最终使用的ko文件部分中间文件。使用 arm-linux-gnueabihf-gcc -o ledstest.o ledstest.c 生成测试程序。


回到串口工具连接Zedboard板卡,使用insmod挂载驱动,并执行测试程序。


挂载成功后,运行APP成功,APP从控制台读入一个字符串65,将其ASCII码转换为数值写入寄存器0,Zedboard板卡上led如图:


2、Petalinux快速开发

Petalinux是xilinx为zynq7000系列移植linux程序设计的一套开发程序。它可以加快嵌入式开发人员的工作速度。

安装petalinux首先需要安装需要的库,本次实验在Ubuntu1604LTS上安装petalinux17.4.注意,petalinux版本和VIVADO版本需要对应。


Petalinux无法以root用户安装,因此如果想安装petalinux到非用户目录下有两种办法:将opt目录下的文件夹所有属性改为当前用户或者安装到当前用户目录下再以root权限复制整个文件夹到opt目录。这里选用第一种方法,首先创建安装目录:


运行petalinux的安装文件,并指定目录为/opt/pkg/petalinux,检查依赖环境无误后就会进行安装,安装前需要根据提示同意用户协议。


安装完成后就可以使用petalinux定制linux操作系统。Petalinux可以识别VIVADO工程,并根据VIVADO工程来智能设置外设信息。VIVADO工程目录下的.sdk目录就包含了petalinux所需的硬件信息。Ubuntu新建文件夹作为petalinux的工程目录,并将.sdk拷贝到petalinux工程目录的上一级目录下,即和petalinux工程目录为同级目录。定位petalinux

source/opt/pkg/petalinux/settings.sh

检查安装环境和库依赖,无问题后可以建立工程。使用命令:

petalinux-create--type project --template zynq –name leds

建立名为leds的工程,cd到leds目录下,读取硬件信息:

petalinux-config--get-hw-description ./led.sdk

在弹出的窗口中可以配置petalinux工程,默认源码从GitHub下载,也可以从本地读取,这里保持默认。在 Advanced bootable images storage Settings选项中配置启动方式,默认从SD卡启动,这里依旧保持默认。


将配置保存,退出,petalinux会下载源码并配置工程。

成功后使用如下命令配置linux内核:

petalinux-config-c kernel

配置成功后保存并退出


编译完成后使用petalinux-config-c rootfs配置根文件系统,同样保存后退出。使用命令petalinux-build编译整个系统工程。经过一段漫长的等待,编译完成。

运行下面命令生成BOOT文件:



将工程目录 images -> linux目录中的BOOT.bin和image.ub复制到SD卡,启动Zedboard,完成Linux操作系统的移植。


注意使用petalinux移植linux使用的是arm-linux-gnueabihf-交叉编译链,因此后续驱动开发时也需要指定交叉编译链为arm-linux-gnueabihf-,后续驱动的开发与传统方式移植linux部分一致,因此不再阐述。

本文转自: 网络交换FPGA,转载此文目的在于传递更多信息,版权归原作者所有。
*本文由网络交换FPGA授权转发,如需转载请联系作者本人

推荐阅读