本帖最后由 LINUX课程 于 2024-9-11 10:03 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 驱动概述 本实验是开始 Linux 驱动的第一个入门实验,将会进行编写一个驱动中最基础的字符驱动。字符驱动是用来驱动字符设备的,而在Linux 中除去与磁盘相关的块设备,与网络接口有关的网络设备,其他的设备都是字符设备。 实验目的: - 理解字符驱动的概念。
- 掌握驱动程序的模板。
- 理解驱动的设备号的概念。
- 掌握驱动程序在 Linux 下的安装与运行。
- 掌握设备节点与设备的创建。
2 驱动调用框图 本实验基于米联客的开发板构建,但是在所有运行 Linux 的系统中都是可以运行的,为了避免不必要的问题,建议使用相同设备。在编写 Linux 的驱动程序之前,我们首先来了解下,驱动程序在 Linux 系统中所处的位置。 在 Linux 中一切皆文件,驱动程序不无例外,也是一个文件。在驱动加载完成之后,可以在 /dev 中查找到该设备文件。 Linux 中将系统分为两个部分,一个部分是用户空间,一个部分是内核空间。用户能够操作的应用程序在用户空间,而驱动程序则在内核空间。 当用户想要通过应用程序操作控制一盏 led 灯亮起或者熄灭的时候,操作的是应用程序中的函数。然后该函数会调用库函数,譬如常见的标准库。库函数又会调用系统,将指令陷入内核中。进入内核之后,便会找到对应的驱动程序接口,进而执行驱动程序中的内容,从而控制 led 灯。 由此,我们可以明白,驱动程序在系统中是作为一个连接硬件和Linux 内核的文件而存在的。而我们对这个文件进行控制,那么就能够对设备进行控制。 3 设备驱动介绍 Linux 的驱动分为三个基础的大类,字符设备驱动,块设备驱动,网络设备驱动。 字符设备驱动提供的就是一种数据流。比如当你在键盘上敲入 “why not?” 的时候,键盘驱动程序会按照与输入相同的顺序返回由这几个字母组成的数据流。字符设备是要按照顺序的,不能随机读取设备中的某一数据。常见的字符设备有鼠标、键盘、串口、控制台、和 LED 等。 块设备驱动则是可能会要求读取磁盘上任意块的内容,然后又转去读别的块的内容,而被读的块在磁盘上的位置不一定要连续,所以说硬盘是可以被随机访问的,而不是以流的方式被访问。所以,显然硬盘是一个块设备。常见的块设备有磁盘、U 盘、硬盘、SD卡等。 网络设备驱动是特殊设备驱动,它负责接收和发送帧数据,可能是物理帧,也可能是 ip 数据包,这些特性都由网络驱动决定。常见的网络设备有网卡、wifi、蓝牙等。 而接下来实验中,将会编写的是字符驱动设备。 3.1 设备号 那么多的设备,那么多的驱动,Linux 系统如何为对应的设备找到对应的程序呢?为了方便管理,Linux 为每个设备都设置了一个设备号,设备号由主设备号,和次设备号组成。 主设备号用来标识与设备文件相连的驱动程序,用来反应设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的的设备。 举个例子,例如一个嵌入式系统,有两个 LED 灯,LED 灯需要独立的打开或者关闭。那么,可以写一个 LED 灯的字符设备驱动程序,可以将其主设备号注册成 5 号设备,次设备号分别为 1 和 2。在这里,次设备号就分别表示两个 LED 灯。 而在本实验中,将会只用主设备号来申请,也就是说,一个主设备号,就是一个设备。这样方便理解。后续,实验中会逐渐添加上次设备号的申请。 3.2 设备节点与设备 光申请一个设备号并不会创建设备,所以为了让设备被找到,我们将在驱动代码里统一使用class_create函数和device_create函数来自动完成设备节点与设备的创建。 这些内容因为属于初始化的工作,所以将写入__init函数的实现中,同样在__exit函数的实现中同样需要完成这些内容对应的释放工作。 申请与注销的流程如下: 3.3 字符设备驱动 我们简单的看下在 Linux 内核中,是如何描述一个字符设备的。 这里的 cdev 结构体就是一个字符设备。该结构体中的 dev_t 成员定义了设备号,深入进去可以看到是一个 u32 类型的数据。其中 12 位为主设备号,20 位为次设备号。 我们再来看下,当我们运行安装和卸载驱动的指令的时候,内核内部的运行过程。 4 环境搭建 4.1 vivado工程和vitis工程的搭建 在vivado中设置好IP之后,导出“.xsa”文件,然后再用vitis生成制作系统所需的文件。 1:打开vivado工程 将附件中“soc_prj”文件夹复制至桌面,然后再通过vivado打开。 2:打开“system.bd” 双击“system.bd”文件。 3:双击右侧的“ZYNQ UltraSCALE+” 进入“Re-customize IP”,查看相关IP已经完成。 4:导出xsa文件 打开的项目,先点击菜单栏的“generate bitstream”,编译文件。完成后,点击“Cancel”。 点击“File”找到“Export”,选择“Export Hardware”。 在跳出的选项中,点击“Next”。 在“Output”部分选择“Include bitstream”,点击“Next”。 在“Files”部分将Export to的文件夹,“soc_prj”改为“soc_hw”,然后点击“Next”。 点击“Finish”。 在“soc_hw”可以查看到生成的文件。 5:从vitis中导出文件。 鼠标双击,打开vitis软件。 选择工作路径,一般输入“soc_sdk”。 在打开的页面的菜单栏选择“Xilinx”下的“Software Repositories”,点击打开。 在打开的页面中为这个工程添加设备树模板。在“Local Repositories”右边点击“New”。 设备树模板“device-tree-xlnx-xilinx-v2020.2.zip”在附件中,解压后使用。 点击右下角“Apply and Close”。 添加好之后,开始新建工程,依次展开“File-New-Platform Project”: 在弹出的页面上输入项目名“soc_base”,然后点击“Next”。 在弹出的Platform页面点击右边的“Browse”,找到之前储存在soc_hw中的xsa文件。 再将下方的“Operating System”改为“device_tree”,然后点击“Finish”,等待文件创建成功。 点击菜单栏上的小锤子,开始编译文件。 编译完成之后,会生成如下的文件。 system_wrapper.bit: {工程位置}\soc_sdk\soc_base\hw\ fsbl.elf: {工程位置}\soc_sdk\soc_base\export\soc_base\sw\soc_base\boot\ pmufw.elf: {工程位置}\soc_sdk\soc_base\export\soc_base\sw\soc_base\boot\ 这三个文件分别是PS和PL端的配置文件、FSBL启动文件和pmu配置文件。 除了这三个文件,还需要一个设备树文件,这里在附件的“soc_dts”文件中会提供dts文件(zynqmp-mzux.dts) 。具体的dts文件,是参考下图中vitis中的设备树文件来进行编写。 至此,编译内核所需的文件全部已经准备完毕。 “system_wrapper.bit”、“fsbl.elf”和“zynq_mz7x.dts”。 4.2 开发环境的部署 在虚拟机安装完成的情况下,接下来进行开发环境的部署。 1:下载开发环境的压缩包 2:打开已经安装好的虚拟机。 米联客的虚拟机的配置与安装请参考基础部分,点击 uisrc,输入密码 root,进入系统。 3:将文件从 windows 桌面转移至虚拟机 这里可以通过 U 盘,也可以通过powershell 的 scp 连接虚拟机进行上传,或者也可以使用vmware tools。这里选择使用 U 盘。 向主机插入 U 盘。虚拟机会跳出如下提示,先选择“连接到主机”。 因为我们要把 window 桌面的文件拷贝到 U 盘中。 把 “uisrc-lab-xlnx20220501.tar.gz”文件拷贝进 U 盘。如下所示。 等待传输完成,再把 U 盘拔出,再次插入,这次选择连接虚拟机。 双击打开在桌面显示的 “16GB Volume”。有时候会和 boot 重叠住,用鼠标按住拖动移开。 将该文件拖动至左边栏的“Home”中,等复制完毕,再点击左下 “16GB Volume”旁的“弹出”按钮,安全的移除 U 盘,从而确保文件已经同步到虚拟机。 4:解压部署安装包 点击左边栏的“Home”,在页面空白处右击,选择“Open in Terminal”。 在打开的终端中输入“tar -xvf uisrc-lab-xlnx20220501.tar.gz”,将文件解压至 home 文件下。 请确认部署安装包的完整,和解压缩的成功。成功解压后的文件夹内的内容如下。 至此,部署部分需要安装的文件就已经完成。接下来将进行系统的制作。 5:制作系统 从vivado 和vitis中导出所需文件的部分,具体可以参考前一节。必须要制作 kernel 之后,才能够完成接下来的交叉编译。如果有已经配置好的开发环境可以跳过这部分。 将准备好的文件拷贝进 “{解压目录}/uisrc-lab-xlnx/boards/mzux/ubuntu/output/file/”,将设备树文件分别移至对应的文件中。 在解压缩后的文件夹内右击打开终端,本实验是在“/home/uisrc-lab-xlnx”中打开的终端,如下图。 在终端,首先输入“source scripts/mz7xcfg.sh”,配置环境变量。 输入“move_file.sh”将文件转移至对应的文档。 先输入“make_uboot.sh”制作uboot程序,作为引导程序。 然后输入“make_kernel.sh”,用来制作Linux内核。 再输入“create_image.sh”,用来制作镜像。 插入读卡器,虚拟机识别到之后,在终端输入“make_parted.sh”,格式化SD卡。 等格式化完成,最后在终端输入“deploy_image.sh”,完成SD卡系统的拷贝。 拷贝需要一点时间,等待拷贝完成,再拔出读卡器。 7100FC 参考“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”,从 qspi 启动 sd 卡中的系统。 4.3 将KernelPrint文件夹上传至虚拟机 按照上一节“开发环境的部署”中移动部署压缩包的方式,把“1. Helloworld”中的“KernelPrint”文件夹上传至虚拟机桌面。 5 程序分析 5.1 驱动程序分析 对代码进行分析,有助于我们理解并掌握驱动的编写。我们将从“1.HelloWorld”中的文件“KernelPrint”开始程序的分析KernelPrint.c - //添加头文件
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/ide.h>
- static char readbuf[100]; // 读缓冲区
- static char writebuf[100]; // 写缓冲区
- static char message[] = {"This message comes from kernel."};
- static int drive_major; //设备号
- static struct class *KernelPrint_cls;
- //1.5.5 驱动程序的打开,读取,写入,关闭。
- static int KernelPrint_open(struct inode *inode, struct file *filp) //打开函数
- {
- //本DEMO无需申请资源,此处留白
- printk("-KernelPrint open-\n");
- return 0;
- }
- static ssize_t KernelPrint_read(struct file *filp, char __user *buf, size_t count, loff_t *fops) //用户读取,内核发送信息
- {
- int flag = 0;
- memcpy(readbuf, message, sizeof(message)); //使用memcpy将内核中要发送的内容写入读缓冲区
- flag = copy_to_user(buf, readbuf, count); //使用copy_to_user函数将读缓冲区的内容发送到用户态
- if (flag == 0) //返回0成功,否则失败
- {
- printk("Kernel send data success!\n");
- }
- else
- {
- printk("Kernel send data failed!\n");
- }
- printk("-KernelPrint read-\n");
- return 0;
- }
- static ssize_t KernelPrint_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops) //用户发送,内核读取信息并打印
- {
- int flag = 0;
- flag = copy_from_user(writebuf, buf, count); //使用copy_from_user读取用户态发送过来的数据
- if (flag == 0)
- {
- printk(KERN_CRIT "Kernel receive data: %s\n", writebuf);
- }
- else
- {
- printk("Kernel receive data failed!\n");
- }
- printk("-KernelPrint write-\n");
- return 0;
- }
- static int KernelPrint_release(struct inode *inode, struct file *filp) //释放设备
- {
- //由于open函数并没有占用什么资源,因此无需释放
- printk("-KernelPrint release-\n");
- return 0;
- }
- //1.5.1 驱动文件描述集合
- static struct file_operations drive_fops = {
- .owner = THIS_MODULE,
- .open = KernelPrint_open,
- .read = KernelPrint_read,
- .write = KernelPrint_write,
- .release = KernelPrint_release,
- };
- //1.5.2 装载入口函数
- static __init int KernelPrint_init(void)
- {
- printk("-------^v^-------\n");
- printk("-KernelPrint init-\n");
- //1.5.3 设备的申请
- //申请主设备号
- //参数1----需要的主设备号,>0静态分配, ==0自动分配
- //参数2----设备的描述 信息,体现在cat /proc/devices, 一般自定义
- //参数3----文件描述集合
- //返回值,小于0报错
- drive_major = register_chrdev(0, "KernelPrint", &drive_fops);
- if (drive_major < 0) //判断是否申请成功
- {
- printk("register chrdev faile!\n");
- return drive_major;
- }
- else
- {
- printk("register chrdev ok!\n");
- }
- //1.5.4
- //自动创建设备节点
- //创建设备的类别
- //参数1----设备的拥有者,当前模块,直接填THIS_MODULE
- //参数2----设备类别的名字,自定义
- //返回值:类别结构体指针,其实就是分配了一个结构体空间
- KernelPrint_cls = class_create(THIS_MODULE, "KernelPrint_class");
- printk("class create ok!\n");
- //创建设备
- //参数1----设备对应的类别
- //参数2----当前设备的父类,直接填NULL
- //参数3----设备节点关联的设备号
- //参数4----私有数据直接填NULL
- //参数5----设备节点的名字
- device_create(KernelPrint_cls, NULL, MKDEV(drive_major, 0), NULL, "KernelPrint_%d", 0);
- printk("device create ok!\n");
- return 0;
- }
- //1.5.2 卸载入口函数
- static __exit void KernelPrint_exit(void)
- {
- //1.5.3 设备的注销
- device_destroy(KernelPrint_cls, MKDEV(drive_major, 0)); //删除设备
- class_destroy(KernelPrint_cls); //删除类
- unregister_chrdev(drive_major, "KernelPrint"); //注销主设备号
- printk("-------^v^-------\n");
- printk("-KernelPrint exit-\n");
- }
- //申明装载入口函数和卸载入口函数
- module_init(KernelPrint_init);
- module_exit(KernelPrint_exit);
- //添加各类信息
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("msxbo");
复制代码接下来对程序进行分析。 1:驱动的文件描述集合 - static struct file_operations drive_fops = {
- .owner = THIS_MODULE,
- .open = XXX_open,
- .read = XXX_read,
- .write = XXX_write,
- .release = XXX_release,
- };
复制代码含义:是文件描述集合,也叫驱动程序接口,之所以这样称呼是因为这个结构体规定了驱动可以进行所有操作。 具体分析: .open 打开文件就相当于打开设备,并初始化。.write 写文件就相当于写入设备。.read 读文件相当于读设备,以此类推。 当内核调用这些点函数的时候,会调用自定义的 xxx_read、xxx_open、xxx_write 等函数。而内部自定义的函数会根据硬件进行不同的处理。 2:驱动的装载与卸载函数 - //装载入口函数
- static __init int XXX_init(void)
- {
- //申请资源、初始化
- return 0;
- }
- //卸载入口函数
- static __exit void XXX_exit(void)
- {
- //释放资源
- }
- //申明装载入口函数和卸载入口函数
- module_init(XXX_init);
- module_exit(XXX_exit);
复制代码含义:系统通过装载入口与卸载入口时将会调用的入口函数与卸载函数。 具体分析: 在初学 C 语言的时候,接触的第一个函数就是 main 函数,所以 main 是默认的一个程序的起点。那驱动程序有没有这样一个起点呢,那就是init函数,与c不同的是,驱动采用的是面向对象的方式,所以函数的名字并不是很重要,我们通常采用以下这个模板: 其中函数名“XXX_init”和“XXX_exit”都是可以自己决定的,但需要在15行和16行将函数名作为参数进行传递。 Linux下装载驱动时,会自动调用module_init内函数名的函数,而卸载时则会自动调用module_exit内函数名的函数。类似于构造函数与析构函数,所以一般初始化和申请资源都会写在_init下,而_exit下则是用作释放申请的资源。 3:设备号的申请与注销函数 - register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
- unregister_chrdev(unsigned int major, const char *name)
复制代码含义:向系统申请设备号,与注销设备号的函数。 具体分析: 它们的参数含义如下: - major:主设备号,设备的编号
- name:主设备号所代表的这个类型的设备名字。注意与设备名区分。
- fops:驱动程序接口,如3.1中所示。
4:创建设备节点与设备的函数 - struct class * __class_create(struct module *owner, const char *name, struct lock_class_key *key)
复制代码含义:向系统申请创建设备节点,与创建设备。 具体分析: - owner:设备的拥有者,当前模块,直接填 THIS_MODULE。
- name:设备节点的名字。
- struct device * device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
复制代码含义:向系统申请创建设备。 具体分析: - class:设备对应的设备节点。
- parent:当前设备的父类,直接填 NULL。
- devt:设备节点关联的设备号,设备号由主设备号与次设备号组成。
- drvdate:私有数据,直接填 NULL。
- fmt:设备的名字。
5:定义设备号 - MKDEV(int major, int minor)
复制代码含义:根据给的主设备号,与次设备号,生成 cdev 所需的 dev_t 设备号。 具体分析: 6:驱动程序的打开,读取,写入,与关闭 - static int XXX_open(struct inode *inode, struct file *filp) //打开函数
- static ssize_t XXX_read(struct file *filp, char __user *buf, size_t count, loff_t *fops) //读取设备
- static ssize_t XXX_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops) //用户发送,内核读取信息并打印
- static int XXX_release(struct inode *inode, struct file *filp) //释放设备
复制代码含义:实现驱动接口中定义的函数的逻辑。 具体分析: 当驱动程序需要申请一些资源时就可以在open函数中进行,而释放资源则在 release 函数中,遵循先申请后注销的流程。 驱动程序的读取为read函数,对应的是应用程序的读操作(非写操作)。 驱动程序的输入为 write 函数,对应的是应用程序的输入,也就是驱动程序的读取,写内部自定义代码的时候,要明白是从用户那读取数据。 7:添加 LICENSE - MODULE_LICENSE() //添加LICENSE信息
- MODULE_AUTHOR() //添加作者信息
复制代码含义:为驱动添加许可证,不添加可能会无法运行。 具体分析: Linux属于开源软件,所以其驱动也必须带有LICENSE才行,否则可能无法正常编译。添加这类信息可以使用如下命令: LICENSE是必选项,作者是可选项。 5.2 应用程序分析 KernelPrintApp.c - #include <stdio.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdlib.h>
- #include <string.h>
- int main(int argc, char *argv[])
- {
- int fd, retvalue;
- char *filename;
- char readbuf[100], writebuf[100];
- filename = argv[1];
- fd = open(filename, O_RDWR); //打开设备
- if (fd < 0)
- {
- printf("Can't open file %s\n", filename);
- return -1;
- }
- switch (*argv[2]) //对操作数进行解析
- {
- case 'r':
- if (argc != 3) //进行鲁棒性检查
- {
- printf("Unknow operation, use the formate: ./APPNAME /dev/DRIVENAME r to read date from kernel.\n");
- return -1;
- }
- retvalue = read(fd, readbuf, 100);
- if (retvalue < 0) //检查是否读取成功
- {
- printf("Read file %s failed!\n", filename);
- }
- else
- {
- printf("User receive data: %s\n", readbuf);
- }
- break;
- case 'w':
- if (argc != 4) //进行鲁棒性检查
- {
- printf("Unknow operation, use the formate: ./APPNAME /dev/DRIVENAME w "USERDATE" to write date to kernel.\n");
- return -2;
- }
- memcpy(writebuf, argv[3], strlen(argv[3])); //将内容拷贝到缓冲区
- retvalue = write(fd, writebuf, 50); //写数据
- if (retvalue < 0)
- {
- printf("Write file %s failed!\n", filename);
- }
- else
- {
- printf("Write file success!\n");
- }
- break;
- default:
- printf("Unknow Operation: %d\n", *argv[2]);
- break;
- }
- retvalue = close(fd); //关闭设备
- if (retvalue < 0)
- {
- printf("Can't close file %s\n", filename);
- return -1;
- }
- return 0;
- }
复制代码第 17、32、49、64 行,在应用部分调用 open、read、write、close 会调用到驱动内部对应的 open、read、write、close 的函数。 6 程序编译 6.1 在虚拟机上进行交叉编译 1:Makefile 分析 驱动的编译还需要使用附件中的 Makefile 文件。首先看下 Makefile 文件内的代码。 Makefile - #已经编译过的内核源码路径
- KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel
- export ARCH=arm
- export CROSS_COMPILE=arm-linux-gnueabihf-
- #当前路径
- CURRENT_DIR = $(shell pwd)
- MODULE = KernelPrint
- APP = KernelPrintApp
- all :
- #进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
- rm -rf *.symvers *.order *.o *.mod.o *.mod.c
- ifneq ($(APP), )
- $(CROSS_COMPILE)gcc $(APP).c -o $(APP)
- endif
- clean :
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
- rm $(APP)
- #指定编译哪个文件
- obj-m += $(MODULE).o
复制代码基础的 Makefile 语法在基础部分已经讲过了,这部分将会讲些比较关键的部分。 第 2 行是标明编译内核的脚本所在的位置,如果未在该目录下找到该文件,将会无法进行编译。 第 15 行是改变到Kernel 目录,然后对在当前目录中文件进行编译。 第 17 到 19 行是如果存在 app.c,那么就将 app.c 文件编译为可执行文件。 Makefile 所需要的编译环境在之前部署的文件中应该已经讲明,建议放在“/home/uisrc”文件下,使编译路径与 KERNEL_DIR 中的一致。当然,也可以更改 KERNEL_DIR 的路径使其与自己部署的开发环境一致,不过依然建议将部署文件的压缩包直接在 home 的目录下进行解压。 2:编译驱动文件 双击已经移动到虚拟机的“1.HelloWorld”文档,进入文档,会显示“KernelPrint”文件,双击进入。 在 “KernelPrint”文件中右击桌面,打开选项,选择“open in Terminal”。 在终端中输入 “make”,在部署了安装包之后,会出现如下图的编译过程。 关闭终端,会发现“FirstDrive”文件夹内出现了许多文件,我们需要的是以“.ko”为结尾的驱动程序。 3:将编译好的 .ko 文件上传至开发板 如之前用 U 盘将文件从 windows 移动到虚拟机中一样的操作,这次反过来,把 .ko 文件从虚拟机中移到 windows 桌面。 打开 PuTTY,或者 xshell,两者都可以,这里选择 xshell。连接至该开发板。如果连接失败,重新插拔一下 PS 端的串口,然后,在虚拟机连接页面选择“连接主机”。成功后页面如下。 米联客的系统名为 uisrc,密码为 root。登录之后进入主目录。输入 “rz”。“lrzsz”在基础部分有对应的讲解。 在弹出的页面中找到之前转移的驱动,点击“打开”。会出现如下的文件传输界面。如果觉得慢,可以将板子的网口和主机的网口连接,在电脑网络设置中,“更改适配器选项”,将网络更改为共享网络,然后通过 ssh 登入传输数据,速度会变快很多。推荐使用 ssh 登录。 如果出现传输失败的现象,查看当前传输的位置是否有同一个文件。如果有,请删除。 7 驱动方案演示 7.1 硬件准备 SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”) 将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。 7.2 程序准备 查看文件是否传输成功。在终端输入“ls”命令。可以看到,“KernelPrint.ko”和“KernelPrintApp”已经被上传到当前的文件夹内了。 然后在root 状态下 insmod 安装驱动。“lsmod”查看当前安装的驱动,再通过 “ls /dev”找到对应的设备。 通过以上操作可以确定驱动已经安装成功。 使用 chmod 改变 “KernelPrintApp”的权限。 7.3 实验结果 进行读测试,在命令行输入“./KernelPrintApp /dev/KernelPrint_0 r”,然后就可以得到如下图所示的结果。“This message comes from kernel.”写在驱动中的信息,被应用调用成功了。 说明驱动的 read 功能正常。 进行写测试,在命令行输入“./KernelPrintApp /dev/KernelPrint_0 w “hello””。在打印内核输出,在“[2140.802655]”处可以看到驱动收到应用程序的输入,并在内核中打印了出来。这说明驱动的 write 功能正常。
|