电子说
kbit——mbit——gbit | 差1024 |
---|---|
bit | 4,294,967,296 |
kbit | 4,194,304 K |
mbit | 4,096 M |
gbit | 4 G |
00000000~ 11111111
,每一个数值都对应内存中的一个内存单元,所以可以找到编号为00000000~ 11111111
号的内存单元。如果传输的数据为00110011
,那么就会找到00110011
号内存单元,如果传输的数据为10110111
,那么就会找到10110111
号内存单元。编号不在[00000000,11111111]
范围内的CPU就寻找不到,例如100000000
号内存单元,CPU就寻找不到。寻址能力就是计算CPU能寻找多少个内存单元,00000000~11111111
号内存单元,一共有256个,一个内存单元的大小为1byte,这256个内存单元的大小为256byte。1024
**(2的10次方)位二进制数据(1B)百度百科解释:网卡物理地址存储器中存储单元对应实际地址称物理地址,与逻辑地址相对应。网卡的物理地址通常是由网卡生产厂家写入网卡的EPROM(一种闪存芯片,通常可以通过程序擦写),它存储的是传输数据时真正赖以标识发出数据的电脑和接收数据的主机的地址。
这里说的 物理地址是内存中的内存单元实际地址,不是外部总线连接的其他电子元件的地址!**==物理地址属于比较好理解的,物理地址就是内存中每个内存单元的编号==**,这个编号是顺序排好的,物理地址的大小决定了内存中有多少个内存单元,物理地址的大小由地址总线的位宽决定!物理地址是硬件实际地址或绝对地址
(基于算法的地址[软件层面的地址:假地址])
称为虚拟地址==,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。而linux没有各种保护模式,本来用的就是虚拟地址。MMU
是Memory Management Unit的缩写,中文名是内存管理单元,它是==中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统==。作用有两点,地址翻译和内存保护。==MMU将虚拟地址翻译为物理地址。==有关各种地址介绍的博文:
物理地址、虚拟地址、总线地址物理地址和总线地址区别
分页管理:
更详细的地址问题看这里
下面截取树莓派芯片手册的一张图:BCM2835是树莓派3B CPU的型号,是ARM-cotexA53架构,cpu Bus是地址总线,00000000~FFFFFFFF
是CPU寻址的范围(4G)。DMA是高速拷贝单元,CPU可以发动DMA直接让DMA进行数据拷贝,直接内存访问单元。物理地址(PA)1G、虚拟地址(VA)4G若程序大于物理地址1G,是不是就跑不了了,不是的,它有个MMU的单元,把物理地址映射成虚拟地址,我们操作的代码基本上都是在虚拟地址,它有一个映射页表(上面提及到过)
通过芯片手册了解树莓派的GPIO:有54条通用I/O GPIO行,分为两行,备用功能通常是外围IO并且可以在每个银行中出现一个外围设备,以允许灵活地选择IO电压。
GPIO有41个寄存器,所有访问都是32位的。
Description是寄存器的功能描述。GPFSEL0(寄存器名)GPIO Function Select 0
(功能选择:输入或输出);GPSET0 (寄存器名)GPIO Pin Output Set 0
(将IO口置0);GPSET1(寄存器名)GPIO Pin Output Set 1
(将IO口置1);GPCLR0(寄存器名)GPIO Pin Output Clear 0
(清0)下图的地址是:总线地址(并不是真正的物理地址)
FSELn表示GPIOn,下图给出第九个引脚的功能选择示例,对寄存器的29-27进行配置,进而设置相应的功能。根据图片下方的register 0表示0~9使用的是register 0这个寄存器。
输出集寄存器用于设置GPIO管脚。SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。分离集和明确功能取消对读-修改-写操作的需要。GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
输出清除寄存器用于清除GPIO管脚。CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。如果的在输入(默认),然后在CLR{n}字段的值是忽略了。然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。分隔集与清函数消除了读-修改-写操作的需要。GPCLRn是清零功能寄存器。
配置树莓派的pin4引脚为输出引脚:
功能选择 输出/输入(GPIO Function Select Registers)32位
14-12 001 = GPIO Pin4 is an output
只需要将GPFSL0这个寄存器的14~12位设置为001就可以了。只需要将0x6(对应的2进制是110)左移12位·然后取反再与上GPFSL0就可以将13、14这两位配置为0,然后再将0x6(对应2进制110)左移12位,然后或上GPFSL0即可将12位置1。
若想找树莓派引脚点这里
一. 一般我们的外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器、数据寄存器三大类。外设的寄存器通常被连续编址,并且根据CPU的体系架构不同CPU对IO端口的编制方式有两种:
二、 在驱动开发过程中,一般来说外设的IO内存资源的物理地址是已知的,由硬件的设计决定。但是CPU不会为这些已知的外设IO内存资源预先指定虚拟地址的值,所以驱动程序不可以直接就通过外设的物理地址访问到IO内存,而必须要将其映射到虚拟地址空间(通过页表),然后才能根据内核映射过后的虚拟地址来通过内存指令访问这些IO内存,并对其进行操作。
三、 在Linux内核的io.h头文件中声明了ioremap()函数,用来将IO内存资源映射到核心虚拟地址空间(3Gb~4GB)中,当然不用了可以将其取消映射iounmap()。这两个函数在mm/ioremap.c文件中:
开始映射:void* ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags)
//用map映射一个设备意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或写入,实际上就是对设备的访问。
第一个参数是映射的起始地址
第二个参数是映射的长度
第二个参数怎么定啊?
====================
这个由你的硬件特性决定。
比如,你只是映射一个32位寄存器,那么长度为4就足够了。
(这里树莓派IO口功能设置寄存器、IO口设置寄存器都是32位寄存器,所以分配四个字节就够了)
比如:GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);
这三行是设置寄存器的地址,volatile的作用是作为指令关键字
确保本条指令不会因编译器的优化而省略,且要求每次直接读值
ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。
解除映射:void iounmap(void* addr)//取消ioremap所映射的IO地址
比如:
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0); //卸载驱动时释放地址映射
#include //file_operations声明
#include //module_init module_exit声明
#include //__init __exit 宏定义声明
#include //class devise声明
#include //copy_from_user 的头文件
#include //设备号 dev_t 类型声明
#include //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;
//这三行是设置寄存器的地址
//volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open
"); //内核的打印函数和printf类似
//配置pin4引脚为输出引脚
*GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0
//0x6是110 <<12左移12位 ~取反 &按位与
*GPFSEL0 |=~(0x1 <<12); //把12置为1 |按位或
return 0;
}
//read函数
static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{
printk("pin4_read
"); //内核的打印函数和printf类似
return 0;
}
//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int usercmd;
printk("pin4_write
"); //内核的打印函数和printf类似
//获取上层write函数的值
copy_from_user(&usercmd,buf,count); //将应用层用户输入的指令读如usercmd里面
//根据值来操作io口,高电平或者低电平
if(usercmd == 1){
printk("set 1
");
*GPSET0 |= 0x01 << 4;
}
else if(usercmd == 0){
printk("set 0
");
*GPCLR0 |= 0x01 << 4;
}
else{
printk("undo
");
}
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
.read = pin4_read,
};
//static限定这个结构体的作用,仅仅只在这个文件。
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");//让代码在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);
printk("insmod driver pin4 success
");
return 0;
}
void __exit pin4_drv_exit(void)
{
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); //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
设置寄存器的地址,但是这样写是有问题的,我们上面讲到了在内核里代码和上层代码访问的是虚拟地址(VA),而现在设置的是物理地址,**==必须把物理地址转换成虚拟地址==**
//这三行是设置寄存器的地址
volatile unsigned int* GPFSEL0 = volatile (unsigned int *)0x3f200000;
volatile unsigned int* GPSET0 = volatile (unsigned int *)0x3f20001C;
volatile unsigned int* GPCLR0 = volatile (unsigned int *)0x3f200028;
//volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
我们先把地址初始
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
在初始化int __init pin4_drv_init(void) //真实的驱动入口
里赋值。
//整数11//0xb 11 00010001即便是16进制也是整数,左边是
volatile unsigned int* GPFSEL0
右边也强制转换成(volatile unsigned int*)
volatile
的作用是作为指令关键字,确保本条 ==指令不会因编译器的优化而省略==,==且要求每次直接读值==因为它是地址我希望它是无符号的unsigned
我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000
,加上GPIO的偏移量0x2000000
,所以GPIO的物理地址应该是从0x3f200000
开始的然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。用到了一个函数ioremap
//物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问
GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4); //4是4个字节
配置pin4引脚为输出引脚 bit 12-14 配置成001
31 30 ······14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 0 ······0 0 1 0 0 0 0 0 0 0 0 0 0 0
//配置pin4引脚为输出引脚 bit 12-14 配置成001
*GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0
//0x6是110 <<12左移12位 ~取反 &按位与
*GPFSEL0 |=~(0x1 <<12); //把12置为1 |按位或
忘记按位与 按位或 点这里
用copy_form_user(char *buf , user_buf , count)
获取上层write函数的值
int usercmd;
copy_from_user(&usercmd,buf,count); //将应用层用户输入的指令读如usercmd里面
//根据值来操作io口,高电平或者低电平
printk("get value
");
if(usercmd == 1){
printk("set 1
"); //置1
*GPSET0 |= 0x01 << 4; //用 | 或操作 目的是不影响其他位
//写1 是让寄存器 开启置1 让bit4为高电平
}
else if(usercmd == 0){
printk("set 0
"); //清0
*GPCLR0 |= 0x01 << 4; //用 | 或操作 目的是不影响其他位
//写1 是让清0寄存器 开启置0 让bit4为低电平
}
else{
printk("undo
"); //提示不支持该指令
}
解除映射:void iounmap(void* addr);
//取消ioremap所映射的IO地址
void __exit pin4_drv_exit(void)
{
iounmap(GPFSEL0); //解除映射 GPFSEL0
iounmap(GPSET0); //解除映射 GPSET0
iounmap(GPCLR0); //解除映射 GPCLR0
device_destroy(pin4_class,devno);//先销毁设备
class_destroy(pin4_class);//再销毁类
unregister_chrdev(major, module_name); //卸载驱动
}
上层测试代码:
#include
#include
#include
#include
#include
#include
int main()
{
int fd;
int cmd;
int data;
fd = open("/dev/pin4",O_RDWR);
if(fd<0){
printf("open failed
");
}else{
printf("open success
");
}
printf("input commnd:1/0
1:set pin4 high
0 :set pin4 low
");
scanf("%d",&cmd);
printf("cmd = %d
",cmd);
fd = write(fd, &cmd,4); //cmd类型是int 所以 写4
}
在装完驱动后可以使用指令:sudo rmmod +驱动名
(不需要写ko)将驱动卸载。
/SYSTEM/linux-rpi-4.14.y
下使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
对驱动模块进行编译生成.ko
文件.scp ./drivers/char/pin4driver.ko pi@192.168.0.104:/home/pi
,然后再将上层代码进行编译:arm-linux-gnueabihf-gcc pin4test.c -o realtest
,然后再将测试代码传到树莓派:scp realtest pi@192.168.43.136:/home/pi/
insmod pin4drive.ko
进行加载驱动(然后lsmod
即可查看到该驱动),sudo chmod 666 /dev/pin4
给予pin4这个设备可访问权限,还可以在虚拟机上面使用mk5sum查看驱动文件的值,并在树莓派上面使用该指令进行查看该驱动文件的值,看是否一致。dmesg
查看内核打印的信息,如下图所示:gpio readall
可以看到BCM
下面的4
号引脚模式是输出模式,电平是低电平或高电平(根据输入的上层代码而定,输入0就是低电平,输入1就是高电平),这里我输入的是0,如下图所示:有关驱动代码里面GPIO口地址的问题:
原文标题:树莓派高级开发——“IO口驱动代码的编写“ 包含总线地址、物理_虚拟地址、BCM2835芯片手册知识
文章出处:【微信公众号:一口Linux】欢迎添加关注!文章转载请注明出处。
全部0条评论
快来发表一下你的评论吧 !