驱动开发的完善 --- 芯片手册导读 + I/O口操控代码的编写

发布时间:2023年12月22日

在我上上节的博文中(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的置1GPSET0寄存器,将其第4位(SET4置1
  • pin4的清0GPCLR0寄存器,将其第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博客):

  1. 先使用“&=”将14和13位 置“0”
  2. 再使用“|=”将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口的操控!

文章来源:https://blog.csdn.net/mjmmm/article/details/135143533
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。