旧接口注册LED字符驱动设备(静态映射)
#include <linux/init.h> // __init __exit #include <linux/module.h> // module_init module_exit #include <linux/fs.h> //file_operations #include <asm/uaccess.h> //copy_from_user copy_to_user #include <mach/regs-gpio.h> #include <mach/gpio-bank.h> #include <asm/string.h> #define rGPJ0CON *((volatile unsigned int *)S5PV210_GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)S5PV210_GPJ0DAT) static int led_open(struct inode *inode, struct file *file); ssize_t led_read(struct file *file, char __user *user, size_t count, loff_t *loff); ssize_t led_write(struct file *file, const char __user *user, size_t count, loff_t *loff); static int led_release(struct inode *inode, struct file *file); static int led_major = -1; static char kbuf[100] = {0}; static const struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; int led_open(struct inode *inode, struct file *file) { printk(KERN_INFO "led_open successful\n"); return 0; } ssize_t led_read(struct file *file, char __user *user, size_t ucount, loff_t *loff) { printk(KERN_INFO "led_read successful\n"); if (copy_to_user(user,kbuf , ucount)) { printk(KERN_INFO "copy_to_user fail\n"); return -EINVAL; } printk(KERN_INFO "copy_to_user successful\n"); return strlen(kbuf); } ssize_t led_write(struct file *file, const char __user *user, size_t ucount, loff_t *loff) { printk(KERN_INFO "led_write successful\n"); memset(kbuf,0,sizeof(kbuf)); if (copy_from_user(kbuf, user, ucount)) { printk(KERN_INFO "copy_from_user fail\n"); return -EINVAL; } if(!strcmp(kbuf,"on")) { rGPJ0CON &=0xff000fff; rGPJ0CON |=0x00111000; rGPJ0DAT &=~((0x01<<3)|(0x01<<4)|(0x01<<5)); } else if(!strcmp(kbuf,"off")) { rGPJ0CON &=0xff000fff; rGPJ0CON |=0x00111000; rGPJ0DAT |=((0x01<<3)|(0x01<<4)|(0x01<<5)); } return ucount; printk(KERN_INFO "copy_from_user successful\n"); } int led_release(struct inode *inode, struct file *file) { printk(KERN_INFO "led_release successful\n"); return 0; } // 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init successful\n"); if ((led_major = register_chrdev (0, "led_dev", &led_fops)) < 0) { printk(KERN_WARNING "led_module.c: Failed to register character device."); return -EINVAL; } return 0; } // 模块卸载函数 static void __exit chrdev_exit(void) { unregister_chrdev(led_major,"led_dev"); printk(KERN_INFO "chrdev_exit successful\n"); } module_init(chrdev_init); module_exit(chrdev_exit); // MODULE_xxx这种宏作用是用来添加模块描述信息 MODULE_LICENSE("GPL"); // 描述模块的许可证 MODULE_AUTHOR("Musk <qq:739112417>"); // 描述模块的作者 MODULE_DESCRIPTION("led driver"); // 描述模块的介绍信息 MODULE_ALIAS("led test driver"); // 描述模块的别名信息
上述程序是led非常简陋,手动注册的字符驱动代码。
此部分我从小往上分析。
一. 模块部分
1.1. 模块相关分析参考上篇文章
二.驱动设备注册
2.1. 认识file_operations结构体
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); };
linux应用层使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。
2.2. 注册&卸载字符驱动函数
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); } static inline void unregister_chrdev(unsigned int major, const char *name) { __unregister_chrdev(major, 0, 256, name); }
2.2.1. 上述两个函数一般是成对出现的,使用注册了,在不使用时可以卸载。
2.2.2. 需要注意的,register_chrdev注册时,注册的设备子设备号为0,@major参数对应需要注册的设备的主设备号,当此参数为0时表示由内核分配主设备号。
2.2.3. 在内核管理设备时,使用数组管理所有设备(1~255),其中主设备号对应数组的下标。
2.2.4. 使用cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
三. 内核空间与用户空间数据交互
3.1. 由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到用户空间的复制
3.2. 认识copy_to_user函数(函数在arch/arm/include/asm/uaccess.h)
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) { if (access_ok(VERIFY_WRITE, to, n)) n = __copy_to_user(to, from, n); return n; }
3.2.1. 此函数表示从内核空间拷贝n个字节到用户空间。
3.2.2. 先看函数的三个参数:*to是内核空间的指针,*from是用户空间指针,n表示从内核空间拷贝到用户空间数据的字节数。如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数
3.3. 认识copy_from_user函数(函数在arch/arm/include/asm/uaccess.h)
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) { if (access_ok(VERIFY_READ, from, n)) n = __copy_from_user(to, from, n); else /* security hole - plug it */ memset(to, 0, n); return n; }
3.3.1. 此函数表示从用户空间拷贝n个字节到内核空间。
3.3.2. 先看函数的三个参数:*to是内核空间的指针,*from是用户空间指针,n表示从用户空间拷贝内核空间到数据的字节数。如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数
四. 设备节点(设备文件)的手动创建
4.1. 关于设备节点(设备文件)
4.1.1. 在Linux中,所有的设备访问都是通过文件的方式,一般的数据文件程序普通文件,设备节点称为设备文件
4.1.2. 对应字符类设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
4.2. 创建设备节点
4.2.1. 使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号,其中xxx:表示设备节点名称(应用层可以使用open打开此文件)
五. 驱动中操作硬件
5.1. 虚拟地址
5.1.1. 由于内核层使用虚拟地址,无法字节使用物理地址操作寄存器。
5.1.2. 内核中物理地址映射分为:动态和静态
5.1.3. 静态映射方法的特点:
a. 在内核启动时建立静态映射表,在内核关机时销毁,中间一直有效,优点是执行效率高,缺点是始终占用虚拟地址空间,空间利用率低。
b. 不同版本内核静态映射表位置,文件名可能不同
c. 不同的SOC静态映射表位置,文件名可能不同
d. 所谓的映射表其实是头文件中的宏定义
5.1.4. 动态映射方法的特点:
a. 驱动程序根据需要随时动态建立使用和销毁映射,映射是短期临时的。类似c语言中的malloc分配内存。
b. 优点是按需使用地址空间,空间利用率高。缺点是每次使用前后都要去建立和销毁映射,操作繁琐
5.1.5. 虚拟地址到物理地址的映射可以多对1,但是不能1对多
5.2. 驱动中操作寄存器
5.2.1. 使用寄存器对应的虚拟地址直接读取。
5.2.2. 操作相关文件
5.2.2.1. 主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
a. CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
b. map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
c. map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。
5.2.2.2. 虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
a. #define S3C_ADDR_BASE(0xFD000000)// 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的
5.2.2.3. GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
a. 表中是GPIO的各个端口的基地址的定义
5.2.2.4. GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
六. 应用层调用内核
6.1. 相关代码如下
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #define DEVFILE "/dev/led_dev" int main(void) { char buf[100] = {0}; int fd = -1; if((fd =open(DEVFILE, O_RDWR))<0) { perror("open"); return -1; } printf("open successful fd = %d\n",fd); if(write(fd, "on", strlen("on"))<0) { perror("write"); return -1; } sleep(3); memset(buf,0,sizeof(buf)); if(read(fd, buf, 3)<0) { perror("read"); return -1; } printf("read data = %s\n",buf); if(write(fd, "off", strlen("off"))<0) { perror("write"); return -1; } sleep(3); memset(buf,0,sizeof(buf)); if(read(fd, buf, 4)<0) { perror("read"); return -1; } printf("read data = %s\n",buf); close(fd); return 0; }