一、关于本demo
1.本demo中,zynq运行linux系统,包含一个自定义的PL端IP外设。
2.开发板从sd卡启动。
3.主要参考文献为xilinx ug1165 zynq embedded design tutorial,
相关参考文献为ug1144 petalinux tools reference guide 以及ug1157 petalinux tools command line guide
二、demo详解
1.应用目标与工作内容
本demo实现的功能是:PL端封装一自定义IP (即RTL模块,封装成IP核)。该IP为一8bit计数器,内含使能寄存器,接收linux 应用程序的使能控制信号。计数器的最高位连接到PL的led上,计数器被linux程序使能后,led开始闪烁。同时,将6bit引出,封装为一个中断信号,连接到arm cpu,cpu接收到该PL端中断后,在驱动程序层面以及应用程序层面,打印中断信息。这样,就实现了linux应用程序使能PL自定义IP,并接收PL IP中断的交互。
因此,本demo的目标是打通PL IP与驱动、驱动与应用程序之间的交互机制。
本demo的主要内容有一下4个部分:
1)带PL IP核的硬件设计
2)带PL设备树的linux系统镜像制作
3)PL设备的驱动开发与调试
4)Linux 应用程序开发与调试
2.整体开发流程
与开发工作内容对应,开发流程主要有一下四个阶段。
1.硬件设计
此阶段与裸板应用的硬件设计类似,在vivado内设计、封装自定义IP核,在block design内梨花IP核与zynq cpu等。完成逻辑设计。生成bit流文件以及输出hdf硬件交付文件,进入软件开发阶段。
2.修改ug1165的驱动文件blink.c,假如中断处理功能(打印信息)。
3.linux系统镜像制作
利用1阶段生成的hdf文件,以及2阶段的blink.c,进入ubuntu系统下的petalinux开发环境。参考本博客“zynq自定义PL IP核linux驱动开发demo”,制作出与逻辑硬件设计匹配的待驱动程序的linux系统镜像。
4.修改ug1165的应用程序c文件linux_blink_application.c,打印装订信息。
3.硬件设计
硬件设计的blockdesign例化自定义的计数器IP核以及zynq CPU即可。其中,自定义IP 引出计数器的最高位到板子的led管脚,同时封装为中断信号,一秒钟向CPU发送一次中断,连接到CPU的中断输入。其余部分令BD自动连接即可。自定义IP封装的过程,可参看本博客“zynq 自定义逻辑IP核的封装”一文。
zynqCPU配置为zc702的preset,并在interrupts一栏中,勾选PL-PS interrupt Ports里的IRQ_F2P,使能PL到PS的中断。
preset 已将led分配到管脚,完成BD后编译生成bit文件并export hardware。
4.驱动程序修改与解读
4.1驱动程序整体功能分析
驱动程序介于硬件与应用程序之间,运行在内核空间,直接操作硬件设备,使得应用程序可以脱离具体的硬件。应用程序运行在用户空间。从应用程序的需求反推,可以知道驱动程序需要实现下图中的这些功能。
如图所示,应用程序的需求是1.写设备寄存器(PL IP 计数使能寄存器)2.接收设备中断。用户空间的应用程序无法直接操作内核空间的内存,必须借由system call(系统调用)函数来实现对内核空间设备文件的操作。常用的system call有read ,write,ioctl等。
system call可以在用户空间直接调用,在内核驱动里,有相应的驱动函数来实现system call的操作。这些驱动函数需要开发者来设计。
本demo中,需要设计device_read函数来读取驱动程序内的中断标志,需要设计device_ioctl来操作IO设备(即PL IP),写PL IP的使能寄存器。
为此,需要做以下操作:
1.申请中断号并向系统注册中断。
2.申请IO端口,分配IO内存,并将内存映射到虚拟地址上去。
这两步操作主要由blink_driver结构体的probe成员完成,该成员是blink_probe()函数。
在执行probe函数之前,需匹配驱动与设备的name,id_table等字段,确保驱动与设备时匹配的。该工作主要由blink_driver.driver->bus->match成员完成。
再反推,要匹配设备与驱动,必先注册设备与驱动。该操作在驱动的入口函数即完成。设备注册函数的file_operation成员内注明了支持的操作种类,可按需裁剪。
4.2驱动程序解读
驱动程序与应用程序不同,没有主函数。加载驱动模块时,会执行入口函数module_init();卸载驱动模块时,会执行module_exit();函数。入口函数接出口函数会调用一系列函数,完成4.1节图的大部分功能。另外的一些函数,如device_read()等函数,则是设计好放在驱动程序里,等待应用程序中的system call函数的调用。下文从入口函数开始,按照调用关系,详细解读驱动代码blink.c该代码在ug1165文末中有下载链接。
原始驱动程序见参考文献,下面详解有所改动。代码详解:
module_init(blink_init);以及module_exit(blink_exit);为驱动函数的入口与出口函数。入口函数调用了blink_init()函数。出口函数调用blink_exit函数。
blink_init()函数首先打印一些问候信息,表明驱动进入了初始化函数。其中的printk函数为printf函数的内核空间版本,用法与printf类似。该函数可注明打印信息的紧急等级(分八级,0-7,数字越小越紧急),等级低于某个等级时,信息不会打印在串口上,但是在开发板系统的var/log/dmsg核var/log/messegges路径下看到。
chrdev_register()第一个参数表示申请的住设备号,该值为0时,系统动态分配住设备号。推荐使用动态分配,可避免设备号冲突。第二个参数为设备名称,该名称为开发板系统/dev设备节点名称。第三个参数为file_operation结构体,该结构体内的成员即为本设备支持的操作。本demo的Fops结构体代码如下:
struct file_operations Fops =
{
.owner = THIS_MODULE,
.read = device_read,
.write = device_write,
.unlocked_ioctl - device_ioctl,
.open -device_open,
.release =device_release
};
可见,本demo注册的设备声明了read,write,ioctl,open,release等几个常用的成员,可以支持语音程序内对应的system call。
mmio -ioremap(BLINK_CTRL_REG,0x100);
将自定义PL IP的外设基址(总线地址)映射到内存空间内。映射地址赋值给mmio,后面对mmio的操作即可被映射到实际的总线地址上,从而改变IP核被寄存器的值。ioremap函数的第一个参数为PL IP核设备的实际基址,该基址与BlockDesign中的基址是一致的。第二个参数为映射地址的长度,其余的程序里可设置为0x100,小于硬件设计的IP核的地址空间64K。
rc=platform_driver)register(&blink_driver)注册驱动、platform+driver_register函数会调用一系列函数,完成设备与驱动的match,进一步调用blink_probe函数,完成IO与中断的初始化操作。参数blink_driver为结构体,代码如下: