這里我們主要介紹Intel 80x86系列CPU保護模式下最核心的部件中幾個寄存器的作用,這些寄存器在Linux內核運行時起著至關重要的作用。至于其他那些各式各樣的硬件設備,我們在講解設備驅動時會針對具體的驅動程序來介紹的。首先,大家先看看CPU的主要架構:
EU(通用寄存器、運算器和控制器)執行部件:完成指令所要求的功能。
SU(段寄存器、段轉換器)分段部件:完成執行單元的地址請求, 將虛地址轉換為線性地址。
PU(TLB、頁轉換器)分頁部件:將線性地址轉換為物理地址。
BIU(總線接口)接口部件:完成指令預取請求和執行單元的數據存取請求,數據存取請求優先于指令預取請求。
IPU(控制邏輯和預取隊列)預取部件:16字節指令預取隊列, 提出預取請求。
IDU(指令譯碼、6字節指令隊列)譯碼部件:完成指令譯碼功能。
FPU(片內集成了浮點協處理器):專用于浮點運算的處理部件。
下面,我們針對EU、SU和PU模塊做做詳細說明,其他模塊就暫時不介紹了。
1 EU模塊
EU模塊是CPU中最核心,最重要的部件?,F在的奔騰CPU已經發展了若干年了,但其中最起作用的還是加法單元ALU,一組通用寄存器組、一個標志和控制邏輯。如圖:
首先,8個32位通用寄存器按使用情況分為三種:指針寄存器、變址寄存器、數據寄存器。
[1] 指針寄存器:主要提供全部或部分偏移量
ESP:專門存放堆棧段中棧頂單元的偏移量。
EBP:存放堆棧段中某個單元的全部/部分偏移量,也可存放32位或16位操作數或運算結果。
[2] 變址寄存器
ESI/EDI:存放主存操作數的全部/部分偏移量,也可存放16位操作數和結果,在多數情況功能可以互換。 但在串操作指令中作用不能互換,源操作數必須用ESI提供偏移量,目的操作數必須用EDI提供偏移量。
[3] 數據寄存器
◆ 數據寄存器既可以作為4個32位的寄存器,也可以作為8個16位的寄存器 ,還可以作為16個8位的寄存器。
◆ 在程序中,數據寄存器用來存放操作數、運算結果或其他信息。
◆ 數據寄存器在許多指令中要求指明使用,但也有隱含或特定使用,詳細情況請查閱相關資料。
其次,4個控制寄存器CR0~CR3
[1] CR0:由80286的MSW寄存器演變而來,并增加了2位,Linux最看重他的PG位——PG=0,允許分頁;PG=1,不允許分頁。
[2] CR1:未使用
[3] CR2:頁故障地址寄存器, 存放出現故障的頁的32位線性地址
[4] CR3:頁目錄基地址寄存器, 存放頁目錄表的基地址。
最后來看看標志寄存器FR
FR用來記錄程序執行時的狀態,即兩個操作數通過ALU后的狀態:
[1] 進位標志位CF(Carry Flag)
[2] 奇偶標志位PF(Parity Flag)
[3] 輔助進位標志位AF(Auxiliary Carry Flag)
[4] 零值標志位ZF(Zero Flag)
[5] 符號標志位SF(Sign Flag)
[6] 溢出標志位OF(Overflow Flag)
[8] 中斷標志位IF(Interrupt-enable Flag)
[9] 方向標志位DF(Direction Flag)
2 SU模塊
下面著重看看SU部件。這個部件也被Linux用到了,但Linux用它的目的并不是遵循Intel手冊對地址進行虛擬化,而是利用它來做用戶態和內核態的切換。而對地址的虛擬化,則是通過PU單元,也就是分頁機制來實現的。
首先來看看SU模塊的架構圖:
處理器提供了6個段寄存器,段寄存器的唯一的目的是存放選擇子(16位)。這些段寄存器稱為cs, ss, ds, es, fs和gs。盡管只有6個段寄存器,但程序可以把同一個段寄存器用于不同的目的,方法是先將其值保存在存儲器中,用完后再恢復。
6個寄存器中3個有專門的用途:
cs——代碼段寄存器,指向包含程序指令的段。
ss——棧段寄存器,指向包含當前程序棧的段。
ds——數據段寄存器,指向包含靜態數據或者全局數據的段。
其它三個段寄存器作一般用途,可以指向任意的數據段。
每個段由一個8字節的描述子(Segment Descriptor)表示,它描述了段的特征。描述子放在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(Local Descriptor Table, LDT)中,這些表位于內存中,如圖所示。如果是多CPU,則每個CPU定義一個GDT,而每個進程除了存放在GDT中的段之外如果還需要創建附加的段,就可以有自己的LDT。GDT在主存中的基地址和大小存放在gdtr處理器寄存器中,當前正被使用的LDT地址和大小放在ldtr處理器寄存器中。
虛擬地址由16位選擇子和32位偏移量組成,段寄存器僅僅存放選擇子。CPU的分段單元(SU)執行以下操作:
[1] 先檢查選擇子的TI字段,以決定描述子對應的描述子保存在哪一個描述符表中。TI字段指明描述子是在GDT中(在這種情況下,分段單元從gdtr寄存器中得到GDT的線性基地址)還是在激活的LDT中(在這種情況下,分段單元從ldtr寄存器中得到LDT的線性基地址)。
[2] 從選擇子的index字段計算描述子的地址,index字段的值乘以8(一個描述子的大小,其實就是屏蔽掉末尾那三位指示特權級的CPL和指示TI的字段),這個結果與gdtr或ldtr寄存器中的內容相加。
[3] 將對應的描述子從內存拷貝到CPU的隱Cache中,這樣,只有在選擇子改變的情況下才會修改Cache中的內容。
[4] 把邏輯地址的偏移量與隱Cache中描述子Base字段的值相加就得到了線性地址。
請注意,多虧了與段寄存器相關的不可編程的隱Cache,只有當段寄存器的內容被改變時才需要執行前三個操作。
LDT在Linux中使用得很少,我們就不細說他了,它跟我們下面講的IDT差不多。
中斷描述符表(Interrupt Descriptor Table,IDT)是一個系統表,它與每一個中斷或異常向量相聯系,每一個向量在表中有相應的中斷或異常處理程序的入口地址。內核在允許中斷發生前,必須適當地初始化IDT。
IDT的格式與這GDT和LDT的格式非常相似,表中的每一項對應一個中斷或異常向量,每個向量由8個字節組成。因此,最多需要256×8=2048字節來存放IDT(Linux有256個中斷向量)。
idtr寄存器使IDT可以位于內存的任何地方,它指定IDT的線性基地址及其大小(最大長度)。在允許中斷之前,必須用lidt匯編指令初始化idtr。
IDT包含三種類型的描述符,下圖顯示了每種描述符中的64位的含義。尤其值得注意的是,在40~43位的Type字段的值表示描述符的類型。
這里還要提一個問題,就是TSS技術是個很過時的技術,Linux并沒有按照Intel要求的那樣把TSSD(任務門)存放到IDT中,而是存放在全局描述符GDT中。每個CPU的tr寄存器包含了對應TSS的TSSD選擇符(這個選擇符可以編程),還包含了兩個隱藏的非編程字段:TSSD的Base字段和Limit字段作為隱Cache,這樣,處理器就能直接對TSS尋址而不用從GDT中檢索TSS的地址。TSS這個東西主要用來在進程切換的時候保存部分CPU寄存器的內容(其實就主要是堆棧切換的時候使用到的那些寄存器)。Linux只為每個CPU準備一個TSS數據結構——tss_struct,只是用來存放當前進程的部分寄存器內容,并沒有按照Intel推薦的那樣為每個進程準備一個TSS數據結構,并存放所有的內容。所以,按照我的理解,每個進程的那個thread_struct結構存放的內容就是當進程被執行的時候需要tss_struct記住的那些寄存器的內容。
當執行了一條指令后,CS和eip這對寄存器包含下一條將要執行的指令的邏輯地址。在處理那條指令之前,控制單元會檢查在運行前一條指令時是否已經發生了一個中斷或異常。如果發生了一個中斷或異常,那么控制單元執行下列操作:
1. 確定與中斷或異常關聯的向量i (0 ≤ i ≤ 255)。
2. 讀由idtr寄存器指向的 IDT表中的第i項。
3. 從gdtr寄存器獲得GDT的基地址,并在GDT中查找,以讀取IDT表項中的選擇符所標識的段描述符。這個描述符將會是一個中斷門或者一個陷阱門,其含有指定中斷或異常處理程序所在段的基地址。
4. 確信中斷是由授權的(中斷)發生源發出的。首先將當前特權級CPL(存放在cs寄存器的低兩位)與段描述符(存放在GDT中)的描述符特權級DPL比較,如果CPL小于DPL,就產生一個“General protection”異常,因為中斷處理程序的特權不能低于引起中斷的程序的特權。對于編程異常,則做進一步的安全檢查:比較CPL與處于IDT中的門描述符的DPL,如果DPL小于CPL,就產生一個“General protection”異常。這最后一個檢查可以避免用戶應用程序訪問特殊的陷阱門或中斷門。
5. 檢查是否發生了特權級的變化,也就是說,CPL是否不同于所選擇的段描述符的DPL。如果是,控制單元必須開始使用與新的特權級相關的棧。通過執行以下步驟來做到這點:
i. 讀tr寄存器,以訪問運行進程的TSS段。
ii. 用與新特權級相關的棧段和棧指針的正確值裝載ss和esp寄存器。這些值可以在TSS中找到。
iii. 在新的棧中保存ss和esp以前的值,這些值定義了與舊特權級相關的棧的邏輯地址。
6. 如果故障已發生,用引起異常的指令地址裝載CS和eip寄存器,從而使得這條指令能再次被執行。
7. 在棧中保存eflags、CS及eip的內容。
8. 如果異常產生了一個硬件出錯碼,則將它保存在棧中。
9. 裝載cs和eip寄存器,其值分別是IDT表中第i項門描述符的段選擇符和偏移量字段。這些值給出了中斷或者異常處理程序的第一條指令的邏輯地址。
控制單元所執行的最后一步就是跳轉到中斷或者異常處理程序。換句話說,處理完中斷信號后,控制單元所執行的指令就是被選中處理程序的第一條指令。
中斷或異常被處理完后,相應的處理程序必須產生一條iret指令,把控制權轉交給被中斷的進程,這將迫使控制單元:
1. 用保存在棧中的值裝載CS、eip或eflags寄存器。如果一個硬件出錯碼曾被壓入棧中,并且在eip內容的上面,那么,執行iret指令前必須先彈出這個硬件出錯碼。
2. 檢查處理程序的CPL是否等于CS中最低兩位的值(這意味著被中斷的進程與處理程序運行在同一特權級)。如果是,iret終止執行;否則,轉入下一步。
3. 從棧中裝載ss和esp寄存器,因此,返回到與舊特權級相關的棧。
4. 檢查ds、es、fs及gs段寄存器的內容,如果其中一個寄存器包含的選擇符是一個段描述符,并且其DPL值小于CPL,那么,清相應的段寄存器??刂茊卧@么做是為了禁止用戶態的程序(CPL=3)利用內核以前所用的段寄存器(DPL=0)。如果不清這些寄存器,懷有惡意的用戶態程序就可能利用它們來訪問內核地址空間。
3 PU模塊
分頁單元PU模塊的目的是把線性地址轉換成物理地址。其中一個關鍵任務是把所請求的訪問類型與線性地址的訪問權限相比較,如果這次內存訪問是無效的,就產生一個缺頁異常。
分頁單元把所有的主存看成一塊一塊的,稱其為頁框(page frame)(有時叫做物理頁)。每一個頁框是固定的大小(跟分段的最大區別,一般為32位處理器為4k、64位處理器為64k)包含一個頁(page)。
把線性地址映射到物理地址的數據結構稱為頁表(page table),其存放在主存中,并在啟用分頁單元之前必須由內核對頁表進行適當的初始化。從80386開始,所有的80x86處理器都支持分頁,它通過設置cr0寄存器的PG標志啟用。當PG=0時,線性地址就被解釋成物理地址。
從80386起,Intel處理器的分頁單元處理4KB的頁。32位的線性地址被分成3個字段:
目錄(Directory)——最高10位
頁表(Table)——中間10位
偏移量(Offset)——最低12位
當一個進程運行時,必須有一個分配給它的頁目錄,其每一個目錄項指向一個頁表的地址。不過,沒有必要馬上為進程的所有頁表都分配內存。Linux是在當進程實際需要一個頁表時才給該頁表分配RAM以提高效率。
正在使用的頁目錄的物理地址存放在控制寄存器cr3中。線性地址內的最高10位(Directory字段)決定頁目錄中的目錄項,而目錄項指向適當的頁表。地址的中間10位(Table字段)依次又決定頁表中的表項,而表項含有頁所在頁框的物理地址。最低12位(Offset字段)決定頁框內的相對位置(見圖)。由于它是12位長,故每一頁含有4096字節的數據。
頁目錄項和頁表項有同樣的結構,每項的內容主要包括對應頁(頁表也是一個頁)的索引以及對應頁的狀態,我們將在存儲管理中分段Linux分段分頁機制博文中詳細介紹。
下面再來談談分頁的硬件保護方案,分頁單元和分段單元的保護方案不同。盡管80x86處理器允許一個段使用四種可能的特權級別,但與頁和頁表相關的特權級只有兩個,由頁目錄項和頁表項有同樣的結構的User/Supervisor標志所控制。若這個標志為0,只有當CPL小于3(這意味著對于Linux而言,處理器處于內核態)時才能對頁尋址;若該標志為1,則總能對頁尋址。
此外,與段的三種存取權限(讀,寫,執行)不同的是,頁的存取權限只有兩種(讀,寫)。如果頁目錄項或頁表項的Read/Write標志等于0,說明相應的頁表或頁是只讀的,否則是可讀寫的。
4 高速緩存
當今的微處理器時鐘頻率接近幾個GHZ,而動態RAM(DRAM)芯片的存取時間是時鐘周期的數百倍。這意味著,當從RAM中取操作數或向RAM中存放結果這樣的指令執行時,CPU可能等待很長時間。
為此,80x86體系結構中引入了一個叫行(line)的新單位。行由幾十個連續的字節組成,它們以脈沖突發模式(burst mode)在慢速DRAM和快速的片上靜態RAM(SRAM)之間傳送,用來實現高速緩存。
具體的高速緩存實現細節太復雜,我只簡單地說說原理:當訪問一個RAM存儲單元時,CPU從物理地址中提取出子集的索引號并把子集中所有行的標簽與物理地址的高幾位相比較。如果發現某一個行的標簽與這個物理地址的高位相同,則CPU命中一個高速緩存(cache hit);否則,高速緩存沒有命中(cache miss)。
當命中一個高速緩存時,高速緩存控制器的操作不同,具體取決于存取類型。 對于讀操作,控制器從高速緩存行中選擇數據并送到CPU寄存器;RAM不被訪問且節約了CPU時間,因此,高速緩存系統起到了其應有的作用。對于寫操作,控制器可能采用以下兩個基本策略之一,分別稱之為通寫(writethrough)和回寫(writeback)。在通寫中,控制器總是既寫RAM 也寫高速緩存行,為了提高寫操作的效率關閉高速緩存?;貙懛绞街桓赂咚倬彺嫘?,不改變RAM的內容,提供了更快的功效。當然,回寫結束以后,RAM最終必須被更新。只有當CPU執行一條要求刷新高速緩存表項的指令時,或者當一個FLUSH硬件信號產生時(通常在高速緩存不命中發生之后), 高速緩存控制器才把高速緩存行寫回到RAM中。
當高速緩存沒有命中時,高速緩存行被寫回到內存中,如果有必要的話,把正確的行從RAM中取出放到高速緩存的表項中。很復雜吧?我們應該大肆慶幸,因為所有這一切都在硬件級處理,內核根本不需要關心。
高速緩存技術正在快速向前發展。例如,第一代Pentium芯片包含一顆稱為L1-cache的片上高速緩存。近期的芯片又包含另外的容量更大,速度較慢,稱之為L2-cache,L3-cache的片上高速緩存。多級高速緩存之間的一致性是由硬件實現的。Linux忽略這些硬件細節并假定只有一個單獨的高速緩存。
處理器的cr0寄存器的CD標志位用來啟用或禁用高速緩存電路。這個寄存器中的NW標志指明高速緩存是使用通寫還是回寫策略。
除了通用硬件高速緩存之外, 80x86處理器還包含了另外一個稱之為翻譯后備緩沖器或TLB(Translation Lookaside Buffer,有些書上也把這組寄存器叫做“聯想存儲器”)的高速緩存用于加快線性地址的轉換。當一個線性地址被第一次使用時,通過慢速訪問RAM中的頁表計算出相應的物理地址。同時,物理地址被存放在一個TLB表項(TLB entry)中,以便以后對同一個線性地址的引用就可以快速地得到轉換,如圖。
例如CPU給出有效地址為(D,P,W),它把頁號P送入輸入寄存器,隨后立即和TLB各單元的頁號進行比較,如與某個單元中的頁號相匹配,則把該單元中的塊號B送入輸出寄存器。這樣,就可以用(D,B,W)訪問相應的主存單元。
在多處理系統中,每個CPU都有自己的TLB,這叫做該CPU的本地TLB。與硬件高速緩存相反,TLB中的對應項不必同步,這是因為運行在現有CPU上的進程可以使同一線性地址與不同的物理地址發生聯系。
當CPU的cr3控制寄存器被修改時,硬件自動使本地TLB中的所有項都無效,這是因為新的一組頁表被啟用而TLB指向的是舊數據。
做嵌入式系統開發,經常要接觸硬件。做嵌入式開發對數字電路和模擬電路要有一定的了解。這樣才能深入的研究下去。下面我們簡單的介紹嵌入式開發中的一些硬件相關的概念。
總線(Bus)
在嵌入式系統中一定會有一塊處理器芯片,此外,還有其它的芯片作為外部設備(后面簡稱外設),這些芯片與處理器協作實現產品的功能。復雜的產品往往是由大量的芯片組成的。那么不可避免的是我們需要將所有的外設與處理器進行相連,最為簡單的是將所有的外設都采用獨立(注意是獨立)的信號線連接至處理器,這樣的好處是容易理解,但問題是:不可行。
因為處理器芯片需要引出太多的線了,從芯片的生產和產品的生產角度來看都不實際。加之,處理器(在此我們假設處理器是單核的,而不是多核的)處理事務在微觀上是串行的,也就是說在某一時刻如果要對外設進行讀寫操作,那只可能是對大量外設中的一個進行,即多個外設不可能在微觀上被處理器同時訪問。
需要注意的是,這里提出了微觀這一概念,這是為了區別于宏觀。從宏觀上來講,一個處理器中可以有多個任務同時運行,但這些任務在微觀上卻是一個一個運行的(后面會用串行來描述這里所說的“一個一個”),多任務的串行運行實現是由操作系統扮演著重要的角色來實現的。
回到我們的話題,即然將每個外設采用獨立的信號線連到處理器不可行,且處理器在單一時間內只會對一個外設進行訪問,那我們能不能采用共享的信號線將所有的芯片連在一起呢?這就是總線概念的由來。通俗的說,如果我們周圍有十個家庭,為了讓這十個家庭每兩個之間都能往來,我們并不需要為每兩個家庭修一條單獨(注意是單獨)的路(如果這樣,要修45條路),而是可以修一條大路,然后,每個家都與大路相連。
對于總線,我們往往說總線是處理器的,而其它的外設是掛在總線上的。那有一個問題,我們每一時間只能訪問掛在總線上的一個外設,那如何區分這些外設呢?和我們的路一樣,我們需要用地址來區分每一個家庭,在總線上,也是采用地址來進行區分的。
這樣,總線就根據其功能分為兩類了。一類是地址總線,這一總線上的數據只會是從處理器向外設“流”,是單向的。另一類則是數據總線,用來將數據從處理器傳送到外設(從處理器的角度來說是寫操作)或者是將數據從外設傳送到處理器(從處理器的角度來說是讀操作),顯然,數據總線是雙向的。也就是說,在我們的嵌入式系統中同時存在地址總線和數據總線將所有需要與處理器進行通訊的芯片連在一起的。
總線是有寬度的,正如我們的路分為“三車道”或是“四車道”,我們說32位處理器,是指其數據總線寬度是32位,也就是“有32輛車能同時跑”,顯然,寬度越是寬我們的處理器速度就越是快,因為我們從外設芯片存取數據的速度會更快,這就是為什么我們的計算機向64位發展的原因。同樣的,地址總線也是有寬度的,對于32位處理器其最大寬度也就是32位。
總線的概念有了,那接下來的一個問題是,即使是每一個外設都有一個地址,那這一地址記在哪里呢?是放在外設芯片上嗎?如果這樣的話,那就有一個問題,每一類外設的地址必須是不能重疊的,而當一個產品中需要兩塊一樣的芯片的話,兩塊芯片的地址就無法區分了,看來這樣操作存在問題。還有,如果這樣的話每一個外設也得與(比如,32根)數據總線完全相連,并監聽數據線以了解處理器是不是在“叫”自己,這樣很是復雜。
此外,地址也有可能因為外設種類的增多而用光??偟膩碚f地址不能存放在外設芯片,那如何讓外設知道,此時它是被處理器招換從而需要進行讀寫訪問的呢?答案就是芯片的片選(CS, chip select)信號,或者又號使能(ENable)信號。
片選(CS 或EN)
片選信號對于外設芯片來講,就是一個(也是一根)通知信號,告訴芯片“嘿,請開門,我要放些東西進來,或是拿些東西走”,這里的東西只能是數據,不可能是玉米棒什么的。那有個問題,這個信號源從哪里來呢?顯然,只能從處理器來。那是不是也是像總線那樣,每一個芯片都共用一根線連在一起呢?
如果這樣,可能處理器“一叫開門”所有的芯片都將“門”打開了。如果是處理器寫數據,那可能所有的芯片都被寫入同樣的數據。而取數據時,每個外設芯片都向外“扔”數據,這一定會造成數據總線沖突,因為有的芯片向總線上“扔”1,有的則“扔”0,這種情況下處理器一定會“發瘋”的,因為它不知道應當得到1還是0。
即然這樣,那顯然不能將所有的片選信號連在一起了,只能是各芯片的片選信號獨立。前面提到了地址總線,我們是采用一根地址線連一個外設芯片呢?還是采用其它的方法。如果采用一根地址線連一個外設芯片,那可能最多只能掛接32個芯片了,這顯然不行。
其實,在現實中,是采用32位的數字來表示一個外設芯片的地址的,比如1可以表示芯片A,而6534可以表示另外一個芯片B,等等。由此看來,理論上我們可以表示2的32次方(4294967296)個設備,之所以說理論上,是因為有的設備要占用大量的地址。即然這樣,那還有一個問題,如果將32位的地址總線轉換成芯片的一根片選信號呢?這需要引入譯碼(器)的概念。
譯碼(器)
譯碼器將一個數據轉換成一根信號線上的信號,比如3/8譯碼器,可以將一個位寬是3位的數據轉換成8根(2的3次方)完全獨立的信號線,當向數據側寫入二進制的011時,對應的是8根線的第3根,當輸入二進制的111時,對應的是8根線中的最后一根。有了譯碼器,處理器的地址線就簡化了,只要32根地址線加上外面的譯碼器,就可以訪問大量的外設芯片了。外部設備的選擇問題,我們已經解決了,現在還得回頭看一看數據總線。
在嵌入式系統中,所有芯片的數據總線可以理解成是直接相連的。之所以用了“可以理解”一詞,是因為為了提高總線的負載能力,其中會加入總線驅動器。為了理解,我們看一看我們生活中的自來水,比如,在北京理論上可能所有的水管是連在一起的,但中間可能為了提高水壓,存在很多小的水站用來增加供水壓力,而不可能全北京所有的自來水自接來自一個水廠。
即然所有的數據總線是連在一起的,那就可能會有問題。當向外部設備寫數據時,處理器先向地址總線輸送目標外設的地址,地址譯碼器將其轉換成一根信號的片選信號送到了目標外設,目標外設收到這一信號后,將“門”打開。接下來處理器將要傳送到外設的數據往數據總線上一放,由于只有目標外設芯片打開了“門”,所以數據只會進入到目標外設,而其它的外設什么也不會收到。很好!處理器向外寫數據應當沒有問題,我們接下來看一看讀。讀的話,由于數據是從外設輸送到處理器的,盡管我們采用和寫一樣的方法打開目標外設的“門”,但此時,其它的外設也在數據總線上,它們有可能處于1也可能處于0,是不是會影響處理器讀取目標外設的數據呢?結果當然不會,但我們得引入另一個概念:高阻態。
高阻態
很顯然,當處理器從目標外設讀數據時,我們希望其它沒有被選上的芯片的數據總線不會對目標外設所要傳送的數據有影響,那怎么辦呢?實際上,當芯片沒有被選中時,其數據總線都處于高阻態。所謂的高阻態,我們可以理解成這一管腳在外設芯片內部是斷開的,如此一來,顯然不會對處理器從目標外設讀取數據造成任何的影響了。我們說當一個芯片沒有被選中或是沒有被使能時,其數據總線一定是處于高阻態的。前面用了“門”的開和關來打比方,那“門”是指什么呢?是指外設的數據總線,片選信號的作用就是控制將外設的數據總線與處理器的數據總線相連或是斷開。更多的關于高阻態的講解可參看前面寫的文章《高阻態和三態門》。
驅動
總線上的數據是誰放上去的我們就說誰是那一時刻的驅動者。也就是說,當處理器向外設寫數據時,它是在驅動數據總線的,而當處理器從目標外設讀取數據時,目標外設是在驅動數據總線的。對于地址總線,因為只可能從處理器向目標外設寫,所以地址總線永遠是由處理器驅動的。當一個芯片沒有被選中時,我們說它并不驅動數據總線。
三態門
前面我們說到外設芯片的數據總線在沒有被選中時其處于高阻態,當被選中時,其電平可能是高(1)或是低(0)。如此一來,我們說外設的數據總線其芯片管腳是屬于三態門的,即存在高電平、低電平和高阻態,三個狀態。更多的關于三態門的講解可參看前面寫的文章《高阻態和三態門》。
電平的有效性
前面我們了解了什么是片選信號,也講到了三態門,需要指出的是片選信號通常不是三態門,其只存在兩個狀態,即高電平或是低電平。前面我們也說了,片選信號是用來“開門”的,而片選信號又有高和低電平,那到底是高電平表示“開門”呢?還是低電平?對于這一問題,我們稱如果一個電平對于一個片選信號表示“開門”那么它就是這一信號的有效電平。比如,對于一個片選信號,如果低電平表示“開門”,那么我們說這個片選信號是低電平有效的。雖然,在這里我們用片選信號來解釋電平的有效性,但是很多信號都存在有效性的問題,比如,后面我們將要談的讀信號和寫信號都存在有效性問題。
時序
在前面我們說到當處理器要向外設芯片寫數據時,需要先將所需訪問的外設的地址放在地址總線上,然后,由譯碼器將地址總線上的數據轉換成片選信號,片選信號則使能目標外設芯片,接下來處理器寫數據到數據總線上,從而完成一個寫操作。顯然,在處理器將數據寫到數據總線之前地址線上的數據必須一直保留一段時間,否則的話譯碼器不能長時間的使片選信號有效。當完成了數據的寫操作后,處理器就不需要保證地址總線上的地址有效了。我們可以看出,這一系列的操作都有一定嚴格的時間順序的,這稱之為時序。時序描述了處理器與外部設備的交互信號 “規程”,大家只有按照這一“規程”來操作,才能保證處理器與外部設備之間能正常的通訊。這好比,我們的道路上的紅綠燈,如果我們行人和車輛不按照其指示來通行的話,就會出現事故。通常,采用時序圖來描述芯片之間通訊的信號“規程”。
從圖中我們可以看出ADDRESS是表示地址總線的,DQ是表示數據總線的,CE是片選信號,且是低電平有效,其寬度要保證在進行讀操作時總是有效的。學會看時序圖對于做嵌入式系統開發非常有幫助,因為我們不可避免的要與芯片打交道。在時序圖中,通常會標識很多的時間需求信息。在寫啟動代碼時需要初始化各地址空間的片選地址寄存器和讀寫時序,時序的配置依據就是來自于外設芯片的時間需求,這是芯片手冊很重要的一部分內容。當一個地址空間中存在多個外設芯片時,我們需要考慮到其中最慢的外設芯片的時間需求,否則的話有的芯片就不能正常工作。
讀信號
當處理器需要從外設芯片讀取信號時,除了需要產生片選信號外,還需要告訴外設芯片這是一個讀操作,而不是一個寫操作,這是通過讀信號來實現的。
寫信號
前面講了讀信號,我想對于寫信號也就不難理解了,這個信號用于告訴外設芯片,這是一個向外設芯片寫數據的操作。
I/O端口
前面提到了外設(芯片)),現在是對外設進行分類的時候了。大體上外設分為兩類,一類是存儲器外設,而另一類是非存儲器外設,后者常被稱之為I/O設備,這里的I/O是Input/Output的簡寫,即輸入、輸出??梢?,I/O外設是一個非常寬泛的概念。對于存儲器外設,其特點是,它所占用的空間是連續的一片。比如,SDRAM內存就是屬于存儲器外設,如果其容量是8M字節,那么其占用的地址空間也會是8M的。
與存儲器外設所不同的是,I/O外設所點用的地址一般都很少。比如一個I/O外設可能存在多個控制寄存器,這些控制寄存器從處理器來看就是多個I/O端口(地址),向這個地址寫數據就是向外設所對應的寄存器寫數據,反之,也可以是讀。比如,一個串口芯片可能存在多個寄存器,一個用來查詢芯片的狀態,一個用來設置芯片的功能,另一個用來讀取芯片從串口線所收到的數據,最后,還有一個用來向芯片寫數據以向串口線上發送數據。對于這一串口芯片的寄存器,從處理器的角度來看,都是獨立的I/O端口。
I/O端口存在讀、寫性問題,有的端口是只讀的,有的端口是只寫的,還有的端口是即可讀也可寫,其讀寫性是由外設芯片的寄存器所決定的,在芯片的數據手冊中能找到。需要指出的是,有些存儲器外設也存在I/O端口,以對其進行一定的控制。從I/O端口這一名字來看,對于處理器來說,就是對從外面讀入數據或是向外面輸出數據的一個接口總稱。
中斷
中斷從硬件的角度來看就是一個能產生高、低電平的一根信號線,但理解它需要從處理器的角度出發。我們說過了,處理器從微觀上看,所做的工作是按順序進行的,其對程序的處理只能是一條指令一條指令的執行。如果存在需要對外設芯片進行訪問,而有可能從處理器發出讀、寫命令后,由于外設通常比處理器慢很多,所以外設芯片需要一些時間來準備好所需的數據。在這種情況下,如果處理器一直等外設芯片的返回數據再執行后續的指令的話,將耗費寶貴的時間,這些時間完全可以用來做其它的工作。
別忘了,從宏觀上看來處理器常常是多任務的,任務是指操作系統所提供的調度單位。當一個任務因為等待外設芯片的數據而阻塞時,我們可以切換到另外的任務,從而提高處理效率。這就有一個問題,當處理器去處理另一個任務時,如果外設芯片的數據好了的話,如果告訴處理器呢?對了!就是通過中斷信號。中斷信號的高、低電平可以用來表示是否有中斷需要處理器注意以處理特定的事件(比如,外設數據準備好了的事件)。
由此看來,中斷的引入能大大的提高處理器的運用效率。為了使用處理器上的中斷,一開始我們需要初始化好處理器的中斷控制器,比如安裝好所需的中斷服務程序或稱之為ISR(Interrupt Service Routine),然后,打開中斷屏蔽位。中斷服務程序中需要做如下的操作:
1. 從外設讀入或向外設寫數據。讀還是寫通常需要讀取外設的中斷狀態寄存器來決定。
2. 清除外設的中斷信號。我們知道,中斷信號是由外設芯片驅動的,為了告訴外設芯片,處理器已經處理完了所需做的工作,那么處理器需要通過一定的方式通知外設芯片。這種方式就是向外設芯片的寄存器中的某一位寫入一個數據,比如,可能是寫入1表示清中斷,也可能是寫入0表示清中斷,這通常在外設的數據手冊中能查到。當外設收到了處理器的清中斷請求后,其就會驅動中斷線使其無效。比如,一個外設的中斷線是當其為低電平表示有中斷,將其從低電平變為高電平就是驅動為無效。
3. 清除處理器的中斷信號標識。處理器中往往也會保存外部中斷信號是否發生過,當我們處理完了外設芯片的中斷時,我們也需要清除處理器上的標識,從而為下一次中斷做準備。需要注意的是,清外設的中斷必須發生在請處理器中斷標識之前!
中斷還存在一個觸發方式問題。有兩種觸發方式 ,一種是電平觸發,另一種是沿觸發。電平觸發是指電平的高低表示外設是否有中斷,而沿觸發則是能過中斷線上的電平的升或降來表示的,顯然,存在兩種沿觸發方式。一種是中斷線從低電平變為高電平,我們稱之為上升沿觸發,另一處是中斷線從高電平轉換為低電平,我們稱之為下降沿觸發??偟膩碚f中斷的觸發方式有電平觸發、上升沿觸發和下降沿觸發。電平觸發方式中處理中中斷設置很重要的一個步驟。
萬用表通常是用來查看電平的高低、電阻的大小等的,是常用且必不可少的工具之一。在嵌入式系統開發中,我們常用的是數字萬用表。
電平(Level)
在數字電路中,分為高電平和低電平,分別用1和0表示。一個數字電路的管腳,總是存在一個電平的,要么高要么低,或者說要么1要到0(其實,還有另一種狀態)。
在嵌入式系統開發中,我們不可避免的要與外設芯片打交道。調試驅動程序時,除了需要完全看明白芯片的數據手冊,且在軟件高度的過程中,還需要看我們所期望的信號電平是否發生在芯片上。比如,我們在寫驅動程序時,需要通過寫I/O端口來對外設芯片進行操作,當寫相應的I/O端口時,我們知道所對應芯片的片選信號應當有效,有時,我們需要驗證是否按預期發生了,這就需要用到示波器。一般的示波器是能同時觀測兩個信號線的信號狀態的。示波器都提供一定的功能,比如設置信號撲捉的方式等等。示波器很重要的一個參數據是其采集頻率,根據Nyquist采集定理,如果我們想用示波器查看頻率是100M赫茲的信號,那么其采樣頻率必須至少是其兩倍,即200M赫茲。有人可能會問:為什么不用萬用表來看呢?因為萬用表的采集頻率很底,無法采集到很快的信號變化。
邏輯分析儀
簡單的說邏輯分析器就是具有很多信號通道的示波器。通過邏輯分析儀,我們可以看到地址總線和數據總線上的數據。邏輯分析儀都提供一定的編程能力,用于編程什么時候開始對總線上的數據進行采集。
-
寄存器
+關注
關注
31文章
5336瀏覽量
120232 -
cpu
+關注
關注
68文章
10854瀏覽量
211587 -
Linux
+關注
關注
87文章
11292瀏覽量
209334
原文標題:要想弄懂Linux內核的工作原理,就必須懂點基本的硬件知識
文章出處:【微信號:gh_c472c2199c88,微信公眾號:嵌入式微處理器】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論