地址總線:
-
百度百科解釋: 地址總線 (Address Bus;又稱:位址總線) 屬于一種電腦總線 (一部份),是由CPU 或有DMA
能力的單元,用來溝通這些單元想要存取(讀取/寫入)電腦內存元件/地方的實體位址。 -
CPU尋找外部的內存單元靠的是地址總線傳輸的數據。 如果CPU有8根地址總線,每根線上傳輸0或1,那么傳輸的數據范圍為00000000~ 11111111,每一個數值都對應內存中的一個內存單元,所以可以找到編號為00000000~ 11111111號的內存單元。如果傳輸的數據為 00110011,那么就會找到00110011號內存單元,如果傳輸的數據為10110111,那么就會找到10110111號內存單元。編號不在[00000000,11111111]范圍內的CPU就尋找不到,例如100000000號內存單元,CPU就尋找不到。尋址能力就是計算CPU能尋找多少個內存單元,00000000~11111111號內存單元,一共有256個,一個內存單元的大小為1byte,這256個內存單元的大小為256byte。
-
CPU是通過地址總線來指定存儲單元的。
-
地址總線決定了cpu所能訪問的最大內存空間的大小。eg: 10根地址線能訪問的最大的內存為1024(2的10次方)位二進制數據(1B)
-
地址總線是地址線數量之和。若CPU的地址總線寬度是32位,那么CPU的尋址范圍是4G(2的32次方位),所以最多支持4G內存。
-
比如有一個現象:裝了32位的win7系統,明明內存條8G,可系統只是別了3.8G ,裝了64位才能識別到8G。裝了32位的操作系統CPU的訪問范圍是2^32 bit,就是4194304kbit,就是4096Mbit,等于4G。樹莓派也是32位 ,一個G的內存,但它只能訪問949M剩下的挪作他用。
數據總線:
- CPU通過地址總線尋址,然后通過數據總線與外部設備互換信息。
- 是CPU與內存或其他器件之間的數據傳送的通道。
- 數據總線的寬度決定了CPU和外界的數據傳送速度。
- 每條傳輸線一次只能傳輸1位二進制數據。eg: 8根數據線一次可傳送一個8位二進制數據(即一個字節)。
- 數據總線是數據線數量之和,數據總線的位數決定CPU單次通信能交換的信息數量。
數據總線的寬度對CPU的性能的影響:
- 首先,總線的速度(即:CPU的主頻,CPU的性能指標之一)決定CPU和外設互換信息的速度。
- 其次,數據總線的寬度也是表示CPU性能的參數之一(通常,我們說“64位的CPU”是指CPU的數據總線的寬度是64位)。 如:64位數據總線的CPU一次就能取出64bit的數據,8位數據總線的CPU一次只能取出8bit的數據,在相同頻率的情況下,8位數據總線的CPU就得連續取8次數據,數據量才能和64位數據總線一次取出的數據量相同,單就比較取數據的性能就相差8倍。況且,通常CPU中的寄存器的位數與數據總線的寬度一樣,所以在數據處理方面,64位的CPU又比8位的CPU快很多。
- CPU的地址總線位數和數據總線可以不同(典型代表就是51單片機),但是一般都相同。16位機有16根數據總線,20根地址總線,能訪問1M(2的20次方),32位機有32根數據總線,32根地址總線,能訪問4G(2的32次方),64位機確實有64根數據總線。
物理地址(PA):
- 百度百科解釋: 網卡物理地址存儲器中存儲單元對應實際地址稱物理地址,與邏輯地址相對應。網卡的物理地址通常是由網卡生產廠家寫入網卡的EPROM(一種閃存芯片,通常可以通過程序擦寫),它存儲的是傳輸數據時真正賴以標識發出數據的電腦和接收數據的主機的地址。
- 這里說的物理地址是內存中的內存單元實際地址,不是外部總線連接的其他電子元件的地址!物理地址屬于比較好理解的,物理地址就是內存中每個內存單元的編號,這個編號是順序排好的,物理地址的大小決定了內存中有多少個內存單元,物理地址的大小由地址總線的位寬決定!
虛擬地址(VA):
- 虛擬地址是Windows程序時運行在386保護模式下,這樣程序訪問存儲器所使用的邏輯地址稱為虛擬地址,與實地址模式下的分段地址類似,虛擬地址也可以寫為“段:偏移量”的形式,這里的段是指段選擇器。而linux沒有各種保護模式,本來用的就是虛擬地址。
- 虛擬地址是CPU保護模式下的一個概念,保護模式是80286系列和之后的x86兼容CPU操作模式,在CPU引導完操作系統內核后,操作系統內核會進入一種CPU保護模式,也叫虛擬內存管理,在這之后的程序在運行時都處于虛擬內存當中,虛擬內存里的所有地址都是不直接的,所以你有時候可以看到一個虛擬地址對應不同的物理地址,比如A進程里的call函數入口虛擬地址是0x001,而B也是,但是它倆對應的物理地址卻是不同的,操作系統采用這種內存管理方法。
- 是防止程序對物理地址寫數據造成一些不可必要的問題,比如知道了A進程的物理地址,那么向這個地址寫入數據就會造成A進程出現問題,在虛擬內存中運行程序永遠不知道自己處于內存中那一段的物理地址上!現在操作系統運行在保護模式下即便知道其他進程的物理地址也不允許向其寫入!但是可以通過操作系統留下的后門函數獲取該進程上的虛擬地址空間所有控制權限并寫入指定數據。
- 虛擬內存管理采用一種拆東墻補西墻的形式,所以虛擬內存的內存會比物理內存要大許多。在進入虛擬模式之前CPU以及Bootloader(BootLoader是在操作系統內核運行之前運行。可以初始化硬件設備、建立內存空間映射圖,從而將系統的軟硬件環境帶到一個合適狀態,以便為最終調用操作系統內核準備好正確的環境),操作系統內核均運行在實模式下,直接對物理地址進行操作!虛擬內存中也有分頁管理,這種管理方法是為了確保內存中不會出現內存碎片,當操作系統內核初始化完畢內存中的分頁表后CPU的分頁標志位會被設置,這個分頁標志位是給MMU看的!
- MMU是Memory Management Unit的縮寫,中文名是內存管理單元,它是中央處理器(CPU)中用來管理虛擬存儲器、物理存儲器的控制線路,同時也負責虛擬地址映射為物理地址,以及提供硬件機制的內存訪問授權,多用戶多進程操作系統。作用有兩點,地址翻譯和內存保護。MMU將虛擬地址翻譯為物理地址。
分頁管理:
- 內存分頁其實就是我們所說的4G空間,內存的所有內存被操作系統內核以4G為每頁劃分開,當我們程序運行時會被加載到內存中的4G空間里,其實說是有4G其實并沒有真正在的4G空間,4G空間中有一小部分被映射到了物理內存中,或者被映射到了硬盤的文件上(fopen),或者沒有被映射,還有一部分在內存當中就會被劃分棧,堆,其中有大片大片的內存是沒有被映射的,同樣物理內存也是被分頁了用來與虛擬內存產生映射關系。將虛擬地址映射為物理地址有一個算法(頁表)決定了將虛擬地址映射到物理地址的哪個位置,頁表是通過MMU(分頁內存管理單元)來管理的,就是設計完頁表后通過MMU來執行將虛擬地址映射為物理地址。
- 其實真正情況下只有3G用戶空間,假如你的內存是4G的那么其中有1G是給操作系統內核使用的,所謂的4G空間只是操作系統基于虛擬內存這種拆東墻補西墻的形式給你一種感覺每個進程都有4G的可用空間一樣!這里來說一下拆東墻補西墻,當我們程序被加載進4G空間時其實根本用不了所謂的4G空間,其中有大片內存被閑置,那么這個時候呢,其他程序被加載進來時發現內存不夠了,就把其他程序里的4G空間里閑置部分拿出來給這個進程用,換之這個進程內存不夠時就會把其他進程里閑置的空間拿過來給該進程使用。銀行也是如此!
- 當我們要對物理地址做操作時比如if語句要根據CPU的狀態標志寄存器來做不同的跳轉,那么這個時候就要對CPU額狀態寄存器做操作了就必須知道它的物理地址,內存中有一個電子元件叫MMU負責從操作系統已經初始化好的內存映射表里查詢與虛擬地址對應的物理地址并轉換,比如mov
0x4h8這個是虛擬地址,當我們要對這個虛擬地址里寫數據時那么MMU會先判斷CPU的分頁狀態寄存器里的標志狀態是否被設定,如果被設定那么MMU就會捕獲這個虛擬地址物理并在操作系統內核初始化好的內存映射表里查詢與之對應的物理地址,并將其轉換成真正的實際物理地址,然后在對這個實際的物理地址給CPU,在由CPU去執行對應的命令,相反CPU往內存里讀數據時比如A進程要讀取內存中某個虛擬地址的數據,A進程里的指令給的是虛擬地址,MMU首先會檢查CPU的分頁狀態寄存器標志位是否被設置,如果被設置MMU會捕獲這個虛擬地址并將其轉換成相應的物理地址然后提交給CPU,在由CPU到內存中去取數據!
下面截取樹莓派芯片手冊的一張圖:
BCM2835是樹莓派3B CPU的型號,是ARM-cotexA53架構,cpu Bus是地址總線,00000000~FFFFFFFF是CPU尋址的范圍。DMA是高速拷貝單元,CPU可以發動DMA直接讓DMA進行數據拷貝,直接內存訪問單元。
- 通過芯片手冊了解樹莓派的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引腳為輸出引腳: 只需要將GPFSL0這個寄存器的14~12位設置為001就可以了。只需要將0x6(對應的2進制是110)左移12位·然后取反再與上GPFSL0就可以將13、14這兩位配置為0,然后再將0x6(對應2進制110)左移12位,然后或上GPFSL0即可將12位置1。
- 可使用copy_from_user()這個函數在驅動代碼里面讀取用戶輸入的指令,使用copy_to_user()這個函數讓引腳反饋現在的狀態,也就是讓用戶讀取到。
ioremap、iounmap:
一、 一般我們的外設都是通過讀寫設備上的寄存器來進行的,通常包括控制寄存器、狀態寄存器、數據寄存器三大類。外設的寄存器通常被連續編址,并且根據CPU的體系架構不同CPU對IO端口的編制方式有兩種:
- IO映射方式(IO-mapped):比較典型的有X86處理器為外設專門實現了一個單獨的地址空間,稱為“IO端口空間”或者“IO地址空間”,此時CPU可以通過專門的指令(比如X86的IN和OUT)來訪問這個“IO端口空間”。
- 內存映射方式(memory-mapped):RISC指令系統的CPU一般只實現一個物理地址空間,外設IO端口成為內存的一部分。此時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); //卸載驅動時釋放地址映射
樹莓派IO口四的驅動代碼:
#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;
//這三行是設置寄存器的地址,volatile的作用是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值//led_open函數
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n"); //內核的打印函數和printf類似*GPFSEL0 &=~(0x6 <<12); // 把13 、14置為0*GPFSEL0 |=~(0x6 <<12); //把12置為1return 0;}
//read函數
static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{printk("pin4_read\n"); //內核的打印函數和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\n"); //內核的打印函數和printf類似copy_from_user(&usercmd,buf,count); //將應用層用戶輸入的指令讀如usercmd里面if(usercmd == 1){printk("set 1\n");*GPSET0 |= 0x01 << 4;}else if(usercmd == 0){printk("set 0\n");*GPCLR0 |= 0x01 << 4;}else{printk("undo\n");}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\n");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");
上層測試代碼:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void main()
{int fd,data;fd = open("/dev/pin4",O_RDWR);printf("plese input 1 or 0 //1:high 0:low\n ");scanf("%d",&data);if(fd<0){printf("open fail\n");perror("reson:");}else{printf("open successful\n");}fd=write(fd,&data,1);
}
- 首先使用指令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
對驅動模塊進行編譯生成.ko
文件,然后將編譯后的驅動發送到樹莓派:scp pin4drive.ko pi@192.168.43.136:/home/pi
,然后再將上層代碼進行編譯:arm-linux-gnueabihf-gcc pin4test.c -o pin4test
,然后再將測試代碼傳到樹莓派:scp pin4test 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,如下圖所示:
- 然后的
dmesg
查看內核打印的消息,如下圖所示:
有關驅動代碼里面GPIO口地址的問題:
- 7Ennnnn意思是7E00000到7EFFFFFF,F2000000是3F000000映射的虛擬地址,然后7E00000和F200000對應,芯片手冊里面使用的是和虛擬地址F200000有著對應關系的地址——7E00000,芯片手冊上面地址偏移多少物理地址就偏移多少
- 根據上方圖片描述,外設的物理地址范圍是m 0x3F000000 to 0x3FFFFFFF,所以你看到的7E200000對應的實際物理地址應該是0x3F000000 + (7E200000-7E000000)
- GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4); - 0x3f200000,0x3f20001C,0x3f200028是物理地址,樹莓派的外設空間的起始地址是0x3f000000,根據芯片手冊可知對應寄存器的偏移量為,比如GPFSEL0寄存器的實際地址是0x3f200000=0x3F000000 + (7E200000-7E000000)
- 然后通過數據手冊可以看到,樹莓派相關寄存器的總線地址(和映射的虛擬地址有某種對應關系的地址)進而可得知偏移量,如下圖所示:
有關各種地址介紹的博文:
- 物理地址、虛擬地址、總線地址
- 物理地址和總線地址區別