最近正在学习设备驱动开发,因此打算写一个系列博客,即是对自己学习的一个总结,也是对自己的一个督促,有不对,不足,需要改正的地方还望大家指出,而且希望结识志同道合的朋友一起学习技术,共同进步。
作者:liufei_learning(转载请注明出处)
email:[email protected]
IT学习交流群:160855096
1.实现
1)程序分析框图:
2)程序流程
3)原理图
2.步骤
3.分析
1)字符设备注册问题
2)file_operations()
3)ioctl
4)设备节点的自动创建
程序分析框图: |
函数 fun:leds_open() fun:leds_ioctl() fun:leds_init() fun:leds_exit() 结构体 struct:led_table struct:led_cfg_table struct:led_fops |
调用 控制程序执行open()时,会调用此函数 控制程序执行ioctl)时,会调用此函数 加载模块时会调用此函数 卸载函数时会调用此函数 功能 控制LED的IO口 LEDIO口的模式 注册字符设备时,通过此结构体关联leds_open(),Leds_ioctl() |
程序流程 |
首先通过入口函数module_init(leds_init),进入leds_init()进行初始化操作,设置GPIO口,注册字符设备,通过led_fops结构体关联leds_open(),Leds_ioctl(),创建设备节点,卸载时调用leds_exit()注销设备删除设备节点 |
由原理图得知LED电路是共阳极的,并分别由2440的GPB5、GPB6、GPB7、GPB8口控制
(参考http://hbhuanggang.cublog.cn,添加自动加载设备节点 )
1.去掉内核已有的LED驱动设置,因为IO口与tq2440开发板不一致 修改arch/arm/plat-s3c24xx/common-smdk.c
2.编写tq2440的LED驱动,代码如下
3.编写应用程序测试LED驱动
4. 把LED驱动代码部署到内核中去
#cp -f tq2440_leds.c/linux-2.6.30.4/drivers/char //把驱动源码复制到内核驱动的字符设备下
#vim /linux-2.6.30.4/drivers/char/Kconfig //添加LED设备配置
#vim /linux-2.6.30.4/drivers/char/Makefile //添加LED设备配置
obj-$(CONFIG_TQ2440_LEDS)+=tq2440_leds.o
5. 配置内核,选择LED设备选项
#make menuconfig
Device Drivers --->
Character devices --->
<*>TQ2440 Leds Device (NEW)
6.编译内核make zImage
7.编译测试程序#arm-linux-gcc -o led_test led_test.c 将生成的文件复制到开发板 /usr/sbin下
8.开发板上测试
查看已加载的设备:#cat/proc/devices,可以看到tq2440_leds的主设备号为231
控制led输入led_test on 1可以看到对应led被点亮
1.字符设备注册问题
register_chrdev()
intregister_chrdev(unsigned int major, const char *name,
const structfile_operations *fops)
register_chrdev()为字符设备注册一个主设备号
参数:majar:主设备号若==0,则系统动态分配一个主设备号,>0系统尝试用所给的值存储主设备号
成功返回0,失败返回错误errno:-ve
name:设备名称
fops:与此设备相关的文件操作
设备号的范围为:0~255
2.6内核以后的大量驱动代码, 有许多字符驱动不使用上面代码的方法. 那是还没有更新到 2.6 内核接口的老代码. 因为那个代码实际上能用,这个更新可能很长时间不会发生. 为完整, 我们描述老的字符设备注册接口, 但是新代码不应当使用它; 这个机制在将来内核中可能会消失(LDD3),所以下一篇blog将讨论新的方法。不过老的方法我觉得还是有必要学习一下,了解系统实现的原理,有助于跟深入的理解字符设备驱动。
在Linux中,字符设备是用一个叫做字符设备结构的数据结构chardevicestruct来描述的。为了管理上的方便,系统维护了一个数组chrdevs[],该数组的每一项都代表一个字符没备。
在文件linux/fs/char_dev.c中定义的char_device_struct的数据结构及数组chrdevs[]代码如下:
结构中的一个域name是指向设备驱动程序名的指针;另一个域fops是指向-个封装了文件操作函数集结构的指针。这些文件操作函数就是对这个字符设备进行具体的如打开、读、写、关闭等文件操作驱动程序。
字符设备注册表结构如图所示。当安装一个字符设各时,须调用注册函数regesterchardev()向注册表插入一个新的表项。函数regester_chardev()的原型如下:
图字符设备驱动程序的注册
当代表-个字符设备的文件被进程打开时,系统根据设备主、次设各号,查询上述的chrdevs[]数组,并获得fops指针和为进程设置-个描述这个字符特眯文件跑数握结构file,进而通过fops指针调用指定的驱动程序。
取消注册的函数为unregister_chrdev()。其原型如下:
intunregister_chrdev int major,const char*name):
设备驱动程序的注册和取消注册应分别在模块的初始化函数和析构函数中完成。
2.file_operations()
struct module *owner
第一个file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载.几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
loff_t (*llseek) (structfile *, loff_t, int);
llseek方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file结构中的位置计数器( 在"file 结构" 一节中描述).
ssize_t (*read) (structfile *, char __user *, size_t, loff_t *);
用来从设备中获取数据.在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败.一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
ssize_t(*aio_read)(struct kiocb *, char __user *, size_t, loff_t);
初始化一个异步读-- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).
ssize_t (*write) (structfile *, const char __user *, size_t, loff_t *);
发送数据给设备.如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
ssize_t(*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);
初始化设备上的一个异步写.
int (*readdir) (structfile *, void *, filldir_t);
对于设备文件这个成员应当为NULL; 它用来读取目录, 并且仅对文件系统有用.
unsigned int (*poll)(struct file *, struct poll_table_struct *);
poll方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll方法为 NULL, 设备假定为不阻塞地可读可写.
int (*ioctl) (structinode *, struct file *, unsigned int, unsigned long);
ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表.如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.
int (*mmap) (struct file*, struct vm_area_struct *);
mmap用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
int (*open) (struct inode*, struct file *);
尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
int (*flush) (struct file*);
flush操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前,flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL,内核简单地忽略用户应用程序的请求.
int (*release) (structinode *, struct file *);
在文件结构被释放时引用这个操作.如同 open, release 可以为 NULL.
int (*fsync) (struct file*, struct dentry *, int);
这个方法是fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
int (*aio_fsync)(structkiocb *, int);
这是fsync 方法的异步版本.
int (*fasync) (int,struct file *, int);
这个操作用来通知设备它的FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.
int (*lock) (struct file*, int, struct file_lock *);
lock方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.
ssize_t (*readv) (structfile *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (structfile *, const struct iovec *, unsigned long, loff_t *);
这些方法实现发散/汇聚读和写操作.应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和write 方法被调用( 可能多于一次 ).
ssize_t(*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
这个方法实现sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用.设备驱动常常使 sendfile 为 NULL.
ssize_t (*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);
sendpage是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.
unsigned long(*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsignedlong, unsigned long);
这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.
int (*check_flags)(int)
这个方法允许模块检查传递给fnctl(F_SETFL...) 调用的标志.
int (*dir_notify)(structfile *, unsigned long);
这个方法在应用程序使用fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify.
3.ioctl
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:
intioctl(int fd, ind cmd, …);
其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。
ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。
用法很简单就是通过switch解析命令
4.设备节点的自动创建
在刚开始写Linux设备驱动程序的时候,很多时候都是利用mknod命令手动创建设备节点,实际上Linux内核为我们提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提条件是用户空间移植了udev。
内核中定义了structclass结构体,顾名思义,一个structclass结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。通过device_destroy();class_destroy();来注销类和节点