注:本篇文章针对字符设备驱动
一、驱动框架
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口操作
以上三篇是连续的,建议一起看。