計算機實驗室之樹莓派:課程 9 屏幕04
屏幕04 課程基於屏幕03 課程來構建,它教你如何操作文本。假設你已經有了課程 8:屏幕03 的操作系統代碼,我們將以它為基礎。
1、操作字元串
能夠繪製文本是極好的,但不幸的是,現在你只能繪製預先準備好的字元串。如果能夠像命令行那樣顯示任何東西才是完美的,而理想情況下應該是,我們能夠顯示任何我們期望的東西。一如既往地,如果我們付出努力而寫出一個非常好的函數,它能夠操作我們所希望的所有字元串,而作為回報,這將使我們以後寫代碼更容易。曾經如此複雜的函數,在 C 語言編程中只不過是一個 sprintf
而已。這個函數基於給定的另一個字元串和作為描述的額外的一個參數而生成一個字元串。我們對這個函數感興趣的地方是,這個函數是個變長函數。這意味著它可以帶可變數量的參數。參數的數量取決於具體的格式字元串,因此它的參數的數量不能預先確定。
變長函數在彙編代碼中看起來似乎不好理解,然而 ,它卻是非常有用和很強大的概念。
這個完整的函數有許多選項,而我們在這裡只列出了幾個。在本教程中將要實現的選項我做了高亮處理,當然,你可以嘗試去實現更多的選項。
函數通過讀取格式化字元串來工作,然後使用下表的意思去解釋它。一旦一個參數已經使用了,就不會再次考慮它了。函數的返回值是寫入的字元數。如果方法失敗,將返回一個負數。
表 1.1 sprintf 格式化規則
選項 | 含義 |
---|---|
除了 % 之外的任何支付 |
複製字元到輸出。 |
%% |
寫一個 % 字元到輸出。 |
%c |
將下一個參數寫成字元格式。 |
%d 或 %i |
將下一個參數寫成十進位的有符號整數。 |
%e |
將下一個參數寫成科學記數法,使用 eN,意思是 ×10 N。 |
%E |
將下一個參數寫成科學記數法,使用 EN,意思是 ×10 N。 |
%f |
將下一個參數寫成十進位的 IEEE 754 浮點數。 |
%g |
與 %e 和 %f 的指數表示形式相同。 |
%G |
與 %E 和 %f 的指數表示形式相同。 |
%o |
將下一個參數寫成八進位的無符號整數。 |
%s |
下一個參數如果是一個指針,將它寫成空終止符字元串。 |
%u |
將下一個參數寫成十進位無符號整數。 |
%x |
將下一個參數寫成十六進位無符號整數(使用小寫的 a、b、c、d、e 和 f)。 |
%X |
將下一個參數寫成十六進位的無符號整數(使用大寫的 A、B、C、D、E 和 F)。 |
%p |
將下一個參數寫成指針地址。 |
%n |
什麼也不輸出。而是複製到目前為止被下一個參數在本地處理的字元個數。 |
除此之外,對序列還有許多額外的處理,比如指定最小長度,符號等等。更多信息可以在 sprintf - C++ 參考 上找到。
下面是調用方法和返回的結果的示例。
表 1.2 sprintf 調用示例
格式化字元串 | 參數 | 結果 |
---|---|---|
"%d" |
13 | 13 |
"+%d degrees" |
12 | +12 degrees |
"+%x degrees" |
24 | +1c degrees |
"'%c' = 0%o" |
65, 65 | 『A』 = 0101 |
"%d * %d%% = %d" |
200, 40, 80 | 200 * 40% = 80 |
"+%d degrees" |
-5 | +-5 degrees |
"+%u degrees" |
-5 | +4294967291 degrees |
希望你已經看到了這個函數是多麼有用。實現它需要大量的編程工作,但給我們的回報卻是一個非常有用的函數,可以用於各種用途。
2、除法
雖然這個函數看起來很強大、也很複雜。但是,處理它的許多情況的最容易的方式可能是,編寫一個函數去處理一些非常常見的任務。它是個非常有用的函數,可以為任何底的一個有符號或無符號的數字生成一個字元串。那麼,我們如何去實現呢?在繼續閱讀之前,嘗試快速地設計一個演算法。
除法是非常慢的,也是非常複雜的基礎數學運算。它在 ARM 彙編代碼中不能直接實現,因為如果直接實現的話,它得出答案需要花費很長的時間,因此它不是個「簡單的」運算。
最簡單的方法或許就是我在 課程 1:OK01 中提到的「除法餘數法」。它的思路如下:
- 用當前值除以你使用的底。
- 保存餘數。
- 如果得到的新值不為 0,轉到第 1 步。
- 將餘數反序連起來就是答案。
例如:
表 2.1 以 2 為底的例子
轉換
值 | 新值 | 餘數 |
---|---|---|
137 | 68 | 1 |
68 | 34 | 0 |
34 | 17 | 0 |
17 | 8 | 1 |
8 | 4 | 0 |
4 | 2 | 0 |
2 | 1 | 0 |
1 | 0 | 1 |
因此答案是 10001001 2
這個過程的不幸之外在於使用了除法。所以,我們必須首先要考慮二進位中的除法。
我們複習一下長除法
假如我們想把 4135 除以 17。
0243 r 4 17)4135 0 0 × 17 = 0000 4135 4135 - 0 = 4135 34 200 × 17 = 3400 735 4135 - 3400 = 735 68 40 × 17 = 680 55 735 - 680 = 55 51 3 × 17 = 51 4 55 - 51 = 4
答案:243 余 4
首先我們來看被除數的最高位。我們看到它是小於或等於除數的最小倍數,因此它是 0。我們在結果中寫一個 0。
接下來我們看被除數倒數第二位和所有的高位。我們看到小於或等於那個數的除數的最小倍數是 34。我們在結果中寫一個 2,和減去 3400。
接下來我們看被除數的第三位和所有高位。我們看到小於或等於那個數的除數的最小倍數是 68。我們在結果中寫一個 4,和減去 680。
最後,我們看一下所有的余位。我們看到小於餘數的除數的最小倍數是 51。我們在結果中寫一個 3,減去 51。減法的結果就是我們的餘數。
在彙編代碼中做除法,我們將實現二進位的長除法。我們之所以實現它是因為,數字都是以二進位方式保存的,這讓我們很容易地訪問所有重要位的移位操作,並且因為在二進位中做除法比在其它高進位中做除法都要簡單,因為它的數更少。
1011 r 1
1010)1101111
1010
11111
1010
1011
1010
1
這個示例展示了如何做二進位的長除法。簡單來說就是,在不超出被除數的情況下,儘可能將除數右移,根據位置輸出一個 1,和減去這個數。剩下的就是餘數。在這個例子中,我們展示了 1101111 2 ÷ 1010 2 = 1011 2 餘數為 1 2。用十進位表示就是,111 ÷ 10 = 11 余 1。
你自己嘗試去實現這個長除法。你應該去寫一個函數 DivideU32
,其中 r0
是被除數,而 r1
是除數,在 r0
中返回結果,在 r1
中返回餘數。下面,我們將完成一個有效的實現。
function DivideU32(r0 is dividend, r1 is divisor)
set shift to 31
set result to 0
while shift ≥ 0
if dividend ≥ (divisor << shift) then
set dividend to dividend - (divisor << shift)
set result to result + 1
end if
set result to result << 1
set shift to shift - 1
loop
return (result, dividend)
end function
這段代碼實現了我們的目標,但卻不能用於彙編代碼。我們出現的問題是,我們的寄存器只能保存 32 位,而 divisor << shift
的結果可能在一個寄存器中裝不下(我們稱之為溢出)。這確實是個問題。你的解決方案是否有溢出的問題呢?
幸運的是,有一個稱為 clz
( 計數前導零 )的指令,它能計算一個二進位表示的數字的前導零的個數。這樣我們就可以在溢出發生之前,可以將寄存器中的值進行相應位數的左移。你可以找出的另一個優化就是,每個循環我們計算 divisor << shift
了兩遍。我們可以通過將除數移到開始位置來改進它,然後在每個循環結束的時候將它移下去,這樣可以避免將它移到別處。
我們來看一下進一步優化之後的彙編代碼。
.globl DivideU32
DivideU32:
result .req r0
remainder .req r1
shift .req r2
current .req r3
clz shift,r1
lsl current,r1,shift
mov remainder,r0
mov result,#0
divideU32Loop$:
cmp shift,#0
blt divideU32Return$
cmp remainder,current
addge result,result,#1
subge remainder,current
sub shift,#1
lsr current,#1
lsl result,#1
b divideU32Loop$
divideU32Return$:
.unreq current
mov pc,lr
.unreq result
.unreq remainder
.unreq shift
你可能毫無疑問的認為這是個非常高效的作法。它是很好,但是除法是個代價非常高的操作,並且我們的其中一個願望就是不要經常做除法,因為如果能以任何方式提升速度就是件非常好的事情。當我們查看有循環的優化代碼時,我們總是重點考慮一個問題,這個循環會運行多少次。在本案例中,在輸入為 1 的情況下,這個循環最多運行 31 次。在不考慮特殊情況的時候,這很容易改進。例如,當 1 除以 1 時,不需要移位,我們將把除數移到它上面的每個位置。這可以通過簡單地在被除數上使用新的 clz
命令並從中減去它來改進。在 1 ÷ 1
的案例中,這意味著移位將設置為 0,明確地表示它不需要移位。如果它設置移位為負數,表示除數大於被除數,因此我們就可以知道結果是 0,而餘數是被除數。我們可以做的另一個快速檢查就是,如果當前值為 0,那麼它是一個整除的除法,我們就可以停止循環了。
clz dest,src
將第一個寄存器dest
中二進位表示的值的前導零的數量,保存到第二個寄存器src
中。
.globl DivideU32
DivideU32:
result .req r0
remainder .req r1
shift .req r2
current .req r3
clz shift,r1
clz r3,r0
subs shift,r3
lsl current,r1,shift
mov remainder,r0
mov result,#0
blt divideU32Return$
divideU32Loop$:
cmp remainder,current
blt divideU32LoopContinue$
add result,result,#1
subs remainder,current
lsleq result,shift
beq divideU32Return$
divideU32LoopContinue$:
subs shift,#1
lsrge current,#1
lslge result,#1
bge divideU32Loop$
divideU32Return$:
.unreq current
mov pc,lr
.unreq result
.unreq remainder
.unreq shift
複製上面的代碼到一個名為 maths.s
的文件中。
3、數字字元串
現在,我們已經可以做除法了,我們來看一下另外的一個將數字轉換為字元串的實現。下列的偽代碼將寄存器中的一個數字轉換成以 36 為底的字元串。根據慣例,a % b 表示 a 被 b 相除之後的餘數。
function SignedString(r0 is value, r1 is dest, r2 is base)
if value ≥ 0
then return UnsignedString(value, dest, base)
otherwise
if dest > 0 then
setByte(dest, '-')
set dest to dest + 1
end if
return UnsignedString(-value, dest, base) + 1
end if
end function
function UnsignedString(r0 is value, r1 is dest, r2 is base)
set length to 0
do
set (value, rem) to DivideU32(value, base)
if rem > 10
then set rem to rem + '0'
otherwise set rem to rem - 10 + 'a'
if dest > 0
then setByte(dest + length, rem)
set length to length + 1
while value > 0
if dest > 0
then ReverseString(dest, length)
return length
end function
function ReverseString(r0 is string, r1 is length)
set end to string + length - 1
while end > start
set temp1 to readByte(start)
set temp2 to readByte(end)
setByte(start, temp2)
setByte(end, temp1)
set start to start + 1
set end to end - 1
end while
end function
上述代碼實現在一個名為 text.s
的彙編文件中。記住,如果你遇到了困難,可以在下載頁面找到完整的解決方案。
4、格式化字元串
我們繼續回到我們的字元串格式化方法。因為我們正在編寫我們自己的操作系統,我們根據我們自己的意願來添加或修改格式化規則。我們可以發現,添加一個 a % b
操作去輸出一個二進位的數字比較有用,而如果你不使用空終止符字元串,那麼你應該去修改 %s
的行為,讓它從另一個參數中得到字元串的長度,或者如果你願意,可以從長度前綴中獲取。我在下面的示例中使用了一個空終止符。
實現這個函數的一個主要的障礙是它的參數個數是可變的。根據 ABI 規定,額外的參數在調用方法之前以相反的順序先推送到棧上。比如,我們使用 8 個參數 1、2、3、4、5、6、7 和 8 來調用我們的方法,我們將按下面的順序來處理:
- 設置 r0 = 5、r1 = 6、r2 = 7、r3 = 8
- 推入 {r0,r1,r2,r3}
- 設置 r0 = 1、r1 = 2、r2 = 3、r3 = 4
- 調用函數
- 將 sp 和 #4*4 加起來
現在,我們必須確定我們的函數確切需要的參數。在我的案例中,我將寄存器 r0
用來保存格式化字元串地址,格式化字元串長度則放在寄存器 r1
中,目標字元串地址放在寄存器 r2
中,緊接著是要求的參數列表,從寄存器 r3
開始和像上面描述的那樣在棧上繼續。如果你想去使用一個空終止符格式化字元串,在寄存器 r1 中的參數將被移除。如果你想有一個最大緩衝區長度,你可以將它保存在寄存器 r3
中。由於有額外的修改,我認為這樣修改函數是很有用的,如果目標字元串地址為 0,意味著沒有字元串被輸出,但如果仍然返回一個精確的長度,意味著能夠精確的判斷格式化字元串的長度。
如果你希望嘗試實現你自己的函數,現在就可以去做了。如果不去實現你自己的,下面我將首先構建方法的偽代碼,然後給出實現的彙編代碼。
function StringFormat(r0 is format, r1 is formatLength, r2 is dest, ...)
set index to 0
set length to 0
while index < formatLength
if readByte(format + index) = '%' then
set index to index + 1
if readByte(format + index) = '%' then
if dest > 0
then setByte(dest + length, '%')
set length to length + 1
otherwise if readByte(format + index) = 'c' then
if dest > 0
then setByte(dest + length, nextArg)
set length to length + 1
otherwise if readByte(format + index) = 'd' or 'i' then
set length to length + SignedString(nextArg, dest, 10)
otherwise if readByte(format + index) = 'o' then
set length to length + UnsignedString(nextArg, dest, 8)
otherwise if readByte(format + index) = 'u' then
set length to length + UnsignedString(nextArg, dest, 10)
otherwise if readByte(format + index) = 'b' then
set length to length + UnsignedString(nextArg, dest, 2)
otherwise if readByte(format + index) = 'x' then
set length to length + UnsignedString(nextArg, dest, 16)
otherwise if readByte(format + index) = 's' then
set str to nextArg
while getByte(str) != ' '
if dest > 0
then setByte(dest + length, getByte(str))
set length to length + 1
set str to str + 1
loop
otherwise if readByte(format + index) = 'n' then
setWord(nextArg, length)
end if
otherwise
if dest > 0
then setByte(dest + length, readByte(format + index))
set length to length + 1
end if
set index to index + 1
loop
return length
end function
雖然這個函數很大,但它還是很簡單的。大多數的代碼都是在檢查所有各種條件,每個代碼都是很簡單的。此外,所有的無符號整數的大小寫都是相同的(除了底以外)。因此在彙編中可以將它們匯總。下面是它的彙編代碼。
.globl FormatString
FormatString:
format .req r4
formatLength .req r5
dest .req r6
nextArg .req r7
argList .req r8
length .req r9
push {r4,r5,r6,r7,r8,r9,lr}
mov format,r0
mov formatLength,r1
mov dest,r2
mov nextArg,r3
add argList,sp,#7*4
mov length,#0
formatLoop$:
subs formatLength,#1
movlt r0,length
poplt {r4,r5,r6,r7,r8,r9,pc}
ldrb r0,[format]
add format,#1
teq r0,#'%'
beq formatArg$
formatChar$:
teq dest,#0
strneb r0,[dest]
addne dest,#1
add length,#1
b formatLoop$
formatArg$:
subs formatLength,#1
movlt r0,length
poplt {r4,r5,r6,r7,r8,r9,pc}
ldrb r0,[format]
add format,#1
teq r0,#'%'
beq formatChar$
teq r0,#'c'
moveq r0,nextArg
ldreq nextArg,[argList]
addeq argList,#4
beq formatChar$
teq r0,#'s'
beq formatString$
teq r0,#'d'
beq formatSigned$
teq r0,#'u'
teqne r0,#'x'
teqne r0,#'b'
teqne r0,#'o'
beq formatUnsigned$
b formatLoop$
formatString$:
ldrb r0,[nextArg]
teq r0,#0x0
ldreq nextArg,[argList]
addeq argList,#4
beq formatLoop$
add length,#1
teq dest,#0
strneb r0,[dest]
addne dest,#1
add nextArg,#1
b formatString$
formatSigned$:
mov r0,nextArg
ldr nextArg,[argList]
add argList,#4
mov r1,dest
mov r2,#10
bl SignedString
teq dest,#0
addne dest,r0
add length,r0
b formatLoop$
formatUnsigned$:
teq r0,#'u'
moveq r2,#10
teq r0,#'x'
moveq r2,#16
teq r0,#'b'
moveq r2,#2
teq r0,#'o'
moveq r2,#8
mov r0,nextArg
ldr nextArg,[argList]
add argList,#4
mov r1,dest
bl UnsignedString
teq dest,#0
addne dest,r0
add length,r0
b formatLoop$
5、一個轉換操作系統
你可以使用這個方法隨意轉換你希望的任何東西。比如,下面的代碼將生成一個換算表,可以做從十進位到二進位到十六進位到八進位以及到 ASCII 的換算操作。
刪除 main.s
文件中 bl SetGraphicsAddress
之後的所有代碼,然後粘貼以下的代碼進去。
mov r4,#0
loop$:
ldr r0,=format
mov r1,#formatEnd-format
ldr r2,=formatEnd
lsr r3,r4,#4
push {r3}
push {r3}
push {r3}
push {r3}
bl FormatString
add sp,#16
mov r1,r0
ldr r0,=formatEnd
mov r2,#0
mov r3,r4
cmp r3,#768-16
subhi r3,#768
addhi r2,#256
cmp r3,#768-16
subhi r3,#768
addhi r2,#256
cmp r3,#768-16
subhi r3,#768
addhi r2,#256
bl DrawString
add r4,#16
b loop$
.section .data
format:
.ascii "%d=0b%b=0x%x=0%o='%c'"
formatEnd:
你能在測試之前推算出將發生什麼嗎?特別是對於 r3 ≥ 128
會發生什麼?嘗試在樹莓派上運行它,看看你是否猜對了。如果不能正常運行,請查看我們的排錯頁面。
如果一切順利,恭喜你!你已經完成了屏幕04 教程,屏幕系列的課程結束了!我們學習了像素和幀緩衝的知識,以及如何將它們應用到樹莓派上。我們學習了如何繪製簡單的線條,也學習如何繪製字元,以及將數字格式化為文本的寶貴技能。我們現在已經擁有了在一個操作系統上進行圖形輸出的全部知識。你可以寫出更多的繪製方法嗎?三維繪圖是什麼?你能實現一個 24 位幀緩衝嗎?能夠從命令行上讀取幀緩衝的大小嗎?
接下來的課程是輸入系列課程,它將教我們如何使用鍵盤和滑鼠去實現一個傳統的計算機控制台。
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen04.html
作者:Alex Chadwick 選題:lujun9972 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive