树莓派底层驱动开发示例(一个简单io口驱动的开发)

1、驱动代码的开发

树莓派的io空间的起始地址是0x3f000000,GPIO的偏移量为20000000,所以GPIO的物理地址应该是从0x3f200000开始的。

本篇示例驱动的功能是将树莓派的一个的4引脚设置为OUTPUT,使其具有OUTPUT的功能(输出0,1)。

在开发之前我们需要查看树莓派的开发手册:

1、找到设置io口功能的寄存器:

从上图可知道FSEL4即为我们要设置的4号脚,它属于GPFSEL0寄存器控制,控制位为14-12位,具体的设置方法参考上图的FSEL9可知,将该寄存器的14-12位设置为001就是将pin4设置为output了。

设置好引脚模式,我们再来看怎么控制该引脚输出'0',和‘1’。

2、找到控制引脚输出1的寄存器:

由上图可知,GPIO Pin Output Set Registers (GPSETn) 翻译过来就是GPIO引脚输出寄存器(GPSETn),该寄存器就是控制引脚输出‘1’的寄存器。我们要设置的是四号脚,从表中可以看出,要设置n号脚为‘1’,就将n位置‘1’,我们要设置4号脚为‘1’,就将寄存器第4位置‘1’(属于0-31位之间所以我们选择的是0号也就是GPSET0寄存器),其余保持0(No effect)即可。

3、找到控制引脚输出‘0’的寄存器:

由上图可知,GPIO Pin Output Clear Registers (GPCLRn) 翻译过来就是GPIO引脚输出清除寄存器(GPCLRn),该寄存器功能就等同于控制引脚输出‘0’。我们要设置的是四号脚,从表中可以知,要设置n号脚为‘0’,就将n位置‘1‘,我们要设置4号脚为‘0’,就将寄存器第4位置'1'(属于0-31位之间所以我们选择的是0号也就是GPCLR0寄存器),其余保持0(No effect)即可。 

4、找出各个寄存器的地址

结合上文所说的所以GPIO的物理地址应该是从0x3f200000开始的,

由图可知:

GPFSEL0 地址为0x3f200000

GPSET0的地址为0x3f20001C

GPCLR0的地址为0x3f200028

这里我们需要注意的是,上面的地址是物理地址,一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。

所以我们必须通过函数void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);来映射获取到对应物理地址映射的虚拟地址,来访问寄存器。

参数:

phys_addr:要映射的起始的IO地址

size:要映射的空间的大小

flags:要映射的IO空间和权限有关的标志

这里我们不指定flags,函数使用方法如下:(volatile的作用是为了保证地址不会因编译器的优化而省略,每次直接读取值)

GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);   //初始化设置引脚功能的寄存器的地址 

GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);  // 初始化控制引脚输出‘1’的寄存器地址

GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);  //初始化控制引脚输出‘0’的寄存器的地址

关于file_operations结构体:Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

我们写驱动都需要根据不同需求选择性的对file_operations结构体中的成员进行配置:

static struct file_operations pin4_fops = {

   .owner = THIS_MODULE,

   .open  = pin4_open,   //配置open函数

   .write = pin4_write,  //配置write函数

};

注意:第一个 file_operations 成员owner根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 这是一个在 <linux/module.h> 中定义的宏.

最后完成的驱动代码如下:

#include <linux/fs.h>

#include <linux/module.h>

#include <linux/init.h>

#include <linux/device.h>

#include <linux/uaccess.h>

#include <linux/types.h>

#include <asm/io.h>

static dev_t devno;                             //总设备号
static int major_no=122;                        //主设备号名称
static int next_no=9;                          //次设备号名称
static char *moudle_name = "pin4_moudle";      //模块名称

volatile unsigned int* GPFSEL0=NULL;      //volatile不会因编译器的优化而省略,每次直接读值

volatile unsigned int* GPSET0=NULL;

volatile unsigned int* GPCLR0=NULL;

static struct class *pin4_class;
static struct device *pin4_class_dev;
static int pin4_open(struct inode *inode,struct file *file)
{
        printk("in open \n");
        *GPFSEL0&=~(0x6<<12);           //在不改变其他位的情况下将13,14位变成0;

        *GPFSEL0|=(0x1<<12);    //在不改变其他位的情况下将12位配置成1

        return 0;

}

static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
        char usercmd;
        printk("usercmd is %c\n",usercmd);
        copy_from_user(&usercmd,buf,count);//从上层获取用户在write中写的数据,该数据存放在&usercmd中。
        if(usercmd=='1')
        {

                *GPSET0|=(0x1<<4);//不改变其他位的情况下将寄存器第四位置1
                printk("set 1\n");
        }
       else if(usercmd=='0')
        {
         *GPCLR0|=(0x1<<4);//不改变其他位的情况下将寄存器第四位配置成1(该寄存器控制io口清零)

         printk("set 0\n");
         }

        else{
        printk("not cmd\n");
        }
        return 0;
}
static struct file_operations pin4_fops = {
   .owner = THIS_MODULE,

   .open  = pin4_open,   //配置open函数

   .write = pin4_write,  //配置write函数

};



int __init pin4_drv_init(void)   // 真实驱动入口

{
        int ret;
        devno=MKDEV(major_no,next_no);

        ret =register_chrdev(major_no,moudle_name,&pin4_fops);          //注册驱动 告诉内核,把这个驱动加入到内核链表中
        pin4_class = class_create(THIS_MODULE,"pin4_demo");        //让代码在dev自动生成设备
        pin4_class_dev=device_create(pin4_class,NULL,devno,NULL,moudle_name);//创建设备文件.
        GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);   //初始化设置引脚功能的寄存器的地址
        GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);  // 初始化控制引脚输出‘1’的寄存器地址
        GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);  //初始化控制引脚输出‘0’的寄存器的地址
        printk("jiazaichenggong\n");
        return 0;

}
void __exit pin4_drv_exit(void)

{
        iounmap(GPCLR0);
        iounmap(GPSET0);
        iounmap(GPFSEL0);

        device_destroy(pin4_class,devno);//解除设备
        class_destroy(pin4_class);         //解除类
        unregister_chrdev(major_no,moudle_name);//取消注册

}

module_init(pin4_drv_init);     //入口,  内核加载驱动的时候,这个宏会被调用

module_exit(pin4_drv_exit);

MODULE_LICENSE("GPL v2");

2、编译驱动代码

完成驱动代码后,修改linux内核文件drivers /char下的Makefile 文件,

添加一条信息obj-m                +=pin4driver.o        (-m是模块方式编译,pin4driver.o是你自己写的驱动的文件名)

只有添加了这条信息,在编译内核的时候,才会去编译你自己写的驱动代码。

添加完成之后:

重新编译内核模块

指令为(该指令需要在内核代码文件路径下执行):ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4  modules

编译好后会在/driver/char/路径多生成一个pin4driver.ko文件

将这个.ko文件放到树莓派上

3、在树莓派上装载该驱动

将该驱动装载进树莓派:sudo insmod  pin4driver.ko

并且给予其操作权限: sudo chmod 666 /dev/pin4_moudle

可以看到驱动列表多了一个

当看见pin4driver就意味着驱动装载成功,这个时候会在根目录下的/dev/内看到一个pin4_moudle文件,这个文件的名字是在你写驱动代码时定义的模块名称

给予这个模块操作权限: sudo chmod 666 /dev/pin4_moudle

此时自己编写的驱动模块已经装载完毕。

4、测试驱动

我们可以在树莓派上编写代码来测试该驱动:

#include <unistd.h>
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
        int fd = open("/dev/pin4_moudle",O_RDWR);
        if(fd<0)
        {
                printf("open file\n");
        }
        while(1){
        char cmd;
        printf("高电平输入1,低电平输入0\n");
        scanf("%c",&cmd);

        fd=write(fd,&cmd,1);
        if (fd<0)
        {
                printf("write fail\n");
        }
        }
        return 0;

}

测试:

未输入之前树莓派gpio状态,pin4号脚输出为0

输入1后

对应BCM4号脚输出变为1

猜你喜欢

转载自blog.csdn.net/bhbhhyg/article/details/107570393