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

一、驱动代码的开发

1.树莓派寄存器的介绍

点击查看:树莓派(bcm2835芯片手册)

在这里插入图片描述

GPFSEL0 GPIO Function Select 0: 功能选择 输入/输出

GPSET0 GPIO Pin Output Set 0 : 输出0
GPSET1 GPIO Pin Output Set 1 : 输出1
0 = No effect
1 = Set GPIO pin n

GPCLR0 GPIO Pin Output Clear 0: 清零
0 = No effect
1 = Clear GPIO pin n
GPCLR1 GPIO Pin Output Clear 1 :清1

每个寄存器都是32位的
在这里插入图片描述
例如:我们把引脚4配置位输出引脚
FSEL4 14-12 001 我们把4引脚的14-12配置成001 GPIO Pin 4 is an output

注意:我们配置的底层引脚对应得是BCM
寄存器第0组位FESL0–9
寄存器第1组位FSEL10–19

在这里插入图片描述
具体的引脚也可通过官方手册查找
https://pinout.xyz/pinout/pin7_gpio4
在这里插入图片描述

2.寄存器的地址问题

我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。

特别注意,BCM2708 和BCM2709 IO起始地址不同,BCM2708是0x20000000,BCM2709是0x3f000000,这是造成大部分人驱动出现“段错误”的原因。树莓派3B的CPU为BCM2709。

查看树莓派型号

cat /proc/cupinfo

在这里插入图片描述
此处指令看到的型号不是树莓派cpu真的型号,其真正型号应该是BCM2837,也就是IO在物理地址上的基址应该是0x3F000000。

在这里插入图片描述

通过查看芯片手册发GPFSEL0寄存器VC  CPU总线地址是0x7E200000,
相对基址(0x7E000000)偏移0x00200000,那么ARM物理地址也是偏移这
么多,所以GPIO的物理地址应该是从0x3f200000 开始 。

 

 

在这里插入图片描述

该图的尾部偏移是对的根据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);来映射获取到对应物理地址映射的虚拟地址,来访问寄存器。

void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);

ioremap宏定义在asm/io.h内:

#define ioremap(cookie,size)           __ioremap(cookie,size,0)

参数:

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

size:要映射的空间的大小

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

该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

这里我们不指定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> 中定义的宏.

 

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

pin4driver.c

#include <linux/fs.h>            //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>        //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件


static struct class *pin4_class;
static struct device *pin4_class_dev;

static dev_t devno;                //设备号
static int major =231;                     //主设备号
static int minor =0;                       //次设备号
static char *module_name="pin4";   //模块名

volatile unsigned int *GPFSEL0 = NULL;
volatile unsigned int *GPSET0 = NULL;
volatile unsigned int *GPCLR0 = NULL; 

//这三行是设置寄存器的地址,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.

//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
        printk("pin4_open\n");  //内核的打印函数
        *GPFSEL0 &=~(0x6<<12);//6的二进制是110左移12位后,110对应的位置是14-12,取反后110变为001其它位为1,和GPFSEL0进行与运算后就实现只有14、13位改变为0
        *GPFSEL0 |=(0x1<<12);//把bit14,bit13配置成0 //把位置12变为1
        //配置pin4引脚为输出引脚,bit 12~14配置成100
        return 0;
}
//read函数
static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{
        printk("pin4_read\n");  //内核的打印函数
		//可以用copy_to_user()函数读取引脚
        return 0;
}

//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
        int usercmd;

        printk("pin4_write\n");  //内核的打印函数
        //获取上层write函数的值
        copy_from_user(&usercmd,buf,count);
        //根据值来操作io口,高电平或者低电平
        if (usercmd==1)
        {
                printk("set 1\n");
                *GPSET0 |=(0x1 <<4); //通常pin4用1左移4位进行或运算
        } 
        else if(usercmd==0){
                printk("set 0\n");
                * GPCLR0 |=(0x1<<4); 
        }
        else{
                printk("do nothing\n");
        }
        return 0;
}
static struct file_operations pin4_fops = {

        .owner = THIS_MODULE,
        .open  = pin4_open,
        .write = pin4_write,
        .read  = pin4_read,
};
//static限定这个结构体的作用,仅仅只在这个文件。
int __init pin4_drv_init(void)   //真实的驱动入口
{

        int ret;
		printk("ismod pin4 successed\n");
        devno = MKDEV(major,minor);  //创建设备号
		ret   = register_chrdev(major, module_name,&pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

        pin4_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
        pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件

		GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);//物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问
        GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);//ioremap函数将物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问
        GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
        //这三行是设置寄存器的地址,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,
		//且要求每次直接读值.ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。
		//ioremap函数第一个参数输物理地址,第二个参数是


        return 0;
}

void __exit pin4_drv_exit(void)
{
        iounmap(GPFSEL0);  //卸载驱动时候为了降低风险,需要把映射解除。
        iounmap(GPSET0);
        iounmap(GPCLR0);
        //卸载驱动时释放地址映射  和创建时是相反的
		
        device_destroy(pin4_class,devno);
        class_destroy(pin4_class);
        unregister_chrdev(major, module_name);  //卸载驱动
}
module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

tips: 

volatile //特征修饰符 作为指令关键字
   作用:
   1.确保指令不会因编译器的优化而省略。(编译器自认为人为给的数据不行,可能会
   被编译器给优化掉)
   2.要求每次直接从寄存器读值。(寄存器随着硬件的执行可能会改变寄存器里面的数
   据,如果没有volatile修饰,读取的数据是原先数据的一个备份,是个老数据,数据
   时效性就很差。)

unsigned 
    作用:
    整数分为有符号与无符号,如果要把类型声明为无符号数就需要使用unsigned来修饰
    (除char以外的数据类型中,默认情况下声明的整型变量都是有符号的类型),两者
    区别在于,有符号的数最高位的数作为符号位,无符号最高位作为值。如两个字节的
    short,有符号表示范围是-32768~32767,无符号范围是0~65535.

注意
为了不影响其他的引脚,配置寄存器的时候要用位操作,寄存器有32位,若想配13、14位为0,可以将二进制的110左移12位然后按位取反,在与上原来的寄存器地址,这样就不影响其他位。

 

上层应用代码如下:

pin4test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
        int fd,data;
        fd = open("/dev/pin4",O_RDWR);
        printf("plese input 1 or 0 //1:high 0:low\n ");
        scanf("%d",&data);
        if(fd<0){
                printf("open fail\n");
                perror("reson:");
        }
        else{
                printf("open successful\n");
        }
        fd=write(fd,&data,1);
        return 0;
}

编译驱动代码

完成驱动代码后,修改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、测试驱动

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

测试:

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

dmesg查看

在这里插入图片描述

gpio readall查看

输入1后

对应BCM4号脚输出变为1

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值