計算機實驗室之樹莓派:課程 3 OK03

2019-02-08 22:59:00

OK03 課程基於 OK02 課程來構建,它教你在組合中如何使用函數讓程式碼可複用和可讀性更好。假設你已經有了 的作業系統,我們將以它為基礎。

1、可複用的程式碼

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

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

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

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

Function as black boxes

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

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

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

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

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

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

表 1.1 ARM ABI 暫存器用法

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

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

Stack diagram

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

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

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

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

2、我們的第一個函數

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

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

.globl GetGpioAddressGetGpioAddress:ldr r0,=0x20200000mov 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 函數中。

.globl SetGpioFunctionSetGpioFunction:cmp r0,#53cmpls r1,#7movhi 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(大於)也一樣有一些細微差別,我們在後面將會講到。

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

push {lr}mov r2,r0bl GetGpioAddress

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

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

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

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

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

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

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

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

functionLoop$:cmp r2,#9subhi r2,#10addhi r0,#4bhi functionLoop$

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

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

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

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

add r2, r2,lsl #1lsl r1,r2str r1,[r0]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,r2add r2,r3add r2,r3 這樣的程式碼。

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

然後,我們將函數計算後的值儲存到 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 檔案的結尾部分。

.globl SetGpioSetGpio:pinNum .req r0pinVal .req r1

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

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

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

cmp pinNum,#53movhi pc,lrpush {lr}mov r2,pinNum.unreq pinNumpinNum .req r2bl GetGpioAddressgpioAddr .req r0

.unreq alias 刪除別名 alias

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

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

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

pinBank .req r3lsr pinBank,pinNum,#5alsl pinBank,#2add gpioAddr,pinBank.unreq pinBank

lsr dst,src,#valsrc 中二進位制表示的數右移 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 去刪除它。

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

and pinNum,#31setBit .req r3mov setBit,#1lsl setBit,pinNum.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 位。這就有了建立我們所需要的二進位制數的效果。

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

teq pinVal,#0.unreq pinValstreq setBit,[gpioAddr,#40]strne setBit,[gpioAddr,#28].unreq setBit.unreq gpioAddrpop {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: 的後面:

b main.section .textmain:mov sp,#0x8000

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

Layout diagram of operating system

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

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

pinNum .req r0pinFunc .req r1mov pinNum,#16mov pinFunc,#1bl SetGpioFunction.unreq pinNum.unreq pinFunc

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

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

pinNum .req r0pinVal .req r1mov pinNum,#16mov pinVal,#0bl SetGpio.unreq pinNum.unreq pinVal

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

6、繼續向目標前進

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

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

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