在我上上节的博文中(linux驱动的学习 & 驱动开发初识-CSDN博客):
我通过一个基本的字符设备驱动框架来测试了驱动的运行,但是在“pin4_open”和“pin4_write”这两个驱动函数的函数体里只写了一句内核打印的代码,作为一个真正的驱动文件这显然是不够的。
同时,在之前的博文中就提到过,驱动位于内核态的最底层,其下方就直接是硬件,所以驱动函数的目标就是直接操控硬件,也就是直接操控寄存器。在我的pin4驱动函数中应该添加的也就是根据函数功能,操作寄存器从而实现I/O口操控的代码。
目录
BCM2835芯片手册导读
寄存器选择
定位pin4
驱动代码的完善
寄存器的物理地址
寄存器在代码中的定义
pin4_open & pin4_write实现逻辑
新的 mydriver_pin4.c:
驱动的编译
驱动的测试
pin4_test.c:
BCM2835芯片手册导读
明确了目标后,就产生了这个问题:我怎么知道应该使用哪些寄存器,又应该怎么使用呢?
答案是根据开发平台的芯片手册/电路图来找到具体的描述,由于我是在树莓派3B+上玩驱动的开发,所以我应该查阅这款树莓派的芯片,也就是BCM2835的芯片手册。
此处我只使用了芯片手册就定位了寄存器,而没有用电路图,原因是树莓派的这个芯片手册已经把用什么寄存器写的很清楚了
但是,芯片手册有几百页,不可能通篇细读,所以这就需要根据我想要查看的内容来锁定具体的范围,我现在写的是GPIO-->pin4的驱动代码,所以显然应该定位到GPIO章节:
- 在P90/91页,介绍了GPIO相关的共41个寄存器,每个寄存器有32个bit:
- 在P92页中,对于“GPFSELn”系列寄存器的介绍中的Table 6-2:
由于设置的是pin4,所以应该使用GPFSEL0寄存器
可见,GPFSEL0寄存器中的14,13,12位对应FSEL4寄存器,给这三位赋不同的值对应的就是pin4的不同模式。
- 在P95页中,对于“GPSET0”和“GPSET1”两个置位寄存器描述的两个表格中:
(对此寄存器写0无效;且如果GPIO被设置成输入模式则此寄存器无效)
由于设置的是pin4,所以应该使用GPSET0寄存器
- 对于GPSET0寄存器,如果将其第4位(SET4)置1,则代表将pin4置位(置1)
- 在P95/96页中,对于“GPCLR0”和“GPCLR1”两个清0寄存器描述的两个表格中:
(对此寄存器写0无效;且如果GPIO被设置成输入模式则此寄存器无效)
由于设置的是pin4,所以应该使用GPCLR0寄存器
- 对于GPCLR0寄存器,如果将其第4位(CLR4)置1,则代表将pin4清0
寄存器选择
此时,已经大致的找到了想要的寄存器,现在总结一下:
- pin4的功能选择:GPFSEL0寄存器中的14,13,12位对应的FSEL4寄存器
配置方法:
- pin4的置1:GPSET0寄存器,将其第4位(SET4)置1
- pin4的清0:GPCLR0寄存器,将其第4位(CLR4)置1
定位pin4
在树莓派中输入“gpio readall”:
pin4指的应该是BCM的4号,对应wiringPi库的7号,物理引脚的7号
驱动代码的完善
在了解了寄存器的选择后,就可以真正开始实现驱动函数的函数体了!
在这之前,回顾一下之前的框架代码:
#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"; //模块名
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
return 0;
}
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_write\n"); //内核的打印函数和printf类似
return 0;
}
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
.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); //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
//以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
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"); //linux内核遵循GPL协议
现在的目的就是真正的实现“pin4_open”和“pin4_write”这两个驱动函数:
寄存器的物理地址
操控寄存器前,首先需要在代码中定义寄存器,在上节关于总线/物理/虚拟地址的学习中了解到,进程的运行首先需要物理地址,然后将其与虚拟地址映射起来。所以先找到寄存器的物理地址。
切记,这个地址不能根据芯片手册P90/91页的最左侧Address来,因为这款芯片手册列出的是“总线地址”,而此处需要的是“物理地址”,对于这款芯片,I/O空间的其实地址是0x3f000000,加上GPIO的偏移量,所以GPIO的物理地址应该是从0x3f200000开始的
可以在树莓派中输入“sudo cat /proc/iomem”查看物理地址分配情况:
了解了GPIO寄存器的起始地址“0x3f200000” 后,各个寄存器的偏移地址倒是可以参考芯片手册,因为偏移地址是相对的:
综上,可以得出三个寄存器的物理地址:
- GPFSEL0 --> 0x3f200000
- GPSET0 --> 0x3f20001C
- GPCLR0 --> 0x3f200028
寄存器在代码中的定义
找到了寄存器的物理地址之后,就可以将其与虚拟地址映射,并在程序中定义了:
//写在驱动代码的开头定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
//写在初识化函数“pin4_drv_init”的函数体中
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
//写在退出函数“pin4_drv_exit”的函数体中
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
- 要使用“volatile”关键字来确保指令不会因编译器的优化而省略,且要求每次直接读值(否则编译器会自认为我给的地址不好,从而自动的重新分配地址;且寄存器的值会经常变化所以要求每次都直接读值,时效性更强)
- int类型在linux中占4个字节,1个字节8个bit,所以int有32个bit。int型可以表示正数,负数和0,所以表示范围是“-2^31 到 2^31 - 1”;而unsigned int通过牺牲负数的表达从而大大提升了正数的表达范围,unsigned int的表示范围是“0 到 2^32 - 1” (此处的地址是一个8位的16进制数,一位16进制需要用4位2进制表示,所以需要32位,只能用unsigned int来表示)
-
ioremap()函数用于将物理地址映射为虚拟地址,其函数原型是ioremap(resource_size_t rescookie, size_t sieze),其中rescookie是物理地址,size是映射的字节大小;此处的寄存器在刚刚提到过是32位,因此为4个字节,size就是4
-
ioremap转化后的地址依然是一个16进制数,如果要赋给指针变量的话,记得要进行强转
pin4_open & pin4_write实现逻辑
- 对于pin4_open:将pin4配置成输出模式,即将GPFSEL0寄存器中的14,13,12位设置为001
- 对于pin4_write:获取上层write函数要写的内容;然后根据值来操作pin4口(置1/清0)
新的 mydriver_pin4.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"; //模块名
//写在驱动代码的开头定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
//将pin4配置成输出模式,即将GPFSEL0寄存器中的14,13,12位设置为001
*GPFSEL0 &= 0xFFFF9FFF;//13,14位 置“0”
*GPFSEL0 |= 0x00001000;//12位 置“1”
return 0;
}
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int usr_cmd;
printk("pin4_write\n"); //内核的打印函数和printf类似
//获取上层write函数要写的内容
copy_from_user(&usr_cmd,buf,count);
printk("get value from write:%d\n",*buf);
//然后根据值来操作pin4口(置1/清0)
if(usr_cmd == 1){
*GPSET0 |= 0x00000010; //置“1”
}else if(usr_cmd == 0){
*GPCLR0 |= 0x00000010; //清“0”
}else{
printk("unknown user command!\n");
}
return 0;
}
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
.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); //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
//以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //先创建‘类’
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //再创建‘设备’
//写在初识化函数“pin4_drv_init”的函数体中
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
printk("pin4_driver init success\n");
return 0;
}
void __exit pin4_drv_exit(void)
{
//写在退出函数“pin4_drv_exit”的函数体中
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"); //linux内核遵循GPL协议
将GPFSEL0寄存器中的14,13,12位设置为001的思路:
(参考C51单片机的定时器 和 中断初识_c51定时器延时-CSDN博客):
- 先使用“&=”将14和13位 置“0”
- 再使用“|=”将12位 置“1”
获取上层write函数要写的内容的思路:使用copy_from_user函数
- 该函数目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0
- 需要包含的头文件:
#include <linux/uaccess.h>
- 函数原型 & 参数
ulong copy_to_user(void __user *to, const void *from, unsigned long n); //第一个参数 to:目标内核空间的地址 //第二个参数 from: 源用户空间地址。保存了用户要发送的数据,或者要拷贝到内核空间的内容的地址 //第三个参数 n:要拷贝的字节数
驱动的编译
在之前驱动学习的时候已经经历过一次,现在将新的代码编译:
- 打开虚拟机,进入Linux源码下的字符驱动设备目录:“linux/drivers/char/”,找到之前写的mydriver_pin4.c,并将刚刚完善过的代码写进去:
- 修改当前路径下的Makefile,确保这个新的驱动会被编译到:
- 回到linux内核源码的路径,运行以下指令尝试编译:
ARCH=arm CROSS_COMPILE=/home/mjm/ras_CrossCompile/gcc-linaro-5.1-2015.08-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules
- 将编译好的“mydriver_pin4.ko”通过以下的scp命令发送到树莓派:
scp drivers/char/mydriver_pin4.ko pi@192.168.2.26:/home/pi/mjm_code
- 在树莓派中,运行以下指令加载模块:
sudo insmod mydriver_pin4.ko
驱动的测试
在加载完新的驱动之后,重新修改驱动的测试代码:
pin4_test.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int driver_fd;
int usr_cmd;
while(1){
driver_fd = open("/dev/pin4",O_RDWR); //以可读可写打开的方式打开驱动
if(driver_fd < 0){
perror("fail to open driver file:");
}else{
printf("open driver file success!\n");
}
printf("input cmd: 1 OR 0\n 1:set pin4 to 1\n0:clear pin4 to 0\n");
scanf("%d",&usr_cmd);
if(usr_cmd == 1 || usr_cmd == 0){
driver_fd = write(driver_fd,&usr_cmd,4);
if(driver_fd < 0){
perror("fail to write:");
}else{
printf("write success!\n");
}
}else{
printf("unkown cmd!\n");
continue;
}
}
return 0;
}
- open函数也要写在while(1)里面
- write函数最后一个参数是4,因为一个int型是4个字节,也可以写成sizeof(int)
编译并运行:
1. gcc pin4_test.c -o pin4_test
2. sudo ./pin4_test
如果报错,可能需要赋予权限:
sudo chmod 666 /dev/pin4 //666代表让所有用户都有所有权限
可见,程序已经成功运行起来了,此时另开一个窗口随时准备输入“gpio readall”来验证结果:
- 如果输入1,再通过gpio readall查看:
- 如果输入0,再通过gpio readall查看:
- 同时,也可以“dmesg”查看内核的日志:
可见,成功的通过识别用户输入的指令而进行了对pin4口的操控!