whilebreak

CH01 miscdevice AXI GPIO LED驱动程序

0
阅读(1177)

CH01 miscdevice AXI GPIO LED驱动程序

1.1概述

本节课开始,我们一起学习基于ZYNQ的驱动程序开发。首先声明由于本人能力有限作为一名LINUX驱动初学者,教程更多以笔记形势展示笔者对于LINUX驱动程序开发的理解,而不是一名资深的LINUX驱动开发者。笔记从自身学习的角度,记录如何学习基于ZYNQ的驱动开发。

1.2驱动的注册

驱动编译后执行insmod ./led_dev.ko把驱动注册到内核中,当驱动被注册到内核总的时候系统会对驱动做一些的初始化操作。首先是操作系统对led_dev的驱动程序地址空间的分配,包括定义的结构体,变量,指针,和函数。分配完成后,首先执行static int led_init(void)。

在linux kernel 中,物理地址是不能直接使用的,必须通过转换才可以。转换分为两种, 静态和动态。不过,静态的地址转换,还需要在kernel 初始化的时候作映射。动态映射是使用 ioremap 函数。本节课程中使用 ioremap 函数动态映射。led_base = ioremap(0x41200000, 4);映射了AXI_GPIO的物理地址0x41200000到用户地址空间,地址长度为BYTE。之后misc_register函数注册led_dev的驱动到内核。

。//LED设备初始化

static int led_init(void)

{

int ret;

//地址映射:把物理地址转换为虚拟地址

led_base = ioremap(0x41200000, 4);

printk("LED: Access address to device is:0x%x\n", (unsigned int)led_base);

//注册设备到/dev/目录

ret = misc_register(&led_dev);

if(ret)

{

printk("led:[ERROR] Misc device register failed.\n");

return ret;

}

printk("Module init complete!\n");

return 0;

}

1.3驱动的打开

当应用程序通过调用open("/dev/led_dev", O_RDWR);函数打开设备的时候,此函数被调用。

static int led_open(struct inode * inode, struct file * filp)

{

printk("dev is open!");

return 0;

}

1.4驱动的卸载

执行rmmod led_dev的时候卸载驱动

//LED设备退出

static void led_exit(void)

{

printk("Module exit!\n");

iounmap(led_base); //取消物理地址的映射

misc_deregister(&led_dev); //删除/dev/目录下的设备节点

}

1.5通过led_write函数

用户程序通过led_write对GPIO进行操作,控制LED.ret = copy_from_user(&val, buf, count);函数实现了把用于空间的程序拷贝到内核空间。iowrite32 可以直接范围我们刚刚ioremap后的地址空间。

//写设备触发的服务函数 file:设备 buf:数据缓存区 count:数据个数单位字节

static int led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

{

int val,ret;

//把buff缓存数据拷贝到val地址空间

ret = copy_from_user(&val, buf, count);

//把val的值写进led_base寄存器

iowrite32(val, led_base);

printk("led : Write 0x%x to 0x%x.\n", val, (unsigned int)led_base);

return 0;

}

1.6 导出的模块函数

Insmod 命令的时候执行module_init()函数,rmmod的时候执行module_exit()函数

module_init(led_init); //模块初始化接口

module_exit(led_exit); //模块推出接口

1.7 GPL v2许可及驱动描述信息

中读者注意最后一条必须这么写,才能符合GPL 的规范,否则驱动可能无法工作。

MODULE_AUTHOR("123src@tjy");

MODULE_DESCRIPTION("ledDriver");

MODULE_ALIAS("It's only a test");

MODULE_LICENSE("Dual BSD/GPL");

1.8 led_dev.c完整源码

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/device.h>

#include <asm/io.h>

#include <linux/init.h>

#include <linux/platform_device.h>

#include <linux/miscdevice.h>

#include <linux/ioport.h>

#include <linux/of.h>

#include <linux/uaccess.h>

static void __iomem *led_base; //led寄存器基地址

//打开设备触发的服务函数

static int led_open(struct inode * inode, struct file * filp)

{

printk("dev is open!");

return 0;

}

//写设备触发的服务函数 file:设备 buf:数据缓存区 count:数据个数单位字节

static int led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)

{

int val,ret;

//把buff缓存数据拷贝到val地址空间

ret = copy_from_user(&val, buf, count);

//把val的值写进led_base寄存器

iowrite32(val, led_base);

printk("led : Write 0x%x to 0x%x.\n", val, (unsigned int)led_base);

return 0;

}

//LED函数接口结构体

static const struct file_operations led_fops =

{

.owner = THIS_MODULE,

.open = led_open,

.write = led_write,

};

//LED设备结构体

static struct miscdevice led_dev=

{

.minor = MISC_DYNAMIC_MINOR,

.name = "led_dev", // /dev/目录下的设备节点名

.fops = &led_fops,

};

//LED设备初始化

static int led_init(void)

{

int ret;

//地址映射:把物理地址转换为虚拟地址

led_base = ioremap(0x41200000, 4);

printk("LED: Access address to device is:0x%x\n", (unsigned int)led_base);

//注册设备到/dev/目录

ret = misc_register(&led_dev);

if(ret)

{

printk("led:[ERROR] Misc device register failed.\n");

return ret;

}

printk("Module init complete!\n");

return 0;

}

//LED设备退出

static void led_exit(void)

{

printk("Module exit!\n");

iounmap(led_base); //取消物理地址的映射

misc_deregister(&led_dev); //删除/dev/目录下的设备节点

}

module_init(led_init); //模块初始化接口

module_exit(led_exit); //模块推出接口

//以下代码可解决一些错误信息

MODULE_AUTHOR("123@ ");

MODULE_DESCRIPTION("ledDriver");

MODULE_ALIAS("It's only a test");

MODULE_LICENSE("Dual BSD/GPL");

1.9应用程序

应用程序中fd = open("/dev/led_dev", O_RDWR); //打开设备。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdio.h>

#include <unistd.h>

int main(int argc, char **argv)

{

int i;

int fd;

int val=1;

fd = open("/dev/led_dev", O_RDWR); //打开设备

if (fd <= 0) {

                Printf(“open %s error!\n”, filename)

                return;;

        }

//LED流水灯

while(1)

{

for(i=0;i<4;i++)

{

val = (1<<i);

write(fd,&val,4); //把val的值写到fd设备中,大小为4字节

sleep(1);

}

}

return 0;

}

这个open函数不简单 ,而且如果你不阅读下面的话,你一定理解为我们驱动里面的open函数范围的0.

open函数是我们开发中经常会遇到的,这个函数是对文件设备的打开操作,这个函数会返回一个句柄fd,我们通过这个句柄fd对设备文件读写操作。

我们在对这个fd作判断的时候,经常会用到:

        fd = open(filename, O_RDONLY);

        If (fd <= 0) {

                Printf(“open %s error!\n”, filename)

                return;;

        }

我们先来看看open函数的原型:

       int open(constchar*pathname,intflags);

       int open(constchar*pathname,intflags,mode_tmode);

函数参数:

                  pathname:打开文件的路径名

                 flags:用来控制打开文件的模式

                 mode:用来设置创建文件的权限(rwx)。当flags中带有O_CREAT时才有效。

返回值:

调用成功时返回一个文件描述符fd

调用失败时返回-1,并修改errno

正确的判断应该是 if(fd < 0),那我们什么时候会fd=0呢,如果fd=0,那么已经正常打开了,但是我们判断了打开错误了。

        open函数返回的文件描述符fd一定是未使用的最小的文件描述符,那么如果0没有使用,那么我们open的时候,首先就会获取到fd=0的情况。默认情况下,0,1,2这三个句柄对应的是标准输入,标准输出,标准错误,系统进程默认会打开0,1,2这三个文件描述符,而且指向了键盘和显示器的设备文件。所以通常我们open的返回值是从3开始的。

如果我们在open之前,close其中的任何一个,则open的时候,则会用到close的最小的值:

            close(0);

            fd = open(filename,O_RDONLY);

            printf(“fd = %d\n”, fd);

则可以发现我们就可以open的时候,返回了0的fd.

1.10 make file

KERN_DIR = /mnt/workspace/linux/linux-xlnx-master

all:

make -C $(KERN_DIR) M=`pwd` modules

clean:

make -C $(KERN_DIR) M=`pwd` modules clean

rm -rf modules.order

obj-m += led_dev.o

说明:
当我们在模块的源代码目录下运行make时,make是怎么执行的呢?
假设模块的源代码目录是/mnt/workspace/osrc/zynq_linux_driver/ch01_axi_led/dev下。

先说明以下makefile中一些变量意义:
(1)KERNELRELEASE在linux内核源代码中的顶层makefile中有定义
(2)shell pwd会取得当前工作路径
(3)shell uname -r会取得当前内核的版本号
(4)KERN_DIR变量便是当前内核(嵌入式LINUX内核)的源代码目录。
make -C $(KERN_DIR) M=`pwd` modules
这就是编译模块了:首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile;
M=选项让该makefile在构造modules目标之前返回到模块源代码目录;然后,modueles目标指向obj-m变量中设定的模块;
在上面的例子中,我们将该变量设置成了led_dev.o。

关于make modules的更详细的过程可以在内核源码目录下的scripts/Makefile.modpost文件的注释 中找到。
如果把led_dev模块移动到内核源代码中。例如放到..kernel/linux/driver/中, KERNELRELEASE就有定义了。
在..kernel/linux/Makefile中有

KERNELRELEASE=$(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)$(LOCALVERSION)。

这时候,led_dev模块也不再是单独用make编译,而是在内核中用make modules进行编译,此时驱动模块便和内核编译在一起。

1.11 测试
编译led.app程序,然后把 刚才编译号的驱动,led_dev.ko和a.out程序一并复制到TF卡(已经准备号了硬件工程)。

执行 mount /dev/mmcblk1p1 /mnt (注意,笔者已经调换了EMMC和SD卡的挂载顺序)

cd /tmp

ls

clip_image002

zynq> insmod led_dev.ko

clip_image004

zynq> lsmod

clip_image006

lsmod命令实际上是读取并分析"/proc/modules"文件,我们可以打开一下这个文件查看究竟:

zynq> cat /proc/modules

clip_image008

内核中已加载模块的信息也存在于/sys/module目录下,加载led_dev.ko后,内核中将包含/sys/module/led_dev目录,

有兴趣的可以看一下该目录中包含哪些内容。

可以使用rmmod led_dev命令来卸载当前装入的内核模块

然后执行./a.out 可以看到 流水的正常运行了。

clip_image010