相关概念
-
linux驱动开发思维
1、裸机驱动开发
很底层,直接和寄存器
打交道,有些mcu会提供库;在linux下开发驱动直接操作寄存器不现实
2、根据linux下的各种驱动框架
进行开发(按框架的基本要求
将io的属性
告诉系统,然后linux会提供api函数
,我们直接操作这些 api函数就可以了,这就要求自己写的驱动要符合框架的要求)一定要满足框架,也就是各种驱动框架
的掌握。
3、驱动最终表现
就是/dev/xxx 文件
。打开(使能)、关闭、读、写
4、现在新的内核
支持设备树
,.dts 文件
,描述板子的设备信息,板子的各种设备的信息,驱动开发第一步
就是向设备树种添加板子的信息。linux内核通过分析设备树信息
可以获得板子上的设备信息
。 -
linux驱动分3大类:
1、字符设备驱动
(最多的,按键、iic、 …):不定长的、顺序的字节流来与设备进行交互。
2、块设备驱动
(nand,flash,emmc,sd,ssd等存储设备):定长的、随机的访问设备
3、网络设备驱动
(如有线网卡、无线网卡)
(一个设备
可能同时属于多个类型
,如 usb wifi,sdio wifi 同属于网络设备驱动
和字符设备驱动
) -
驱动的功能
1、发送数据给app。
2、接收app的数据来控制外设。 -
用户空间(用户态) 和 内核空间(内核态)
linux kernel
和驱动程序
运行在内核空间
。
应用程序
运行在用户空间
。
为了安全性区分,内核态
对cpu的所有资源
是没有限制的;用户空间的操作权限
是受限的。
而且用户空间不能直接对内核进行操作,必须用过系统调用
来实现。 -
设备号
dev_t
其实就是unsigned int
类型,32位,设备号
包括主设备号
和从设备号
组成。
设备号高12位
为主设备号
,设备号低20位
为从设备号
。
主设备号
表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
cat /proc/devices
查看当前系统有哪些主设备号被使用了。只能看到主设备号。 -
cat /proc/sys/kernel/printk
来查看
7 4 1 7
7 : 控制台日志级别,优先级高于该值(数值上看就是比这个值小)的消息将在控制台
显示
4 : 默认消息日志级别 ,printk没定义优先级时,只打印这个优先级以上的消息。即 printk(“hell world”);优先级为4, 由于4<7,故可以被打印到控制台。
DEFAULT_MESSAGE_LOGLEVEL
1 :最小控制台日志级别,控制台日志级别
可被设置的最小值(最高优先级)
MINIMUM_CONSOLE_LOGLEVEL
7 :默认的控制台日志级别 ,也是第一个参数
的默认优先级(7)。
DEFAULT_CONSOLE_LOGLEVEL
这4个值统一定义在数组 int console_printk[4]
里面 -
linux驱动的两种运行方式
1、将驱动编译进 linux内核 中,当 linux kernel 启动的时候会自动运行驱动程序。
2、将驱动编译成模块(linux 下模块扩展名为.ko
),在内核启动后可以使用modprobe / insmod
命令来加载驱动模块。
调试驱动时一般都会选择将其编译成模块,修改驱动以后只要编译一下驱动代码即可。驱动开发完成后再将其编译进内核。
几个函数
-
module_init(xxx_init) ;
:注册模块加载函数
用来向 Linux 内核注册一个模块加载函数
,参数 xxx_init 就是需要注册的具体函数
。当在 shell 下调用modprobe / insmod
命令时xxx_init
这个函数就会被调用。 -
module_exit(xxx_exit) ;
:注册模块卸载函数
用来向 Linux 内核注册一个模块卸载函数
,参数xxx_exit
就是需要注册的具体函数
,当在 shell 下调用rmmod
命令卸载具体驱动的时候xxx_exit
函数就会被调用。 -
字符设备的注册函数
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
用于注册字符设备,此函数一共有三个参数:
major: 主设备号
name:设备名字,指向一串字符串
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行
此函数不能指定次设备号,一旦选择了某个主设备号,那么其它的设备无法再使用该主设备号。 -
字符设备的注销函数
static inline void unregister_chrdev(unsigned int major, const char *name)
注销字符设备,此函数有两个参数,
major: 要注销的设备对应的主设备号。
name: 要注销的设备对应的设备名。
字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。 -
动态分配设备号函数(由系统分配一个未被使用的设备号)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
用于申请设备号,此函数有 4 个参数:
dev:保存申请到的设备号
baseminor: 次设备号起始地址,alloc_chrdev_region
可以申请一段连续的多个设备号,这些设备号的主设备号
一样,但是次设备号
不同,次设备号
以baseminor
为起始地址
开始递增。一般baseminor
为 0,也就是说次设备号
从 0 开始。
count: 要申请的设备号数量。
name:设备名字。 -
设备号释放函数(注销字符设备之后要释放掉设备号)
void unregister_chrdev_region(dev_t from, unsigned count)
from:要释放的设备号
count: 表示从 from 开始,要释放的设备号数量。 -
复制
内核空间的数据
到用户空间
的函数
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
参数 to :目的,是指向用户空间的一个地址
参数 from :源,内核空间的一个地址
参数 n :要复制的数据长度
如果复制成功,返回值为 0,如果复制失败则返回负数。 -
复制
用户空间的数据
到内核空间
的函数
static inline long copy_to_user(void *to, const void __user *from, unsigned long n)
参考上面 -
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块: insmod和 modprobe
insmod 命令不能解决模块的依赖关系。modprobe 可以。
modprobe 命令默认会去/lib/modules/<kernel-version>
目录中查找模块
,比如本书使用的 Linux kernel 的版本号为4.1.15
,
因此 modprobe 命令默认会到/lib/modules/4.1.15
这个目录中查找相应的驱动模块
,一般自己制作的根文件系统
中是不会有这个目录的,所以需要自己手动创建。 -
驱动模块的卸载使用命令
rmmod
即可。
代码
- 参数 filp 有个叫做 private_data 的成员变量, private_data 是个 void 指针,一般在驱动中将private_data 指向设备结构体,设备结构体会存放设备的一些属性。
chrdevbase 不是实际存在的一个设备,是笔者为了方便讲解字符设备的开发而引入的一个虚拟设备。chrdevbase 设备有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。
- chrdevbase.c
// chrdevbase 驱动文件
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include <linux/fs.h>
//#include<linux/slab.h>
//#include<linux/io.h>
#include<linux/uaccess.h>
#define CHRDEVBASE_MAJOR 200 // 在代码里设置主设备号,加载模块前要先看看该主设备号是否已经被占用
#define CHRDEVBASE_NAME "chrdevbase" // 设备名称
static char readbuf[100]; // 内核空间,读缓存区
static char writebuf[100]; // 内核空间,写缓存区
static char kerneldata[] = {"kernel data!"};
// @param – inode : 传递给驱动的 inode
// @param - filp : 设备文件, file 结构体有个叫做 private_data 的成员变量,一般在 open 的时候将 private_data 指向设备结构体。
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase_open\r\n");
return 0;
}
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase_release\r\n");
return 0;
}
// @param - filp : 要打开的设备文件(文件描述符)
// @param - buf : 返回给用户空间的数据缓冲区
// @param - count : 要读取的数据长度
// @param - offt : 相对于文件首地址的偏移
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count,
loff_t *ppos)
{
int ret = 0;
memcpy(readbuf, kerneldata, sizeof(kerneldata));
ret = copy_to_user(buf, readbuf, count);
if(ret == 0)
{
;
}
else
{
;
}
return 0;
}
// @param - filp : 设备文件,表示打开的文件描述符
// @param - buf : 要写给设备写入的数据
// @param - count : 要写入的数据长度
// @param - offt : 相对于文件首地址的偏移
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret = 0;
ret = copy_from_user(writebuf, buf, count);
if(ret == 0)
{
printk("Kernel receive data : %s\r\n", writebuf);
}
else
{
;
}
return 0;
}
// 设备操作函数结构体
static struct file_operations chrdevbase_fops=
{
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_release,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
// 驱动入口函数,调用modprobe时会执行此函数
static int __init chrdevbase_init(void)
{
int ret = 0;
// 此处位于内核空间,不能用printf;内核如何想要向控制台输出一些信息,要用printk
printk("chrdevbase_init\r\n");
// register char dev func 注册字符设备
ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, \
&chrdevbase_fops);
if(ret < 0)
{
printk("chrdevbase init failed\r\n");
}
return 0;
}
static void __exit chrdevbase_exit(void)
{
// unregister char dev func,注销字符设备,此为kernel提供的api
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit\r\n");
}
//向内核注册一个模块加载函数,入口函数
module_init(chrdevbase_init);
//向内核注册一个模块卸载函数,出口函数
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JL<MAIL ADDRESS>");
- chrdevbaseAPP.c
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
// chrdevApp filename 1/2
// 1 -> read
// 2 -> write
int main(int argc, char **argv)
{
int ret = 0;
int fd = 0;
char *filename;
char readbuf[100], writebuf[100];
static char usrdata[] = {"user data!"};
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)
{
ret = read(fd, readbuf, 50);
if(ret < 0)
{
printf("Read file \"%s\" error!\r\n", filename);
return -1;
}
else
{
printf("App read data: %s\r\n", readbuf);
}
}
if(atoi(argv[2]) == 2)
{
memcpy(writebuf, usrdata, sizeof(usrdata));
ret = write(fd, writebuf, 50);
if(ret < 0)
{
printf("Write file \"%s\" error!\r\n", filename);
return -1;
}
else
{
;
}
}
ret = close(fd);
if(ret < 0)
{
printf("Close file \"%s\" error!\r\n", filename);
return -1;
}
else
{
;
}
return 0;
}
- Makefile
chrdevbaseAPP.c 需要在shell下使用交叉编译器来编译。
arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP
KERNELDIR := /home/jl/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
-
一般将上面的代码编译成模块,在调试的时候加载或者卸载驱动模块即可。
module_init(xxx_init);
//注册模块加载函数
module_exit(xxx_exit);
//注册模块卸载函数 -
将编译出来的.ko文件放到根文件系统里面。
2、对于一个新的模块,使用modprobe
加载时,需要先调用一下depmod
命令。
3、卸载驱动使用shell命令:rmmod xxx.ko
。(执行chrdevbase_exit 函数
)
4、查看已有的驱动使用命令:lsmod
测试流程
-
1、加载驱动
初次实验的话需要手动创建目录/lib/modules/4.1.15
,(如果你的内核版本不是4.1.15
的话就要修改成你自己的版本。)
初次实验的话需要在shell下面输入depmod
这个命令就能自动生成modules.dep
(,然后就会自动生成modules.alias
、modules.symbols
和modules.dep
这三个文件。否则会提示无法打开modules.dep
)
cat /proc/devices
查看代码里使用的主设备号是否已经被占用
modprobe chrdevbase.ko
加载驱动模块
lsmod
查看当前系统中存在的模块,看一下有没有加载成功 -
2、创建设备结点
进入 /dev 查看设备文件,名为chrdevbase
ls /dev/
看看有没有chrdevbase
这个文件
会发现没有,因为没有创建设备节点。
创建/dev/chrdevbase
这个设备节点文件:mknod /dev/chrdevbase c 200 0
(c 表示字符设备,200是主设备号,0是次设备号)
然后就能在/dev
目录下看到chedevbase
这个设备文件了 -
3、运行测试app 并 观察现象
app在/lib/modules/4.1.15
中。(sudo cp chrdevbase.ko chrdevbaseApp /home/.../linux/nfs/rootfs/lib/modules/4.1.15/ -f
是编译完手动拷贝过去的)
./chrdevbaseAPP /dev/chrdevbase 1(2)