Linux字符设备驱动开发一

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 驱动介绍

驱动类型:

  1. 字符设备驱动:以一个字节一个字节读写的硬件对应的驱动。比如串口、watchdog、rtc、鼠标、触摸屏等。字符设备必须以串行顺序依次进行访问。
  2. 块设备驱动:以一个扇区一个扇区(512kb、4096kb)读写的硬件对应的驱动。比如硬盘。块设备可以按任意顺序进行访问。
  3. 网络设备驱动:走内核里的协议栈内存。比如网卡驱动、can驱动。网络设备面向数据包的接收和发送而设计。

除网络设备外,字符设备与块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open()、write()、read()、close()等即可访问字符设备和块设备。

驱动的动态加载和静态加载区别:

  1. 编译方式不同:静态加载--obj-y,动态加载--obj-m
  2. 存在位置不同:静态加载,则直接编译到linux内核镜像中uimag;动态加载,则是独立的ko,驱动代码不在内核镜像中,而是独立存在于文件系统中
  3. 加载时机不同:静态加载,内核启动时就加载;动态加载,在系统启动的时候首先uboot的启动,然后linux内核启动,最后文件系统启动。静态加载,随linux内核一起加载启动,动态加载则需要等文件系统启动后手动insmod加载。

动态加载优势:

  1. 实现热插拔机制:硬件要实现热插拔,则驱动必须使用动态加载。设备插入时,加载驱动;设备移除时,卸载驱动。
  2. 驱动调试:调试更方便。单独编译驱动,通过insmod、rmmod加载和卸载进行单独调试。
  3. 开机优化:有的驱动加载非常耗时,影响设备启动速度。动态加载,可以等用户的图形界面起来后,再加载驱动,让用户感觉系统启动速度加快了。

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 0232 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步:

  1. 创建设备号MKDEV、注册设备号register_chrdev_region;
  2. 编写设备行为结构体的成员函数:成员函数实现硬件的open/close/read/write等操作。
  3. 建立设备信息结构体cdev与设备行为结构体file_operations之间的联系:cdev_init
  4. 向内核注册设备,建立设备信息结构体与设备号之间的联系:cdev_add
  5. make编译、insmod加载设备驱动。
  6. 创建设备文件,建立设备文件与设备号之间的联系:mknod /dev/chrDev c 主设备号 次设备号
  7. 编写测试脚本,测试驱动程序的open/close/read/write功能。
相关推荐
yaosheng_VALVE3 分钟前
稀硫酸介质中 V 型球阀的材质选择与选型要点-耀圣
运维·spring cloud·自动化·intellij-idea·材质·1024程序员节
看山还是山,看水还是。44 分钟前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
扣得君1 小时前
C++20 Coroutine Echo Server
运维·服务器·c++20
keep__go1 小时前
Linux 批量配置互信
linux·运维·服务器·数据库·shell
矛取矛求1 小时前
Linux中给普通账户一次性提权
linux·运维·服务器
Fanstay9851 小时前
在Linux中使用Nginx和Docker进行项目部署
linux·nginx·docker
大熊程序猿1 小时前
ubuntu 安装kafka-eagle
linux·ubuntu·kafka
death bell3 小时前
Docker基础概念
运维·docker·容器
ʚɞ4963 小时前
应用程序部署(IIS的相关使用,sql server的相关使用)
运维·服务器
少陽君3 小时前
服务器显卡和桌面pc显卡有什么不同
运维·服务器