驱动入门的进一步深入

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

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

注册字符设备驱动新接口

老接口:

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

相关推荐
魔都吴所谓21 小时前
【Ubuntu】22.04安装 CMake 3.24
linux·运维·ubuntu
齐潇宇21 小时前
Rsync+sersync 实现数据实时同步故障排查
linux·自动化·rsync·排障·数据同步排障
The Mr.Nobody21 小时前
基于STM32F407的 TFTP Server
arm开发·stm32·嵌入式硬件
飞凌嵌入式21 小时前
如何用JishuShell在RK3588核心板上快速部署OpenClaw?
arm开发·人工智能·嵌入式硬件·openclaw
余生皆假期-21 小时前
永磁同步电机的星形 (Y) 和三角形 (Δ) 有何不同?
单片机·嵌入式硬件
点灯小铭21 小时前
基于单片机的空气质量检测仪系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
86Eric21 小时前
基于 Rclone 实现 Linux 数据库备份自动同步至 Windows 本地
linux·windows·rclone 自动同步
SPC的存折21 小时前
6、MySQL设置TLS加密访问
linux·运维·服务器·数据库·mysql
鸟电波21 小时前
硬件笔记——Allegro绘制PCB(未完待续)
笔记·嵌入式硬件·智能硬件
cyber_两只龙宝21 小时前
【Docker】Docker的自定义网络详解
linux·运维·网络·docker·云原生·容器