1、 学习之前先学习 Linux 应用开发,并确保有搭建好 ubuntu 捏造机
2、 Linux驱动开发开发环境:NFS 和 SSH服务、Ubuntu 交叉编译工具链、VSCode 安装、CH340 串口驱动、SecureCRT 软件安装、MobaXterm 软件安装
3、 Linux 字符设备驱动实验
认识Linux字符设备驱动撰写和实验
板子:IMX6ULL 阿尔法 原子
安装部分:
· 交叉编译工具链地点:https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/
不想去下官方好大的安装包可以直接捏造机的火狐欣赏器里面下,省得ftp再传一次
Ubuntu 自带的 gcc 编译器是针对 X86 架构的!而我们现在要编译的是 ARM 架构的代码,所以我们必要一个在 X86 架构的 PC 上运行,可以编译 ARM 架构代码的 GCC 编译器,这个编译器就叫做交叉编译器,总结一下交叉编译器就是:
1、它肯定是一个 GCC 编译器。
2、这个 GCC 编译器是运行在 X86 架构的 PC 上的。
3、这个 GCC 编译器是编译 ARM 架构代码的,也就是编译出来的可实行文件是在 ARM 芯片上运行的。
交叉编译器中“交叉”的意思就是在一个架构上编译别的一个架构的代码,相当于两种架构“交叉”起来了
解压:
sudo tar -vxf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz
修改环境变量:使用 VI 打开/etc/profile 文件,命令如下:
设置文件:
打开/etc/profile 以后,在最背面输入如下所示内容
- export PATH=$PATH:/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin
复制代码 修改好以后就生存退出,重启 Ubuntu 体系,交叉编译工具链(编译器)就安装成功了
在使用交叉编译器之前还必要安装一下其它的库:
- sudo apt-get install lsb-core lib32stdc++6
复制代码 首先查察一下交叉编译工具的版本号,输入如下命令:
- arm-linux-gnueabihf-gcc -v
复制代码 //
· NFS 服务开启:
背面进行 Linux 驱动开发的时候必要 NFS 启动,因此要先安装并开启 Ubuntu 中的 NFS 服务,使用如下命令安装 NFS 服务:
- sudo apt-get install nfs-kernel-server rpcbind
复制代码 安装完成以后在用户根目录下创建一个名为“linux”的文件夹,以后所有
的东西都放到这个“linux”文件夹里面,在“linux”文件夹里面新建一个名为“nfs”的文件夹,以后我们可以在开发板上通过网络文件体系来访问 nfs 文件夹,要先设置 nfs,使用如下命令打开 nfs 设置文件/etc/exports:
打开/etc/exports 以后在背面添加如下所示内容:
- /home/自己的用户名/linux/nfs *(rw,sync,no_root_squash)
复制代码 (记得:wq生存退出)
///
· SSH 服务开启
- sudo apt-get install openssh-server
复制代码 ssh 的设置文件为/etc/ssh/sshd_config,使用默认设置即可
//
· VS Code安装
要安装的插件:
设置头文件的支持: “Ctrl+Shift+P”打开搜索框,然后输入“Edit configurations”,选择“C/C++:Edit configurations…”
//
驱动就是获取外设、大概传感器数据、控制外设。数据会提交给应用程序。 Linux 驱动编译既要编写一个驱动,还要编写一个简朴的测试应用程序,app。单片机下驱动和应用都是放到一个文件里面,杂糅在一起,Linux下驱动和应用是完全分开的
//
三类设备:字符设备、块设备、网络设备
背面会以一个捏造的设备为例,讲解如何进行字符设备驱动开发,以及如何编写测试 APP 来测试驱动工作是否正常,为以后的学习打底子
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。好比我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动
Linux 应用程序对驱动程序的调用流程图:
Linux 内核文件 include/linux/fs.h 中有个叫file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合
简朴先容一下 file_operation 结构体中比力告急的、常用的函数:
owner 拥有该结构体的模块的指针,一样平常设置为THIS_MODULE; llseek 函数用于修改文件当前的读写位置; read 函数用于读取设备文件;
write函数用于向设备文件写入(发送)数据; poll 是个轮询函数,用于查询设备是否可以进行非壅闭的读写;
unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应;
compat_ioctl 函数与 unlocked_ioctl函数功能一样,区别在于在 64 位体系上,32 位的应用程序调用将会使用此函数。在 32 位的体系上运行 32位的应用程序调用的是unlocked_ioctl;
mmap函数用于将设备的内存映射到历程空间中(也就是用户空间),一样平常帧缓冲设备会使用此函数,好比 LCD 驱动的显存,将帧缓冲(LCD显存)映射到用户空间中以后应用程序就可以直接操作显存了,如许就不消在用户空间和内核空间之间来回复制;
open 函数用于打开设备文件;
release 函数用于开释(关闭)设备文件,与应用程序中的 close 函数对应;
fasync函数用于刷新待处置惩罚的数据,用于将缓冲区中的数据刷新到磁盘中;
aio_fsync 函数与 fasync 函数的功能类似,只是aio_fsync 是异步刷新待处置惩罚的数据;
/
在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、release、write、read等都是必要实现的
学习部分:
在 Linux 驱动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在 Linux 驱动开发中我们必要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架
驱动模块的加载和卸载:
Linux 驱动有两种运行方式:
第一种就是将驱动编译进 Linux 内核中,如许当 Linux 内核启动的时候就会主动运行驱动程序。
第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用 “ insmod ” 命令加载驱动模块。
在调试驱动的时候一样平常都选择将其编译为模块,如许我们修改驱动以后只必要编译一下驱动代码即可,不必要编译整个 Linux 代码。而且在调试的时候只必要加载大概卸载驱动模块即可,不必要重启整个体系。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有题目以后就可以将驱动编译进Linux 内核中,固然也可以不编译进 Linux 内核中,具体看需求。
模块有加载和卸载两种操作:
- module_init(xxx_init); //注册模块加载函数
- module_exit(xxx_exit); //注册模块卸载函数
- 在编写驱动的时候需要注册这两种操作函数;
- module_init 函数用来向 Linux 内核注册一个模块加载函数,
- 参数xxx_init 就是需要注册的具体函数,当使用“insmod”命令
- 加载驱动的时候,xxx_init 这个函数就会被调用。module_exit()
- 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit
- 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候,
- xxx_exit 函数就会被调用
复制代码- 示例代码 字符设备驱动模块加载和卸载函数模板
- 1 /* 驱动入口函数 */
- 2 static int __init xxx_init(void)
- 3 {
- 4 /* 入口函数具体内容 */
- 5 return 0;
- 6 }
- 7
- 8 /* 驱动出口函数 */
- 9 static void __exit xxx_exit(void)
- 10 {
- 11 /* 出口函数具体内容 */
- 12 }
- 13
- 14 /* 将上面两个函数指定为驱动的入口和出口函数 */
- 15 module_init(xxx_init);
- 16 module_exit(xxx_exit);
复制代码 驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod 和 modprobe,insmod是最简朴的模块加载命令,此命令用于加载指定的.ko 模块,好比加载 drv.ko 这个驱动模块,命令如下:
insmod 命令不能办理模块的依赖关系,好比 drv.ko 依赖 first.ko 这个模块,就必须先使用
insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。但是 modprobe 就不会存在这个题目,modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe 命令相比 insmod 要智能一些。modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误陈诉等功能,保举使用 modprobe 命令来加载驱动。
驱动模块的卸载使用命令“rmmod”:如rmmod drv.ko
也可以使用“modprobe -r”命令卸载驱动:如modprobe -r drv.ko
(使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,条件是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是
保举使用 rmmod 命令)
字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后必要注册字符设备,同样,卸载驱动模块的时候也必要注销掉字符设备。
- static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
- static inline void unregister_chrdev(unsigned int major,const char *name)
复制代码
一样平常字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块
的出口函数 xxx_exit 中进行。
- static struct file_operations test_fops;
-
- /* 驱动入口函数 */
- static int __init xxx_init(void)
- {
- /* 入口函数具体内容 */
- int retvalue = 0;
-
- /* 注册字符设备驱动 */
- retvalue = register_chrdev(200, "chrtest", &test_fops);
- if(retvalue < 0){
- /* 字符设备注册失败,自行处理 */
- }
- return 0;
- }
-
- /* 驱动出口函数 */
- static void __exit xxx_exit(void)
- {
- /* 注销字符设备驱动 */
- unregister_chrdev(200, "chrtest");
- }
-
- /* 将上面两个函数指定为驱动的入口和出口函数 */
- module_init(xxx_init);
- module_exit(xxx_exit);
复制代码 调用函数 register_chrdev 注册字符设备,主设备号为 200,设备名字为“chrtest”,设备操作函数集合就是第 1 行定义的 test_fops。要留意的一点就是,选择没有被使用的主设备号,输入命令 “cat /proc/devices” 可以查察当前已经被使用掉的设备号
file_operations 结构体就是设备的具体操作函数,我们定义了file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、release、read 和 write 等具体的设备操作函数。
我们应该实现哪些操作函数?
假设对 chrtest这个设备有如下两个要求:
1、能够对 chrtest 进行打开和关闭操作
设备打开和关闭是最基本的要求,险些所有的设备都得提供打开和关闭的功能。因此我们必要实现 file_operations 中的 open 和 release 这两个函数。
2、对 chrtest 进行读写操作
假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序必要通过 read 和 write 这两个函数对 chrtest 的缓冲区进行读写操作。所以必要实现 file_operations 中的 read 和 write 这两个函数。
- 1 /* 打开设备 */
- 2 static int chrtest_open(struct inode *inode, struct file *filp)
- 3 {
- 4 /* 用户实现具体功能 */
- 5 return 0;
- 6 }
- 7
- 8 /* 从设备读取 */
- 9 static ssize_t chrtest_read(struct file *filp, char __user *buf,
- size_t cnt, loff_t *offt)
- 10 {
- 11 /* 用户实现具体功能 */
- 12 return 0;
- 13 }
- 14
- 15 /* 向设备写数据 */
- 16 static ssize_t chrtest_write(struct file *filp,
- const char __user *buf,
- size_t cnt, loff_t *offt)
- 17 {
- 18 /* 用户实现具体功能 */
- 19 return 0;
- 20 }
- 21
- 22 /* 关闭/释放设备 */
- 23 static int chrtest_release(struct inode *inode, struct file *filp)
- 24 {
- 25 /* 用户实现具体功能 */
- 26 return 0;
- 27 }
- 28
- 29 static struct file_operations test_fops = {
- 30 .owner = THIS_MODULE,
- 31 .open = chrtest_open,
- 32 .read = chrtest_read,
- 33 .write = chrtest_write,
- 34 .release = chrtest_release,
- 35 };
- 36
- 37 /* 驱动入口函数 */
- 38 static int __init xxx_init(void)
- 39 {
- 40 /* 入口函数具体内容 */
- 41 int retvalue = 0;
- 42
- 43 /* 注册字符设备驱动 */
- 44 retvalue = register_chrdev(200, "chrtest", &test_fops);
- 45 if(retvalue < 0){
- 46 /* 字符设备注册失败,自行处理 */
- 47 }
- 48 return 0;
- 49 }
- 50
- 51 /* 驱动出口函数 */
- 52 static void __exit xxx_exit(void)
- 53 {
- 54 /* 注销字符设备驱动 */
- 55 unregister_chrdev(200, "chrtest");
- 56 }
- 57
- 58 /* 将上面两个函数指定为驱动的入口和出口函数 */
- 59 module_init(xxx_init);
- 60 module_exit(xxx_exit);
复制代码 末了我们必要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否
则的话编译的时候会报错,作者信息可以添加也可以不添加
- 1 /* 打开设备 */
- 2 static int chrtest_open(struct inode *inode, struct file *filp)
- 3 {
- 4 /* 用户实现具体功能 */
- 5 return 0;
- 6 }
- ......
- 57
- 58 /* 将上面两个函数指定为驱动的入口和出口函数 */
- 59 module_init(xxx_init);
- 60 module_exit(xxx_exit);
- 61
- 62 MODULE_LICENSE("GPL");
- 63 MODULE_AUTHOR("bat合伙人");
复制代码 设备号的分配:
静态分配设备号 :
注册字符设备的时候必要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,好比选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查察文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“cat /proc/devices”命令即可查察当前体系中所有已经使用了的设备号。
动态分配设备号:
静态分配设备号必要我们检查当前体系中所有被使用了的设备号,然后挑选一个没有使用
的。而且静态分配设备号很轻易带来冲突题目,Linux 社区保举使用动态分配设备号,在注册字符设备之前先申请一个设备号,体系会主动给你一个没有被使用的设备号,如许就避免了冲突。
卸载驱动的时候开释掉这个设备号即可,设备号的申请函数如下:
/
Linux 字符设备驱动实验:
字符设备驱动开发的基本步骤我们已经相识了,我们就以 chrdevbase 这个捏造设备为例,完整的编写一个字符设备驱动模块。chrdevbase 不是现实存在的一个设备,是为了方便学习字符设备的开发而引入的一个捏造设备。chrdevbase 设备有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的巨细都为 100 字节。在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。chrdevbase 这个捏造设备的功能很简朴,但是它包含了字符设备的最基本功能。
打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入:
C/C++: Edit configurations(JSON),打开 C/C++编辑设置文件,打开以后会主动在.vscode 目录下天生一个名为 c_cpp_properties.json 的文件,在里面设置
(includePath 表现头文件路径,必要将 Linux 源码里面的头文件路径添加进来,分别是开发板所使用的Linux 源码下的 include、arch/arm/include 和 arch/arm/include/generated 这三个目录的路径,留意,这里使用了绝对路径)
编写实验程序:
- #include <linux/types.h>
- #include <linux/kernel.h>
- #include <linux/delay.h>
- #include <linux/ide.h>
- #include <linux/init.h>
- #include <linux/module.h>
- #define CHRDEVBASE_MAJOR 200 /* 主设备号 */
- #define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
- static char readbuf[100]; /* 读缓冲区 */
- static char writebuf[100]; /* 写缓冲区 */
- static char kerneldata[] = {"kernel data!"};
- /*
- * @description : 打开设备
- * @param – inode : 传递给驱动的 inode
- * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
- * 一般在 open 的时候将 private_data 指向设备结构体。
- * @return : 0 成功;其他 失败
- */
- static int chrdevbase_open(struct inode *inode, struct file *filp)
- {
- //printk("chrdevbase open!\r\n");
- return 0;
- }
- /*
- * @description : 从设备读取数据
- * @param - filp : 要打开的设备文件(文件描述符)
- * @param - buf : 返回给用户空间的数据缓冲区
- * @param - cnt : 要读取的数据长度
- * @param - offt : 相对于文件首地址的偏移
- * @return : 读取的字节数,如果为负值,表示读取失败
- */
- static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
- size_t cnt, loff_t *offt)
- {
- int retvalue = 0;
- /* 向用户空间发送数据 */
- memcpy(readbuf, kerneldata, sizeof(kerneldata));
- retvalue = copy_to_user(buf, readbuf, cnt);
- if(retvalue == 0){
- printk("kernel senddata ok!\r\n");
- }else{
- printk("kernel senddata failed!\r\n");
- }
-
- //printk("chrdevbase read!\r\n");
- return 0;
- }
- /*
- * @description : 向设备写数据
- * @param - filp : 设备文件,表示打开的文件描述符
- * @param - buf : 要写给设备写入的数据
- * @param - cnt : 要写入的数据长度
- * @param - offt : 相对于文件首地址的偏移
- * @return : 写入的字节数,如果为负值,表示写入失败
- */
- static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
- {
- int retvalue = 0;
- /* 接收用户空间传递给内核的数据并且打印出来 */
- retvalue = copy_from_user(writebuf, buf, cnt);
- if(retvalue == 0){
- printk("kernel recevdata:%s\r\n", writebuf);
- }else{
- printk("kernel recevdata failed!\r\n");
- }
-
- //printk("chrdevbase write!\r\n");
- return 0;
- }
- /*
- * @description : 关闭/释放设备
- * @param - filp : 要关闭的设备文件(文件描述符)
- * @return : 0 成功;其他 失败
- */
- static int chrdevbase_release(struct inode *inode, struct file *filp)
- {
- //printk("chrdevbase release!\r\n");
- return 0;
- }
- /*
- * 设备操作函数结构体
- */
- static struct file_operations chrdevbase_fops = {
- .owner = THIS_MODULE,
- .open = chrdevbase_open,
- .read = chrdevbase_read,
- .write = chrdevbase_write,
- .release = chrdevbase_release,
- };
- /*
- * @description : 驱动入口函数
- * @param : 无
- * @return : 0 成功;其他 失败
- */
- static int __init chrdevbase_init(void)
- {
- int retvalue = 0;
-
- /* 注册字符设备驱动 */
- retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
- if(retvalue < 0){
- printk("chrdevbase driver register failed\r\n");
- }
- printk("chrdevbase_init()\r\n");
- return 0;
- }
- /*
- * @description : 驱动出口函数
- * @param : 无
- * @return : 无
- */
- static void __exit chrdevbase_exit(void)
- {
- /* 注销字符设备驱动 */
- unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
- printk("chrdevbase_exit()\r\n");
- }
- /*
- * 将上面两个函数指定为驱动的入口和出口函数
- */
- module_init(chrdevbase_init);
- module_exit(chrdevbase_exit);
- /*
- * LICENSE 和作者信息
- */
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("zuozhongkai");
复制代码 增补:
chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用,本例程中没有做任何工作,只是输出一串字符,用于调试。
chrdevbase_write 函数,应用程序调用 write 函数向设备写数据的时候此函数就会实行。参数 buf 就是应用程序要写入设备的数据,也是用户空间的内存,参数 cnt 是要写入的数据长度,参数 offt 是相对文件首地点的偏移。第 75 行通过函数 copy_from_user 将 buf 中的数据复制到写缓冲区 writebuf 中,因为用户空间内存不能直接访问内核空间的内存,所以必要借助函数copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中
chrdevbase_release 函数,应用程序调用 close 关闭设备文件的时候此函数会实行,一样平常会在此函数里面实行一些开释操作。如果在 open 函数中设置了 filp 的 private_data成员变量指向设备结构体,那么在 release 函数最终就要开释掉
//
(printk在内核源码中用来记录日志信息的函数,只能在内核源码范围内使用)
这里使用了 printk 来输出信息,而不是 printf!因为在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用printk 这个函数。
差别之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件include/linux/kern_levels.h 里面,定义如下:
一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低
如果要设置消息级别,参考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
在具体的消息前面加上 KERN_EMERG 就可以将这条消息的级别设置为 KERN_EMERG;
用 printk 的 时 候 不 显 式 的 设 置 消 息 级 别 , 那 么 printk 将 会 采 用 默 认 级 别MESSAGE_LOGLEVEL_DEFAULT,MESSAGE_LOGLEVEL_DEFAULT 默以为 4
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认
为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。
这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。
/
编写测试 APP
编写测试 APP 就是编写 Linux 应用,必要用到 C 库里面和文件操作有关的一些函数,好比
open、read、write 和 close 这四个函数
- #include "stdio.h"
- #include "unistd.h"
- #include "sys/types.h"
- #include "sys/stat.h"
- #include "fcntl.h"
- #include "stdlib.h"
- #include "string.h"
- static char usrdata[] = {"usr data!"};
- /*
- * @description : main主程序
- * @param - argc : argv数组元素个数
- * @param - argv : 具体参数
- * @return : 0 成功;其他 失败
- */
- int main(int argc, char *argv[])
- {
- int fd, retvalue;
- char *filename;
- char readbuf[100], writebuf[100];
- if(argc != 3){
- printf("Error Usage!\r\n");
- return -1;
- }
- filename = argv[1];
- /* 打开驱动文件 */
- fd = open(filename, O_RDWR);
- if(fd < 0){
- printf("Can't open file %s\r\n", filename);
- return -1;
- }
- if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
- retvalue = read(fd, readbuf, 50);
- if(retvalue < 0){
- printf("read file %s failed!\r\n", filename);
- }else{
- /* 读取成功,打印出读取成功的数据 */
- printf("read data:%s\r\n",readbuf);
- }
- }
- if(atoi(argv[2]) == 2){
- /* 向设备驱动写数据 */
- memcpy(writebuf, usrdata, sizeof(usrdata));
- retvalue = write(fd, writebuf, 50);
- if(retvalue < 0){
- printf("write file %s failed!\r\n", filename);
- }
- }
- /* 关闭设备 */
- retvalue = close(fd);
- if(retvalue < 0){
- printf("Can't close file %s\r\n", filename);
- return -1;
- }
- return 0;
- }
复制代码
编译驱动程序
创建Makefile 文件
- KERNELDIR := xxxxxx/linux/IMX6ULL/linux/temp/linux-imxrel_imx_4.1.15_2.1.0_ga_alientek
- CURRENT_PATH := $(shell pwd)
- obj-m := chrdevbase.o
-
- build: kernel_modules
-
- kernel_modules:
- $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
- clean:
- $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
复制代码 KERNELDIR 表现开发板所使用的 Linux 内核源码目录,使用绝对路径;
CURRENT_PATH 表现当前路径,直接通过运行“pwd”命令来获取当前所处路径;
obj-m 表现将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块;
具体的编译命令,背面的 modules 表现编译模块,-C 表现将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。M 表现模块源码目录,“make modules”命令中加入 M=dir 以后程序会主动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件
Makefile 编写好以后输入“make”命令编译驱动模块
编译成功以后就会天生一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动
模块。至此,chrdevbase 设备的驱动就编译成功
编译测试 APP
测试 APP 比力简朴,只有一个文件,因此就不必要编写 Makefile 了,直接输入命令编译。
因为测试 APP 是要在 ARM 开发板上运行的,所以必要使用 arm-linux-gnueabihf-gcc 来编译,
输入如下命令:
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
编译完成以后会天生一个叫做 chrdevbaseApp 的可实行程序,输入如下命令查察chrdevbaseAPP 这个程序的文件信息:
file chrdevbaseApp
运行测试
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |