42 新字符设备驱动

一、新字符设备驱动原理

  • 1、以前的缺点
    使用了register_chrdev 浪费了很多 次设备号 。而且需要我们手动设置 主设备号

  • 2、设备号申请函数 (page 1029,1060)
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 直向 内核 申请设备号。
    dev : 申请好的设备号 存放在第一个参数里面
    baseminor : 次设备号起始地址,此函数可以申请 一段连续的多个设备号 ,这些设备号的 主设备号 一样,次设备号 不同,次设备号以 baseminor 为起始地址开始递增。一般设置 baseminor 为0,也就是说次设备号从 0 开始。
    count:要申请的设备号的数量
    name:字符设备的名字

  • 3、设备号注册函数
    如果给定了设备的 主设备号次设备号 就使用如下所示函数来注册设备号即可 (page 1060)
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    一般是给定主设备号,然后使用 MKDEV(major, 0) (一般次设备号选择为0),构建完整的 dev_t,
    参数 from 是 要申请的起始设备号,也就是给定的设备号;
    参数 count 是 要申请的数量 ,一般都是一个;
    参数 name 是设备名字

  • 4、设备号释放函数
    不管是使用了设备号申请函数还是设备号注册函数取得了可用的设备号,在出口函数中都要统一使用下面的设备号释放函数来释放掉当前的设备号
    void unregister_chrdev_region(dev_t from, unsigned count) 来释放掉前面申请的设备号。page 1029
    from:要释放的设备号
    count:表示从 from 开始,要释放的设备号数量

int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */

if (major) 
{ 	/* 定义了主设备号 */
	devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
	register_chrdev_region(devid, 1, "test");
} 
else 
{ 	/* 没有定义设备号 */
	alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
	major = MAJOR(devid); /* 获取分配号的主设备号 */
	minor = MINOR(devid); /* 获取分配号的次设备号 */
}
  • 5、实际的驱动编写
    考虑要完善,两种情况:1、给定主设备号。2、没有给定主设备号
    1、dev_t = MKDEV(major, 0); register_chrdev_region(dev_t, 1, "test") ; major是给定的主设备号,0表是次设备号设置为0,dev_t 是申请到的设备号
    2、alloc_chrdev_region(&dev_t, 0, 1, "test"); major = MAJOR(dev_t); minor = MINOR(dev_t);

  • 6、字符设备注册
    struct cdev 结构体表示字符设备(page 1061)
    定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化。
    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
    初始化完cdev结构体之后,使用cdev_add 函数向 Linux 系统添加字符设备(cdev 结构体变量)。
    int cdev_add(struct cdev *p, dev_t dev, unsigned count)

二、自动创建设备节点

  • 1、在以前的实验中,都需要手动调用 mknod /dev/xxx c/b major minot 来手动创建设备节点。
    为此,2.6内核引入了 udev 机制,替换 devfs。udev机制提供了热插拔管理,可以在 加载驱动的时候 自动创建 /dev/xx 设备文件,卸载时会自动删除 /dev/xx 文件。最终自动创建设备节点的工作是在驱动程序的入口函数中完成的
    busybox 提供了 udev 的简化版本,mdev。嵌入式中常用,用于提供热插拔管理。
    mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理
  • 2、struct class 结构体 / 类 创建宏
    自动创建设备节点的工作 是在 驱动程序的入口函数 中完成的,一般在 cdev_add 函数后面添加 自动创建设备节点相关代码 。首先要创建一个struct class 类,定义在文件 include/linux/device.h里面。
    struct class *class_create (struct module *owner, const char *name)
    参数 owner 一般为 THIS_MODULE
    参数 name 是类名字
    返回值是个指向结构体 class 的指针,也就是创建的类
  • 3、类删除函数
    卸载驱动程序的时候(出口函数中)需要删除掉类
    void class_destroy(struct class *cls)
    参数 cls 就是要删除的类。
  • 4、设备创建函数(可变参数函数)
    创建好 struct class 类以后还不能实现 自动创建设备节点,我们还需要在这个类下创建一个设备。使用 此函数 在类下面创建设备
    struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
    参数 class 就是设备要创建哪个类下面
    参数 parent 是父设备,一般为 NULL,也就是没有父设备
    参数 devt 是设备号
    参数 drvdata 是设备可能会使用的一些数据,一般为 NULL
    参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成 /dev/xxx 这个设备文件
    返回值就是创建好的设备
  • 5、设备删除函数
    卸载驱动的时候需要删除掉创建的设备
    void device_destroy(struct class *class, dev_t devt)
    参数 classs 是要删除的设备所处的类
    参数 devt 是要删除的设备号

三、文件私有数据

  • 1、在 open 函数里面设置 filp->private_data 为设备变量,指向这个结构体。
  • 2、在其它的函数里面,如 read、write里面要访问设备的时候,通过读取私有数据访问设备结构体。在此结构体里面记录一个设备的所有属性信息(参照 cm4000_cs.c
// led 设备结构体,自定义用来描述这个 led 设备(体现这个设备的所有属性信息)
struct newchrled_dev
{
    struct cdev chrdev; // 用来注册字符设备
    dev_t devid; // 设备号
    struct class *class; // 用来自动创建设备节点
    struct device *device; // 用来自动创建设备节点
    int major;
    int minor;
};

static int test_open(struct inode *inode, struct file *filp)
{
	// struct file 结构体定义在 include/linux/fs.h 中
	// struct file 有一个成员(空指针):void *private_data
	filp->private_data = &testdev; /* 设置私有数据 */
	return 0;
}

四、源码

  • newcheled.c
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include <linux/fs.h>
#include<linux/slab.h> // kmalloc 和 kfree 头文件
#include<linux/io.h>
#include<linux/uaccess.h>
#include<linux/cdev.h>
#include<linux/device.h>


// LED外设相关寄存器的物理地址
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)

// 定义几个全局变量,用来存放 iomap函数 得到的虚拟地址
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

#define LED_OFF 0
#define LED_ON 1

#define NEWCHRLED_NAME "newchrled"

// 自定义一个 led 设备结构体,自定义用来描述这个 led 设备
struct newchrled_dev
{
    struct cdev chrdev; // 用来注册字符设备,操作集结构体存于此
    dev_t devid; // 设备号
    struct class *class; // 用来自动创建设备节点
    struct device *device; // 用来自动创建设备节点
    int major;
    int minor;
};

static void led_switch(u8 state)
{
    u32 val = 0;
    if(state == LED_ON)
    {
        val = readl(GPIO1_DR);
        val &= (~(1<<3));
        writel(val, GPIO1_DR);
    }
    else if(state == LED_OFF)
    {
        val = readl(GPIO1_DR);
        val |= (1<<3);
        writel(val, GPIO1_DR);
    }
}

// 自定义结构体类型的变量
static struct newchrled_dev newchrled;

static int newchrled_open(struct inode *inode, struct file *filp)
{
	// struct file 结构体定义在 include/linux/fs.h 中
	// struct file 有一个成员(空指针):void *private_data
    filp->private_data = &newchrled;
    return 0;
}

static int newchrled_release(struct inode *inode, struct file *filp)
{
    struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
    return 0;
}

static ssize_t newchrled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    int ret;
    u8 databuf[1];

    ret = copy_from_user(databuf, buf, count);
    if(ret < 0)
    {
        printk("Kernel write failed.\r\n");
        return -1;
    }
    led_switch(databuf[0]);
    return 0;
}

// 操作集结构体,存于 struct cdev 中
static const struct file_operations newchrled_fops =
{
    .owner = THIS_MODULE,
    .write = newchrled_write,
    .open = newchrled_open,
    .release = newchrled_release,
};

// 驱动入口函数
static int __init newchrled_init(void)
{
    int ret = 0;
    int val = 0;
   
    // led 寄存器地址映射
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    // 初始化 led 相关寄存器
    val = readl(IMX6U_CCM_CCGR1);
    val &= (~(3<<26));
    val |= (3<<26);
    writel(val, IMX6U_CCM_CCGR1);
   
    writel(0X5, SW_MUX_GPIO1_IO03);//io 复用
    writel(0X10B0, SW_PAD_GPIO1_IO03); // 设置电气属性
   
    val = readl(GPIO1_GDIR); // 设置方向
    val |= (1<<3); 
    writel(val, GPIO1_GDIR);

    val = readl(GPIO1_DR);
    val &= (~(1<<3));// 设置高低电平
    writel(val, GPIO1_DR);

    // 获得可用的设备号
    newchrled.major = 0;//(准备采用系统分配的方式来获取设备号)
    if(newchrled.major) // 注意是判断主设备号
    {
        // 主设备号非 0 的话表示用户给定设备号
        newchrled.devid = MKDEV(newchrled.major, 0); // 使用此函数获得完整的设备号,次设备号设置为0
        ret = register_chrdev_region(newchrled.devid, 1, NEWCHRLED_NAME);
    }
    else
    {
        //  未指定设备号,选择由系统分配,次设备号从 0 开始,分配 1 个
        ret = alloc_chrdev_region(&newchrled.devid, 0, 1, NEWCHRLED_NAME);
        newchrled.major = MAJOR(newchrled.devid);
        newchrled.minor = MINOR(newchrled.devid);
    }
    if(ret < 0)
    {
        printk("Newchrled chedev_region err.\r\n");
        //return -1;
        goto fail_devid;
    }
    printk("newchrdevled: major = %d, minor = %d.\r\n", newchrled.major, newchrled.minor);

    // 新的字符设备注册方法,采用 cdev 结构体(对结构体里面的两个成员赋值即可)
    newchrled.chrdev.owner = THIS_MODULE;
    // 注意两个取地址符!注意两个取地址符!注意两个取地址符!
    cdev_init(&newchrled.chrdev, &newchrled_fops);
    // 初始化完 cedv 结构体后进行添加
    ret =  cdev_add(&newchrled.chrdev, newchrled.devid, 1);
    if(ret < 0)
    {
        goto fail_cdev;
    }

    // 创建设备结点
    newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
    if(IS_ERR(newchrled.class))
    {
        //return PTR_ERR(newchrled.class);
        ret = PTR_ERR(newchrled.class);
        goto fail_class;
    }
	// 创建好类以后还不能实现自动创建设备节点,还需要在这个类下创建一个设备。
    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
    if(IS_ERR(newchrled.device))
    {
        //return PTR_ERR(newchrled.device);
        ret = PTR_ERR(newchrled.device);
        goto fail_device;
    }

    return 0;

fail_device:
    class_destroy(newchrled.class);
fail_class:
    cdev_del(&newchrled.chrdev);
fail_cdev:
    unregister_chrdev_region(newchrled.devid, 1);
fail_devid:
    return ret;
}

// 出口函数
static void __exit newchrled_exit(void)
{
    unsigned int val = 0;

	// 关闭 led 灯
    val = readl(GPIO1_DR);
    val |= (1<<3); 
    writel(val, GPIO1_DR);
    
    // 取消 led 相关寄存器的地址映射
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    printk("---%d---%s---\r\n", __LINE__, __func__);
    
    // 删除字符设备
    cdev_del(&newchrled.chrdev);
    
    // 释放设备号
    unregister_chrdev_region(newchrled.devid, 1);

	// 删除掉创建的设备
    device_destroy(newchrled.class, newchrled.devid);
    
	// 删除掉用于创建设备结点的类
    class_destroy(newchrled.class);
}

// 向内核注册 驱动加载函数 和 驱动卸载函数
module_init(newchrled_init);
module_exit(newchrled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("J");
  • ledAPP.c
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
// ./ledAPP filename  0/1
// 0 -> turn off led
// 1 -> turn on led

#define LED_OFF 0
#define LED_ON 1
int main(int argc, char **argv)
{
    int ret = 0;
    int fd = 0;
    char *filename;
    unsigned char databuf[1];

    if(argc != 3)
    {
        printf("Error usage!\r\n");// user room
        return -1;
    }
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd < 0)
    {
        printf("Can't open file \"%s\"\r\n", filename);
        return -1;
    }
    databuf[0] = atoi(argv[2]);
    ret = write(fd, databuf, sizeof(databuf));
    if(ret < 0)
    {
        printf("Led control failed.\r\n");
        ret = close(fd);
        if(ret < 0)
        {
            printf("Close file \"%s\" error!\r\n", filename);
            return -1;
        }
        return -1;
    }
    ret = close(fd);
    if(ret < 0)
    {
        printf("Close file \"%s\" error!\r\n", filename);
        return -1;
    }
    else
    {
        ;
    }

    return 0;
}
  • Makefile
KERNELDIR := /home/jl/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := newchrled.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

五、总结

  • 入口函数 / 驱动模块加载函数中
    1、两种方法获取可用设备号
    2、初始化、添加 struct cedv 结构体(两个参数:owner 指针、struct operation 结构体)。这就是新的字符设备注册方法。
    3、创建 struct class 类 和 struct device 结构体,用于自动创建设备结点。

  • 出口函数 / 驱动模块卸载函数中
    1、删除字符设备,释放 struct del 结构体
    2、释放设备号
    3、删除设备(没用到 struct device 结构体)
    4、删除类

  • 文件私有数据相关
    注意下面这个描述 设备属性结构体各项成员变量
    注意 filp->private_data = &newchrled_dev; ,在 struct operations 结构体里的 成员函数openreadwriterelease参数列表 里都有 struct file *filp,可以由 filp->private_data 来访问我们自定义的这个用来描述 设备属性 的结构体。

// led 设备结构体,自定义用来描述这个 led 设备(体现这个设备的所有属性信息)
struct newchrled_dev
{
    struct cdev chrdev;
    dev_t devid; //major + minor
    struct class *class;
    struct device *device;
    int major;
    int minor;
};

static int test_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled_dev; /* 设置私有数据 */
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值