Linux中國

計算機實驗室之樹莓派:課程 1 OK01

OK01 課程講解了樹莓派如何入門,以及在樹莓派上如何啟用靠近 RCA 和 USB 埠的 OK 或 ACT 的 LED 指示燈。這個指示燈最初是為了指示 OK 狀態的,但它在第二版的樹莓派上被改名為 ACT。

1、入門

我們假設你已經訪問了下載頁面,並且已經獲得了必需的 GNU 工具鏈。也下載了一個稱為操作系統模板的文件。請下載這個文件並在一個新目錄中解開它。

2、開始

現在,你已經展開了這個模板文件,在 source 目錄中創建一個名為 main.s 的文件。這個文件包含了這個操作系統的代碼。具體來看,這個文件夾的結構應該像下面這樣:

build/
   (empty)
source/
   main.s
kernel.ld
LICENSE
Makefile

用文本編輯器打開 main.s 文件,這樣我們就可以輸入彙編代碼了。樹莓派使用了稱為 ARMv6 的彙編代碼變體,這就是我們即將要寫的彙編代碼類型。

擴展名為 .s 的文件一般是彙編代碼,需要記住的是,在這裡它是 ARMv6 的彙編代碼。

首先,我們複製下面的這些命令。

.section .init
.globl _start
_start:

實際上,上面這些指令並沒有在樹莓派上做任何事情,它們是提供給彙編器的指令。彙編器是一個轉換程序,它將我們能夠理解的彙編代碼轉換成樹莓派能夠理解的機器代碼。在彙編代碼中,每個行都是一個新的命令。上面的第一行告訴彙編器 1 在哪裡放我們的代碼。我們提供的模板中將它放到一個名為 .init 的節中的原因是,它是輸出的起始點。這很重要,因為我們希望確保我們能夠控制哪個代碼首先運行。如果不這樣做,首先運行的代碼將是按字母順序排在前面的代碼!.section 命令簡單地告訴彙編器,哪個節中放置代碼,從這個點開始,直到下一個 .section 或文件結束為止。

在彙編代碼中,你可以跳行、在命令前或後放置空格去提升可讀性。

接下來兩行是停止一個警告消息,它們並不重要。 2

3、第一行代碼

現在,我們正式開始寫代碼。計算機執行彙編代碼時,是簡單地一行一行按順序執行每個指令,除非明確告訴它不這樣做。每個指令都是開始於一個新行。

複製下列指令。

ldr r0,=0x20200000

ldr reg,=val 將數字 val 載入到名為 reg 的寄存器中。

那是我們的第一個命令。它告訴處理器將數字 0x20200000 保存到寄存器 r0 中。在這裡我需要去回答兩個問題, 寄存器 register 是什麼?0x20200000 是一個什麼樣的數字?

寄存器在處理器中就是一個極小的內存塊,它是處理器保存正在處理的數字的地方。處理器中有很多寄存器,很多都有專門的用途,我們在後面會一一接觸到它們。最重要的有十三個(命名為 r0r1r2、…、r9r10r11r12),它們被稱為通用寄存器,你可以使用它們做任何計算。由於是寫我們的第一行代碼,我們在示例中使用了 r0,當然你可以使用它們中的任何一個。只要後面始終如一就沒有問題。

樹莓派上的一個單獨的寄存器能夠保存任何介於 04,294,967,295(含)之間的任意整數,它可能看起來像一個很大的內存,實際上它僅有 32 個二進位比特。

0x20200000 確實是一個數字。只不過它是以十六進位表示的。下面的內容詳細解釋了十六進位的相關信息:

延伸閱讀:十六進位解釋

十六進位是另一種表示數字的方式。你或許只知道十進位的數字表示方法,十進位共有十個數字:0123456789。十六進位共有十六個數字:0123456789abcdef

你可能還記得十進位是如何用位制來表示的。即最右側的數字是個位,緊接著的左邊一位是十位,再接著的左邊一位是百位,依此類推。也就是說,它的值是 100 × 百位的數字,再加上 10 × 十位的數字,再加上 1 × 個位的數字。

567 is 5 hundreds, 6 tens and 7 units.

從數學的角度來看,我們可以發現規律,最右側的數字是 10 0 = 1s,緊接著的左邊一位是 10 1 = 10s,再接著是 10 2 = 100s,依此類推。我們設定在系統中,0 是最低位,緊接著是 1,依此類推。但如果我們使用一個不同於 10 的數字為冪底會是什麼樣呢?我們在系統中使用的十六進位就是這樣的一個數字。

567 is 5x10^2+6x10^1+7x10^0

567 = 5x10^2+6x10^1+7x10^0 = 2x16^2+3x16^1+7x16^0

上面的數學等式表明,十進位的數字 567 等於十六進位的數字 237。通常我們需要在系統中明確它們,我們使用下標 10 表示它是十進位數字,用下標 16 表示它是十六進位數字。由於在彙編代碼中寫上下標的小數字很困難,因此我們使用 0x 來表示它是一個十六進位的數字,因此 0x237 的意思就是 237 16

那麼,後面的 abcdef 又是什麼呢?好問題!在十六進位中為了能夠寫每個數字,我們就需要額外的東西。例如 9 16 = 9×16 0 = 9 10 ,但是 10 16 = 1×16 1 + 1×16 0 = 16 10 。因此,如果我們只使用 0、1、2、3、4、5、6、7、8 和 9,我們就無法寫出 10 10 、11 10 、12 10 、13 10 、14 10 、15 10 。因此我們引入了 6 個新的數字,這樣 a 16 = 10 10 、b 16 = 11 10 、c 16 = 12 10 、d 16 = 13 10 、e 16 = 14 10 、f 16 = 15 10

所以,我們就有了另一種寫數字的方式。但是我們為什麼要這麼麻煩呢?好問題!由於計算機總是工作在二進位中,事實證明,十六進位是非常有用的,因為每個十六進位數字正好是四個二進位數字的長度。這種方法還有另外一個好處,那就是許多計算機的數字都是十六進位的整數倍,而不是十進位的整數倍。比如,我在上面的彙編代碼中使用的一個數字 20200000 16 。如果我們用十進位來寫,它就是一個不太好記住的數字 538968064 10

我們可以用下面的簡單方法將十進位轉換成十六進位:

Conversion example

  1. 我們以十進位數字 567 為例來說明。
  2. 將十進位數字 567 除以 16 並計算其餘數。例如 567 ÷ 16 = 35 餘數為 7。
  3. 在十六進位中餘數就是答案中的最後一位數字,在我們的例子中它是 7。
  4. 重複第 2 步和第 3 步,直到除法結果的整數部分為 0。例如 35 ÷ 16 = 2 餘數為 3,因此 3 就是答案中的下一位。2 ÷ 16 = 0 餘數為 2,因此 2 就是答案的接下來一位。
  5. 一旦除法結果的整數部分為 0 就結束了。答案就是反序的餘數,因此 567 10 = 237 16

轉換十六進位數字為十進位,也很容易,將數字展開即可,因此 237 16 = 2×16 2 + 3×16 1 +7 ×16 0 = 2×256 + 3×16 + 7×1 = 512 + 48 + 7 = 567。

因此,我們所寫的第一個彙編命令是將數字 20200000 16 載入到寄存器 r0 中。那個命令看起來似乎沒有什麼用,但事實並非如此。在計算機中,有大量的內存塊和設備。為了能夠訪問它們,我們給每個內存塊和設備指定了一個地址。就像郵政地址或網站地址一樣,它用於標識我們想去訪問的內存塊或設備的位置。計算機中的地址就是一串數字,因此上面的數字 20200000 16 就是 GPIO 控制器的地址。這個地址是由製造商的設計所決定的,他們也可以使用其它地址(只要不與其它的衝突即可)。我之所以知道這個地址是 GPIO 控制器的地址是因為我看了它的手冊, 3 地址的使用沒有專門的規範(除了它們都是以十六進位表示的大數以外)。

4、啟用輸出

A diagram showing key parts of the GPIO controller.

閱讀了手冊可以得知,我們需要給 GPIO 控制器發送兩個消息。我們必須用它的語言告訴它,如果我們這樣做了,它將非常樂意實現我們的意圖,去打開 OK 的 LED 指示燈。幸運的是,它是一個非常簡單的晶元,為了讓它能夠理解我們要做什麼,只需要給它設定幾個數字即可。

mov r1,#1
lsl r1,#18
str r1,[r0,#4]

mov reg,#val 將數字 val 放到名為 reg 的寄存器中。

lsl reg,#val 將寄存器 reg 中的二進位操作數左移 val 位。

str reg,[dest,#val] 將寄存器 reg 中的數字保存到地址 dest + val 上。

這些命令的作用是在 GPIO 的第 16 號插針上啟用輸出。首先我們在寄存器 r1 中獲取一個必需的值,接著將這個值發送到 GPIO 控制器。因此,前兩個命令是嘗試取值到寄存器 r1 中,我們可以像前面一樣使用另一個命令 ldr 來實現,但 lsl 命令對我們後面能夠設置任何給定的 GPIO 針比較有用,因此從一個公式中推導出值要比直接寫入來好一些。表示 OK 的 LED 燈是直接連線到 GPIO 的第 16 號針腳上的,因此我們需要發送一個命令去啟用第 16 號針腳。

寄存器 r1 中的值是啟用 LED 針所需要的。第一行命令將數字 1 10 放到 r1 中。在這個操作中 mov 命令要比 ldr 命令快很多,因為它不需要與內存交互,而 ldr 命令是將需要的值從內存中載入到寄存器中。儘管如此,mov 命令僅能用於載入某些值。 4 在 ARM 彙編代碼中,基本上每個指令都使用一個三字母代碼表示。它們被稱為助記詞,用於表示操作的用途。mov 是 「move」 的簡寫,而 ldr 是 「load register」 的簡寫。mov 是將第二個參數 #1 移動到前面的 r1 寄存器中。一般情況下,# 肯定是表示一個數字,但我們已經看到了不符合這種情況的一個反例。

第二個指令是 lsl(邏輯左移)。它的意思是將第一個參數的二進位操作數向左移第二個參數所表示的位數。在這個案例中,將 1 10 (即 1 2 )向左移 18 位(將它變成 1000000000000000000 2=262144 10 )。

如果你不熟悉二進位表示法,可以看下面的內容:

延伸閱讀: 二進位解釋

與十六進位一樣,二進位是寫數字的另一種方法。在二進位中只有兩個數字,即 01。它在計算機中非常有用,因為我們可以用電路來實現它,即電流能夠通過電路表示為 1,而電流不能通過電路表示為 0。這就是計算機能夠完成真實工作和做數學運算的原理。儘管二進位只有兩個數字,但它卻能夠表示任何一個數字,只是寫起來有點長而已。

567 in decimal = 1000110111 in binary

這個圖片展示了 567 10 的二進位表示是 1000110111 2 。我們使用下標 2 來表示這個數字是用二進位寫的。

我們在彙編代碼中大量使用二進位的其中一個巧合之處是,數字可以很容易地被 2 的冪(即 124816)乘或除。通常乘法和除法都是非常難的,而在某些特殊情況下卻變得非常容易,所以二進位非常重要。

13*4 = 52, 1101*100=110100

將一個二進位數字左移 n 位就相當於將這個數字乘以 2 n。因此,如果我們想將一個數乘以 4,我們只需要將這個數字左移 2 位。如果我們想將它乘以 256,我們只需要將它左移 8 位。如果我們想將一個數乘以 12 這樣的數字,我們可以有一個替代做法,就是先將這個數乘以 8,然後再將那個數乘以 4,最後將兩次相乘的結果相加即可得到最終結果(N × 12 = N × (8 + 4) = N × 8 + N × 4)。

53/16 = 3, 110100/10000=11

右移一個二進位數 n 位就相當於這個數除以 2 n 。在右移操作中,除法的餘數位將被丟棄。不幸的是,如果對一個不能被 2 的冪次方除盡的二進位數字做除法是非常難的,這將在 課程 9 Screen04 中講到。

Binary Terminology

這個圖展示了二進位常用的術語。一個 比特 bit 就是一個單獨的二進位位。一個「 半位元組 nibble 「 是 4 個二進位位。一個 位元組 byte 是 2 個半位元組,也就是 8 個比特。 半字 half 是指一個字長度的一半,這裡是 2 個位元組。 word 是指處理器上寄存器的大小,因此,樹莓派的字長是 4 位元組。按慣例,將一個字最高有效位標識為 31,而將最低有效位標識為 0。頂部或最高位表示最高有效位,而底部或最低位表示最低有效位。一個 kilobyte(KB)就是 1000 位元組,一個 megabyte 就是 1000 KB。這樣表示會導致一些困惑,到底應該是 1000 還是 1024(二進位中的整數)。鑒於這種情況,新的國際標準規定,一個 KB 等於 1000 位元組,而一個 Kibibyte(KiB)是 1024 位元組。一個 Kb 是 1000 比特,而一個 Kib 是 1024 比特。

樹莓派默認採用小端法,也就是說,從你剛才寫的地址上載入一個位元組時,是從一個字的低位位元組開始載入的。

再強調一次,我們只有去閱讀手冊才能知道我們所需要的值。手冊上說,GPIO 控制器中有一個 24 位元組的集合,由它來決定 GPIO 針腳的設置。第一個 4 位元組與前 10 個 GPIO 針腳有關,第二個 4 位元組與接下來的 10 個針腳有關,依此類推。總共有 54 個 GPIO 針腳,因此,我們需要 6 個 4 位元組的一個集合,總共是 24 個位元組。在每個 4 位元組中,每 3 個比特與一個特定的 GPIO 針腳有關。我們想去啟用的是第 16 號 GPIO 針腳,因此我們需要去設置第二組 4 位元組,因為第二組的 4 位元組用於處理 GPIO 針腳的第 10-19 號,而我們需要第 6 組 3 比特,它在上面的代碼中的編號是 18(6×3)。

最後的 str(「store register」)命令去保存第一個參數中的值,將寄存器 r1 中的值保存到後面的表達式計算出來的地址上。這個表達式可以是一個寄存器,在上面的例子中是 r0,我們知道 r0 中保存了 GPIO 控制器的地址,而另一個值是加到它上面的,在這個例子中是 #4。它的意思是將 GPIO 控制器地址加上 4 得到一個新的地址,並將寄存器 r1 中的值寫到那個地址上。那個地址就是我們前面提到的第二組 4 位元組的位置,因此,我們發送我們的第一個消息到 GPIO 控制器上,告訴它準備啟用 GPIO 第 16 號針腳的輸出。

5、生命的信號

現在,LED 已經做好了打開準備,我們還需要實際去打開它。意味著需要給 GPIO 控制器發送一個消息去關閉 16 號針腳。是的,你沒有看錯,就是要發送一個關閉的消息。晶元製造商認為,在 GPIO 針腳關閉時打開 LED 更有意義。 5 硬體工程師經常做這種反常理的決策,似乎是為了讓操作系統開發者保持警覺。可以認為是給自己的一個警告。

mov r1,#1
lsl r1,#16
str r1,[r0,#40]

希望你能夠認識上面全部的命令,先不要管它的值。第一個命令和前面一樣,是將值 1 推入到寄存器 r1 中。第二個命令是將二進位的 1 左移 16 位。由於我們是希望關閉 GPIO 的 16 號針腳,我們需要在下一個消息中將第 16 比特設置為 1(想設置其它針腳只需要改變相應的比特位即可)。最後,我們寫這個值到 GPIO 控制器地址加上 40 10 的地址上,這將使那個針腳關閉(加上 28 將打開針腳)。

6、永遠幸福快樂

似乎我們現在就可以結束了,但不幸的是,處理器並不知道我們做了什麼。事實上,處理器只要通電,它就永不停止地運轉。因此,我們需要給它一個任務,讓它一直運轉下去,否則,樹莓派將進入休眠(本示例中不會,LED 燈會一直亮著)。

loop$:
b loop$

name: 下一行的名字。

b label 下一行將去標籤 label 處運行。

第一行不是一個命令,而是一個標籤。它給下一行命名為 loop$,這意味著我們能夠通過名字來指向到該行。這就稱為一個標籤。當代碼被轉換成二進位後,標籤將被丟棄,但這對我們通過名字而不是數字(地址)找到行比較有用。按慣例,我們使用一個 ​$ 表示這個標籤只對這個代碼塊中的代碼起作用,讓其它人知道,它不對整個程序起作用。b(「branch」)命令將去運行指定的標籤中的命令,而不是去運行它後面的下一個命令。因此,下一行將再次去運行這個 b 命令,這將導致永遠循環下去。因此處理器將進入一個無限循環中,直到它安全關閉為止。

代碼塊結尾的一個空行是有意這樣寫的。GNU 工具鏈要求所有的彙編代碼文件都是以空行結束的,因此,這就可以你確實是要結束了,並且文件沒有被截斷。如果你不這樣處理,在彙編器運行時,你將收到煩人的警告。

7、樹莓派上場

由於我們已經寫完了代碼,現在,我們可以將它上傳到樹莓派中了。在你的計算機上打開一個終端,改變當前工作目錄為 source 文件夾的父級目錄。輸入 make 然後回車。如果報錯,請參考排錯章節。如果沒有報錯,你將生成三個文件。 kernel.img 是你的編譯後的操作系統鏡像。kernel.list 是你寫的彙編代碼的一個清單,它實際上是生成的。這在將來檢查程序是否正確時非常有用。kernel.map 文件包含所有標籤結束位置的一個映射,這對於跟蹤值非常有用。

為安裝你的操作系統,需要先有一個已經安裝了樹莓派操作系統的 SD 卡。如果你瀏覽 SD 卡中的文件,你應該能看到一個名為 kernel.img 的文件。將這個文件重命名為其它名字,比如 kernel_linux.img。然後,複製你編譯的 kernel.img 文件到 SD 卡中原來的位置,這將用你的操作系統鏡像文件替換現在的樹莓派操作系統鏡像。想切換回來時,只需要簡單地刪除你自己的 kernel.img 文件,然後將前面重命名的文件改回 kernel.img 即可。我發現,保留一個原始的樹莓派操作系統的備份是非常有用的,萬一你要用到它呢。

將這個 SD 卡插入到樹莓派,並打開它的電源。這個 OK 的 LED 燈將亮起來。如果不是這樣,請查看故障排除頁面。如果一切如願,恭喜你,你已經寫出了你的第一個操作系統。課程 2 OK02 將指導你讓 LED 燈閃爍和關閉閃爍。

  1. 是的,我說錯了,它告訴的是鏈接器,它是另一個程序,用於將彙編器轉換過的幾個代碼文件鏈接到一起。直接說是彙編器也沒有大問題。
  2. 其實它們對你很重要。由於 GNU 工具鏈主要用於開發操作系統,它要求入口點必須是名為 _start 的地方。由於我們是開發一個操作系統,無論什麼時候,它總是從 _start 開時的,而我們可以使用 .section .init 命令去設置它。因此,如果我們沒有告訴它入口點在哪裡,就會使工具鏈困惑而產生警告消息。所以,我們先定義一個名為 _start 的符號,它是所有人可見的(全局的),緊接著在下一行生成符號 _start 的地址。我們很快就講到這個地址了。
  3. 本教程的設計減少了你閱讀樹莓派開發手冊的難度,但是,如果你必須要閱讀它,你可以在這裡 SoC-Peripherals.pdf 找到它。由於添加了混淆,手冊中 GPIO 使用了不同的地址系統。我們的操作系統中的地址 0x20200000 對應到手冊中是 0x7E200000。
  4. mov 能夠載入的值只有前 8 位是 1 的二進位表示的值。換句話說就是一個 0 後面緊跟著 8 個 10
  5. 一個很友好的硬體工程師是這樣向我解釋這個問題的:

原因是現在的晶元都是用一種稱為 CMOS 的技術來製成的,它是互補金屬氧化物半導體的簡稱。互補的意思是每個信號都連接到兩個晶體管上,一個是使用 N 型半導體的材料製成,它用於將電壓拉低,而另一個使用 P 型半導體材料製成,它用於將電壓升高。在任何時刻,僅有一個半導體是打開的,否則將會短路。P 型材料的導電性能不如 N 型材料。這意味著三倍大的 P 型半導體材料才能提供與 N 型半導體材料相同的電流。這就是為什麼 LED 總是通過降低為低電壓來打開它,因為 N 型半導體拉低電壓比 P 型半導體拉高電壓的性能更強。

還有一個原因。早在上世紀七十年代,晶元完全是由 N 型材料製成的(NMOS),P 型材料部分使用了一個電阻來代替。這意味著當信號為低電壓時,即便它什麼事都沒有做,晶元仍然在消耗能量(並發熱)。你的電話裝在口袋裡什麼事都不做,它仍然會發熱並消耗你的電池電量,這不是好的設計。因此,信號設計成 「活動時低」,而不活動時為高電壓,這樣就不會消耗能源了。雖然我們現在已經不使用 NMOS 了,但由於 N 型材料的低電壓信號比 P 型材料的高電壓信號要快,所以仍然使用了這種設計。通常在一個 「活動時低」 信號名字上方會有一個條型標記,或者寫作 SIGNAL_n/SIGNAL。但是即便這樣,仍然很讓人困惑,那怕是硬體工程師,也不可避免這種困惑!

via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok01.html

作者:Robert Mullins 選題:lujun9972 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國