歡迎光臨
每天分享高質量文章

計算機實驗室之樹莓派:課程 3 OK03 | Linux 中國

雖然我們的作業系統除了做 課程 2:OK02 中的事情,還做不了別的任何事情,但我們已經學會了函式和格式有關的知識,並且我們現在可以更好更快地編寫新特性了。

— Robert Mullins

 

OK03 課程基於 OK02 課程來構建,它教你在彙編中如何使用函式讓程式碼可復用和可讀性更好。假設你已經有了 課程 2:OK02[1] 的作業系統,我們將以它為基礎。

1、可復用的程式碼

到目前為止,我們所寫的程式碼都是以我們希望發生的事為順序來輸入的。對於非常小的程式來說,這種做法很好,但是如果我們以這種方式去寫一個完整的系統,所寫的程式碼可讀性將非常差。我們應該去使用函式。

一個函式是一段可復用的程式碼片斷,可以用於去計算某些答案,或執行某些動作。你也可以稱它們為過程procedure例程routine子例程subroutine。雖然它們都是不同的,但人們幾乎都沒有正確地使用這個術語。

你應該在數學上遇到了函式的概念。例如,餘弦函式應用於一個給定的數時,會得到介於 -1 到 1 之間的另一個數,這個數就是角的餘弦。一般我們寫成 cos(x) 來表示應用到一個值 x 上的餘弦函式。

在程式碼中,函式可以有多個輸入(也可以沒有輸入),然後函式給出多個輸出(也可以沒有輸出),並可能導致副作用。例如一個函式可以在一個檔案系統上建立一個檔案,第一個輸入是它的名字,第二個輸入是檔案的長度。

Function as black boxes

函式可以認為是一個“黑匣子”。我們給它輸入,然後它給我們輸出,而我們不需要知道它是如何工作的。

在像 C 或 C++ 這樣的高階程式碼中,函式是語言的組成部分。在彙編程式碼中,函式只是我們的創意。

理想情況下,我們希望能夠在我們的暫存器中設定一些輸入值,然後分支切換到某個地址,然後預期在某個時刻分支傳回到我們程式碼,並透過程式碼來設定輸出值到暫存器。這就是我們所設想的彙編程式碼中的函式。困難之處在於我們用什麼樣的方式去設定暫存器。如果我們只是使用平時所接觸到的某種方法去設定暫存器,每個程式員可能使用不同的方法,這樣你將會發現你很難理解其他程式員所寫的程式碼。另外,編譯器也不能像使用彙編程式碼那樣輕鬆地工作,因為它們壓根不知道如何去使用函式。為避免這種困惑,為每個組合語言設計了一個稱為應用程式二進位制介面Application Binary Interface(ABI)的標準,由它來規範函式如何去執行。如果每個人都使用相同的方法去寫函式,這樣每個人都可以去使用其他人寫的函式。在這裡,我將教你們這個標準,而從現在開始,我所寫的函式將全部遵循這個標準。

該標準規定,暫存器 r0r1r2 和 r3 將被依次用於函式的輸入。如果函式沒有輸入,那麼它不會在意值是什麼。如果只需要一個輸入,那麼它應該總是在暫存器 r0 中,如果它需要兩個輸入,那麼第一個輸入在暫存器 r0 中,而第二個輸入在暫存器 r1 中,依此類推。輸出值也總是在暫存器 r0 中。如果函式沒有輸出,那麼 r0 中是什麼值就不重要了。

另外,該標準要求當一個函式執行之後,暫存器 r4 到 r12 的值必須與函式啟動時的值相同。這意味著當你呼叫一個函式時,你可以確保暫存器 r4 到 r12 中的值沒有發生變化,但是不能確保暫存器 r0 到 r3 中的值也沒有發生變化。

當一個函式執行完成後,它將傳回到啟動它的程式碼分支處。這意味著它必須知道啟動它的程式碼的地址。為此,需要一個稱為 lr(連結暫存器)的專用暫存器,它總是在儲存呼叫這個函式的指令後面指令的地址。

表 1.1 ARM ABI 暫存器用法

< 如顯示不全,請左右滑動 >
暫存器 簡介 保留 規則
r0 引數和結果 r0 和 r1 用於給函式傳遞前兩個引數,以及函式傳回的結果。如果函式傳回值不使用它,那麼在函式執行之後,它們可以攜帶任何值。
r1 引數和結果
r2 引數 r2 和 r3 用去給函式傳遞後兩個引數。在函式執行之後,它們可以攜帶任何值。
r3 引數
r4 通用暫存器 r4 到 r12 用於儲存函式執行過程中的值,它們的值在函式呼叫之後必須與呼叫之前相同。
r5 通用暫存器
r6 通用暫存器
r7 通用暫存器
r8 通用暫存器
r9 通用暫存器
r10 通用暫存器
r11 通用暫存器
r12 通用暫存器
lr 傳回地址 當函式執行完成後,lr 中儲存了分支的傳回地址,但在函式執行完成之後,它將儲存相同的地址。
sp 棧指標 sp 是棧指標,在下麵有詳細描述。它的值在函式執行完成後,必須是相同的。

通常,函式需要使用很多的暫存器,而不僅是 r0 到 r3。但是,由於 r4 到 r12 必須在函式完成之後值必須保持相同,因此它們需要被儲存到某個地方。我們將它們儲存到稱為棧的地方。

Stack diagram

一個stack就是我們在計算中用來儲存值的一個很形象的方法。就像是摞起來的一堆盤子,你可以從上到下來移除它們,而新增它們時,你只能從下到上來新增。

在函式執行時,使用棧來儲存暫存器值是個非常好的創意。例如,如果我有一個函式需要去使用暫存器 r4 和 r5,它將在一個棧上存放這些暫存器的值。最後用這種方式,它可以再次將它拿回來。更高明的是,如果為了執行完我的函式,需要去執行另一個函式,並且那個函式需要儲存一些暫存器,在那個函式執行時,它將把暫存器儲存在棧頂上,然後在結束後再將它們拿走。而這並不會影響我儲存在暫存器 r4 和 r5 中的值,因為它們是在棧頂上新增的,拿走時也是從棧頂上取出的。

用來表示使用特定的方法將值放到棧上的專用術語,我們稱之為那個方法的“棧幀stack frame”。不是每種方法都使用一個棧幀,有些是不需要儲存值的。

因為棧非常有用,它被直接實現在 ARMv6 的指令集中。一個名為 sp(棧指標)的專用暫存器用來儲存棧的地址。當需要有值新增到棧上時,sp 暫存器被更新,這樣就總是保證它儲存的是棧上第一個值的地址。push {r4,r5} 將推送 r4 和 r5 中的值到棧頂上,而 pop {r4,r5} 將(以正確的次序)取回它們。

2、我們的第一個函式

現在,關於函式的原理我們已經有了一些概念,我們嘗試來寫一個函式。由於是我們的第一個很基礎的例子,我們寫一個沒有輸入的函式,它將輸出 GPIO 的地址。在上一節課程中,我們就是寫到這個值上,但將它寫成函式更好,因為我們在真實的作業系統中經常需要用到它,而我們不可能總是能夠記住這個地址。

複製下列程式碼到一個名為 gpio.s 的新檔案中。就像在 source 目錄中使用的 main.s一樣。我們將把與 GPIO 控制器相關的所有函式放到一個檔案中,這樣更好查詢。

  1. .globl GetGpioAddress
  2. GetGpioAddress:
  3. ldr r0,=0x20200000
  4. mov pc,lr

.globl lbl 使標簽 lbl 從其它檔案中可訪問。

mov reg1,reg2 複製 reg2 中的值到 reg1 中。

這就是一個很簡單的完整的函式。.globl GetGpioAddress 命令是通知彙編器,讓標簽 GetGpioAddress 在所有檔案中全域性可訪問。這意味著在我們的 main.s 檔案中,我們可以使用分支指令到標簽 GetGpioAddress 上,即便這個標簽在那個檔案中沒有定義也沒有問題。

你應該認得 ldr r0,=0x20200000 命令,它將 GPIO 控制器地址儲存到 r0 中。由於這是一個函式,我們必須要讓它輸出到暫存器 r0 中,我們不能再像以前那樣隨意使用任意一個暫存器了。

mov pc,lr 將暫存器 lr 中的值複製到 pc 中。正如前面所提到的,暫存器 lr 總是儲存著方法完成後我們要傳回的程式碼的地址。pc 是一個專用暫存器,它總是包含下一個要執行的指令的地址。一個普通的分支命令只需要改變這個暫存器的值即可。透過將 lr 中的值複製到 pc 中,我們就可以將要執行的下一行命令改變成我們將要傳回的那一行。

理所當然這裡有一個問題,那就是我們如何去執行這個程式碼?我們將需要一個特殊的分支型別 bl 指令。它像一個普通的分支一樣切換到一個標簽,但它在切換之前先更新 lr 的值去包含一個在該分支之後的行的地址。這意味著當函式執行完成後,將傳回到 bl 指令之後的那一行上。這就確保了函式能夠像任何其它命令那樣執行,它簡單地執行,做任何需要做的事情,然後推進到下一行。這是理解函式最有用的方法。當我們使用它時,就將它們按“黑匣子”處理即可,不需要瞭解它是如何執行的,我們只瞭解它需要什麼輸入,以及它給我們什麼輸出即可。

到現在為止,我們已經明白了函式如何使用,下一節我們將使用它。

3、一個大的函式

現在,我們繼續去實現一個更大的函式。我們的第一項任務是啟用 GPIO 第 16 號針腳的輸出。如果它是一個函式那就太好了。我們能夠簡單地指定一個針腳號和一個函式作為輸入,然後函式將設定那個針腳的值。那樣,我們就可以使用這個程式碼去控制任意的 GPIO 針腳,而不只是 LED 了。

將下列的命令複製到 gpio.s 檔案中的 GetGpioAddress 函式中。

  1. .globl SetGpioFunction
  2. SetGpioFunction:
  3. cmp r0,#53
  4. cmpls r1,#7
  5. movhi pc,lr

帶字尾 ls 的命令只有在上一個比較命令的結果是第一個數字小於或與第二個數字相同的情況下才會被執行。它是無符號的。

帶字尾 hi 的命令只有上一個比較命令的結果是第一個數字大於第二個數字的情況下才會被執行。它是無符號的。

在寫一個函式時,我們首先要考慮的事情就是輸入,如果輸入錯了我們怎麼辦?在這個函式中,我們有一個輸入是 GPIO 針腳號,而它必須是介於 0 到 53 之間的數字,因為只有 54 個針腳。每個針腳有 8 個函式,被編號為 0 到 7,因此函式編號也必須是 0 到 7 之間的數字。我們可以假設輸入應該是正確的,但是當在硬體上使用時,這種做法是非常危險的,因為不正確的值將導致非常糟糕的副作用。所以,在這個案例中,我們希望確保輸入值在正確的範圍。

為了確保輸入值在正確的範圍,我們需要做一個檢查,即 r0 <= 53 並且 r1 <= 7。首先我們使用前面看到的比較命令去將 r0 的值與 53 做比較。下一個指令 cmpls 僅在前一個比較指令結果是小於或與 53 相同時才會去執行。如果是這種情況,它將暫存器 r1 的值與 7 進行比較,其它的部分都和前面的是一樣的。如果最後的比較結果是暫存器值大於那個數字,最後我們將傳回到執行函式的程式碼處。

這正是我們所希望的效果。如果 r0 中的值大於 53,那麼 cmpls 命令將不會去執行,但是 movhi 會執行。如果 r0 中的值 <= 53,那麼 cmpls 命令會執行,它會將 r1 中的值與 7 進行比較,如果 r1 > 7,movhi 會執行,函式結束,否則 movhi 不會執行,這樣我們就確定 r0 <= 53 並且 r1 <= 7。

ls(低於或相同)與 le(小於或等於)有一些細微的差別,以及字尾 hi(高於)和 gt(大於)也一樣有一些細微差別,我們在後面將會講到。

將這些命令複製到上面的程式碼的下麵位置。

  1. push {lr}
  2. mov r2,r0
  3. bl GetGpioAddress

push {reg1,reg2,...} 複製列出的暫存器 reg1reg2、… 到棧頂。該命令僅能用於通用暫存器和 lr 暫存器。

bl lbl 設定 lr 為下一個指令的地址並切換到標簽 lbl

這三個命令用於呼叫我們第一個方法。push {lr} 命令複製 lr 中的值到棧頂,這樣我們在後面可以獲取到它。當我們呼叫 GetGpioAddress 時必須要這樣做,我們將需要使用 lr去儲存我們函式要傳回的地址。

如果我們對 GetGpioAddress 函式一無所知,我們必須假設它改變了 r0r1r2 和 r3 的值 ,並移動我們的值到 r4 和 r5 中,以在函式完成之後保持它們的值一樣。幸運的是,我們知道 GetGpioAddress 做了什麼,並且我們也知道它僅改變了 r0 為 GPIO 地址,它並沒有影響 r1r2 或 r3 的值。因此,我們僅去將 GPIO 針腳號從 r0 中移出,這樣它就不會被改寫掉,但我們知道,可以將它安全地移到 r2 中,因為 GetGpioAddress 並不去改變 r2

最後我們使用 bl 指令去執行 GetGpioAddress。通常,執行一個函式,我們使用一個術語叫“呼叫”,從現在開始我們將一直使用這個術語。正如我們前面討論過的,bl 呼叫一個函式是透過更新 lr 為下一個指令的地址並切換到該函式完成的。

當一個函式結束時,我們稱為“傳回”。當一個 GetGpioAddress 呼叫傳回時,我們已經知道了 r0 中包含了 GPIO 的地址,r1 中包含了函式編號,而 r2 中包含了 GPIO 針腳號。

我前面說過,GPIO 函式每 10 個儲存在一個塊中,因此首先我們需要去判斷我們的針腳在哪個塊中。這似乎聽起來像是要使用一個除法,但是除法做起來非常慢,因此對於這些比較小的數來說,不停地做減法要比除法更好。

將下麵的程式碼複製到上面的程式碼中最下麵的位置。

  1. functionLoop$:
  2. cmp r2,#9
  3. subhi r2,#10
  4. addhi r0,#4
  5. bhi functionLoop$

add reg,#val 將數字 val 加到暫存器 reg 的內容上。

這個簡單的迴圈程式碼將針腳號(r2)與 9 進行比較。如果它大於 9,它將從針腳號上減去 10,並且將 GPIO 控制器地址加上 4,然後再次執行檢查。

這樣做的效果就是,現在,r2 中將包含一個 0 到 9 之間的數字,它是針腳號除以 10 的餘數。r0 將包含這個針腳的函式所設定的 GPIO 控制器的地址。它就如同是 “GPIO 控制器地址 + 4 × (GPIO 針腳號 ÷ 10)”。

最後,將下麵的程式碼複製到上面的程式碼中最下麵的位置。

  1. add r2, r2,lsl #1
  2. lsl r1,r2
  3. str r1,[r0]
  4. pop {pc}

移位引數 reg,lsl #val 表示將暫存器 reg 中二進製表示的數邏輯左移 val 位之後的結果作為與前面運算的運算元。

lsl reg,amt 將暫存器 reg 中的二進位制數邏輯左移 amt 中的位數。

str reg,[dst] 與 str reg,[dst,#0] 相同。

pop {reg1,reg2,...} 從棧頂複製值到暫存器串列 reg1reg2、… 僅有通用暫存器與 pc 可以這樣彈出值。

這個程式碼完成了這個方法。第一行其實是乘以 3 的變體。乘法在彙編中是一個大而慢的指令,因為電路需要很長時間才能給出答案。有時使用一些能夠很快給出答案的指令會讓它變得更快。在本案例中,我們知道 r2 × 3 與 r2 × 2 + r2 是相同的。一個暫存器乘以 2 是非常容易的,因為它可以透過將二進製表示的數左移一位來很方便地實現。

ARMv6 組合語言其中一個非常有用的特性就是,在使用它之前可以先移動引數所表示的位數。在本案例中,我將 r2 加上 r2 中二進製表示的數左移一位的結果。在彙編程式碼中,你可以經常使用這個技巧去更快更容易地計算出答案,但如果你覺得這個技巧使用起來不方便,你也可以寫成類似 mov r3,r2; add r2,r3; add r2,r3 這樣的程式碼。

現在,我們可以將一個函式的值左移 r2 中所表示的位數。大多數對數量的指令(比如 add和 sub)都有一個可以使用暫存器而不是數字的變體。我們執行這個移位是因為我們想去設定表示針腳號的位,並且每個針腳有三個位。

然後,我們將函式計算後的值儲存到 GPIO 控制器的地址上。我們在迴圈中已經算出了那個地址,因此我們不需要像 OK01 和 OK02 中那樣在一個偏移量上儲存它。

最後,我們從這個方法呼叫中傳回。由於我們將 lr 推送到了棧上,因此我們 pop pc,它將複製 lr 中的值並將它推送到 pc 中。這個操作類似於 mov pc,lr,因此函式呼叫將傳回到執行它的那一行上。

敏銳的人可能會註意到,這個函式其實並不能正確工作。雖然它將 GPIO 針腳函式設定為所要求的值,但它會導致在同一個塊中的所有的 10 個針腳的函式都歸 0!在一個大量使用 GPIO 針腳的系統中,這將是一個很惱人的問題。我將這個問題留給有興趣去修複這個函式的人,以確保只設定相關的 3 個位而不去覆寫其它位,其它的所有位都保持不變。關於這個問題的解決方案可以在本課程的下載頁面上找到。你可能會發現非常有用的幾個函式是 and,它是計算兩個暫存器的布林與函式,mvns 是計算布林非函式,而 orr 是計算布林或函式。

4、另一個函式

現在,我們已經有了能夠管理 GPIO 針腳函式的函式。我們還需要寫一個能夠開啟或關閉 GPIO 針腳的函式。我們不需要寫一個開啟的函式和一個關閉的函式,只需要一個函式就可以做這兩件事情。

我們將寫一個名為 SetGpio 的函式,它將 GPIO 針腳號作為第一個輸入放入 r0 中,而將值作為第二個輸入放入 r1 中。如果該值為 0,我們將關閉針腳,而如果為非零則開啟針腳。

將下列的程式碼複製貼上到 gpio.s 檔案的結尾部分。

  1. .globl SetGpio
  2. SetGpio:
  3. pinNum .req r0
  4. pinVal .req r1

alias .req reg 設定暫存器 reg 的別名為 alias

我們再次需要 .globl 命令,標記它為其它檔案可訪問的全域性函式。這次我們將使用暫存器別名。暫存器別名允許我們為暫存器使用名字而不僅是 r0 或 r1。到目前為止,暫存器別名還不是很重要,但隨著我們後面寫的方法越來越大,它將被證明非常有用,現在開始我們將嘗試使用別名。當在指令中使用到 pinNum .req r0 時,它的意思是 pinNum 表示 r0

將下麵的程式碼複製貼上到上述的程式碼下麵位置。

  1. cmp pinNum,#53
  2. movhi pc,lr
  3. push {lr}
  4. mov r2,pinNum
  5. .unreq pinNum
  6. pinNum .req r2
  7. bl GetGpioAddress
  8. gpioAddr .req r0

.unreq alias 刪除別名 alias

就像在函式 SetGpio 中所做的第一件事情是檢查給定的針腳號是否有效一樣。我們需要同樣的方式去將 pinNumr0)與 53 進行比較,如果它大於 53 將立即傳回。一旦我們想要再次呼叫 GetGpioAddress,我們就需要將 lr 推送到棧上來保護它,將 pinNum 移動到 r2 中。然後我們使用 .unreq 陳述句來刪除我們給 r0 定義的別名。因為針腳號現在儲存在暫存器 r2 中,我們希望別名能夠反映這個變化,因此我們從 r0 移走別名,重新定義到 r2。你應該每次在別名使用結束後,立即刪除它,這樣當它不再存在時,你就不會在後面的程式碼中因它而產生錯誤。

然後,我們呼叫了 GetGpioAddress,並且我們建立了一個指向 r0的別名以反映此變化。

將下麵的程式碼複製貼上到上述程式碼的後面位置。

  1. pinBank .req r3
  2. lsr pinBank,pinNum,#5a
  3. lsl pinBank,#2
  4. add gpioAddr,pinBank
  5. .unreq pinBank

lsr dst,src,#val 將 src 中二進製表示的數右移 val 位,並將結果儲存到 dst

對於開啟和關閉 GPIO 針腳,每個針腳在 GPIO 控制器上有兩個 4 位元組組。第一個 4 位元組組每個位控制前 32 個針腳,而第二個 4 位元組組控制剩下的 22 個針腳。為了判斷我們要設定的針腳在哪個 4 位元組組中,我們需要將針腳號除以 32。幸運的是,這很容易,因為它等價於將二進製表示的針腳號右移 5 位。因此,在本案例中,我們將 r3 命名為 pinBank,然後計算 pinNum ÷ 32。因為它是一個 4 位元組組,我們需要將它與 4 相乘的結果。它與二進製表示的數左移 2 位相同,這就是下一行的命令。你可能想知道我們能否只將它右移 3 位呢,這樣我們就不用先右移再左移。但是這樣做是不行的,因為當我們做 ÷ 32 時答案有些位可能被捨棄,而如果我們做 ÷ 8 時卻不會這樣。

現在,gpioAddr 的結果有可能是 2020000016(如果針腳號介於 0 到 31 之間),也有可能是 2020000416(如果針腳號介於 32 到 53 之間)。這意味著如果加上 2810,我們將得到開啟針腳的地址,而如果加上 4010 ,我們將得到關閉針腳的地址。由於我們用完了 pinBank ,所以在它之後立即使用 .unreq 去刪除它。

將下麵的程式碼複製貼上到上述程式碼的下麵位置。

  1. and pinNum,#31
  2. setBit .req r3
  3. mov setBit,#1
  4. lsl setBit,pinNum
  5. .unreq pinNum

and reg,#val 計算暫存器 reg 中的數與 val 的布林與。

該函式的下一個部分是產生一個正確的位集合的數。至於 GPIO 控制器去開啟或關閉針腳,我們在針腳號除以 32 的餘數裡設定了位的數。例如,設定 16 號針腳,我們需要第 16 位設定數字為 1 。設定 45 號針腳,我們需要設定第 13 位數字為 1,因為 45 ÷ 32 = 1 餘數 13。

這個 and 命令計算我們需要的餘數。它是這樣計算的,在兩個輸入中所有的二進位制位都是 1 時,這個 and 運算的結果就是 1,否則就是 0。這是一個很基礎的二進位制操作,and 操作非常快。我們給定的輸入是 “pinNum and 3110 = 111112”。這意味著答案的後 5 位中只有 1,因此它肯定是在 0 到 31 之間。尤其是在 pinNum 的後 5 位的位置是 1 的地方它只有 1。這就如同被 32 整除的餘數部分。就像 31 = 32 – 1 並不是巧合。

binary division example

程式碼的其餘部分使用這個值去左移 1 位。這就有了建立我們所需要的二進位制數的效果。

將下麵的程式碼複製貼上到上述程式碼的下麵位置。

  1. teq pinVal,#0
  2. .unreq pinVal
  3. streq setBit,[gpioAddr,#40]
  4. strne setBit,[gpioAddr,#28]
  5. .unreq setBit
  6. .unreq gpioAddr
  7. pop {pc}

teq reg,#val 檢查暫存器 reg 中的數字與 val 是否相等。

這個程式碼結束了該方法。如前面所說,當 pinVal 為 0 時,我們關閉它,否則就開啟它。teq(等於測試)是另一個比較操作,它僅能夠測試是否相等。它類似於 cmp ,但它並不能算出哪個數大。如果你只是希望測試數字是否相同,你可以使用 teq

如果 pinVal 是 0,我們將 setBit 儲存在 GPIO 地址偏移 40 的位置,我們已經知道,這樣會關閉那個針腳。否則將它儲存在 GPIO 地址偏移 28 的位置,它將開啟那個針腳。最後,我們透過彈出 pc 傳回,這將設定它為我們推送連結暫存器時儲存的值。

5、一個新的開始

在完成上述工作後,我們終於有了我們的 GPIO 函式。現在,我們需要去修改 main.s 去使用它們。因為 main.s 現在已經有點大了,也更複雜了。將它分成兩節將是一個很好的設計。到目前為止,我們一直使用的 .init 應該盡可能的讓它保持小。我們可以更改程式碼來很容易地反映出這一點。

將下列的程式碼插入到 main.s 檔案中 _start: 的後面:

  1. b main
  2. .section .text
  3. main:
  4. mov sp,#0x8000

在這裡重要的改變是引入了 .text 節。我設計了 makefile 和聯結器指令碼,它將 .text節(它是預設節)中的程式碼放在地址為 800016 的 .init 節之後。這是預設載入地址,並且它給我們提供了一些空間去儲存棧。由於棧存在於記憶體中,它也有一個地址。棧向下增長記憶體,因此每個新值都低於前一個地址,所以,這使得棧頂是最低的一個地址。

Layout diagram of operating system

圖中的 “ATAGs” 節的位置儲存了有關樹莓派的資訊,比如它有多少記憶體,預設螢幕解析度是多少。

用下麵的程式碼替換掉所有設定 GPIO 函式針腳的程式碼:

  1. pinNum .req r0
  2. pinFunc .req r1
  3. mov pinNum,#16
  4. mov pinFunc,#1
  5. bl SetGpioFunction
  6. .unreq pinNum
  7. .unreq pinFunc

這個程式碼將使用針腳號 16 和函式編號 1 去呼叫 SetGpioFunction。它的效果就是啟用了 OK LED 燈的輸出。

用下麵的程式碼去替換開啟 OK LED 燈的程式碼:

  1. pinNum .req r0
  2. pinVal .req r1
  3. mov pinNum,#16
  4. mov pinVal,#0
  5. bl SetGpio
  6. .unreq pinNum
  7. .unreq pinVal

這個程式碼使用 SetGpio 去關閉 GPIO 第 16 號針腳,因此將開啟 OK LED。如果我們(將第 4 行)替換成 mov pinVal,#1 它將關閉 LED 燈。用以上的程式碼去替換掉你關閉 LED 燈的舊程式碼。

6、繼續向標的前進

但願你能夠順利地在你的樹莓派上測試我們所做的這一切。到目前為止,我們已經寫了一大段程式碼,因此不可避免會出現錯誤。如果有錯誤,可以去檢視我們的排錯頁面。

如果你的程式碼已經正常工作,恭喜你。雖然我們的作業系統除了做 課程 2:OK02[1] 中的事情,還做不了別的任何事情,但我們已經學會了函式和格式有關的知識,並且我們現在可以更好更快地編寫新特性了。現在,我們在作業系統上修改 GPIO 暫存器將變得非常簡單,而它就是用於控制硬體的!

在 課程 4:OK04[2] 中,我們將處理我們的 wait 函式,目前,它的時間控制還不精確,這樣我們就可以更好地控制我們的 LED 燈了,進而最終控制所有的 GPIO 針腳。

贊(0)

分享創造快樂