計算機實驗室之樹莓派:課程 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
的結果有可能是 2020000016(如果針腳號介於 0 到 31 之間),也有可能是 2020000416(如果針腳號介於 32 到 53 之間)。這意味著如果加上 2810,我們將得到開啟針腳的地址,而如果加上 4010 ,我們將得到關閉針腳的地址。由於我們用完了 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 3110 = 111112”。這意味著答案的後 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
節(它是預設節)中的程式碼放在地址為 800016 的 .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
相關文章
- 計算機實驗室之樹莓派:課程 11 輸入02計算機樹莓派
- 計算機實驗室之樹莓派:課程 8 螢幕03計算機樹莓派
- 計算機實驗室之樹莓派:課程 9 螢幕04計算機樹莓派
- 計算機實驗室之樹莓派:課程 7 螢幕02計算機樹莓派
- 計算機實驗室之樹莓派:課程 4 OK04計算機樹莓派
- 計算機實驗室之樹莓派:課程 5 OK05計算機樹莓派
- 計算機實驗室之樹莓派:課程 1 OK01計算機樹莓派
- 計算機實驗室之樹莓派:課程 2 OK02計算機樹莓派
- 計算機實驗室之樹莓派計算機樹莓派
- 5、樹莓派3 Model B ——— 樹莓派PWM控制直流電機速度樹莓派
- 樹莓派桌面體驗樹莓派
- 《計算機網路實驗課程》——Wireshark實驗計算機網路
- 第七天 樹莓派+計算棒配置過程樹莓派
- 用樹莓派計算模組搭建的工業單板計算機樹莓派計算機
- 「玩轉樹莓派」樹莓派 3B+ 配置無線WiFi樹莓派WiFi
- 樹莓派:開機使用樹莓派
- 樹莓派 - 實戰篇 [基於 websocket 實現手機遠端控制樹莓派小車]樹莓派Web
- 樹莓派 | 修改樹莓派3熱點的名稱和密碼樹莓派密碼
- 樹莓派 3 新手指南樹莓派
- 樹莓派基金會:截止2015年2月樹莓派微型計算機銷量超500萬臺樹莓派計算機
- 樹莓派使用入門:如何更新樹莓派樹莓派
- 樹莓派搭建下載機樹莓派
- 樹莓派是什麼 樹莓派能做什麼 樹莓派的功能用途樹莓派
- 樹莓派自建 NAS 雲盤之——樹莓派搭建網路儲存盤樹莓派
- 樹莓派GUI程式設計樹莓派GUI程式設計
- 樹莓派實戰:微信機器人(itchat實現)樹莓派機器人
- 樹莓派使用樹莓派
- 樹莓派 Zero WH 初使用體驗樹莓派
- 樹莓派之安裝webserver(LEMP)樹莓派WebServer
- 【.NET 與樹莓派】控制舵機樹莓派
- 從零做樹莓派挖掘機樹莓派
- 計算機課程程式設計經驗分享計算機程式設計
- 樹莓派使用入門:用樹莓派學 Linux樹莓派Linux
- 樹莓派的GPIO程式設計樹莓派程式設計
- 基於樹莓派的叢集實驗(一)--spark on yarn樹莓派SparkYarn
- 樹莓派使用入門:如何用樹莓派來娛樂樹莓派
- 樹莓派使用入門:如何購買一個樹莓派樹莓派
- 樹莓派使用入門:慶祝樹莓派的 14 天樹莓派