-
2021-11-21 20:40:23
系列文章目录
Linux字符设备驱动详解
Linux字符设备驱动详解二(使用设备驱动模型)
Linux字符设备驱动详解三(使用class)
Linux字符设备驱动详解四(使用自属的xbus驱动总线)
Linux字符设备驱动详解五(使用platform虚拟平台总线)
Linux字符设备驱动详解六(设备树实现RGB灯驱动)
Linux字符设备驱动详解七(“插件“设备树实现RGB灯驱动)前言
很久没有认真写一篇博客了,刚好最近学习了Linux字符设备驱动,好记性不如烂笔头,当然是要抓紧记下来,在开始之前安利一位师弟写的几篇博客,写得很不错。本文主要来自正点原子、野火Linux教程及本人理解,若有侵权请及时联系本人删除。
从单片机到ARM Linux驱动——Linux驱动入门篇
Linux字符设备驱动开发(2)——让开发板上的灯闪烁驱动目录
/dev/xxx
正文
Linux内核是怎么设计字符设备
结合前两篇文章,我这里讲的就比较简洁,下图是字符设备的整体框图。将其分为三步。
第一步:填充并保存硬件接口
这一步就是驱动文件所实现的,以原子LED驱动为例,第一步主要完成通过操作寄存器填充硬件接口,然后通过cdev_init函数保存接口file_operation到cdev中,通过cdev_add函数,根据哈希函数保存cdev到probes哈希表中,方便内核查找file_operation使用。而这两个函数下面代码register_chrdev(LED_MAJOR, LED_NAME, &led_fops) 已经帮我们都完成了,详情参考Linux源码。
下图说明了Linux使用一张哈希表(数组+链表)来管理设备号。
talk is cheap, show you the code(原子驱动文件)#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> #define LED_MAJOR 200 /* 主设备号 */ #define LED_NAME "led" /* 设备名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* 寄存器物理地址 */ #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) /* 映射后的寄存器虚拟地址指针 */ 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; /* * @description : LED打开/关闭 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED * @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) { val = readl(GPIO1_DR); val &= ~(1 << 3); writel(val, GPIO1_DR); }else if(sta == LEDOFF) { val = readl(GPIO1_DR); val|= (1 << 3); writel(val, GPIO1_DR); } } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat; retvalue = copy_from_user(databuf, buf, cnt); if(retvalue < 0) { printk("kernel write failed!\r\n"); return -EFAULT; } ledstat = databuf[0]; /* 获取状态值 */ if(ledstat == LEDON) { led_switch(LEDON); /* 打开LED灯 */ } else if(ledstat == LEDOFF) { led_switch(LEDOFF); /* 关闭LED灯 */ } return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 设备操作函数 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static int __init led_init(void) { int retvalue = 0; u32 val = 0; /* 初始化LED */ /* 1、寄存器地址映射 */ 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); /* 2、使能GPIO1时钟 */ val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); /* 清楚以前的设置 */ val |= (3 << 26); /* 设置新值 */ writel(val, IMX6U_CCM_CCGR1); /* 3、设置GPIO1_IO03的复用功能,将其复用为 * GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03); /*寄存器SW_PAD_GPIO1_IO03设置IO属性 *bit 16:0 HYS关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper功能 *bit [12]: 1 pull/keeper使能 *bit [11]: 0 关闭开路输出 *bit [7:6]: 10 速度100Mhz *bit [5:3]: 110 R0/6驱动能力 *bit [0]: 0 低转换率 */ writel(0x10B0, SW_PAD_GPIO1_IO03); /* 4、设置GPIO1_IO03为输出功能 */ val = readl(GPIO1_GDIR); val &= ~(1 << 3); /* 清除以前的设置 */ val |= (1 << 3); /* 设置为输出 */ writel(val, GPIO1_GDIR); /* 5、默认关闭LED */ val = readl(GPIO1_DR); val |= (1 << 3); writel(val, GPIO1_DR); /* 6、注册字符设备驱动 */ retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops); if(retvalue < 0){ printk("register chrdev failed!\r\n"); return -EIO; } return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); /* 注销字符设备驱动 */ unregister_chrdev(LED_MAJOR, LED_NAME); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("mu-xin");
通过Makefile编译,make后生成名为“led.ko”的驱动模块文件,使用insmod命令加载模块,到此完成第一步
第二步:创建设备文件(节点)
第二步使用mknod命令创建设备文件(节点)。接下来我们就可以在用户态操作这个文件(节点),比如向此设备文件(节点)写入数据。
sudo mknod /dev/xxx c 244 0
注意inode上的file_operation并不是自己构造的file_operation,而是字符设备通用的def_chr_fops,那么自己构建的file_operation等在应用程序调用open函数之后,才会绑定在文件上。第三步:用户空间编程
完成前两步我们就可以通过写APP文件,或者使用echo命令操作设备文件(节点)。
野火echo命令测试
野火驱动文件
#include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <asm/io.h> #define DEV_MAJOR 0 /* 动态申请主设备号 */ #define DEV_NAME "red_led" /*led设备名字 */ /* GPIO虚拟地址指针 */ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO04; static void __iomem *SW_PAD_GPIO1_IO04; static void __iomem *GPIO1_DR; static void __iomem *GPIO1_GDIR; static int led_open(struct inode *inode, struct file *filp) { return 0; } static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return -EFAULT; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { unsigned char databuf[10]; if(cnt >10) cnt =10; /*从用户空间拷贝数据到内核空间*/ if(copy_from_user(databuf, buf, cnt)){ return -EIO; } if(!memcmp(databuf,"on",2)) { iowrite32(0 << 4, GPIO1_DR); } else if(!memcmp(databuf,"off",3)) { iowrite32(1 << 4, GPIO1_DR); } /*写成功后,返回写入的字数*/ return cnt; } static int led_release(struct inode *inode, struct file *filp) { return 0; } /* 自定义led的file_operations 接口*/ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; int major = 0; static int __init led_init(void) { /* GPIO相关寄存器映射 */ IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4); SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4); SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4); GPIO1_GDIR = ioremap(0x0209c004, 4); GPIO1_DR = ioremap(0x0209c000, 4); /* 使能GPIO1时钟 */ iowrite32(0xffffffff, IMX6U_CCM_CCGR1); /* 设置GPIO1_IO04复用为普通GPIO*/ iowrite32(5, SW_MUX_GPIO1_IO04); /*设置GPIO属性*/ iowrite32(0x10B0, SW_PAD_GPIO1_IO04); /* 设置GPIO1_IO04为输出功能 */ iowrite32(1 << 4, GPIO1_GDIR); /* LED输出高电平 */ iowrite32(1<< 4, GPIO1_DR); /* 注册字符设备驱动 */ major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops); printk(KERN_ALERT "led major:%d\n",major); return 0; } static void __exit led_exit(void) { /* 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO04); iounmap(SW_PAD_GPIO1_IO04); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR); /* 注销字符设备驱动 */ unregister_chrdev(major, DEV_NAME); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL2"); MODULE_AUTHOR("embedfire "); MODULE_DESCRIPTION("led_module"); MODULE_ALIAS("led_module");
echo命令示例(注意echo命令的功能是在显示器上显示一段文字,该命令的一般格式为: echo [ -n ] 字符串,其中选项n表示输出文字后不换行;字符串能加单引号,也能不加单引号。该篇Linux字符设备驱动详解三(使用class))未加单引号
sudo sh -c "echo on >/dev/xxx" 开灯 sudo sh -c "echo on >/dev/xxx" 灭灯
原子APP文件测试
原子APP代码如下
#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" #define LEDOFF 0 #define LEDON 1 /* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功;其他 失败 */ int main(int argc, char *argv[]) { int fd, retvalue,i; char *filename; unsigned char databuf[1]; if(argc != 3){ printf("Error Usage!\r\n"); return -1; } filename = argv[1]; /* 打开led驱动 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("file %s open failed!\r\n", argv[1]); return -1; } databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭,使用字符转整形函数atoi()函数*/ /* 向/dev/led文件写入数据 */ for(i=0;i<10;i++) { retvalue = write(fd, databuf, sizeof(databuf)); if(retvalue < 0){ printf("LED Control Failed!\r\n"); close(fd); return -1; } sleep(1); retvalue = write(fd, 0, sizeof(0)); if(retvalue < 0){ printf("LED Control Failed!\r\n"); close(fd); return -1; } sleep(1); } retvalue = close(fd); /* 关闭文件 */ if(retvalue < 0){ printf("file %s close failed!\r\n", argv[1]); return -1; } return 0; }
通过命令编译生成ledApp这个应用程序
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
命令测试
./ledApp /dev/led 1 开灯 ./ledApp /dev/led 0 灭灯
当我们在用户空间调用open后发生了什么,在用户空间调用open函数后,系统由用户态进入内核态
- get_unused_fd_flags
- 为本次操作分配一个未使用过的文件描述符
- do_file_open
- 生成一个空白struct file结构体
- 从文件系统中查找到文件对应的inode
- do_dentry_open
static int do_dentry_open(struct file *f, struct inode *inode, int (*open)(struct inode *, struct file *)) { ... /*把inode的i_fop赋值给struct file的f_op*/ f->f_op = fops_get(inode->i_fop); ... if (!open) open = f->f_op->open; if (open) { error = open(inode, f); if (error) goto cleanup_all; } ... }
在do_dentry_open函数中调用下面的open函数
- def_chr_fops->chrdev_open
static int chrdev_open(struct inode *inode, struct file *filp) { const struct file_operations *fops; struct cdev *p; struct cdev *new = NULL; ... struct kobject *kobj; int idx; /*从内核哈希表cdev_map中,根据设备号查找自己注册的sturct cdev,获取cdev中的file_operation接口*/ kobj = kobj_lookup(cdev_map, inode>i_rdev,&idx); new = container_of(kobj, struct cdev, kobj); ... inode->i_cdev = p = new; ... fops = fops_get(p->ops); ... /*把cdev中的file_operation接口赋值给struct file的f_op*/ replace_fops(filp, fops); /*调用自己实现的file_operation接口中的open函数*/ if (filp->f_op->open) { ret = filp->f_op->open(inode, filp); if (ret) goto out_cdev_put; } ... }
总结
完成上面三步后就完成了LED字符设备的驱动编写,其他的字符设备可参考此驱动。
用户空间调用open函数打开某个设备文件后,在进程中会为设备文件分配一个未使用过的文件描述符,并且生成一个空白struct file结构体,然后从文件系统中查找到文件对应的inode,这里的inode也就是第二步自己在文件系统中创建的设备节点,然后把该inode的i_fop赋值给进程中的struct file的f_op,也就是说此时进程已经找到了设备节点了,但是还没有调用我们自己写的驱动,即真正的file_operation接口。
接下来为了调用真正的file_operation接口,我们从内核哈希表cdev_map中,根据设备号查找自己注册的sturct cdev,获取cdev中的file_operation接口,把cdev中的file_operation接口赋值给struct file的f_op,此时进程已经可以最终调用自己实现的file_operation接口中的open函数,至此进程获得了该设备文件的自己实现的读写等操作。
注意glibc库的fopen函数、系统调用open函数和自己实现的硬件接口open函数不能混淆。在字符设备驱动中系统调用open函数最终调用的是自己实现的硬件接口open函数。
最后,本篇基于Linux早期内核(2.4之前),没有统一的设备驱动模型,后面介绍Linux内核2.6版本以后的设备驱动模型,即使用挂载在/sys目录下的sysfs。
详情请阅读:Linux字符设备驱动详解二(使用设备驱动模型)更多相关内容 - get_unused_fd_flags
-
Linux字符设备驱动实现
2020-11-06 18:26:56编写一个字符设备驱动,并利用对字符设备的同步操作,设计实现一个聊天程序。可以有一个读,一个写进程共享该字符设备,进行聊天;也可以由多个读和多个写进程共享该字符设备,进行聊天 -
字符设备驱动
2018-10-13 22:04:57Linux设备驱动PPT,字符设备驱动PPT,真心讲的挺不错的 -
Linux字符设备驱动框架
2021-01-29 20:23:32字符设备是Linux三大设备...编写一个外部模块的字符设备驱动,除了要实现编写一个模块所需要的代码之外,还需要编写作为一个字符设备的代码。 Linux一切皆文件,那么作为一个设备文件,它的操作方法接口封装在structfi -
基于linux系统的字符设备驱动研究与设计.pdf
2021-09-06 18:22:35基于linux系统的字符设备驱动研究与设计.pdf -
Linux 字符设备驱动模板
2021-01-06 03:57:56在Linux内核里面,设备(device)主要分为字符设备,块设备,网络设备,字符设备驱动是Linux驱动基础,在看《Linux 设备驱动开发详解》这本书的过程中,把字符设备相知识记录整理如下。 字符设备驱动的组成 字符设备... -
基于Linux字符设备驱动程序的设计与实现.pdf
2021-09-06 23:43:55基于Linux字符设备驱动程序的设计与实现.pdf -
Linux 字符设备驱动框架详细介绍
2021-01-10 15:53:12Linux 字符设备驱动框架 字符设备是Linux三大设备之一(另外两种是块设备,网络设备),字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,常见的字符设备包括鼠标、键盘、显示器、串口等等,当我们执行... -
Linux内核设备驱动之字符设备驱动笔记整理
2020-09-15 01:33:31今天小编就为大家分享一篇关于Linux内核设备驱动之字符设备驱动笔记整理,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧 -
Linux字符设备驱动实验代码
2020-01-08 13:09:32简单的字符设备的驱动程序,并对所编写的设备驱动程序进行测试,了解Linux操作系统如何管理字符设备。由于网上许多资源不完整,本资源整合了许多内容。包括驱动程序memdev.c,memdev.h,app-mem.c,MakeFile文件。... -
linux 字符设备驱动 led驱动
2016-08-25 11:46:48linux,字符设备驱动,led驱动,gpio,有驱动源码和测试程序 -
嵌入式Linux字符设备驱动的设计与应用
2021-02-03 13:07:14嵌入式Linux字符设备驱动的设计与应用、电子技术,开发板制作交流 -
Linux设备驱动程序学习(1)-字符设备驱动程序.pdf
2021-10-08 16:09:40Linux设备驱动程序学习(1)-字符设备驱动程序.pdf -
基于嵌入式Linux的字符设备驱动程序的设计
2021-02-03 14:10:44基于嵌入式Linux的字符设备驱动程序的设计、电子技术,开发板制作交流 -
Linux内核设备驱动之高级字符设备驱动笔记整理
2021-01-10 05:28:56* 高级字符设备驱动 ******************/ (1)ioctl 除了读取和写入设备外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。比如弹出介质,改变波特率等等。这些操作通过ioctl方法... -
字符设备驱动编写流程.pdf
2019-06-29 14:17:47字符设备驱动编写流程,对设备初始化和释放; 2、把数据从内核传送到硬件和从硬件读取数据; 3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据; 4、检测和处理设备出现的错误 -
虚拟字符设备驱动程序在Linux的实现.pdf
2021-09-06 23:50:57虚拟字符设备驱动程序在Linux的实现.pdf -
基于ARM的嵌入式Linux字符设备驱动设计研究.pdf
2021-09-06 18:16:33基于ARM的嵌入式Linux字符设备驱动设计研究.pdf -
Linux 字符设备驱动(一).pdf
2019-08-03 15:29:09字符型驱动原理,适合刚入门的小白! -
linux 字符设备驱动程序 示例代码
2013-11-28 09:42:39linux字符设备驱动程序,示例代码。 共8个文件。包括内核态的驱动程序和用户态的测试例程。 -
Linux字符设备驱动总结
2011-11-29 00:26:30linux 字符设备驱动 字符设备是指在I/O传输过程中以字符为单位进行传输的设备,例如键盘,打印机等。请注意,以字符为单位并不一定意味着是以字节为单位,因为有的编码规则规定,1个字符占16比特,合2个字节。 在... -
linux 字符设备驱动-led
2017-07-25 15:07:34博客http://blog.csdn.net/qq_30951423/article/details/76071333中的字符设备驱动,调试通过的。 -
字符设备驱动程序实现读写功能
2014-01-10 00:11:50这是字符设备驱动的经典程序,globalmem可以实现对设备的读写操作,很有意思,希望大神们多多指教。 -
字符设备驱动程序_操作系统OS_完整课程设计报告
2014-03-28 13:25:43字符设备驱动程序 操作系统OS的课程设计 附带完整课程设计报告 -
linux字符设备驱动实例
2012-08-04 20:15:24本例子是一个linux字符设备驱动的最简单的例子,有详细的说明,适合初次接触者。 -
Linux增加字符设备驱动实验
2017-05-11 19:08:44Linux增加字符设备驱动实验,可以打印出一个hello world