树莓派(六)IO口驱动初级入门

本文详细讲解了树莓派中总线地址、物理地址和虚拟地址的区别,以及GPIO寄存器的操作,包括如何通过ioremap和ioreunmap进行地址映射,以及在驱动开发中配置GPIO引脚和编写相关代码的过程。
摘要由CSDN通过智能技术生成

一、三个 "地址"

1.总线地址

总线地址,也被称为地址总线或位址总线(Address Bus),是一种计算机总线。它由CPU或者具有DMA能力的单元使用,用于沟通这些单元想要访问(读取/写入)计算机内存组件/地方的物理地址。

通俗讲就是CPU能够访问内存的范围。

比如32位的系统,CPU最多能访问到2^32bit,也就是4Gb。

2^32 = 4294967296 bit = 4194304 Kb = 4096 Mb = 4 Gb

而64位系统则是能访问到8Gb。  

树莓派装载32位操作系统,寻址自然是4G。

树莓派查看内存指令。

cat /proc/meninfo

 内存大概为926Mb。

 

2.物理地址

在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。

3.虚拟地址

如何解释虚拟地址呢?

以树莓派为例,树莓派是32位系统,内存可以访问到4G,但是物理地址只有1G,假如运行的程序是超过1G的,那么把程序全部加载到内存是不可行的。

解决方法就是利用虚拟地址。

首先要说明一点,我们在实际程序中操作的其实都是虚拟地址。

虚拟地址其实就是基于算法上的逻辑地址,是软件层面的,是假的。虚拟地址可以比1G大,树莓派本身是能够访问到4G的,当物理地址1G不能满足程序运行空间需要时,那就可以把1G的物理地址映射成4G的虚拟地址。

二、芯片手册导读

查看芯片手册的目的性很强:做哪一块的开发,就只看那一块,现在要开发的是GPIO,熟悉控制IO口的寄存器最为重要。

如果看完这部分的文档,你对于以下几个问题(后面有解析)有清晰的答案,说明你真正读懂了这一部分的开发。

①操作逻辑:简言之就是怎么进行配置相关寄存器,这些配置步骤和思想其实都很类似。
②需要重点掌握的寄存器有哪些?

例如输入 / 输出控制寄存器

输出 0 / 1控制寄存器

清除状态寄存器

训练如何捕捉信息 。

在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,0/1,怎么清除,上升沿下降沿等。

从这里可以看出,所有IO口被分成0~5,共6个分组。每组的访问都被设置为32位。

引脚功能选择

其中pin0~pin9是位于第0分组。目标pin4对应14-12位,FSEL9有配置示例,由此可知,将14-12位配置为001,即可让pin4成为输出引脚。

GPFSEL0是pin0 ~ pin9的配置j寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。

 配置引脚输出状态

输出集寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。分离集和明确功能取消对读-修改-写操作的需要。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。

第0~31位归寄存器0管理,若设置为0,则无影响,若将第n位配置为1,则pin n 就会触发,开启置1,而第32位~53位由寄存器1管理。

清除状态

输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果的在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器。

当寄存器写入1时,寄存器触发,开启清除1,置0

三、代码编写

在原来框架的基础上,添加寄存器的定义

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

注意以下几点:

 

1.弄清楚寄存器的分组
其中寄存器的0表示的是分组,目标操作的IO是pin4,由文档可知,属于寄存器分组0。

2.volatile的使用(笔试)
加volatile在此处是 : 防止编译器优化(可能是省略,也可能是更改)这些寄存器变量,常见于在内核中对IO口进行操作。

volatile的作用是作为指令关键字,确保本条 指令不会因编译器的优化而省略,且要求每次直接读值

获取、配置寄存器地址

编写驱动程序时,首先要知道它的地址,IO口空间的起始地址是0x3f00 0000(文档的起始地址是错误的),加上GPIO的偏移量0x200 0000,所以GPIO的物理地址应该是0x3f20 0000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。

1.文档中,外设IO口的起始地址为0x20000000是错误的,应该是0x3F000000。

2.外设物理起始地址为0x3F000000,对应虚拟起始地址为0xF2000000,对应总线起始地址为0x7E000000。

3.表中的地址就是总线地址,寄存器GPFSEL0的0x7E200000就是总线地址。

4.驱动程序中要使用的地址就是物理地址,需要通过查文档换算:

        已知外设IO口起始物理地址为0x3F000000,

               外设IO口起始总线地址为0x7E000000,

         寄存器GPFSEL0总线地址为0x7E200000,

        (0x7E200000-0x7E000000)= 0x200000就是寄存器GPFSEL0距起始地址的偏移量,

        寄存器GPFSEL0物理地址为(0x3F000000+0x200000)=  0x3F200000

所以GPSET0的物理地址为0x3F20001C 

GPCLR0的物理地址为0x3F200028

GPFSEL0 = (volatile unsigned int *)ioremap(0x3F200000,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3F20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3F200028,4);

物理地址转虚拟地址 

因为代码操作的是虚拟地址,代码中直接用物理地址肯定不行,需要进行转换,将IO口寄存器映射成普通内存单元进行访问。

使用函数ioremap来完成。

函数原型:void *ioremap(unsigned long phys_addr, unsigned long size)
phys_addr:要映射的起始的IO物理地址;
size:要映射的空间的大小;

进行功能配置 

在函数pin4_open中配置pin4为输出引脚

只要将32位寄存器GPFSEL014-12位配置为001,其它位不管,即可配置pin4为输出引脚。

运用与(&) 、或(|)运算进行位操作

//配置pin4引脚为输出引脚
    *GPFSEL0 &= ~(0x6 << 12); //bit14、bit13配置为0
    *GPFSEL0 |= (0x1 << 12); //bit12配置为1

在函数pin4_write中配置pin4输出 0 / 1

获取应用程序中write函数的值,通过copy_from_user函数来实现。

函数原型:

unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)

此函数将from指针指向的用户空间地址开始的连续n个字节的数据产送到to指针指向的内核空间地址,简言之是用于将用户空间的数据传送到内核空间。

第一个参数to是内核空间的数据目标地址指针,
第二个参数from是用户空间的数据源地址指针,
第三个参数n是数据长度

如果数据拷贝成功,则返回;否则,返回没有拷贝成功的数据字节数。

根据值来操作IO口 

int userCmd;上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
 
copy_from_user(&userCmd,buf,count);
 
if(userCmd == 1){
	printk("set 1\n");
	*GPSET0 |= 0x1 << 4;
}else if(userCmd == 0){
	printk("set 0\n");
	*GPCLR0 |= 0x1 << 4;
}else{
	printk("cmd error\n");
}

 说明(这也是操作逻辑的一部分啦):
①这个GPSET0,0指的是分组,不是设置成低电平。
②左移4位,是因为GPSET0寄存器的第4位对应pin4,只要把第4位设置为1,表示这个寄存器就对pin4发挥作用,设置成高电平,如果是0则 no effct(手册内容)。

解除虚拟地址映射 

退出程序卸载驱动的时候,解除映射:iounmap函数

void iounmap(void* addr)//取消ioremap所映射的IO地址

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

 四、完整代码

内核驱动框架

#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;
 
//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数,和printf类似
    //open的时候配置pin4为输出引脚
    *GPFSEL0 &= ~(0x6 << 12);
	*GPFSEL0 |= (0x1 << 12);
    
    return 0;
}
 
//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
	int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
 
	printk("pin4_write\n");
	//获取上层write的值
	copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
	
	//根据值来执行操作
	if(userCmd == 1){
		printk("set 1\n");
		*GPSET0 |= 0x1 << 4;
	}else if(userCmd == 0){
		printk("set 0\n");
		*GPCLR0 |= 0x1 << 4;
	}else{
		printk("cmd error\n");
	}
	
    return 0;
}
 
static struct file_operations pin4_fops = {
 
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
 
int __init pin4_drv_init(void)   //驱动的真正入口
{
 
    int ret;
    printk("insmod driver pin4 success\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);
	GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
	GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);
 	
 	return 0;
}
 
void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{
	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);  //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

应用程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
 
int main()
{
        int fd;
        int cmd;
 
        fd = open("/dev/pin4",O_RDWR);
        if(fd < 0){
                printf("open failed\n");
                perror("reson");
        }else{
                printf("open success\n");
        }
 
        printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");
        scanf("%d",&cmd);
 
        if(cmd == 0){
                printf("pin4设置成低电平\n");
        }else if(cmd == 1){
                printf("pin4设置成高电平\n");
        }
 
        fd = write(fd,&cmd,1);//写一个字符'1',写一个字节
        return 0;
}
 

五、交叉编译、安装驱动、测试

运行上层程序之前。​​​​​​​

​​​​​​​

安装驱动,运行应用程序之后。

 

六、参考博文

树莓派(十二)树莓派驱动开发入门:从读懂框架到自己写驱动(下)_树莓派4g 驱动开发-CSDN博客

 【树莓派】GPIO驱动代码编写-CSDN博客

 树莓派高级开发——“IO口驱动代码的编写“ 包含总线地址、物理_虚拟地址、BCM2835芯片手册知识 - 知乎 (zhihu.com)

  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值