一、概念
应用层的open,read,write实际上是执行swi val这么一条指令,这条指令类似于一条中断,在C库下有一个系统调用的接口,根据val的值调用sys_open,sys_read,sys_write等,当我们在应用层open(“dev/led”,ORDWR)时,sys_open会根据打开的不同文件找到不同的驱动程序。
那么如何写出一个字符设备驱动程序呢?首先要实现例如led_open,led_write等设备操作函数,将这些函数挂在file_operation结构体中,然后在驱动的入口函数和出口函数注册注销这个结构体。
那么app是如何调用我们定的file_operation结构体,实现例如点亮LED这样一个功能呢?
根据主设备号。在我们注册这个结构体的时候要指定或者由系统指定一个主设备号major。根据这么主设备号进行查找。那么多的major是如何管理的?虚拟文件系统VFS,里面存在着已经注册的major。所以,我们可以在注册函数里major参数传入0,系统将会自己去查询分配主设备号返回。
那么open(“dev/led”,ORDWR)里“dev/led”是如何而来的?两种方式:一种是手动创建,一种是自动创建。
自动创建需要系统信息创建设备节点,用代码实现即在注册结构体之后class_create创建类,然后在用这个类创建一个设备class_device_create,这样我们在insmod时驱动就可以用这个的设备name在应用层open了。
那么我们如何操作硬件。在单片机中程序可以直接使用物理地址进行操作,但在Linux里,需要ioremap(mac,size)映射成虚拟地址才能进行操作
怎么来总结字符设备呢?这个过程像极了校园爱情。
第一:上课期间不管要表示爱意还是想下课操场见都只能通过传纸条 => open,read,write本质是执行swi val指令
第二:纸条上要写收件人的名字,不然可能就没了,而这个名字是人家对方的父母取的 => open(“dev/led”,ORDWR),而“dev/led”是在驱动创建的时候由class_device_create设置的
第三:在班级里唯有座号不会重复,每个人的座号都贴在黑板上方便老师叫号 => 注册结构体的时候要分配一个主设备号major,major在VFS虚拟文件系统里放着
二、驱动框架搭建
①写出 led_open、led_read
②怎么告诉内核
--->定义一个file_operations结构,填充它
Linux下的file_operations结构体(linux-2.6.22.6\include\linux)
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);
省略
};
---------------------------------------------------------------------------------------------------------------------------------------------------
填充完成的结构体
static struct file_operations sencod_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = sixth_drv_open,
.read = sixth_drv_read,
.release = sixth_drv_close,
.poll = sixth_drv_poll,
.fasync = sixth_drv_fasync,
};
--->把这个结构告诉内核
--->使用register_chrdev注册:register_chrdev(主设备号, 名字,上面填充完成的结构体); 注册驱动程序
register_chrdev(0, "sixth_drv", &sencod_drv_fops);
--->谁来调用register_chrdev,即驱动的入口
static int sixth_drv_init(void)
{
register_chrdev(0, "sixth_drv", &sencod_drv_fops);
return 0;
}
--->入口函数如何被识别
module_init(sixth_drv_init);//module_init是一个结构体,结构体指向定义的入口函数,内核在启动的时候会调用这个结构体。
--->应用程序如何找到register_chrdev并对应到相应的file_operations:依据设备号和主设备名称
--->谁来注销register_chrdev,即驱动的出口
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
}
--->出口函数如何被注销
module_exit(sixth_drv_exit);
--->实现sencod_drv_fops结构体中定义的方法类
static int sixth_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
return 1;
}
.
.
.
省略
使用Makefile
KERN_DIR = /work/system/linux-2.6.22.6 //内核的目录,程序依赖于linux-2.6.22.6内核开发,内核需要编译好
all:
make -C $(KERN_DIR) M=`pwd` modules // make -C $(KERN_DIR):使用make -C $转到KERN_DIR里面进行编译;M=`pwd`当前目录是什么;modules:目标
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += buttons.o
测试代码
int main(int argc,char *argv){
int fd;
int val = 1;
fd = open("/dev/xxx",O_PDWR);
if(fd<0){
printf("can't open!\n");
return -1;
}
write(fd,&val,4);
return 0;
}
register_chrdev(0, "sixth_drv", &sencod_drv_fops); 中如果第一个参数填写0,则系统自动分配设备号
一、驱动:1、自动分配主设备号
2、手工指定
二、应用 open("/dev/xxx",O_PDWR);
"/dev/xxx"怎么来的
a、手工创建 mknod /dev/xxx
b、自动创建 udev,mdev ----->根据系统信息创建设备节点
----->如何创建设备节点
static struct class *sixthdrv_class;
static struct class_device *sixthdrv_class_dev;
static int sixth_drv_init(void)
{
major = register_chrdev(0, "sixth_drv", &sencod_drv_fops);
sixthdrv_class = class_create(THIS_MODULE, "sixth_drv");//先创建类
/* 为了让mdev根据这些信息来创建设备节点 */
sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
return 0;
}
---->删除设备节点
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
class_device_unregister(sixthdrv_class_dev);
class_destroy(sixthdrv_class);
return 0;
}
二、在框架内写LED驱动
--->配置硬件
--->映射引脚:如何映射?-->定义全局变量,在入口函数映射
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
static int sixth_drv_init(void)
{
....省略上面的代码
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);//ioremap(地址, 长度);
gpfdat = gpfcon + 1;//+1等于+4,以unsigned long为单位的
return 0;
}
--->取消映射
static void sixth_drv_exit(void)
{
...省略上面的代码
iounmap(gpfcon);
return 0;
}
--->在sixth_drv_open中配置引脚
static int sixth_drv_open(struct inode *inode, struct file *file){
/*配置GPF4,5,6为输出*/
*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
*gpfcon |= ((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
return 0;
}
static ssize_t sixth_drv_write(struct file *file, char __user *buf, size_tcount, loff_t *ppos){
int val;
copy_from_user(&val,buf,count);
if(val==1){
//点灯
*gpfdat |= (1<<4)|(1<<5)|(1<<6);
}else{
//灭灯
*gpfdat &= ~(1<<4)|(1<<5)|(1<<6);
}
return 0;
}
三、按键查询的方式控制LED
--->框架搭建
--->实现file_operations结构体,实现结构体需要用到的方法
--->在入口函数注册file_operations结构体,和注销入口函数
static int sixth_drv_init(void)
{
register_chrdev(0, "sixth_drv", &sencod_drv_fops);
return 0;
}
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
}
--->出口函数卸载
module_exit(sixth_drv_exit);
--->给内核提供更多信息:udev机制,可以自动创建设备节点>>>>>创建一个class,在class上创建一个设备节点
static struct class *sixthdrv_class;
static struct class_device *sixthdrv_class_dev;
static int sixth_drv_init(void)
{
major = register_chrdev(0, "sixth_drv", &sencod_drv_fops);
sixthdrv_class = class_create(THIS_MODULE, "sixth_drv");//先创建类
/* 为了让mdev根据这些信息来创建设备节点 */
sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
return 0;
}
--->卸载设备节点
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
class_device_unregister(sixthdrv_class_dev);
class_destroy(sixthdrv_class);
return 0;
}
--->硬件配置:.open配置引脚,.read 返回引脚状态
--->引脚映射
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static int sixth_drv_init(void)
{
.....省略上面的file_operations结构体注册步骤.....
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
return 0;
}
--->在出口函数注销掉这些映射的引脚
static void sixth_drv_exit(void)
{
....省略....
iounmap(gpfcon);
iounmap(gpgcon);
return 0;
}
---->在open函数中配置引脚
static int sixth_drv_open(struct inode *inode, struct file *file)
{
/*配置GPF0,2为输出*/
*gpfcon &= ~((0x3<<(0*2)) | (0x3<<2*2)));
/*配置GPF3,11为输出*/
*gpfcon &= ~((0x3<<(3*2)) | (0x3<<(11*2)));
return 0;
}
--->在read函数中读取引脚电平
static int sixth_drv_read(struct file *file, char __user *buf, size_tcount, loff_t *ppos){
unsigned char key_vals[4];
int regval;
if(size!=sizeof(key_vals)){
return -EINVAL;
}
/*读GPF0,2*/
regval = *gpfdat;
key_vals[0] = (regval & (1<<0))?1:0;
key_vals[1] = (regval & (1<<2))?1:0;
/*读GPF3,11*/
regval = *gpgdat;
key_vals[2] = (regval&(1<<3))?1:0;
key_vals[3] = (regval&(1<<4))?1:0;
copy_to_user(buf,key_vals,sizeof(key_vals));
return sizeofn(key_vals);
}
--->实现测试程序
int main(int argc,char *argv){
int fd;
unsigned char key_vals[4];
int cnt = 0;
fd = open("/dev/xxx",O_PDWR);
while(1){
read(fd,key_vals,sizeof(key_vals));
if(!key_vals[0]||!key_vals[1]||!key_vals[2]||!key_vals[3]){
printf("%04d key pressed :%d %d %d %d\n",cnt++,key_vals[0],key_vals[1],key_vals[2],key_vals[3]);
}
}
return 0;
}
四、中断方式控制LED
--->框架
--->内核分析
--->1、分辨是哪一个中断---2、调用处理函数---3、清中断:都是由asm_do_IRQ函数实现的:在\arch\arm\kernel中的irq.c实现
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc = irq_desc + irq;
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
irq_enter();
desc_handle_irq(irq, desc);//三项功能都是在这里实现的
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
}
//desc_handle_irq内定义的方法,文件在\include\asm-arm\mach的irq.h中
static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);
}
--->通过中断配置引脚,request_irq能够自动配置引脚为输入输出。
static int sixth_drv_open(struct inode *inode, struct file *file)
{
/* 配置GPF0,2为输入引脚 */
/* 配置GPG3,11为输入引脚 */
request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", 1);
//中断号 处理函数 触发方式 取名 设备号
request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", 1);
request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", 1);
request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", 1);
return 0;
}
--->释放中断
int sixth_drv_close(struct inode *inode, struct file *file)
{
//atomic_inc(&canopen);
free_irq(IRQ_EINT0,1);
free_irq(IRQ_EINT2,1);
free_irq(IRQ_EINT11,1);
free_irq(IRQ_EINT19,1);
up(&button_lock);
return 0;
}
--->定义buttons_irq处理函数
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
printk("irq = %d\n");
return IRQ_HANDLED;
}
五、poll机制(查询机制)——可以休眠
在file_operations结构体中加.poll
static struct file_operations sencod_drv_fops = {
.....省略.....
.poll = sixth_drv_poll,
.....省略.....
};
在.read中会调用sys_read,那么poll也会调用sys_read,接下来分析一下sys_read
所有的系统调用,基于都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。
应用层:app ------>.poll
内核:kernel ------> sys_poll:(fs/select.c)
----->do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, s64 *timeout)
----->poll_initwait()
-----> init_poll_funcptr(poll_table *pt, poll_queue_proc qproc):pt->qproc = qproc;
------>do_poll(nfds, head, &table, timeout);
for(;;){
}