40 字符设备驱动开发

本文深入介绍了Linux驱动开发的基础知识,包括驱动分类(字符设备、块设备、网络设备)、驱动功能、用户空间与内核空间的交互、设备号管理以及驱动的加载与卸载。通过示例代码解析了字符设备驱动的注册、注销过程,并展示了如何通过insmod和modprobe命令加载和卸载驱动模块。此外,还提供了驱动测试的完整流程,帮助读者理解Linux驱动开发的实践操作。
摘要由CSDN通过智能技术生成

相关概念

  • 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.aliasmodules.symbolsmodules.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)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值