首先我们要知道,驱动的两大利器:电路图(通过电路图去寻找寄存器)和芯片手册
一、寄存器的介绍
芯片手册第六章的89页,GPIO有41个寄存器,所有访问都是32位的。Description是寄存器的功能描述。GPFSEL0(寄存器名) GPIO Function Select 0(功能选择:输入或输出);GPSET0 (寄存器名) GPIO Pin Output Set 0(将IO口置0);GPSET1(寄存器名) GPIO Pin Output Set 1(将IO口置1);GPCLR0(寄存器名) GPIO Pin Output Clear 0 (清0)下图的地址是:总线地址(并不是真正的物理地址)
GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。
字段名 | 描述 | 用法 |
---|
GPFSEL0 | GPIO Function select 0,功能选择输出/输入 | 以引脚9举例:000 = GPIO Pin 9 is an input,001 = GPIO Pin 9 is an output | GPSET0 | GPIO Pin output Set 0,输出0 | 0 = No effect ,1 = Set GPIO pin n | GPSET1 | GPIO Pin output set 1,输出1 | 0 = No effect ,1 = Set GPIO pin n | GPCLR0 | GPIO Pin output clear 0,清0 | 0 = No effect ,1 = Clear GPIO pin n |
在上面的文档里已经说的很清楚了,000是引脚输入,而001则是引脚输出,在这里要注意每个寄存器都是32位的
- FSELn表示GPIOn,下图给出第九个引脚的功能选择示例,对寄存器的29-27进行配置,进而设置相应的功能。根据图片下方的register
0表示0~9使用的是register 0(即GPFSEL0)这个寄存器。
- 输出集寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。分离集和明确功能取消对读-修改-写操作的需要。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
- 输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器。
把pin4引脚配置为输出引脚: FSEL4 14-12 001 我们把4引脚的14-12配置成001 GPIO Pin 4 is an output 详细操作: 只需要将GPFSL0这个寄存器的14~12位设置为001就可以了。只需要将0x6(对应的2进制是110)左移12位·然后取反再与上GPFSL0就可以将13、14这两位配置为0,然后再将0x6(对应2进制110)左移12位,然后或上GPFSL0即可将12位置1。
特别提示:进行取反后再进行按位与操作是为了不影响其他引脚
配置pin4引脚为输出引脚 bit 12-14 配置成001
31 30 ······14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 0 ······0 0 1 0 0 0 0 0 0 0 0 0 0 0
*GPFSEL0 &= ~(0x6 <<12);
*GPFSEL0 |= (0x1 <<12);
忘了按位与和按位或的点这里
代码实现:
*GPFSEL0 &=~(0x6 <<12);
*GPFSEL0 |= (0x1 <<12);
- 注意:我们配置的底层引脚对应得是BCM 寄存器第0组位FESL0–9, 这个就是在寄存器GPFSEL0里,寄存器已经分好组了
寄存器第1组位FSEL10–19,这个在寄存器GPFSEL1里
更多的引脚对应的寄存器可以去树莓派官网进行查看 树莓派引脚查看官网 在上图中我们可以点击对应的引脚编号,就可以查看到对应的引脚的相关的信息
二、寄存器的地址问题
我们在编写驱动程序的时候,IO口空间的起始地址是0x3f00 0000,加上GPIO的偏移量0x200 0000,所以GPIO的物理地址应该是0x3f20 0000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。 上图的尾部偏移是对的,根据GPIO的物理地址0x3f20 0000可以知道:
GPFSEL0 0x3f20 0000
GPSET0 0x3f20 001c
GPCLR0 0x3f20 0028
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
完成以上代码需要搞清楚的几点
-
弄清楚寄存器的分组 GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。这个由查阅芯片手册可以得知 -
volatile关键字的使用(笔试可能会考)
-
在此处的作用:防止编译器优化(可能是省略,也可能是更改)这些寄存器地址变量,常见于在内核中对IO口进行操作 -
作用:确保指令不会因编译器的优化而省略,且要求每次直接读值,在这里的意思就是确保地址不会被编译器更换
- 如何配置寄存器的地址
首先是在1.的基础上,在pin4_drv_init 这个函数里面添加寄存器地址的配置
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
写出以上的代码,要搞清楚以下几点 分别找到几个IO寄存器的物理地址(非常易错),弄清楚GPIO的物理地址(真实地址) 记住并不是用下面这张图的地址来对应GPIO功能选择寄存器0的地址,否则编译后运行会有段错误。 IO口的起始地址是0x3f000000 ,加上GPIO的偏移量0x2000000 ,所以GPIO的实际物理地址应该是从0x3f200000 开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。
然后我们可以根据这个偏移值来确定寄存器的物理地址(真实的地址) 可以看到寄存器GPSET0相对于GPIO物理地址的偏移值为1C 。即0x3f20001C 同样的方法,寄存器GPCLR0的偏移值为28,即0x3f200028 寄存器GPFSEL0的偏移值为0,即0x3f200000
- 如何让引脚拉高或拉低电平
代码实现:
if(userCmd == 1)
{
printk("set 1\n");
*GPSET0 |= (0x1 << 4);
}
else if(userCmd == 0)
{
printk("set 0\n");
*GPCLR0 |= (0x1 << 4);
}
else
{
printk("nothing undo\n");
}
引脚输出高电平: *GPSET0 |= (0x1 << 4); 左移4位, 这里无论什么寄存器都是写1,写1并不是为某个io口去写1,而是1是驱动(SET)设置寄存器工作将bit4的电平拉高即变为高电平,为什么要进行或操作,是因为为了不影响其他引脚的状态
引脚输出低电平: *GPCLR0 |= (0x1 << 4); 同样道理,左移4位,这里的1也并不是为了某个io口去写1,而是1是驱动(CLR)清零寄存器将电平拉低,即变为低电平,进行或操作也一样是为了不影响其他引脚的电平状态
- 在Linux内核的io.h头文件中声明了
ioremap() 函数,用来将IO内存资源映射到核心虚拟地址空间(3Gb~4GB)中,当然不用了可以将其取消映射iounmap ()。这两个函数在mm/ioremap.c 文件中:
开始映射:void* ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags)
第一个参数是映射的起始地址
第二个参数是映射的长度
第二个参数怎么定啊?
====================
这个由你的硬件特性决定。
比如,你只是映射一个32位寄存器,那么长度为4就足够了。
(这里树莓派IO口功能设置寄存器、IO口设置寄存器都是32位寄存器,所以分配四个字节就够了)
比如:GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
这三行是设置寄存器的地址,volatile的作用是作为指令关键字
确保本条指令不会因编译器的优化而省略,且要求每次直接读值
ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。
解除映射:void iounmap(void* addr)
比如:
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
- 浅谈一下
copy_from_user 和copy_to_user 函数的用法 copy_from_user 和copy_to_user 这两个函数相信做内核开发的人都非常熟悉,分别是将用户空间的数据拷贝到内核空间以及将内核空间中的数据拷贝到用户空间
函数copy_from_user原型:
copy_from_user(void *to, const void __user *from, unsigned long n)
返回值:失败返回没有被拷贝成功的字节数,成功返回0
参数详解:
1. to 将数据拷贝到内核的地址,即内核空间的数据目标地址指针
2. from 需要拷贝数据的地址,即用户空间的数据源地址指针
3. n 拷贝数据的长度(字节)
也就是将@from地址中的数据拷贝到@to地址中去,拷贝长度是n
详细了解copy_from_user和copy_to_user
三、驱动代码与应用测试代码
3.1 相关代码 底层驱动代码:
#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 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;
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n");
*GPFSEL0 &= ~(0x6 << 12);
*GPFSEL0 |= (0x1 << 12);
return 0;
}
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;
int copy_cmd;
printk("pin4_write\\n");
copy_cmd = copy_from_user(&userCmd,buf,count);
if(copy_cmd != 0)
{
printk("fail to copy from user\n");
}
if(userCmd == 1)
{
printk("set 1\n");
*GPSET0 |= (0x1 << 4);
}
else if(userCmd == 0)
{
printk("set 0\n");
*GPCLR0 |= (0x1 << 4);
}
else
{
printk("nothing undo\n");
}
return 0;
}
static ssize_t pin4_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
printk("pin4_read\n");
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
.read = pin4_read,
};
int __init pin4_drv_init(void)
{
int ret;
devno = MKDEV(major,minor);
ret = register_chrdev(major, module_name,&pin4_fops);
pin4_class=class_create(THIS_MODULE,"myfirstdemo");
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)
{
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
int userCmd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0)
{
printf("fail to open the pin4\n");
perror("the reason:");
}
else
{
printf("success to open the pin4\n");
}
printf("please Input 1-HIGH,0-LOW \n");
scanf("%d",&userCmd);
write(fd,&userCmd,4);
return 0;
}
至于怎么在虚拟机中编译驱动文件以及如何将编译好的文件发送至树莓派,敬请关注以下博文 如何将编译好的文件发送至树莓派底下
3.2 在树莓派底下进行代码的测试与验证 相关的驱动的装载与卸载也查看驱动装载与卸载
- 先来查看一下树莓派4号引脚的初始状态是什么
输入1,将引脚电平变为高电平 输入0,将引脚电平变为低电平 到目前为止,我们经过那么多节的对驱动的学习的博文,现在终于自己终于实现了类似于wiringPi 这样的一个驱动文件,我们在这里做的是引脚4的驱动,那么我们就可以按着模样来写引脚5,引脚6,甚至其他引脚的驱动,在这里想说一句,驱动代码的编写,都是基于linux内核源码来进行编写的,linux内核源码这个文件在前面的博文有,如有需要自行去下载。
学习笔记,仅供参考
树莓派高级开发之IO口驱动代码编写 优秀博文参考一 树莓派高级开发之IO口驱动代码编写 优秀博文参考二
|