驱动
之前在嵌入式系统移植停留了一段时间,枯燥地看uboot、kernel和跟文件系统的源码感觉没有成就感,在这之前已经对嵌入式linux的启动流程有了大体的了解,因此先跳到驱动开发,体验学习一下子哈!!。
Linux 中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。
字符设备驱动:IO口的驱动,比如点灯、 I2C、 SPI、音频等。
块设备驱动:存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备
网络设备驱动:网络驱动,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
Linux 下的应用程序是如何调用驱动程序的?
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如, led 灯/dev/led 的驱动文件,应用程序使用open()、close()、write()、read()等函数对文件操作,进而控制硬件。
字符设备驱动简介
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。
如上图,从应用程序到具体驱动程序都有相应的函数与之对应,比如调用open 这个函数。
在内核部分: Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。(听名字就知道是管理驱动文件操作的)
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
......
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };
file_operation 结构体中比较重要的、常用的函数:
第 1589 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 1590 行, llseek 函数用于修改文件当前的读写位置。
第 1591 行, read 函数用于读取设备文件。
第 1592 行, write 函数用于向设备文件写入(发送)数据。
第 1596 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 1599 行, mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行, open 函数用于打开设备文件。
第 1603 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
字符设备驱动开发步骤
在 Linux 驱动开发中肯定也是要初始化相应的外设寄存器, Linux 驱动开发中我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。
驱动模块的加载和卸载
Linux 驱动有两种运行方式:
第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。
调试时推荐使用将驱动编译成模块,在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 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、rmmod,另外modprobe命令可以将具有依赖关系的模块一锅端的加载和卸载:
//加载驱动模块
insmod drv.ko
或 modprobe drv.ko
//卸载驱动模块
rmmod drv.ko
或 modprobe -r drv.ko
字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。
(就是你不仅要加载自己的驱动模块,还要向内核注册申请,这样才能是合法公民)
字符设备的注册和注销函数原型:
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)
major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
注册函数register_chrdev()位于加载函数调用的驱动入口函数中;
注销函数unregister_chrdev()位于卸载函数调用的驱动出口函数中。
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 }
设备号不能和其他驱动文件冲突,可使用命令查看当前已经被使用掉的设备号:
cat /proc/devices
实现设备的具体操作函数
比如,对chrtest 设备进行驱动,对file_operations结构体类型的变量 test_fops进行初始化,内部存有驱动文件操作函数,用什么操作函数就添加什么操作函数。
打开和关闭操作:最基本的要求;
读写操作:拥有读写一段缓冲区(内存)。
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 };
添加 LICENSE 和作者信息
LICENSE 是必须添加的,否则的话编译的时候会报错,LICENSE 采用 GPL 协议。
作者信息可以添加也可以不添加。
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息
MODULE_AUTHOR("zxy") //添加模块作者信息
总结
一个完整的字符设备驱动模板就上以上4条:
/*1、文件操作函数file_operations结构体 变量初始化*/
......
/*2、驱动入口函数注册 驱动出口函数注销*/
......
/*3、加载驱动模块 卸载驱动模块*/
......
/*4、license和作者信息*/
(执行流程:3调用2,2调用1)
Linux 设备号
设备号的组成
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,
主设备号表示某一个具体的驱动,
次设备号表示使用这个驱动的各个设备。
设备号由dev_t数据类型表示:
typedef __kernel_dev_t dev_t;
......
typedef __u32 __kernel_dev_t;
......
typedef unsigned int __u32;
......
经过来回的定义,设备号就是c语言中unsigned int的32位数据类型;
其中高 12 位为主设备号, 低 20 位为次设备号。
文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏函数):
6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
设备号的分配
1、静态分配设备号
使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,人为选择没有使用的设备号。
2、动态分配设备号
Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统自动给你一个没有被使用的设备号,这样就避免了冲突。
设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev: 保存申请到的设备号。
baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name: 设备名字。
设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。
chrdevbase 字符设备驱动开发实验
为了完整的编写一个字符设备驱动模块,假设一个虚拟设备: chrdevbase
设备描述:chrdevbase 设备有两个缓冲区,一个为读缓冲
区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。
应用程序描述:在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。
实验程序编写
实现功能描述:
应用程序调用 open 函数打开 chrdevbase 这个设备,
使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),
使用 read 函数读取读缓冲区 readbuf 中的数据操作,
应用程序使用 close 函数关闭 chrdevbase 设备。
驱动程序chrdevbase.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 : 左忠凯
版本 : V1.0
描述 : chrdevbase驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
#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");
分析:文件架构
头文件调用的都是linux内核文件;
4个驱动文件操作函数;
驱动文件操作函数结构体初始化;
驱动入口函数,注册设备;
驱动出口函数,注销设备;
加载驱动模块
卸载驱动模块
许可证
人名
printk函数 相当于 printf 的孪生兄妹, printf运行在用户态 printk 运行在内核态,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件include/linux/kern_levels.h。
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息*/
比如:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); //0级别
printk("gsmi: Log Shutdown Reason\n"); //MESSAGE_LOGLEVEL_DEFAULT 默认级别为4
参数 filp 有个叫做 private_data 的成员变量, private_data 是个 void 指针,一般在驱动中将private_data 指向设备结构体,设备结构体会存放设备的一些属性。
用户程序chrdevbaseApp.c
C 库文件操作基本函数
编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read、 write 和 close 这四个函数。
①、 open 函数
int open(const char *pathname, int flags)
pathname:要打开的设备或者文件名。
flags: 文件打开模式,以下三种模式必选其一:
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
另外,<逻辑或> 来选择多种模式:
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行。
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。
返回值:如果文件打开成功的话返回文件的文件描述符。
在 Ubuntu 中输入“man 2 open” 即可查看 open 函数的详细内容
②、close 函数
int close(int fd);
fd:要关闭的文件描述符。
返回值: 0 表示关闭成功,负值表示关闭失败。
③、read 函数
当然是从文件里读内容啦~需要open函数返回的文件描述符。
ssize_t read(int fd, void *buf, size_t count)
fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 数据读取到此 buf 中。
count: 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
在 Ubuntu 中输入“man 2 read”命令即可查看 read 函数的详细内容。
④、write 函数
ssize_t write(int fd, const void *buf, size_t count);
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 要写入的数据。
count: 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
编写一个简单的测试 APP,控制驱动文件,测试 APP 运行在用户空间,测试 APP 很简单通过输入相应的指令来对 chrdevbase 设备执行读或者写操作。
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbaseApp.c
作者 : 左忠凯
版本 : V1.0
描述 : chrdevbase驱测试APP。
其他 : 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
argv[2] 1:读文件
argv[2] 2:写文件
论坛 : www.openedv.com
日志 : 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
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];
//要求只能传入3个参数
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;
}
分析:程序架构
调用C库函数;
打开文件;
读写文件;
关闭文件。
判断 argv[2]参数的值是 1 或 2 ,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字。
编译驱动程序和测试 APP
1、编译驱动程序chrdevbase.c 这个文件,将其编译为.ko 模块,创建Makefile 文件,路径自己改改:
KERNELDIR := /home/zxy/linux/kernel_lib/linux-imx-rel_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 内核源码目录(原子官方linux内核源码,需要编译了好久),使用绝对路径,大家根据自己的实际情况填写即可。
CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。
obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。
后面的modules 表示编译模块,
-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。
M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
2、编译 chrdevbaseApp.c生成可执行程序,这里差不多就是单纯的C语言程序,用交叉编译器arm-linux-gnueabihf-gcc编译即可:
//编译
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
//查看文件信息,chrdevbaseAPP 这个可执行文件是 32 位 LSB 格式, ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下运行。
file chrdevbaseApp
3、传文件到开发板验证
左老师使用的 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统,说白了就是从uboot层面网络启动linux内核和挂在根文件系统,但是这能保存到DRAM中。
本人这里研究了一下ssh传输协议,实现linux系统client和server之间的文件互传:
1、两者连接在同一网络地址;
2、服务器安装并配置ssh server协议;
3、板子可以直接scp获取文件啦。
就是这么简单,有手就行。
具体把驱动文件chrdevbase.ko放到/lib/modules/4.1.15-gb8ddbbc目录中,就是放到这个一种库里;
应用程序chrdevApp放到home目录下或者其他正常的位置都行。
4、验证操作
insmod chrdevbase.ko //加载驱动模块
cat /proc/devices //查找能用的设备号
mknod /dev/chrdevbase c 200 0 //创建节点 “ 200”是设备的主设备号,“ 0”是设备的次设备号。
//操作这个设备节点
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2
rmmod chrdevbase.ko //卸载驱动模块
lsmod //查看某些模块还存不存在,
总结
这种简单的驱动模块,整体就传到板子上驱动文件.ko和应用程序文件。
驱动文件放到/lib下的库文件中,一直放着就行;
使用insmod命令加载,就是跟内核申请了一块儿空间;
使用mknod命令创建节点再/dev下,就是跟内核申请营业许可证;
APP可以放到任何正常的目录下,使用APP应用程序控制/dev下这个节点,就能实现驱动的控制了;
使用rmmod命令卸载,就是跟内核说一声退还这一块儿空间,APP就控制不了了。
遇到的大问题:
在加载驱动模块的时候出现的这个问题,原因是板子上的内核版本和编译驱动时候用到的内核不匹配。
解决办法: 强行将编译驱动的内核相关版本信息改成一样的,哪里不对改哪里。
内核顶层的Makefile中:
VERSION = 4
PATCHLEVEL = 1
SUBLEVEL = 15
EXTRAVERSION = -gb8ddbbc
NAME = Series 4800
手动修改或者图形界面配置两个文件
make menuconfig
System Type --->
Multiple platform selection --->
[ ] ARMv6 based platforms (ARM11)
[*] ARMv7 based platforms (Cortex-A, PJ4, Scorpion, Krait)
.config文件和arch/arm/configs/imx_alientek_emmc_config文件:
CPU Core family selection,CPU内核全家桶中只留下----v7=y
然后再重新编译驱动:使用modinfo xxx.ko查看模块版本信息就和板子内核版本信息一样啦。
非常感谢原子论坛,好多问题都是从那上边儿找到的。