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

在我上上节的博文中(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口的操控!

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Table of Contents 1 Introduction 4 1.1 Overview 4 1.2 Address map 4 1.2.1 Diagrammatic overview 4 1.2.2 ARM virtual addresses (standard Linux kernel only) 6 1.2.3 ARM physical addresses 6 1.2.4 Bus addresses 6 1.3 Peripheral access precautions for correct memory ordering 7 2 Auxiliaries: UART1 & SPI1, SPI2 8 2.1 Overview 8 2.1.1 AUX registers 9 2.2 Mini UART 10 2.2.1 Mini UART implementation details. 11 2.2.2 Mini UART register details. 11 2.3 Universal SPI Master (2x) 20 2.3.1 SPI implementation details 20 2.3.2 Interrupts 21 2.3.3 Long bit streams 21 2.3.4 SPI register details. 22 3 BSC 28 3.1 Introduction 28 3.2 Register View 28 3.3 10 Bit Addressing 36 4 DMA Controller 38 4.1 Overview 38 4.2 DMA Controller Registers 39 4.2.1 DMA Channel Register Address Map 40 4.3 AXI Bursts 63 4.4 Error Handling 63 4.5 DMA LITE Engines 63 5 External Mass Media Controller 65 o Introduction 65 o Registers 66 6 General Purpose I/O (GPIO) 89 6.1 Register View 90 6.2 Alternative Function Assignments 102 6.3 General Purpose GPIO Clocks 105 7 Interrupts 109 7.1 Introduction 109 7.2 Interrupt pending. 110 7.3 Fast Interrupt (FIQ). 110 7.4 Interrupt priority. 110 7.5 Registers 112 8 PCM / I2S Audio 119 8.1 Block Diagram 120 8.2 Typical Timing 120 8.3 Operation 121 8.4 Software Operation 122 8.4.1 Operating in Polled mode 122 8.4.2 Operating in Interrupt mode 123 8.4.3 DMA 123 8.5 Error Handling. 123 8.6 PDM Input Mode Operation 124 8.7 GRAY Code Input Mode Operation 124 8.8 PCM Register Map 125 9 Pulse Width Modulator 138 9.1 Overview 138 9.2 Block Diagram 138 9.3 PWM Implementation 139 9.4 Modes of Operation 139 9.5 Quick Reference 140 9.6 Control and Status Registers 141 10 SPI 148 10.1 Introduction 148 10.2 SPI Master Mode 148 10.2.1 Standard mode 148 10.2.2 Bidirectional mode 149 10.3 LoSSI mode 150 10.3.1 Command write 150 10.3.2 Parameter write 150 10.3.3 Byte read commands 151 10.3.4 24bit read command 151 10.3.5 32bit read command 151 10.4 Block Diagram 152 10.5 SPI Register Map 152 10.6 Software Operation 158 10.6.1 Polled 158 10.6.2 Interrupt 158 10.6.3 DMA 158 10.6.4 Notes 159 11 SPI/BSC SLAVE 160 11.1 Introduction 160 11.2 Registers 160 12 System Timer 172 12.1 System Timer Registers 172 13 UART 175 13.1 Variations from the 16C650 UART 175 13.2 Primary UART Inputs and Outputs 176 13.3 UART Interrupts 176 13.4 Register View 177 14 Timer (ARM side) 196 14.1 Introduction 196 14.2 Timer Registers: 196 15 USB 200 15.1 Configuration 200 15.2 Extra / Adapted registers. 202
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值