概述
linux系统将设备分为了字符设备、块设备和网络设备三大类。
字符设备是指在I/O传输过程中以字符为单位进行传输的设备,字符设备是面向流的设备,占linux设备驱动的绝大部分,常见的字符设备有鼠标、键盘、LED等。
注意
1、之前的文章已经提到过,在linux下,一切皆文件,字符设备当然也不例外,linux将字符设备抽象成设备文件,。因此用户操作硬件,实则是操作设备文件。
2、目标机中dev目录下存放着所有加载的设备文件,执行命令
ls /dev/dev_feng -lh,如下图所示,可以看到设备文件dev_feng 的具体属性。
/ # ls /dev/dev_feng -l crw-rw---- 1 0 0 249, 0 Mar 9 06:05 /dev/dev_feng
c:表示此文件为字符设备文件
249:表示此字符设备文件包含的主设备号
0:表示字符设备文件的次设备号
dev_feng :表示字符设备文件名
3、每个设备驱动文件在创建之前都必须向内核申请设备号,设备号用来唯一标识当前设备,由主设备号和次设备号两部分组成,数据类型为dev_t(unsigned int)。
主设备号:设备号的高12bit位,用于标明驱动,一个驱动仅有唯一的主设备号。
次设备号:设备号的低20bit位,用于标明硬件,一个硬件个体仅有唯一的次设备号。
linux内核提供设备号、主设备号、次设备号相互转换的宏。
/* 设备号、主设备号、次设备号相互转换的宏, 在linux/kdev_t.h中 */ #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
接口
1、在linux中字符设备通过结构体struct cdev表示。
/* linux内核描述字符设备的数据结构, 在linux/cdev.h中 */ struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; /* 操作接口 */ struct list_head list; /* 内核链表 */ dev_t dev; /* 设备号 */ unsigned int count; /* 次设备号个数 */ };
2、应用和驱动接口的调用关系:
应用程序调用open/.../close->C库调用open/.../close->软中断->内核调用sys_open/.../sys_close->驱动调用open/.../close接口->应用open/.../close返回
3、linux内核用结构体struct file_operations来描述字符设备驱动的硬件操作接口数据结构, 在linux/fs.h中定义。
/* linux内核描述字符设备驱动的硬件操作接口数据结构, 在linux/fs.h中 */ 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 *); /* 写入 */ long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* ioctl */ int (*open) (struct inode *, struct file *); /* 打开 */ int (*release) (struct inode *, struct file *); /* 关闭 */ ... };
4、字符设备的注册(register_chrdev)和卸载(unregister_chrdev),一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行 。
/** * @向内核注册字符设备 * @major: 主设备号,当用户设置为0时,内核会动态分配一个设备号 * @name: 字符名,使用cat /proc/devices查看 * @fops: 与此设备相关联的文件操作 */ static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops); /** * @向内核卸载字符设备 * @major: 主设备号 * @name: 字符名 */ static inline void unregister_chrdev(unsigned int major, const char *name);
5、创建字符设备节点文件。
a)、手动方式创建,使用mknode命令。该方法相对比较繁琐,且存在以下缺点:
①. 加载完驱动后,如果驱动加载顺序改变,必须重新通过cat /proc/devices命令获取申请的主设备号
②. 每次系统重启,要手动mknod创建。
/** * @创建字符设备节点 mknod:创建节点命令 * @chr_name: 节点名字 c: 表明字符设备 * @244:主设备号 0:次设备号 */ mknod /dev/chr_name c 244 0 chr_name - 字符设备名字
b)、自动方式创建。
①. 前提:自动创建方式使用busybox的mdev命令实现,所以必须保证根文件系统包含mdev命令,且在/etc/init.d/rcS 文件中有如下语句:echo /sbin/mdev > /proc/sys/kernel/hotplug。
②. 创建类:在创建设备节点之前必须先创建类,类的创建和销毁通过函数class_create和class_destroy完成,函数在linux/device.h中定义。
/** * @创建类 * @owner: 一般为THIS_MODULE * @name: 类名,后期通过cd /sys/class查看 * @返回创建的类 */ extern struct class * __must_check __class_create(struct module *owner, const char *name, struct lock_class_key *key); #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ }) /** * @销毁类 * @cls: 销毁的类 */ void class_destroy(struct class *cls);
③. 创建设备节点:通过device_create函数创建,device_destroy函数销毁。
/** * @创建设备节点 * @class: 类 parent:父设备,一般为NULL devt:设备号 * @drvdata:设备可能会使用的一些数据,一般为 NULL * @fmt: 设备名,后期通过cd /sys/class/xxx/下查找 * @返回创建好的设备 */ struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); /** * @销毁设备节点 * @class: 类 devt:设备号 */ void device_destroy(struct class *class, dev_t devt);
示例
★一直以来,C语言一直被定义为面向过程语言,主要是因为其缺少一些面向对象的语法(class类),但是我们在构建大型程序的时候一定要具备面向对象的思想来构建。
★C语言实现面向对象的思路大多是通过结构体和函数指针的方式,本示例中将字符设备处理单独抽象为一个模块(chrdev.c/chrdev.h),使用结构体struct class_chrdev定义为类名。
属性:设备号、设备名、类、设备节点。
行为:无。
★字符设备模块feng_chrdev,通过调用chrdev模块,创建字符设备,并实现了设备的打开、关闭、读写等操作。
★应用程序模块main,用户通过对字符设备文件操作映射到设备驱动,从而实现对硬件的操作。注意,在main和驱动层之间应该再多一层,用于封装设备节点相关操作接口,不过考虑到篇幅,所以此处省略了,直接在main中操作。
★包含头文件chrdev.h和源文件chrdev.c、feng_chrdev.c、main.c和编译规则文件Makefile(均已验证通过)。
chrdev.h
/** * @Filename : chr_dev.h * @Revision : $Revision: 1.00 $ * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅) * @Description : 字符设备驱动框架类定义 **/ #ifndef __CHR_DEV_H__ #define __CHR_DEV_H__ #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/device.h> #include <linux/string.h> /* 设备类资源定义 */ struct class_chrdev { unsigned int major; /* 记录设备号 */ char chr_name[20]; /* 字符设备名 */ struct class *cls; /* 记录类 */ struct device *dev; /* 记录设备节点 */ }; /** * 创建字符设备对象,成功返回类地址,失败返回NULL * @cls_name: 类名,使用cd /sys/class查看 * @chr_name: 字符名,使用cat /proc/devices查看 * @dev_name: 设备名,使用cd /sys/class/xxx/下查找 * @fops: file operations */ struct class_chrdev *chrdev_create(char *cls_name, char *chr_name, char *dev_name, struct file_operations *fops); /** * 销毁字符设备对象 * @chrdev: 字符设备对象 */ void chrdev_destroy(struct class_chrdev *chrdev); #endif
chrdev.c
/** * @Filename : chr_dev.c * @Revision : $Revision: 1.00 $ * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅) * @Description : 字符设备驱动框架类定义 **/ #include "chrdev.h" /** * 创建字符设备对象,成功返回类地址,失败返回NULL * @cls_name: 类名,使用cd /sys/class查看 * @chr_name: 字符名,使用cat /proc/devices查看 * @dev_name: 设备名,使用cd /sys/class/xxx/下查找 * @fops: file operations */ struct class_chrdev *chrdev_create(char *cls_name, char *chr_name, char *dev_name, struct file_operations *fops) { struct class_chrdev *chrdev = NULL; if ((chrdev = kmalloc(sizeof(struct class_chrdev), GFP_KERNEL)) == NULL) { printk(KERN_ERR "Allocation of port list failed\n"); return NULL; } /* 创建设备号,动态分配 */ if ((chrdev->major = register_chrdev(0, chr_name, fops)) > 0) printk("register chrdev ok...\n"); else printk("register chrdev error...\n"); strcpy(chrdev->chr_name, chr_name); /* 创建类和节点 */ chrdev->cls = class_create(THIS_MODULE, cls_name); chrdev->dev = device_create(chrdev->cls, NULL, MKDEV(chrdev->major, 0), NULL, dev_name); return chrdev; } /** * 销毁字符设备对象 * @chrdev: 字符设备对象 */ void chrdev_destroy(struct class_chrdev *chrdev) { device_destroy(chrdev->cls, MKDEV(chrdev->major, 0)); class_destroy(chrdev->cls); unregister_chrdev(chrdev->major, chrdev->chr_name); kfree(chrdev); } EXPORT_SYMBOL_GPL(chrdev_create); EXPORT_SYMBOL_GPL(chrdev_destroy); MODULE_LICENSE("GPL"); MODULE_AUTHOR("feng"); /* 模块的作者 */ MODULE_VERSION ("1.00"); /* 模块版本号 */
feng_chrdev.c
/** * @Filename : feng_chrdev.c * @Revision : $Revision: 1.00 $ * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅) * @Description : 字符设备驱动框架示例 **/ #include "chrdev.h" struct class_chrdev *feng_chr; /* 字符设备对象定义 */ /** * 打开接口实现 */ static int feng_open(struct inode *inode, struct file *file); /** * 关闭接口实现 */ static int feng_close(struct inode *inode, struct file *file); /** * 读取接口实现 */ static ssize_t feng_read(struct file *file, char __user *user, size_t size, loff_t *off); /** * 写入接口实现 */ static ssize_t feng_write(struct file *file, const char __user *user, size_t size, loff_t *off); /** * ioctl接口实现 */ static long feng_ioctl(struct file *file, unsigned int cmd, unsigned long buf); /* 字符设备驱动硬件操作接口 */ static struct file_operations feng_fops = { .read = feng_read, .write = feng_write, .unlocked_ioctl = feng_ioctl, .open = feng_open, .release = feng_close, }; /** * 打开接口实现 */ static int feng_open(struct inode *inode, struct file *file) { printk("kernel:feng_open...\n---------------------------------\n"); return 0; } /** * 关闭接口实现 */ static int feng_close(struct inode *inode, struct file *file) { printk("kernel:feng_close...\n---------------------------------\n"); return 0; } /** * 读取接口实现 */ static ssize_t feng_read(struct file *file, char __user *user, size_t size, loff_t *off) { printk("kernel:feng_read...\n---------------------------------\n"); return 0; } /** * 写入接口实现 */ static ssize_t feng_write(struct file *file, const char __user *user, size_t size, loff_t *off) { printk("kernel:feng_write...\n---------------------------------\n"); return 0; } /** * ioctl接口实现 */ static long feng_ioctl(struct file *file, unsigned int cmd, unsigned long buf) { printk("kernel:feng_ioctl...\n---------------------------------\n"); return 0; } /* 驱动模块入口函数 */ static int __init feng_drv_init(void) { /* 注册设备 */ if ((feng_chr = chrdev_create("cls_feng", "chr_feng", "dev_feng", &feng_fops)) == NULL) { printk("create feng char device error...\n"); return -1; } return 0; } /* 驱动模块出口函数 */ static void __exit feng_drv_exit(void) { chrdev_destroy(feng_chr); /* 释放设备 */ } module_init(feng_drv_init); module_exit(feng_drv_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("feng"); /* 模块的作者 */ MODULE_VERSION ("1.00"); /* 模块版本号 */
main.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <string.h> #define CHR_DEV_FILE "/dev/dev_feng" /* 设备驱动文件名 */ int main(int argc, char *argv[]) { /* 打开文件 */ int index, ret; int fd = 0; /* 测试open */ printf("app:test open...\n"); if ((fd = open(CHR_DEV_FILE, O_RDWR)) < 0) { printf("app:%s file open failed...\n", CHR_DEV_FILE); return -1; } /* 测试read */ printf("app:test read...\n"); ret = read(fd, &index, sizeof(index)); /* 测试write */ printf("app:test write...\n"); ret = write(fd, &index, sizeof(index)); /* 测试ioctl */ printf("app:test ioctl...\n"); ioctl(fd, 10, &index); /* 关闭文件 */ printf("app:test close...\n"); close(fd); return 0; }
Makefile
#根文件所在目录 ROOTFS_DIR = /home/feng/atomic/rootfs #交叉编译工具链 CROSS_COMPILE = arm-linux-gnueabihf- CC = $(CROSS_COMPILE)gcc #目标文件名 TAR_NAME = feng #应用程序名字 APP_NAME = chr_test #驱动目录路径 DRV_DIR = $(ROOTFS_DIR)/home/drv DRV_DIR_LIB = $(ROOTFS_DIR)/lib/modules/4.1.15 #动态库目录路径 LIB_DIR = $(ROOTFS_DIR)/home/lib #应用程序目录路径 APP_DIR = $(ROOTFS_DIR)/home/app #KERNELRELEASE由内核makefile赋值 ifeq ($(KERNELRELEASE), ) #内核路径 KERNEL_DIR =/home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga #当前文件路径 CURR_DIR = $(shell pwd) all: #编译模块 make -C $(KERNEL_DIR) M=$(CURR_DIR) modules #编译应用程序 -$(CC) -o $(APP_NAME) main.c clean: #清除模块文件 make -C $(KERNEL_DIR) M=$(CURR_DIR) clean #清除应用文件 -rm $(APP_NAME) install: #拷贝模块文件 #cp -raf $(TAR_KEY_NAME)_drv.ko $(TAR_KEY_NAME)_dev.ko $(DRV_DIR) #cp -raf keyin.ko wq.ko timer.ko $(DRV_DIR_LIB) cp -raf *.ko $(DRV_DIR_LIB) #拷贝应用文件 -cp -raf $(APP_NAME) $(APP_DIR) else #指定编译什么文件 obj-m += $(TAR_NAME)_chrdrv.o chrdev.o #obj-m += $(TAR_NAME).o endif
结论
1、进入目录,执行make命令编译模块;然后执行make install命令,拷贝模块到目标机指定目录。
feng:chr_dev$ make #编译模块 make -C /home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/mnt/hgfs/Share/linux/atomic/driver/chr_dev modules make[1]: 进入目录“/home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga” Building modules, stage 2. MODPOST 2 modules make[1]: 离开目录“/home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga” #编译应用程序 arm-linux-gnueabihf-gcc -o chr_test main.c feng:chr_dev$ make install #拷贝模块文件 #cp -raf _drv.ko _dev.ko /home/feng/atomic/rootfs/home/drv #cp -raf keyin.ko wq.ko timer.ko /home/feng/atomic/rootfs/lib/modules/4.1.15 cp -raf *.ko /home/feng/atomic/rootfs/lib/modules/4.1.15 #拷贝应用文件 cp -raf chr_test /home/feng/atomic/rootfs/home/app feng:chr_dev$
2、在目标机上执行modprobe命令加载模块。
注意:在模块加载之前,需要先调用depmod命令,生成模块依赖文件。
/sys/class/cls_feng # depmod /sys/class/cls_feng # modprobe feng_chrdrv register chrdev ok... /sys/class/cls_feng #
3、使用ls /sys/class命令查看是否存在cls_feng类。
/ # ls /sys/class ata_device firmware mem rc thermal ata_link gpio misc regulator tty ata_port graphics mmc_host rfkill ubi backlight i2c-dev mtd rtc udc bdi ieee80211 net scsi_device vc block input power_supply scsi_disk video4linux cls_feng lcd pps scsi_host vtconsole dma leds ptp sound watchdog drm mdio_bus pwm spi_master
4、在目标机上执行cat /proc/devices命令查看是否存在chr_feng设备。
/ # cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 207 ttymxc 226 drm 249 chr_feng 250 ttyLP 251 watchdog 252 ptp 253 pps 254 rtc Block devices: 1 ramdisk 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc / #
5、在目标机上执行ls /sys/class/cls_feng/或者ls /dev/命令查看是否存在dev_feng设备文件。
/ # ls /sys/class/cls_feng/ dev_feng / # / # ls /dev/ autofs ram14 tty37 bus ram15 tty38 console ram2 tty39 cpu_dma_latency ram3 tty4 dev_feng ram4 tty40 dri ram5 tty41 fb0 ram6 tty42 full ram7 tty43 fuse ram8 tty44 hwrng ram9 tty45 i2c-0 random tty46 i2c-1 rfkill tty47 input rtc0 tty48 kmsg snd tty49 loop-control tty tty5 loop0 tty0 tty50 loop1 tty1 tty51 loop2 tty10 tty52 loop3 tty11 tty53 loop4 tty12 tty54 loop5 tty13 tty55 loop6 tty14 tty56 loop7 tty15 tty57 mem tty16 tty58 memory_bandwidth tty17 tty59 mmcblk1 tty18 tty6 mmcblk1boot0 tty19 tty60 mmcblk1boot1 tty2 tty61 mmcblk1p1 tty20 tty62 mmcblk1p2 tty21 tty63 mmcblk1rpmb tty22 tty7 network_latency tty23 tty8 network_throughput tty24 tty9 null tty25 ttymxc0 pps0 tty26 ttymxc1 pps1 tty27 ubi_ctrl ptmx tty28 urandom ptp0 tty29 vcs ptp1 tty3 vcs1 pts tty30 vcsa ram0 tty31 vcsa1 ram1 tty32 video0 ram10 tty33 watchdog ram11 tty34 watchdog0 ram12 tty35 zero ram13 tty36 / #
6、在目标机上运行应用测试程序,查看是否正确调用驱动相应的接口信息。
/ # /home/app/chr_test app:test open...kernel:feng_open... --------------------------------- app:test read...kernel:feng_read... --------------------------------- app:test write...kernel:feng_write... --------------------------------- app:test ioctl...kernel:feng_ioctl... --------------------------------- app:test close...kernel:feng_close... --------------------------------- / #
7、在目标机上执行modprobe -r命令卸载模块。
/ # modprobe -r feng_chrdrv / # lsmod Module Size Used by Tainted: G / #
8、综上、示例展示了字符设备驱动框架,linux系统将字符设备驱动抽象为设备文件,应用程序使用open/close/read/write等接口操作设备文件,最终映射到驱动相当于调用驱动的open/release/read/write接口,所以对于驱动而言,最主要就是实现这些文件操作接口函数。
往期 · 推荐
帮你自动化办公的python-自动提取pdf指定页(文件处理篇)
帮你自动化办公的python-自动提取pdf指定页(项目概述)
也没想象中那么神秘的数据结构-一种通用化的双向链表设计(底层源码)
关注
更多精彩内容,请关注微信公众号:不只会拍照的程序猿,本人致力分享linux、设计模式、C语言、嵌入式、编程相关知识,也会抽空分享些摄影相关内容,同样也分享大量摄影、编程相关视频和源码,另外你若想要本文章源码请关注公众号:不只会拍照的程序猿,后台回复:linux驱动源码。