本文是昨天发的文章《龙芯杯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安装目录下的SDK2017.4dataembeddedswdevicetreebsp目录下,重命名为device-tree-xlnx_v2017_4(我的VIVADO版本为17.4)。打开SDK,在SDK中操作点击菜单: Xilinx Tools -> Repositories,然后在LocalRepositories中添加我们刚才下载的SDK2017.4dataembeddedswdevicetreebspdevice-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!"); } else { printk("leds_drv_init success!"); } return ret;}// 卸载Linux驱动static void __exit leds_drv_exit(void){ iounmap(leds); // 删除设备文件 misc_deregister(&misc); // 输出日志信息 printk("leds_drv_exit success!");} // 注册初始化Linux驱动的函数module_init( leds_drv_init);// 注册卸载Linux驱动的函数module_exit( leds_drv_exit);MODULE_LICENSE("Dual BSD/GPL");
应用程序调用驱动程序接口,从控制台读取一个数字,写入到寄存器0,寄存器0的低八位就可以在led灯上显示出来。
应用程序源码:
#include #include #include #include #include #include int main(int argc, char** argv){ int fd; fd = open("/dev/leds