驱动入门的进一步深入

发布于:2025-06-24 ⋅ 阅读:(20) ⋅ 点赞:(0)

继上一篇驱动入门的进一步深入

*********************

注册字符设备驱动新接口
老接口:
ret = register_chrdev(MYMAJOR,MYNAME,&test_fops);

包括主设备号、名字、file_operations

新接口:
register_chrdev_region/alloc_chrdev_region + cdev_xx

register_chrdev_region:
需要指定设备号


alloc_chrdev_region :
内核分配设备号

cdev_xx:
包括cdev_init 、 cdev_add 、cdev_del等操作


设备号:
MKDEV、MAJOR、MINOR三个宏
MKDEV 返回值类型:dev_t类型

***************************************
指定设备号方法:
*

首先,定义static dev_t dev_id;

先用MKDEV申请一个设备号:
dev_id = MKDEV(MYMAJOR, 0);    //设备号

MYMAJOR是指定的主设备号
后面的是次设备号

然后注册:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

retval = register_chrdev_region(dev_id, MYCNT,MYNAME);
    if (retval) {
        printk(KERN_ERR "Unable to register minors for usb_device\n");
        return retval;
    }
用返回的dev_id
MYCNT:注册的数量
MYNAME:注册的名字
retval:返回值成功是0 ,失败返回负数


然后,注册驱动:
static struct cdev test_cdev;
static const struct file_operations test_fops
cdev_init(&test_cdev, &test_fops);    //绑定两个结构体

retval = cdev_add(&test_cdev, dev_id, 1);        //添加驱动
    if (retval)
    cdev_del(&test_cdev);


最后,注销
unregister_chrdev_region(dev_id, 1);

**************************************
上面就是完整的用新接口注册驱动的步骤


***********************
用内核自动分配设备号:

自动分配的话就不需要先用MKDEV申请设备号:
dev_id = MKDEV(MYMAJOR, 0);    //设备号

而是直接用定义的结构体变量加上定义定义的子设备号,加上名字
就可以完成自动分配设备号

typedef u_long dev_t;(源结构体)

static dev_t mydev;

// 第1步:分配主次设备号
    retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);

int alloc_chrdev_region(dev_t *dev, u[nsigned baseminor, unsigned count,
            const char *name)
{
    struct char_device_struct *cd;
    cd = __register_chrdev_region(0, baseminor, count, name);
    if (IS_ERR(cd))
        return PTR_ERR(cd);
    *dev = MKDEV(cd->major, cd->baseminor);
    return 0;
}


*****************
这里对led灯的驱动操作
在模块层次代码中,应该要简洁,及对read进来的相关指令,这个指令应该是
简单的,例如1、2、3这样的,可以用宏定义
就是说,较为复杂的一些操作,例如对输入的指令的判断,用到的
strcmp等,应该在应用层完成


**********************
这里是对设备注册的一个操作
之前注册驱动时用到的是一个全局变量的结构体,
占据固定内存
static struct cdev test_cdev;

现在,用到
struct cdev *cdev_alloc(void)
{
    struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    if (p) {
        INIT_LIST_HEAD(&p->list);
        kobject_init(&p->kobj, &ktype_cdev_dynamic);
    }
    return p;
}

这一个函数,可以设置一个struct cdev类型的指针
struct cdev *cdev;

static struct cdev *test_cdev;


    //注册驱动,使用cdev_alloc
    test_cdev = cdev_alloc();
    if (!test_cdev)
        return -EINVAL;

    test_cdev->owner = THIS_MODULE;
    test_cdev->ops = test_fops;
    
    //cdev_init(&test_cdev, &test_fops);    //绑定两个结构体
    
这里使用的*指针的意义就是,用全局变量指定一个指针,只占一个指针大小的内存,
当需要使用时,就用cdev_alloc为指针动态分配一个堆内存,指向用的结构体,
需要注意的就是,在内核中,堆内存的释放一般都不是简单的对应关系,
它是内部有一个数的增加机制,有用到申请堆内存的就加一,用完就减一,
目前还不太了解其中机制

释放函数:
kfree(cls);


struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

这个就是用到的结构体,里面有指向模块、文件操作、链表、设备等
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}

这里不使用cdev_init,
而是直接实例化结构体其中的几个,
实际上也是和上面的函数是一样的,只不过是少实例几个而已

还有就是,上面实例化的owner
是在file_operation中的那个,
.owner        = THIS_MODULE,


********************
总结:

以上的就是关于设备号的新旧使用,手动/自动分配使用,
关于设备注册的函数使用,
一个是全局变量直接定义,一个是定义一个全局变量的指针使用
关于设备的初始化,
可以用init函数,也可以直接实例化

*************
解决每次需要手动增添设备文件的不足:
手动添加设备文件:
[root@sugar /root]# mknod /dev/test c 250 0


解决方案:
udev(嵌入式中使用mdev)
udev 是 Linux 系统中的一个子系统,用于管理设备事件。

mdev :
BusyBox 提供的轻量级设备管理工具,相当于精简版的 udev。


rootfs中Etc/init.d/rcS文件
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

echo /sbin/mdev > /proc/sys/kernel/hotplug


mdev -s

作用:初始化 /dev 目录下的设备节点。

原理:

-s 参数让 mdev 扫描 /sys 文件系统,根据当前已连接的硬件信息,
在 /dev 目录下动态创建设备节点(如 /dev/sda、/dev/ttyUSB0 等)

作用:将 /sbin/mdev 设置为内核的 热插拔事件处理程序。

补充:
/proc/sys/kernel/hotplug 的特殊性:
该文件存在于内存中(procfs),重启后失效,
需在启动脚本(如 /etc/rc.local)中持久化配置

完整 Linux 系统通常使用 udev 或 systemd-udevd,
但 mdev 更适合资源受限的嵌入式环境


总结:
这两条命令共同实现了 设备节点的动态创建/删除,
是嵌入式 Linux 自动识别硬件的基础

udev是应用层的一个应用程序
内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
应用层启动udev,内核驱动中使用相应接口
驱动注册和注销时信息会传给udev,由udev在应用层进行设备
文件的创建和删除


使用相关函数添加设备文件:
1、class_create
//源定义源函数
static struct class *adb_dev_class;
#define class_create(owner, name)        \
({                        \
    static struct lock_class_key __key;    \
    __class_create(owner, name, &__key);    \
})

owner:fos中的owner,指定类的所有者是哪个模块
name:自定义的class名字

//创建类
test_dev_class = class_create(THIS_MODULE, "sugar_class");
if (IS_ERR(test_dev_class))
    return -EINVAL;
    
    
//结束时销毁
void class_destroy(struct class *cls)
{
    if ((cls == NULL) || (IS_ERR(cls)))
        return;

    class_unregister(cls);
}    

class_destroy(test_dev_class);
    


2、device_create
//设备文件创建,就是mknod 创建的文件名
struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...)
{
    va_list vargs;
    struct device *dev;

    va_start(vargs, fmt);
    dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
    va_end(vargs);
    return dev;
}


device_create(test_dev_class, NULL, dev_id, NULL, "test");
    

void device_destroy(struct class *class, dev_t devt)
{
    struct device *dev;

    dev = class_find_device(class, NULL, &devt, __match_devt);
    if (dev) {
        put_device(dev);
        device_unregister(dev);
    }
}

device_destroy(test_dev_class,dev_id);

************************
上面的使用就是一个设备类的创建,
类中的一个设备文件的创建(就是mknod创建的设备文件的名字)
结合:
创建:
class_create()
device_create()

销毁:
device_destroy()
class_destroy()

*********************
成功现象:
[root@sugar /root]# insmod  module_test.ko 
[   39.180515] major = 250, minor = 0.
[root@sugar /root]# lsmod
    Not tainted
module_test 2102 0 - Live 0xbf000000
[root@sugar /root]# ls /dev/test -l
crw-rw----    1 root     0         250,   0 Jan  1 12:00 /dev/test
[root@sugar /root]# ./app 

直接安装执行,不需要再手动mknod创建设备文件
insmod后直接查看也可以查看到device_destroy中输入的test文件


******************
查看创建的class
[root@sugar /sys]# cd class/
[root@sugar class]# ls
sugar_class

[root@sugar class]# cd sugar_class/
[root@sugar sugar_class]# ls
test

[root@sugar sugar_class]# cd test/
[root@sugar test]# ls
dev        power      subsystem  uevent

用cat查看,可以查看里面的相关信息
[root@sugar test]# cat uevent 
MAJOR=250
MINOR=0
DEVNAME=test

[root@sugar test]# cat dev
250:0

函数代码分析:
class_create()
    __class_create(owner, name, &__key); //实际调用
        cls = kzalloc(sizeof(*cls), GFP_KERNEL);//申请堆内存
        __class_register    //注册
            cp = kzalloc(sizeof(*cp), GFP_KERNEL);
            kset_register
                kobject_uevent
                    struct kobject *kobj //输出型参数,作为返回值
                    返回的就是kobject结构体的相应参数
                    struct kobject {
                        const char        *name;
                        struct list_head    entry;
                        struct kobject        *parent;
                        struct kset        *kset;
                        struct kobj_type    *ktype;
                        struct sysfs_dirent    *sd;
                        struct kref        kref;
                        unsigned int state_initialized:1;
                        unsigned int state_in_sysfs:1;
                        unsigned int state_add_uevent_sent:1;
                        unsigned int state_remove_uevent_sent:1;
                        unsigned int uevent_suppress:1;
                    };
                    
        kfree(cls);


device_create()
    device_create_vargs
        dev = kzalloc(sizeof(*dev), GFP_KERNEL);
        
        dev->devt = devt;
        dev->class = class;
        dev->parent = parent;
        dev->release = device_create_release;
        dev_set_drvdata(dev, drvdata);
        
        kobject_set_name_vargs
        device_register
            device_initialize
            device_add
                kobject_add
                device_create_file
                device_create_sys_dev_entry
                devtmpfs_create_node
                device_add_class_symlinks
                device_add_attrs
                bus_add_device
                dpm_sysfs_add
                device_pm_add //power management
                kobject_uevent //输出结构体相关参数
                
                kfree(dev->p);

*************************
以上就是两个类和设备文件的函数分析

***********************
新旧字符设备号的注册:
register_chrdev
register_chrdev_region/alloc_chrdev_region


register_chrdev
    __register_chrdev(major, 0, 256, name, fops);//实际调用
        __register_chrdev_region(major, baseminor, count, name);
            if (major == 0) {
                for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
                    if (chrdevs[i] == NULL)
                        break;
            }//这里的处理就是解释到为什么设备号的分配从后往前分配


register_chrdev_region
    __register_chrdev_region(MAJOR(n), MINOR(n),
                   next - n, name);


alloc_chrdev_region
    __register_chrdev_region(0, baseminor, count, name);
    


总结:
实际上,三个都是调用的__register_chrdev_region这个函数,
只不过是里面的传进去的参数的定义不同
新的这个两个主要是设备号定义,为0

旧的是无法定义子设备号

*************************
静态页表的分析:
之前对寄存器的操作有:
静态映射、动态映射两种方法

静态映射:
使用的就是自定义好的寄存器地址相关的虚拟地址

其中,涉及到的一个就是静态映射表,
有了这个表才能实现一段物理地址到虚拟地址的映射关系

1、映射表涉及到的物理地址和虚拟地址的值的相关宏定义
2、映射表的建立函数
/kernel/arch/arm/mach-s5pv210/mach-smdkc110.c
smdkc110_map_io
    s5p_init_io(NULL, 0, S5P_VA_CHIPID);
        iotable_init(s5p_iodesc, ARRAY_SIZE(s5p_iodesc));
            create_mapping
上面函数涉及到的就是io表的初始化,创建映射

iotable_init中涉及到的映射有s5p_iodesc
这是一个结构体数组,数组中的每一个元素就是一个映射,
描述了一段物理地址到虚拟地址之间的映射

表中记录的这些映射关系被iotable_init调用,这个函数负责将这个结构体
数组格式的表建立称MMU所能识别的页表映射关系,这样开机后就可以直接
使用对应的虚拟地址访问对应的物理地址

其中,页表显示都是4k、8k、16k...

arch/arm/plat-s5p/cpu.c
映射表对应的结构体数组:
/* Initial IO mappings */

static struct map_desc s5p_iodesc[] __initdata = {
    {
{
        .virtual    = (unsigned long)S5P_VA_GPIO,
        .pfn        = __phys_to_pfn(S5P_PA_GPIO),
        .length        = SZ_4K,
        .type        = MT_DEVICE,
    },

这块就是我们要操作的gpio对应的映射关系转换模块


还有一个问题,就是开机时调用映射表建立函数
开机时(kernel启动时)smdkc110_map_io怎么被调用的
start_kernel(main.c)
    setup_arch(573)
        paging_init
            devicemaps_init
                /*
                * Ask the machine support to map in the statically mapped devices.
                */
                    if (mdesc->map_io)
                    mdesc->map_io();


************************
动态映射
这里解决一个问题,就是动态映射对多个寄存器需要多次映射的问题

解决:
仿效真实驱动中,用结构体封装的方式来进行单次多个寄存器的地址映射
这样就可以不用多次进行单个寄存器的映射了

//定义一个结构体包含两个寄存器
typedef struct GPJ0REG{
    volatile unsigned int    gpj0_con;
    volatile unsigned int    gpj0_dat;
}gpj0_refs;

注意:
定义结构体不能用static,
如果我们在文件作用域内使用`static`关键字修饰一个结构体定义,
这是不允许的,编译器会报错。
因为结构体定义本身并不占用存储空间,它只是一个类型描述


还有,结构体内定义的变量不要与外部变量名字有冲突,否则有错

#define GPJ0_PA_BASE     0xe0200240

gpj0_refs *pGPJ0_REG;

//用两步完成动态映射
if (!request_mem_region(GPJ0_PA_BASE, sizeof(gpj0_refs), "GPJ0REG"))
    return -EINVAL;
pGPJ0_REG=ioremap(GPJ0_PA_BASE, sizeof(gpj0_refs));
    


//销毁
iounmap(pGPJ0_REG);
    
release_mem_region(GPJ0_PA_BASE, sizeof(gpj0_refs));
    

**********************************
最后,是一个writel、readl函数
用来操作寄存器(当完成映射后)

writel()
    #define writel(b,addr) __raw_writel(__cpu_to_le32(b),addr)
    __raw_writel
        #define __raw_writel(v,a)    (__chk_io_ptr(a), *(volatile unsigned int __force   *)(a) = (v))
        这个就是实际工作的函数
        实质就是我们之前设置的一个定义即操作的寄存器地址+要进行的操作
        #define rGPJ0CON    *((volatile unsigned int *)S5PV210_GPJ0CON)
        #define rGPJ0DAT    *((volatile unsigned int *)S5PV210_GPJ0DAT)

        #define GPJ0CON_PA    0xe0200240
        static void __iomem *baseaddr;            // 寄存器的虚拟地址的基地址
        通过ioremap 得到虚拟地址
        writel(0x11111111, baseaddr+0);
        writel(((0<<3) | (0<<4) | (0<<5)), baseaddr+4);
        
iowrite32()
    #define iowrite32(v, addr)    writel((v), (addr))
    writel
实际就是上面的函数


************************
至此,可以完成一个led的驱动
其中,包括使用
1、新旧设备号注册函数、驱动的alloc申请(alloc_chrdev_region)
2、对test_cdev的一个操作,一个是用全局变量做全部的初始化,
一个使用一个全局指针,后面对齐实例化操作(替换一个cdev_init()函数)
//static struct cdev test_cdev;
static struct cdev *test_cdev;
3、类的创建
目的就是不用手动mknod取创建设备文件
    class_create
    device_create

4、对动态映射的申请优化,使用一个指针接收虚拟地址映射,
可以用结构体定义其中的寄存器,亦可以自己在地址中加上相应的偏移量
request_mem_region
ioremap

5、对寄存器的操作
iowrite32
writel


 


网站公告

今日签到

点亮在社区的每一天
去签到