linux字符设备驱动
-
- [0 驱动介绍](#0 驱动介绍)
- [1 字符设备驱动](#1 字符设备驱动)
-
- [1.1 字符设备相关概念和结构体](#1.1 字符设备相关概念和结构体)
- [1.2 实现简单的字符设备模块](#1.2 实现简单的字符设备模块)
- [1.3 创建字符设备](#1.3 创建字符设备)
- [1.4 总结](#1.4 总结)
应用程序调用文件系统的API(open、close、read、write) -> 文件系统根据访问的设备类型,调用对应设备的驱动API -> 驱动对硬件进行操作。
0 驱动介绍
驱动类型:
- 字符设备驱动:以一个字节一个字节读写的硬件对应的驱动。比如串口、watchdog、rtc、鼠标、触摸屏等。字符设备必须以串行顺序依次进行访问。
- 块设备驱动:以一个扇区一个扇区(512kb、4096kb)读写的硬件对应的驱动。比如硬盘。块设备可以按任意顺序进行访问。
- 网络设备驱动:走内核里的协议栈内存。比如网卡驱动、can驱动。网络设备面向数据包的接收和发送而设计。
除网络设备外,字符设备与块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open()、write()、read()、close()
等即可访问字符设备和块设备。
驱动的动态加载和静态加载区别:
- 编译方式不同:静态加载--
obj-y
,动态加载--obj-m
- 存在位置不同:静态加载,则直接编译到linux内核镜像中
uimag
;动态加载,则是独立的ko,驱动代码不在内核镜像中,而是独立存在于文件系统中 - 加载时机不同:静态加载,内核启动时就加载;动态加载,在系统启动的时候首先
uboot
的启动,然后linux内核启动,最后文件系统启动。静态加载,随linux内核一起加载启动,动态加载则需要等文件系统启动后手动insmod加载。
动态加载优势:
- 实现热插拔机制:硬件要实现热插拔,则驱动必须使用动态加载。设备插入时,加载驱动;设备移除时,卸载驱动。
- 驱动调试:调试更方便。单独编译驱动,通过insmod、rmmod加载和卸载进行单独调试。
- 开机优化:有的驱动加载非常耗时,影响设备启动速度。动态加载,可以等用户的图形界面起来后,再加载驱动,让用户感觉系统启动速度加快了。
1 字符设备驱动
字符设备必须以串行顺序依次进行访问,常见的字符设备:串口、watchdog、rtc、鼠标、触摸屏等。字符设备的读写以字节为单位。
1.1 字符设备相关概念和结构体
了解字符设备相关概念和结构体,为之后阅读字符设备驱动代码打下坚实基础。
1 设备号dev_t :类似一个人的身份证号。内核中通过dev_t
来描述设备号,其实质是unsigned int
32位整数,其中高12位为主设备号,低20位为次设备号。
- 为每一个外设设置一个独一无二的ID,让内核能够区分每一个设备。
- 高12区分不同类别的设备
- 低20位,区分同一类别的不同设备
linux如何为每一个设备生成设备号呢?
可以是驱动自行构造一个设备号,然后调用函数register_chrdev_region
向内核注册。如果注册成功,该函数返回0;注册失败,返回一个负数,就不能用这个设备号。
c
int register_chrdev_region(dev_t from, unsigned count, const char *name);
2 设备描述cdev、file_operations:
一个linux设备,需要通过cdev
结构体来描述设备信息,通过file_operations
来描述如何操作设备。最后要通过cdev_add
函数来注册设备,让linux内核知道这个设备。
c
/*设备信息的描述*/
struct cdev {
struct kobject kobj; /**/
struct module *owner;
const struct file_operations *ops; /*一组函数集*/
struct list_head list; /*链表*/
dev_t dev; /*设备号*/
unsigned int count; /*已支持的设备*/
}
/*设备行为的描述:驱动设备开发,就要实现这些函数*/
struct file_operations {
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
...
};
/*相关注册函数*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
1.2 实现简单的字符设备模块
c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/fs.h>
#define OK (0)
#define ERROR (-1)
#define DEV_NUM (10)
/*
创建设备ID:MKDEV
注册设备ID:
给设备行为结构体的成员函数赋值
初始化设备:建立设备行为结构体file_operations和设备信息结构体cdev的关系;
添加设备:向内核注册设备,使内核知道设备,建立设备ID和设备信息cdev的关系;
加载设备:insmod
创建设备文件:建立设备文件与设备ID的关系:mknod /dev/my_chr_dev c 232 0
测试设备行为结构体的成员函数功能
*/
dev_t g_dev_num; /*设备号*/
struct cdev *g_cdev; /*设备信息*/
struct file_operations *g_fops; /*设备行为信息*/
const int g_major = 232; /*主设备号*/
const int g_minor = 0; /*次设备号*/
unsigned g_dev_count = 3;
const char g_dev_name[] = "my_char_dev"; /*设备名称,内核管理用的,随便写*/
ssize_t my_dev_read(struct file *my_file, char __user *my_user, size_t my_size, loff_t *my_loff)
{
printk(KERN_EMERG "read my dev success\n");
return OK;
}
ssize_t my_dev_write(struct file *my_file, const char __user *my_user, size_t my_size, loff_t *my_loff)
{
printk(KERN_EMERG "write my dev success\n");
return OK;
}
int my_dev_open(struct inode *my_inode, struct file *my_file)
{
printk(KERN_EMERG "open my dev success\n");
return OK;
}
int my_dev_release(struct inode *my_inode, struct file *my_file)
{
printk(KERN_EMERG "release my dev success\n");
return OK;
}
static int __init chr_init(void)
{
g_dev_num = MKDEV(g_major, g_minor); /*生成设备号*/
printk("dev id:%d\n", g_dev_num);
/*注册设备号*/
if (register_chrdev_region(g_dev_num, g_dev_count, g_dev_name) != 0) {
printk(KERN_EMERG "register dev id failed\n");
return ERROR;
}
/*内核函数:自动生成设备函数, 避免自定义设备号冲突,导致注册失败
if (alloc_chrdev_region(&g_dev_num, g_major, g_dev_count, g_dev_name) != OK) {
printk(KERN_EMERG "register dev id failed\n");
return ERROR;
}
*/
/*为设备信息结构体cdev和设备行为结构体file_operations申请内存;kzalloc相比kmalloc,会将申请的内存地址置为0,*/
g_cdev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
g_fops = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
/*为设备行为结构体成员函数赋值*/
g_fops->read = my_dev_read;
g_fops->write = my_dev_write;
g_fops->open = my_dev_open;
g_fops->release = my_dev_release;
g_fops->owner = THIS_MODULE; /*一般就赋值为THIS_MODULE宏*/
cdev_init(g_cdev, g_fops); /*初始化设备:建立cdev和file_operations的联系*/
cdev_add(g_cdev, g_dev_num, g_dev_count); /*向内核注册设备:建立cdev和设备号的联系*/
printk(KERN_EMERG "add my_chr_dev success\n");
return OK;
}
static void __exit chr_exit(void)
{
cdev_del(g_cdev); /*删除设备*/
unregister_chrdev_region(g_dev_num, g_dev_count); /*注销设备号*/
printk(KERN_EMERG "unregister my_chr_dev success\n");
}
module_init(chr_init);
module_exit(chr_exit);
MODULE_LICENSE("GPL");
make编译模块后,insmod my_chr_dev
加载字符设备驱动。
makefile文件解读:
- ifneq,首先判断是否定义了内核宏,第一次执行由于没有定义内核宏,先执行else分支,然后执行all。
- make -C,切换到内核目录内,-M指定编译的模块。此时进入-M指定的模块内,再次执行makefile文件。
- 此时,内核宏定义了,执行if分支,编译内核模块。
makefile
ifneq ($(KERNELRELEASE),)
obj-m := my_chr_dev.o
else
KERDIR ?="/lib/modules/$(shell uname -r)"/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERDIR) M=$(PWD)
clean:
rm -rf *.o *.ko *mod.c 9.module.o *.order
endif
1.3 创建字符设备
mknod /dev/my_chr_dev c 232 0
,232 0
为主、次设备号。
mknod
,实际上就是建立设备文件my_chr_dev
与设备号232 0
之间的联系。
驱动测试程序:
c
#include <stdio.h>
#include <fcntl.h> /*O_RDWR*/
#include <unistd.h> /*read open write close usleep*/
#define DATA_NUM (10)
int main()
{
int fd;
int wd, rd;
char my_buffer[DATA_NUM] = "12345";
fd = open("/dev/my_chr_dev", O_RDWR);
printf("fd=%d\n", fd);
if (fd == -1){
printf("open failed\n");
return -1;
}
wd = write(fd, my_buffer, DATA_NUM);
rd = read(fd, my_buffer, DATA_NUM);
printf("wd=%d, rd=%d\n", wd, rd);
return 0;
}
该程序用于测试驱动程序的open、write、read、release
。编译执行该驱动程序后,执行dmesg
,可以查看内核调用驱动程序对应的open、write、read、release
函数时日志信息。
1.4 总结
开发字符设备驱动主要有7步:
- 创建设备号
MKDEV
、注册设备号register_chrdev_region
; - 编写设备行为结构体的成员函数:成员函数实现硬件的
open/close/read/write
等操作。 - 建立设备信息结构体
cdev
与设备行为结构体file_operations
之间的联系:cdev_init
。 - 向内核注册设备,建立设备信息结构体与设备号之间的联系:
cdev_add
。 make
编译、insmod
加载设备驱动。- 创建设备文件,建立设备文件与设备号之间的联系:
mknod /dev/chrDev c 主设备号 次设备号
。 - 编写测试脚本,测试驱动程序的
open/close/read/write
功能。