树莓派开发—初识驱动开发


注:本篇文章针对字符设备驱动

一、驱动框架

pin4引脚的驱动代码 pin4driver.c :

/*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";   //模块名

//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    
    
    printk("pin4_open\n");  //内核的打印函数,和printf类似 
    return 0;
}

//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    
    
	printk("pin4_write\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;
    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);  //创建设备文件

    return 0;
}

void __exit pin4_drv_exit(void)
{
    
    
    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");

以上是基于pin4引脚写的一个驱动基本框架,没有任何的业务功能。以后进行其它引脚驱动的开发也是基于该框架进行修改。

Linux下一切皆文件,因此访问一个设备和访问文件是一样的,同样是用open(),write(),read()等函数进行操作。上层应用代码 pin4test.c :

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

int main()
{
    
    
	int fd;
	fd = open("/dev/pin4",O_RDWR);
	if(fd < 0){
    
    
		printf("open failed\n");
		perror("reson");
	}else{
    
    
		printf("open success\n");
	}
	fd = write(fd,'1',1);//写一个字符'1',写一个字节
	
	return 0;
}

关于对于驱动的认知,以及对驱动框架代码的解读,可以参考文章:Linux底层驱动的简单认知,讲的很不错,很详细。

二、驱动模块编译及测试

接下来我们基于上面驱动框架代码,在Linux环境下生成驱动,并拿到树莓派上运行。
准备工作:

  • 之前我们移植过树莓派内核,此次驱动模块编译生成要在之前的内核源码的driver/char/目录下,通过配置Makefile文件进行。
  • 交叉编译工具链要配置好了,驱动模块的编译、上层代码的编译都需要交叉编译,因为都要拿到树莓派上使用。

接下来我们把前文编写好的驱动代码放入内核源码的 driver/char 目录下:
在这里插入图片描述
修改该目录下的Makefile文件,添加下面红色方框内容:
在这里插入图片描述
-m :表示编译成模块,之前的文章中提到过。

回到内核源码目录/linux-rpi-4.14.y下,执行以下命令:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules

对比之前文章里编译内核命令:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs

此处我们只需要编译驱动模块,因此只需要 make modules 即可。执行结果:
在这里插入图片描述在这里插入图片描述
生成了 pin4driver.ko 文件,驱动模块编译成功。
接下来再交叉编译一下上层程序 pin4test.c
在这里插入图片描述
将 pin4test 和 pin4driver.ko 文件上传到树莓派:
在这里插入图片描述
装载驱动,执行命令:

sudo insmod pin4drive.ko

我们去 /dev 目录下查看驱动是否装载成功:
在这里插入图片描述
pin4 驱动装载成功。

接下来我们运行上层代码试试:
在这里插入图片描述
文件打开失败,查看 pin 的权限:
在这里插入图片描述
发现只有超级用户有权限,修改权限
在这里插入图片描述
再次执行上层程序:
在这里插入图片描述
使用 dmesg 命令(注意不是demsg哦)查看内核打印信息:
在这里插入图片描述
说明内核的驱动被成功调用。

小知识:另外再介绍两个命令
内核驱动卸载:sudo rmmod xxx 不需要写ko
查看内核模块:lsmod

三、配置寄存器实现IO口操作

实现功能:通过配置寄存器实现pin17的驱动编写,达到操作它输出高\低电平。

进行驱动开发,必须借助芯片手册,包括以后在别的芯片平台进行开发,都需要阅读芯片手册。

树莓派3B用的是(博通)BCM2835芯片,我们先观察一下树莓派的芯片外设:
(这里注意一下:用树莓派的wiringPi库操作的引脚和寄存器操作的引脚标号是不一样的,比如BCM的pin17对应的是wiringPi的pin0,有点坑哦)
在这里插入图片描述
这里我们进行的是普通IO口的开发,因此需要阅读芯片手册的通用I/O(GPIO)开发部分。

详细的芯片解读(寄存器解读)参看文章:Linux底层驱动之树莓派IO口操作,讲的清除细致。

根据我们要实现的功能和芯片手册的解读,我们需要配置三个寄存器:
在这里插入图片描述

1. GPFSELn (n:0~5):GPIO功能选择寄存器
一共有六组,对应总共54个GPIO,具体哪个组的哪个引脚怎么设置参考芯片手册以及后面的具体代码。

2. GPSET0,GPSET1:GPIO引脚输出设置寄存器
在这里插入图片描述
GPSET0: pin0~pin31的设置寄存器,1位高电平,0为低电平,复位后为0。
在这里插入图片描述
GPSET1: pin32~pin53的设置寄存器,1位高电平,0为低电平,复位后为0。

3. GPCLR0,GPCLR1:GPIO输出清除寄存器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
清除这三种寄存器的使用之后就可以进行io口驱动开发了,同样,还是根据上面的驱动模板进行开发。

驱动代码:

//pin17driver.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 *pin17_class;  
static struct device *pin17_class_dev;

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

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

//pin17_open函数
static int pin17_open(struct inode *inode,struct file *file)
{
    
    
    printk("pin17_open\n");  //内核的打印函数,和printf类似
    
    //open的时候配置pin17为输出引脚
    *GPFSEL1 &= ~(0x6 << 21);
	*GPFSEL1 |= (0x1 << 21);
    
    return 0;
}

//pin17_write函数
static ssize_t pin17_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    
    
	int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用char

	printk("pin17_write\n");
	
	//获取上层write的值
	copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
	
	//根据值来执行操作
	if(userCmd == 1){
    
    
		printk("set 1\n");
		*GPSET0 |= 0x1 << 17;//设置pin17口为1
	}else if(userCmd == 0){
    
    
		printk("set 0\n");
		*GPCLR0 |= 0x1 << 17;//清除pin17口
	}else{
    
    
		printk("cmd error\n");
	}
	
    return 0;
}

static struct file_operations pin17_fops = {
    
    

    .owner = THIS_MODULE,
    .open  = pin17_open,
    .write = pin17_write,
};

int __init pin17_drv_init(void)   //驱动的真正入口
{
    
    

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

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

	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 pin17_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{
    
    
	iounmap(GPFSEL1);
	iounmap(GPSET0);
	iounmap(GPCLR0);
	//解除虚拟地址映射
	
    device_destroy(pin17_class,devno);
    class_destroy(pin17_class);
    unregister_chrdev(major, module_name);  //卸载驱动

}

module_init(pin17_drv_init);  //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
module_exit(pin17_drv_exit);
MODULE_LICENSE("GPL v2");

上层程序代码:

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

int main()
{
    
    
        int fd;
        int cmd;
        
        fd=open("/dev/pin17",O_RDWR);
        if(fd<0){
    
    
                perror("reson");
                return -1;
        }
        
        printf("input:1/0(1:高电平,0:低电平)\n");
        scanf("%d",&cmd);
        
		if(cmd == 0){
    
    
                printf("pin17设置成低电平\n");
        }else if(cmd == 1){
    
    
                printf("pin17设置成高电平\n");
        }
        
        fd=write(fd,&cmd,sizeof(int));
        
        return 0;
}

代码解读:
我们要操作的是pin17,并由上层控制输出高低电平。需要将pin17设置为输出模式,内核接收上层指令,达到控制输出高低电平的目的。

①定义寄存器

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

pin17引脚在GPFSEL1分组,引脚输出寄存器在GPSETO分组。

volatile关键字的作用:
防止编译器优化(可能是省略,也可能是更改)这些寄存器变量,CPU每次直接在寄存器中读取变量,而不是在内存中,保证数据的时效性。常见于在内核中对IO口进行的操作。

②初始化寄存器的虚拟地址
BCM2835芯片手册里面的地址是总线地址,需要加上偏移地址才能转换为真正的物理地址。
在这里插入图片描述
BCM2835芯片手册这里都是总线地址,不是物理地址,我们需要GPIO真正的物理地址。
IO口的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的实际物理地址应该是从0x3f200000开始的。(这部分自己查实,没办法就是这么坑,树莓派3B好像不能通过指令cat /proc/iomen直接得到虚拟地址)
因此三个寄存器的物理地址为:
0x3f200004
0x3f20001C
0x3f200028

③物理地址转换为虚拟地址
我们对于上层代码的访问和内核代码的访问都是基于虚拟地址的,因此需要将物理地址转换为虚拟地址。用到函数void *ioremap(unsigned long phys_addr, unsigned long size)
三个寄存器的初始化虚拟地址:

GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200004,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);

④pin17口功能配置
接下来就是pin17引脚功能的实现了,具体就是:
pin17_open()函数中,配置pin17引脚为输出。

*GPFSEL1 &= ~(0x6 << 21);
*GPFSEL1 |= (0x1 << 21);

pin17_write()函数中,根据上层代码实现Pin17输出高低电平。

if(userCmd == 1){
    
    
		printk("set 1\n");
		*GPSET0 |= 0x1 << 17;//设置pin17口为1
	}else if(userCmd == 0){
    
    
		printk("set 0\n");
		*GPCLR0 |= 0x1 << 17;//清除pin17口
	}else{
    
    
		printk("cmd error\n");
	}

⑤退出程序,解除虚拟地址映射

iounmap(GPFSEL1);
iounmap(GPSET0);
iounmap(GPCLR0);

测试
底层驱动代码和上层程序代码都已经编写完成,接下来和上面一样,利用交叉编译工具编译驱动模块和上层程序。

驱动编译:
在这里插入图片描述在这里插入图片描述
上层代码编译:
在这里插入图片描述
将驱动和上层运行程序发送至树莓派:
在这里插入图片描述
装载驱动:

sudo insmod pin17drive.ko

在这里插入图片描述
在这里插入图片描述
给普通用户组操作pin17的权限:

sudo chmod 666 /dev/pin17

加可执行权限并运行上层程序:
在这里插入图片描述
在这里插入图片描述
实现了Pin17的高低电平输出。

参考文章:
详细到吐血 —— 树莓派驱动开发入门:从读懂框架到自己写驱动

Linux底层驱动的简单认知
树莓派4B Linux的底层驱动编写体验
Linux底层驱动之树莓派IO口操作
以上三篇是连续的,建议一起看。

猜你喜欢

转载自blog.csdn.net/little_rookie__/article/details/118825941