一、三个 "地址"
1.总线地址
总线地址,也被称为地址总线或位址总线(Address Bus),是一种计算机总线。它由CPU或者具有DMA能力的单元使用,用于沟通这些单元想要访问(读取/写入)计算机内存组件/地方的物理地址。
通俗讲就是CPU能够访问内存的范围。
比如32位的系统,CPU最多能访问到2^32bit,也就是4Gb。
2^32 = 4294967296 bit = 4194304 Kb = 4096 Mb = 4 Gb
而64位系统则是能访问到8Gb。
树莓派装载32位操作系统,寻址自然是4G。
树莓派查看内存指令。
cat /proc/meninfo
内存大概为926Mb。
2.物理地址
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
3.虚拟地址
如何解释虚拟地址呢?
以树莓派为例,树莓派是32位系统,内存可以访问到4G,但是物理地址只有1G,假如运行的程序是超过1G的,那么把程序全部加载到内存是不可行的。
解决方法就是利用虚拟地址。
首先要说明一点,我们在实际程序中操作的其实都是虚拟地址。
虚拟地址其实就是基于算法上的逻辑地址,是软件层面的,是假的。虚拟地址可以比1G大,树莓派本身是能够访问到4G的,当物理地址1G不能满足程序运行空间需要时,那就可以把1G的物理地址映射成4G的虚拟地址。
二、芯片手册导读
查看芯片手册的目的性很强:做哪一块的开发,就只看那一块,现在要开发的是GPIO,熟悉控制IO口的寄存器最为重要。
如果看完这部分的文档,你对于以下几个问题(后面有解析)有清晰的答案,说明你真正读懂了这一部分的开发。
①操作逻辑:简言之就是怎么进行配置相关寄存器,这些配置步骤和思想其实都很类似。
②需要重点掌握的寄存器有哪些?例如输入 / 输出控制寄存器
输出 0 / 1控制寄存器
清除状态寄存器
训练如何捕捉信息 。
在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,0/1,怎么清除,上升沿下降沿等。
从这里可以看出,所有IO口被分成0~5,共6个分组。每组的访问都被设置为32位。
引脚功能选择
其中pin0~pin9是位于第0分组。目标pin4对应14-12位,FSEL9有配置示例,由此可知,将14-12位配置为001,即可让pin4成为输出引脚。
GPFSEL0是pin0 ~ pin9的配置j寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此类推,GPFSEL5就是pin50~pin53的配置寄存器。
配置引脚输出状态
输出集寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。分离集和明确功能取消对读-修改-写操作的需要。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
第0~31位归寄存器0管理,若设置为0,则无影响,若将第n位配置为1,则pin n 就会触发,开启置1,而第32位~53位由寄存器1管理。
清除状态
输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果的在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器。
当寄存器写入1时,寄存器触发,开启清除1,置0
三、代码编写
在原来框架的基础上,添加寄存器的定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
注意以下几点:
1.弄清楚寄存器的分组
其中寄存器的0表示的是分组,目标操作的IO是pin4,由文档可知,属于寄存器分组0。2.volatile的使用(笔试)
加volatile在此处是 : 防止编译器优化(可能是省略,也可能是更改)这些寄存器变量,常见于在内核中对IO口进行操作。volatile的作用是作为指令关键字,确保本条 指令不会因编译器的优化而省略,且要求每次直接读值
获取、配置寄存器地址
编写驱动程序时,首先要知道它的地址,IO口空间的起始地址是0x3f00 0000(文档的起始地址是错误的),加上GPIO的偏移量0x200 0000,所以GPIO的物理地址应该是0x3f20 0000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。
1.文档中,外设IO口的起始地址为0x20000000是错误的,应该是0x3F000000。
2.外设物理起始地址为0x3F000000,对应虚拟起始地址为0xF2000000,对应总线起始地址为0x7E000000。
3.表中的地址就是总线地址,寄存器GPFSEL0的0x7E200000就是总线地址。
4.驱动程序中要使用的地址就是物理地址,需要通过查文档换算:
已知外设IO口起始物理地址为0x3F000000,
外设IO口起始总线地址为0x7E000000,
寄存器GPFSEL0总线地址为0x7E200000,
(0x7E200000-0x7E000000)= 0x200000就是寄存器GPFSEL0距起始地址的偏移量,
寄存器GPFSEL0物理地址为(0x3F000000+0x200000)= 0x3F200000
所以GPSET0的物理地址为0x3F20001C
GPCLR0的物理地址为0x3F200028
GPFSEL0 = (volatile unsigned int *)ioremap(0x3F200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3F20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3F200028,4);
物理地址转虚拟地址
因为代码操作的是虚拟地址,代码中直接用物理地址肯定不行,需要进行转换,将IO口寄存器映射成普通内存单元进行访问。
使用函数ioremap来完成。
函数原型:
void *ioremap(unsigned long phys_addr, unsigned long size)
phys_addr:要映射的起始的IO物理地址;
size:要映射的空间的大小;
进行功能配置
在函数pin4_open中配置pin4为输出引脚
只要将32位寄存器GPFSEL0
的14-12位配置为001,其它位不管,即可配置pin4为输出引脚。
运用与(&) 、或(|)运算进行位操作
//配置pin4引脚为输出引脚 *GPFSEL0 &= ~(0x6 << 12); //bit14、bit13配置为0 *GPFSEL0 |= (0x1 << 12); //bit12配置为1
在函数pin4_write中配置pin4输出 0 / 1
获取应用程序中write函数的值,通过copy_from_user函数来实现。
函数原型:
unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)
此函数将from指针指向的用户空间地址开始的连续n个字节的数据产送到to指针指向的内核空间地址,简言之是用于将用户空间的数据传送到内核空间。
第一个参数to是内核空间的数据目标地址指针,
第二个参数from是用户空间的数据源地址指针,
第三个参数n是数据长度。如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。
根据值来操作IO口
int userCmd;上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
copy_from_user(&userCmd,buf,count);
if(userCmd == 1){
printk("set 1\n");
*GPSET0 |= 0x1 << 4;
}else if(userCmd == 0){
printk("set 0\n");
*GPCLR0 |= 0x1 << 4;
}else{
printk("cmd error\n");
}
说明(这也是操作逻辑的一部分啦):
①这个GPSET0
,0指的是分组,不是设置成低电平。
②左移4位,是因为GPSET0
寄存器的第4位对应pin4,只要把第4位设置为1,表示这个寄存器就对pin4发挥作用,设置成高电平,如果是0则 no effct(手册内容)。
解除虚拟地址映射
退出程序卸载驱动的时候,解除映射:iounmap
函数
void iounmap(void* addr)//取消ioremap所映射的IO地址
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
四、完整代码
内核驱动框架
#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;
//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数,和printf类似
//open的时候配置pin4为输出引脚
*GPFSEL0 &= ~(0x6 << 12);
*GPFSEL0 |= (0x1 << 12);
return 0;
}
//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用char
printk("pin4_write\n");
//获取上层write的值
copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
//根据值来执行操作
if(userCmd == 1){
printk("set 1\n");
*GPSET0 |= 0x1 << 4;
}else if(userCmd == 0){
printk("set 0\n");
*GPCLR0 |= 0x1 << 4;
}else{
printk("cmd error\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;
printk("insmod driver pin4 success\n");
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name,&pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //由代码在/dev下自动生成设备
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
return 0;
}
void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{
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");
应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd;
int cmd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
printf("open failed\n");
perror("reson");
}else{
printf("open success\n");
}
printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");
scanf("%d",&cmd);
if(cmd == 0){
printf("pin4设置成低电平\n");
}else if(cmd == 1){
printf("pin4设置成高电平\n");
}
fd = write(fd,&cmd,1);//写一个字符'1',写一个字节
return 0;
}
五、交叉编译、安装驱动、测试
运行上层程序之前。
安装驱动,运行应用程序之后。
六、参考博文
树莓派(十二)树莓派驱动开发入门:从读懂框架到自己写驱动(下)_树莓派4g 驱动开发-CSDN博客
树莓派高级开发——“IO口驱动代码的编写“ 包含总线地址、物理_虚拟地址、BCM2835芯片手册知识 - 知乎 (zhihu.com)