树莓派3B 底层io驱动开发(实现火灾警报器)

编写驱动代码前的必要准备工作

驱动代码编译前,需要一个提前编译好的内核
所以在编写驱动代码前需要配置和编译内核
内核的配置的编译可以参考另外一个博文【树莓派3B Linux源码配置和内核编译】

BCM2835芯片手册部分的简单解读

GPIO寄存器一览(位于手册的90~91页)

以下两张图用于查找GPIO寄存器的地址
项目的实现主要使用到以下四个寄存器
GPFSEL用于配置某一引脚的输入输出模式,分为6组
GPSET用于将某一引脚置高平,分为2组
GPCLR用于清0某一引脚,即置低平,分为2组
GPLEV用于查看引脚的电平,返回值为各引脚的高低电平状态,分为2组
在这里插入图片描述
在这里插入图片描述

注意:芯片手册左边列表所列出的地址是总线地址,不是物理地址!!!树莓派的物理始址是0x 3f00 0000!!!简单来说就是把7E改为3f,即为物理地址;拿GPSET0来说,其总线地址是0x 7E20 001C,物理地址就是0x 3f20 001C

用于设置引脚功能的GPFSEL寄存器

下面是GPFSEL0寄存器的内容,即第0组(GPFSEL寄存器的内容在P91~94)
Field Name那一竖列中的FSELn,其中的n代表第n引脚
假设你要找引脚9的话,就去找FSEL9
在这里插入图片描述
FSEL9那一横栏如下
在这里插入图片描述

引脚输入输出模式的设置方法

通过上上一张图可以看到一个寄存器有32个bit(0~31),每个引脚占3个bit(拿FESL9来说,其占据29-27bit);上面那张图的中间栏具体的说明了:若要将引脚设置为输入模式,就将该引脚所占的3个bit设置为000;若要将引脚设置为输出模式,就将该引脚所占的3个bit设置为001

如何确定引脚在GPFSEL的第几组

通过阅读手册中列出的表格,GPFSEL寄存器差不多都是以10个引脚为一组;
找到引脚所在的表格后,通过查看表格下面那一段英文:
在这里插入图片描述
Table…GPIO Alternate function selelect register X(X即为所在的组数)

如何设置某一引脚的模式(关键步骤)

输入模式设置

以引脚7为例:
1、先查找所在的GPFSEL的组数:查看可得到为GPFSEL0
2、查看GPFSEL0寄存器的物理地址:通过查找该文章的第一个图,可得到GPFSEL0的总线地址是0x 7E20 0000,即物理地址是0x 3f20 0000
3、查看引脚7在GPFSEL0寄存器中所占的bit值:通过查看表格,为23-21这3个bit
在这里插入图片描述
4、关键代码实现(并非完整的驱动代码)

volatile unsigned int* GPFSEL0 = NULL;

//ioremap函数用于将物理地址转换为虚拟地址
GPFSEL0 = (volatile unsigned int*)ioremap(0x3f200000, 4);

//将GPFSEL0寄存器的第23到21个bit设置为0,且不影响其他位
GPFESL0 &= ~(0x7 << 21);

输出模式设置

以引脚7为例:
1、先查找所在的GPFSEL的组数:查看可得到为GPFSEL0
2、查看GPFSEL0寄存器的物理地址:通过查找该文章的第一个图,可得到GPFSEL0的总线地址是0x 7E20 0000,即物理地址是0x 3f20 0000
3、查看引脚7在GPFSEL0寄存器中所占的bit值:通过查看表格,为23-21这3个bit
4、关键代码实现(并非完整的驱动代码)

volatile unsigned int* GPFSEL0 = NULL;

//ioremap函数用于将物理地址转换为虚拟地址
GPFSEL0 = (volatile unsigned int*)ioremap(0x3f200000, 4);

//先将GPFSEL0寄存器的第23和22个bit设置为0,且不影响其他位
GPFESL0 &= ~(0x6 << 21);
//再将GPFSEL0寄存器的第21个bit设置为1,且不影响其他位
GPFESL0 |= (0x1 << 21);

用于置1的GPSET寄存器

GPSET寄存器在芯片手册中分为2组,内容为下面的截图(GPSET寄存器的内容在P95)
在这里插入图片描述
可以看到GPSET寄存器分为2组,每组占32个bit,对应32个引脚
Description中说明的是:当你要将引脚9置高电平时,就将第9个bit置为1;置为0是没有任何作用的(即不能用于将某一引脚置低电平);
引脚9位于bit9,所以只需操作GPSET0组,将其第9个bit置为1,即可;

如何将某一引脚置为高电平(关键步骤)

以引脚7为例:
1、先查找所在的GPSET的组数:查看可得到为GPSET0
2、查看GPFSEL0寄存器的物理地址:通过查找该文章的第一个图,可得到GPSET0的总线地址是0x 7E20 001C,即物理地址是0x 3f20 001C
3、查看引脚7在GPSET0寄存器中所占的bit值:第7引脚,即为第7个bit
4、关键代码实现(并非完整的驱动代码)

volatile unsigned int* GPSET0 = NULL;

//ioremap函数用于将物理地址转换为虚拟地址
GPSET0 = (volatile unsigned int*)ioremap(0x3f20001C, 4);

//先将GPSET0寄存器的第7个bit设置为1,且不影响其他位
GPFESL0 = (0x1 << 7);

用于清0的GPCLR寄存器

GPCLR寄存器在芯片手册中分为2组,内容为下面的截图(GPCLR寄存器的内容在P95-96)
在这里插入图片描述
可以看到GPCLR寄存器分为2组,每组占32个bit,对应32个引脚
Description中说明的是:当你要将引脚9置低电平时,就将第9个bit置为1;置为0是没有任何作用的;
引脚9位于bit9,所以只需操作GPCLR0组,将其第9个bit置为1,即可;

如何将某一引脚置为低电平(关键步骤)

以引脚7为例:
1、先查找所在的GPCLR的组数:查看可得到为GPCLR0
2、查看GPCLR0寄存器的物理地址:通过查找该文章的第一个图,可得到GPCLR0的总线地址是0x 7E20 0028,即物理地址是0x 3f20 0028
3、查看引脚7在GPCLR0寄存器中所占的bit值:第7引脚,即为第7个bit
4、关键代码实现(并非完整的驱动代码)

volatile unsigned int* GPCLR0 = NULL;

//ioremap函数用于将物理地址转换为虚拟地址
GPCLR0 = (volatile unsigned int*)ioremap(0x3f200028, 4);

//先将GPCLR0寄存器的第7个bit设置为1,且不影响其他位
GPCLR0 = (0x1 << 7);

用于查看引脚电平的GPLEV寄存器

GPLEV寄存器在芯片手册中分为2组,内容为下面的截图(GPLEV寄存器的内容在P96)
Description中有些错误:“0 = GPIO pin n is high” 应该为 “1 = GPIO pin n is high”

在这里插入图片描述
可以看到GPLEV寄存器分为2组,每组占32个bit,对应32个引脚
Description中说明的是:当你要查看第n个引脚的电平高低时,就通过获取相应GPLEV组的第n个bit的值,即可;
若为1,则为高电平;若为0,则为低电平;

如何查看某一引脚的电平(关键步骤)

以引脚7为例:
1、先查找所在的GPLEV的组数:查看可得到为GPLEV0
2、查看GPLEV0寄存器的物理地址:通过查找该文章的第一个图,可得到GPLEV0的总线地址是0x 7E20 0034,即物理地址是0x 3f20 0034
3、查看引脚7在GPLEV0寄存器中所占的bit值:第7引脚,即为第7个bit
4、关键代码实现(并非完整的驱动代码)

volatile unsigned int* GPLEV0 = NULL;

//ioremap函数用于将物理地址转换为虚拟地址
GPLEV0 = (volatile unsigned int*)ioremap(0x3f200034, 4);

int status;

/*  
    读取电平的状态
    获取的解决方式:
    先将除了bit7以外的值变为0,再通过右移,此时的值即为bit7的值
*/
status = (*GPLEV0 & (0x1 << 7)) >> 7;

如何确定所选引脚对应芯片手册GPIO PIN的编号

通过gpio readall指令来查看
查看图中的BCM那一栏,就是对应芯片手册GPIO PIN的编号
在这里插入图片描述

火灾警报器驱动代码的编写

运用到火焰监测(输入模块,用到引脚4)和蜂鸣器(输出模块,用到引脚17)两个模块

1、输入模块底层驱动代码:用到引脚4

#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 *fire_class;
static struct device *fire_class_dev;
 
static dev_t devno;                  //设备号
static int major = 360;              //主设备号
static int minor = 0;                //次设备号
static char *module_name = "fire";   //模块名

volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPLEV0 = NULL;

//实现上层对open函数的调用
static int fire_open(struct inode *inode, struct file *file)
{
    
    
        //设置引脚4为输入模式
        *GPFSEL0 &= ~(0x7 << 12);
 
        return 0;
}

//实现上层对read函数的调用
static ssize_t fire_read(struct file *filp, char __user *buf, size_t  count, loff_t *f_pos){
    
    
        
        int ret = 0;
        int status;

        status = (*GPLEV0 & (0x1 << 4)) >> 4;
        
        //将内核空间中的数据拷贝到用户空间
        ret = copy_to_user(buf, &status, count);

        return ret;
}
 
//static限定这个结构体的作用只在这个文件
 static struct file_operations fire_fops = {
    
    
         .owner = THIS_MODULE,
         .open  = fire_open,
         .read = fire_read,
 };
 
 int __init fire_drv_init(void)  //真实驱动入口
 {
    
    
         int ret;
         devno = MKDEV(major, minor);  //创建设备号
         ret = register_chrdev(major, module_name, &fire_fops);  //注册驱动 告诉内核,把这个驱动加入到内核的链表中
 
         fire_class = class_create(THIS_MODULE, "fireClass"); //让代码在dev自动生成设备   创建类
         fire_class_dev = device_create(fire_class, NULL, devno, NULL, module_name); //创建设备文件   创建设备
        
        /*
        为这两个指针变量赋值上相应寄存器的地址
        volatile作为指令关键字,作用是确保本条指令不会因编译器的优化而省略,且要求每次直接读值
        ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问
		ioremap函数第一个参数是物理地址,第二个参数是寄存器所占的字节大小
		*/
        GPFSEL0 = (volatile unsigned int*)ioremap(0x3f200000, 4);
        GPLEV0  = (volatile unsigned int*)ioremap(0x3f200034, 4);

        return 0;
}

void __exit fire_drv_exit(void)
{
    
    
		//为了降低风险,需要把映射解除
        iounmap(GPFSEL0);
        iounmap(GPLEV0);

        device_destroy(fire_class, devno);
        class_destroy(fire_class);
        unregister_chrdev(major, module_name);  //卸载驱动
}

module_init(fire_drv_init); //程序入口,内核加载该驱动的时候,这个宏会被调用
module_exit(fire_drv_exit); //内核卸载该驱动的时候,这个宏会被调用
MODULE_LICENSE("GPL v2");

2、输出模块底层驱动代码:用到引脚17

#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>
#include <linux/string.h>

static struct class *beep_class;
static struct device *beep_class_dev;

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

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

//实现上层对open函数的调用
static int beep_open(struct inode *inode, struct file *file)
{
    
    
        //设置引脚17为输出模式
        *GPFSEL1 &= ~(0x6 << 21);
        *GPFSEL1 |= (0x1 << 21);
		
		//初始化蜂鸣器为关闭状态
        *GPSET0 = (0x1 << 17);
        
        return 0;
}

//实现上层对write函数的调用
static ssize_t beep_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    
    
        char cmd[10] = {
    
    '\0'};

		//将用户空间的数据拷贝到内核空间
        copy_from_user(cmd, buf, count);

        if(strcmp(cmd, "open") == 0){
    
    //当上层write所传的字符串为"open"时
                *GPCLR0 = (0x1 << 17);//启用蜂鸣器
        }else if(strcmp(cmd, "close") == 0){
    
    //当上层write所传的字符串为"close"时
                *GPSET0 = (0x1 << 17);//关闭蜂鸣器
        }

        return 0;
}

//static限定这个结构体的作用只在这个文件
static struct file_operations beep_fops = {
    
    
        .owner = THIS_MODULE,
        .open  = beep_open,
        .write = beep_write,
};

int __init beep_drv_init(void)   //真实驱动入口
{
    
    
        int ret;
        devno = MKDEV(major, minor);    //创建设备号
        ret = register_chrdev(major, module_name, &beep_fops);    //注册驱动告诉内核,把这个驱动加入到内核的链表中

        beep_class = class_create(THIS_MODULE, "beepClass"); //让代码在dev自动生成设备   创建类
        beep_class_dev = device_create(beep_class, NULL, devno, NULL, module_name); //创建设备文件   创建设备

		/*
        为这3个指针变量赋值上相应寄存器的地址
        volatile作为指令关键字,作用是确保本条指令不会因编译器的优化而省略,且要求每次直接读值
        ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问
		ioremap函数第一个参数是物理地址,第二个参数是寄存器所占的字节大小
		*/
        GPFSEL1 = (volatile unsigned int*)ioremap(0x3f200004, 4);
        GPSET0  = (volatile unsigned int*)ioremap(0x3f20001C, 4);
        GPCLR0  = (volatile unsigned int*)ioremap(0x3f200028, 4);

        return 0;
}

void __exit beep_drv_exit(void)
{
    
    
		//为了降低风险,需要把映射解除
        iounmap(GPFSEL1);
        iounmap(GPSET0);
        iounmap(GPCLR0);

        device_destroy(beep_class, devno);
        class_destroy(beep_class);
        unregister_chrdev(major, module_name);  //卸载驱动
}

module_init(beep_drv_init); //入口,内核加载该驱动的时候,这个宏会被调用
module_exit(beep_drv_exit);//内核卸载该驱动的时候,这个宏会被调用
MODULE_LICENSE("GPL v2");

运用到的相关重要函数介绍

1、ioremap函数

用于将物理地址转换为虚拟地址
void __iomem* ioremap(unsigned long offset, unsigned long size)

参数:
offset: 物理空间( I/O设备上的一块物理内存 )的起始地址
size: 物理空间的大小

返回值:虚拟地址

2、iounmap函数

用于解除地址的映射
void iounmap(void __iomem* addr)
参数:
addr:虚拟地址

3、copy_from_user函数

将用户空间的数据拷贝到内核空间,一般在实现上层调用write函数时使用
unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

参数:
to:指定将用户空间的数据拷贝到内核空间中的某个字符数组;
from:从用户空间拷贝到的数据;一般为实现write函数模板中所给的第二个参数buf
n:拷贝的字符个数;一般为实现write函数模板中所给的第三个参数count

4、copy_to_user函数

将内核空间中的数据拷贝到用户空间,一般在实现上层调用read函数时使用
unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

参数:
to:内核空间中的数据拷贝到用户空间的位置;一般为实现write函数模板中所给的第二个参数buf
from:内核空间中要拷贝给用户空间的数据,即为内核要传递的字符数组指针
n:拷贝的字符个数;一般为实现write函数模板中所给的第三个参数count

上层应用代码

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

int main()
{
    
    
	int fd1,fd2;
	fd1 = open("/dev/fire", O_RDWR);
	fd2 = open("/dev/beep", O_RDWR);
	
	int status;
	
	while(1){
    
    
		read(fd1, &status, 1);
		if(status == 1){
    
    //未检测到火灾,引脚为高电平
			write(fd2, "close", 5);//关闭蜂鸣器
		}else{
    
    //检测到火灾,引脚为低电平
			write(fd2, "open", 4);//开启蜂鸣器
		}
	}

	return 0;
}

驱动代码的编译,生成.ko模块文件

1、修改linux-rpi-4.14.y/drivers/char中的Makefile文件
在其中添加以下两行

obj-m                  			+= beepPinDriver.o
obj-m                  			+= firePinDriver.o

在这里插入图片描述
2、回到linux-rpi-4.14.y/目录,进行模块的编译
使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules

3、将生成.ko文件拷贝到树莓派上
完成第二步且无报错的情况下,会在linux-rpi-4.14.y/drivers/char文件中会生成beepPinDriver.kofirePinDriver.ko这两个文件;将这两个文件拷贝到树莓派上

在树莓派上装载和卸载.ko模块文件

装载驱动

1、装载.ko驱动文件的命令:
sudo insmod beepPinDriver.ko
sudo insmod firePinDriver.ko

2、通过 lsmod 指令能够查看是否装载成功

3、修改所装载的两个设备的权限
sudo chmod 666 /dev/beep
sudo chmod 666 /dev/beep

4、执行上层应用程序

卸载驱动

通过指令:
sudo rmmod beepPinDriver
sudo rmmod firePinDriver

猜你喜欢

转载自blog.csdn.net/weixin_50438937/article/details/114018586