Linux下三大驱动:字符设备,块设备,网络设备。一个硬件可以从属于不同的设备分类。
0. Linux应用程序对驱动程序的调用流程
驱动加载成功后会在/dev目录下生成一个文件,对该文件的操作就是对设备的操作。当我们在用户态调用一个函数,会经过“系统调用(C库的一部分)”方法陷入内核空间,内核空间中有对应的驱动函数,由此实现控制。应用访问内核资源的三个方式:系统调用,异常(中断),陷入(软中断)。
Linux内核文件include/linux/fs.h中file_operations结构体保存了内核驱动操作函数集合。
**Linux驱动有两种运行方式:①将驱动编译进内核,内核启动后自动运行;②驱动编译成.ko模块,内核启动后需要以“insmod”命令加载。**调试驱动一般编译为模块,避免每次都编译一遍linux。
1. 字符驱动设备
字符设备是指逐字节流进行顺序读写操作的设备,比如LED,KEY,IIC,SPI,LCD等。
1.1 驱动模块加载卸载
字符驱动设备加载卸载函数模板:
static struct file_operations test_fops; # 设备具体操作函数在这里面
/* 驱动入口函数 */
static int __init xxx_init(void) # 定义xxx_init驱动入口函数,使用__init修饰
{
/* 入口函数具体内容 */
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_init注册xxx_init为驱动入口函数,加载驱动时调用
module_exit(xxx_exit);
模块加载命令:
①insmod:如果一个模块依赖另一个模块,必须先insmod依赖模块。
②modprobe:会自动分析依赖并全部加载。
模块卸载命令:
①rmmod:对应insmod。
②modprobe -r:对应modpeobe。全部卸载依赖可能导致其它模块受到影响。
1.2 字符设备加载卸载
模块加载之后需要进行字符设备加载,字符设备注册一般在模块入口函数xxx_init中进行,注销在模块卸载函数xxx_exit中进行。函数原型为:
# major-主设备号 name-设备名 fops-指向设备操作函数集合变量
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)
注册设备时必须确保主设备号没被占用,使用**“cat /proc/devices”查看当前已用设备号**。
1.3 设备具体操作函数
假设对设备有打开(open)关闭(release),读(read)写(write)操作的要求。
App中的函数就这样绑定到了驱动函数。
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
1.4 添加LICENSE和作者信息
MODULE_LICENSE("GPL"); # 必须添加,采用GPL协议
MODULE_AUTHOR("finches"); # 可加可不加
2. Linux设备号
Linux中每个设备都有设备号,由主设备号和次设备号组成。
主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。设备号由dev_t数据类型表示(其实就是unsigned int 32类型),其中高12位主设备号,低20位次设备号。所以主设备号范围为0-4095。
2.1 主设备号分配
1)静态分配:开发者指定一个设备号,使用“cat /proc/devices”查看,没被占用就可以用。
2)动态分配:注册字符设备之前申请一个设备号,卸载时释放设备号。推荐使用。
设备号申请函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
# dev:保存申请到的设备号
# baseminor:次设备号起始地址,一个alloc_chrdev_region可以申请多个主设备号相同,次设备号不同的硬件
# count:申请设备数
# name:设备名
设备号释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
# from:要释放的设备号
# count:从from开始,要释放的设备号数量
3. chrdevbase字符设备驱动开发实验
chrdevbase设备是一个虚拟设备,具有各100字节的读和写缓冲区。
1)编写驱动文件chrdevbase.c:
2)编写测试APP:
# open:pathname-要打开的设备或文件名。 flags-打开模式。
# 打开模式(必选的3个):O_RDONLY只读;O_WRONLY只写;O_RDWR读写。
# 返回值:打开成功返回文件的描述符
int open(const char *pathname, int flags)
# read:fd-要读取的文件描述符。 buf:数据读取到该缓冲区。 count:要读取的字节数。
# 返回值:读取成功返回读取到的字节数,返回0代表读到了文件末尾。
ssize_t read(int fd, void *buf, size_t count)
# write:fd-要进行写的文件描述符。
# 返回值:返回写入的字节数。 返回0表示没有写入任何数据。
ssize_t write(int fd, const void *buf, size_t count);
# close:
int close(int fd);
3)编译驱动程序为.ko模块:
4)测试应用:
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp # 编译
file chrdevbaseApp # 查看应用信息
在ubuntu中将.ko和App拷贝到/lib/modules/4.1.15下,在开发板上运行:
1)加载驱动模块并检查:
# 加载模块
modprobe chrdevbase.ko
# 若报错没有‘modules.dep’,输入‘depmod’命令即可自动生成
# 查看当前存在的模块
lsmod
# 查看系统所有设备
cat /proc/devices
2)创建设备节点文件:
# 创建了chrdevbase节点文件,c表示字符设备,200为主设备号,0是次设备号
mknod /dev/chrdevbase c 200 0
3)设备操作测试:
返回第一行:驱动程序中的chrdevbase_read函数输出信息
返回第二行:App输出的接收到的数据
4)卸载驱动模块:
rmmod chrdevbase.ko