計算機實驗室之樹莓派:課程 3 OK03
OK03 課程基於 OK02 課程來構建,它教你在彙編中如何使用函數讓代碼可復用和可讀性更好。假設你已經有了 課程 2:OK02 的操作系統,我們將以它為基礎。
1、可復用的代碼
到目前為止,我們所寫的代碼都是以我們希望發生的事為順序來輸入的。對於非常小的程序來說,這種做法很好,但是如果我們以這種方式去寫一個完整的系統,所寫的代碼可讀性將非常差。我們應該去使用函數。
一個函數是一段可復用的代碼片斷,可以用於去計算某些答案,或執行某些動作。你也可以稱它們為 過程 、 常式 或 子常式 。雖然它們都是不同的,但人們幾乎都沒有正確地使用這個術語。
你應該在數學上遇到了函數的概念。例如,餘弦函數應用於一個給定的數時,會得到介於 -1 到 1 之間的另一個數,這個數就是角的餘弦。一般我們寫成
cos(x)
來表示應用到一個值x
上的餘弦函數。在代碼中,函數可以有多個輸入(也可以沒有輸入),然後函數給出多個輸出(也可以沒有輸出),並可能導致副作用。例如一個函數可以在一個文件系統上創建一個文件,第一個輸入是它的名字,第二個輸入是文件的長度。
函數可以認為是一個「黑匣子」。我們給它輸入,然後它給我們輸出,而我們不需要知道它是如何工作的。
在像 C 或 C++ 這樣的高級代碼中,函數是語言的組成部分。在彙編代碼中,函數只是我們的創意。
理想情況下,我們希望能夠在我們的寄存器中設置一些輸入值,然後分支切換到某個地址,然後預期在某個時刻分支返回到我們代碼,並通過代碼來設置輸出值到寄存器。這就是我們所設想的彙編代碼中的函數。困難之處在於我們用什麼樣的方式去設置寄存器。如果我們只是使用平時所接觸到的某種方法去設置寄存器,每個程序員可能使用不同的方法,這樣你將會發現你很難理解其他程序員所寫的代碼。另外,編譯器也不能像使用彙編代碼那樣輕鬆地工作,因為它們壓根不知道如何去使用函數。為避免這種困惑,為每個彙編語言設計了一個稱為 應用程序二進位介面 (ABI)的標準,由它來規範函數如何去運行。如果每個人都使用相同的方法去寫函數,這樣每個人都可以去使用其他人寫的函數。在這裡,我將教你們這個標準,而從現在開始,我所寫的函數將全部遵循這個標準。
該標準規定,寄存器 r0
、r1
、r2
和 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
必須在函數完成之後值必須保持相同,因此它們需要被保存到某個地方。我們將它們保存到稱為棧的地方。
一個 棧 就是我們在計算中用來保存值的一個很形象的方法。就像是摞起來的一堆盤子,你可以從上到下來移除它們,而添加它們時,你只能從下到上來添加。
在函數運行時,使用棧來保存寄存器值是個非常好的創意。例如,如果我有一個函數需要去使用寄存器
r4
和r5
,它將在一個棧上存放這些寄存器的值。最後用這種方式,它可以再次將它拿回來。更高明的是,如果為了運行完我的函數,需要去運行另一個函數,並且那個函數需要保存一些寄存器,在那個函數運行時,它將把寄存器保存在棧頂上,然後在結束後再將它們拿走。而這並不會影響我保存在寄存器r4
和r5
中的值,因為它們是在棧頂上添加的,拿走時也是從棧頂上取出的。用來表示使用特定的方法將值放到棧上的專用術語,我們稱之為那個方法的「 棧幀 」。不是每種方法都使用一個棧幀,有些是不需要存儲值的。
因為棧非常有用,它被直接實現在 ARMv6 的指令集中。一個名為 sp
(棧指針)的專用寄存器用來保存棧的地址。當需要有值添加到棧上時,sp
寄存器被更新,這樣就總是保證它保存的是棧上第一個值的地址。push {r4,r5}
將推送 r4
和 r5
中的值到棧頂上,而 pop {r4,r5}
將(以正確的次序)取回它們。
2、我們的第一個函數
現在,關於函數的原理我們已經有了一些概念,我們嘗試來寫一個函數。由於是我們的第一個很基礎的例子,我們寫一個沒有輸入的函數,它將輸出 GPIO 的地址。在上一節課程中,我們就是寫到這個值上,但將它寫成函數更好,因為我們在真實的操作系統中經常需要用到它,而我們不可能總是能夠記住這個地址。
複製下列代碼到一個名為 gpio.s
的新文件中。就像在 source
目錄中使用的 main.s
一樣。我們將把與 GPIO 控制器相關的所有函數放到一個文件中,這樣更好查找。
.globl GetGpioAddress
GetGpioAddress:
ldr r0,=0x20200000
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
函數中。
.globl SetGpioFunction
SetGpioFunction:
cmp r0,#53
cmpls r1,#7
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
(大於)也一樣有一些細微差別,我們在後面將會講到。
將這些命令複製到上面的代碼的下面位置。
push {lr}
mov r2,r0
bl GetGpioAddress
push {reg1,reg2,...}
複製列出的寄存器reg1
、reg2
、… 到棧頂。該命令僅能用於通用寄存器和lr
寄存器。
bl lbl
設置lr
為下一個指令的地址並切換到標籤lbl
。
這三個命令用於調用我們第一個方法。push {lr}
命令複製 lr
中的值到棧頂,這樣我們在後面可以獲取到它。當我們調用 GetGpioAddress
時必須要這樣做,我們將需要使用 lr
去保存我們函數要返回的地址。
如果我們對 GetGpioAddress
函數一無所知,我們必須假設它改變了 r0
、r1
、r2
和 r3
的值 ,並移動我們的值到 r4
和 r5
中,以在函數完成之後保持它們的值一樣。幸運的是,我們知道 GetGpioAddress
做了什麼,並且我們也知道它僅改變了 r0
為 GPIO 地址,它並沒有影響 r1
、r2
或 r3
的值。因此,我們僅去將 GPIO 針腳號從 r0
中移出,這樣它就不會被覆蓋掉,但我們知道,可以將它安全地移到 r2
中,因為 GetGpioAddress
並不去改變 r2
。
最後我們使用 bl
指令去運行 GetGpioAddress
。通常,運行一個函數,我們使用一個術語叫「調用」,從現在開始我們將一直使用這個術語。正如我們前面討論過的,bl
調用一個函數是通過更新 lr
為下一個指令的地址並切換到該函數完成的。
當一個函數結束時,我們稱為「返回」。當一個 GetGpioAddress
調用返回時,我們已經知道了 r0
中包含了 GPIO 的地址,r1
中包含了函數編號,而 r2
中包含了 GPIO 針腳號。
我前面說過,GPIO 函數每 10 個保存在一個塊中,因此首先我們需要去判斷我們的針腳在哪個塊中。這似乎聽起來像是要使用一個除法,但是除法做起來非常慢,因此對於這些比較小的數來說,不停地做減法要比除法更好。
將下面的代碼複製到上面的代碼中最下面的位置。
functionLoop$:
cmp r2,#9
subhi r2,#10
addhi r0,#4
bhi 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 #1
lsl r1,r2
str r1,[r0]
pop {pc}
移位參數
reg,lsl #val
表示將寄存器reg
中二進位表示的數邏輯左移val
位之後的結果作為與前面運算的操作數。
lsl reg,amt
將寄存器reg
中的二進位數邏輯左移amt
中的位數。
str reg,[dst]
與str reg,[dst,#0]
相同。
pop {reg1,reg2,...}
從棧頂複製值到寄存器列表reg1
、reg2
、… 僅有通用寄存器與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
文件的結尾部分。
.globl SetGpio
SetGpio:
pinNum .req r0
pinVal .req r1
alias .req reg
設置寄存器reg
的別名為alias
。
我們再次需要 .globl
命令,標記它為其它文件可訪問的全局函數。這次我們將使用寄存器別名。寄存器別名允許我們為寄存器使用名字而不僅是 r0
或 r1
。到目前為止,寄存器別名還不是很重要,但隨著我們後面寫的方法越來越大,它將被證明非常有用,現在開始我們將嘗試使用別名。當在指令中使用到 pinNum .req r0
時,它的意思是 pinNum
表示 r0
。
將下面的代碼複製粘貼到上述的代碼下面位置。
cmp pinNum,#53
movhi pc,lr
push {lr}
mov r2,pinNum
.unreq pinNum
pinNum .req r2
bl GetGpioAddress
gpioAddr .req r0
.unreq alias
刪除別名alias
。
就像在函數 SetGpio
中所做的第一件事情是檢查給定的針腳號是否有效一樣。我們需要同樣的方式去將 pinNum
(r0
)與 53 進行比較,如果它大於 53 將立即返回。一旦我們想要再次調用 GetGpioAddress
,我們就需要將 lr
推送到棧上來保護它,將 pinNum
移動到 r2
中。然後我們使用 .unreq
語句來刪除我們給 r0
定義的別名。因為針腳號現在保存在寄存器 r2
中,我們希望別名能夠反映這個變化,因此我們從 r0
移走別名,重新定義到 r2
。你應該每次在別名使用結束後,立即刪除它,這樣當它不再存在時,你就不會在後面的代碼中因它而產生錯誤。
然後,我們調用了 GetGpioAddress
,並且我們創建了一個指向 r0
的別名以反映此變化。
將下面的代碼複製粘貼到上述代碼的後面位置。
pinBank .req r3
lsr pinBank,pinNum,#5a
lsl pinBank,#2
add gpioAddr,pinBank
.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
的結果有可能是 20200000 16(如果針腳號介於 0 到 31 之間),也有可能是 20200004 16(如果針腳號介於 32 到 53 之間)。這意味著如果加上 28 10,我們將得到打開針腳的地址,而如果加上 40 10 ,我們將得到關閉針腳的地址。由於我們用完了 pinBank
,所以在它之後立即使用 .unreq
去刪除它。
將下面的代碼複製粘貼到上述代碼的下面位置。
and pinNum,#31
setBit .req r3
mov setBit,#1
lsl 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 31 10 = 11111 2」。這意味著答案的後 5 位中只有 1,因此它肯定是在 0 到 31 之間。尤其是在 pinNum
的後 5 位的位置是 1 的地方它只有 1。這就如同被 32 整除的餘數部分。就像 31 = 32 - 1 並不是巧合。
代碼的其餘部分使用這個值去左移 1 位。這就有了創建我們所需要的二進位數的效果。
將下面的代碼複製粘貼到上述代碼的下面位置。
teq pinVal,#0
.unreq pinVal
streq setBit,[gpioAddr,#40]
strne setBit,[gpioAddr,#28]
.unreq setBit
.unreq gpioAddr
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:
的後面:
b main
.section .text
main:
mov sp,#0x8000
在這裡重要的改變是引入了 .text
節。我設計了 makefile
和鏈接器腳本,它將 .text
節(它是默認節)中的代碼放在地址為 8000 16 的 .init
節之後。這是默認載入地址,並且它給我們提供了一些空間去保存棧。由於棧存在於內存中,它也有一個地址。棧向下增長內存,因此每個新值都低於前一個地址,所以,這使得棧頂是最低的一個地址。
圖中的 「ATAGs」 節的位置保存了有關樹莓派的信息,比如它有多少內存,默認屏幕解析度是多少。
用下面的代碼替換掉所有設置 GPIO 函數針腳的代碼:
pinNum .req r0
pinFunc .req r1
mov pinNum,#16
mov pinFunc,#1
bl SetGpioFunction
.unreq pinNum
.unreq pinFunc
這個代碼將使用針腳號 16 和函數編號 1 去調用 SetGpioFunction
。它的效果就是啟用了 OK LED 燈的輸出。
用下面的代碼去替換打開 OK LED 燈的代碼:
pinNum .req r0
pinVal .req r1
mov pinNum,#16
mov pinVal,#0
bl SetGpio
.unreq pinNum
.unreq pinVal
這個代碼使用 SetGpio
去關閉 GPIO 第 16 號針腳,因此將打開 OK LED。如果我們(將第 4 行)替換成 mov pinVal,#1
它將關閉 LED 燈。用以上的代碼去替換掉你關閉 LED 燈的舊代碼。
6、繼續向目標前進
但願你能夠順利地在你的樹莓派上測試我們所做的這一切。到目前為止,我們已經寫了一大段代碼,因此不可避免會出現錯誤。如果有錯誤,可以去查看我們的排錯頁面。
如果你的代碼已經正常工作,恭喜你。雖然我們的操作系統除了做 課程 2:OK02 中的事情,還做不了別的任何事情,但我們已經學會了函數和格式有關的知識,並且我們現在可以更好更快地編寫新特性了。現在,我們在操作系統上修改 GPIO 寄存器將變得非常簡單,而它就是用於控制硬體的!
在 課程 4:OK04 中,我們將處理我們的 wait
函數,目前,它的時間控制還不精確,這樣我們就可以更好地控制我們的 LED 燈了,進而最終控制所有的 GPIO 針腳。
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok03.html
作者:Robert Mullins 選題:lujun9972 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive