hdmi 屏幕旋转 树莓派_计算机实验室之树莓派:课程 9 屏幕04
屏幕04 課程基于屏幕03 課程來構建,它教你如何操作文本。假設你已經有了課程 8:屏幕03 的操作系統代碼,我們將以它為基礎。
1、操作字符串
能夠繪制文本是極好的,但不幸的是,現在你只能繪制預先準備好的字符串。如果能夠像命令行那樣顯示任何東西才是完美的,而理想情況下應該是,我們能夠顯示任何我們期望的東西。一如既往地,如果我們付出努力而寫出一個非常好的函數,它能夠操作我們所希望的所有字符串,而作為回報,這將使我們以后寫代碼更容易。曾經如此復雜的函數,在 C 語言編程中只不過是一個 sprintf 而已。這個函數基于給定的另一個字符串和作為描述的額外的一個參數而生成一個字符串。我們對這個函數感興趣的地方是,這個函數是個變長函數。這意味著它可以帶可變數量的參數。參數的數量取決于具體的格式字符串,因此它的參數的數量不能預先確定。
變長函數在匯編代碼中看起來似乎不好理解,然而 ,它卻是非常有用和很強大的概念。這個完整的函數有許多選項,而我們在這里只列出了幾個。在本教程中將要實現的選項我做了高亮處理,當然,你可以嘗試去實現更多的選項。
函數通過讀取格式化字符串來工作,然后使用下表的意思去解釋它。一旦一個參數已經使用了,就不會再次考慮它了。函數的返回值是寫入的字符數。如果方法失敗,將返回一個負數。
表 1.1 sprintf 格式化規則
除此之外,對序列還有許多額外的處理,比如指定最小長度,符號等等。更多信息可以在 sprintf - C++ 參考 上找到。
下面是調用方法和返回的結果的示例。
表 1.2 sprintf 調用示例
希望你已經看到了這個函數是多么有用。實現它需要大量的編程工作,但給我們的回報卻是一個非常有用的函數,可以用于各種用途。
2、除法
雖然這個函數看起來很強大、也很復雜。但是,處理它的許多情況的最容易的方式可能是,編寫一個函數去處理一些非常常見的任務。它是個非常有用的函數,可以為任何底的一個有符號或無符號的數字生成一個字符串。那么,我們如何去實現呢?在繼續閱讀之前,嘗試快速地設計一個算法。
除法是非常慢的,也是非常復雜的基礎數學運算。它在 ARM 匯編代碼中不能直接實現,因為如果直接實現的話,它得出答案需要花費很長的時間,因此它不是個“簡單的”運算。最簡單的方法或許就是我在 課程 1:OK01 中提到的“除法余數法”。它的思路如下:
例如:
表 2.1 以 2 為底的例子
轉換
因此答案是 100010012
這個過程的不幸之外在于使用了除法。所以,我們必須首先要考慮二進制中的除法。
我們復習一下長除法
假如我們想把 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)11011111010111111010101110101這個示例展示了如何做二進制的長除法。簡單來說就是,在不超出被除數的情況下,盡可能將除數右移,根據位置輸出一個 1,和減去這個數。剩下的就是余數。在這個例子中,我們展示了 11011112 ÷ 10102 = 10112余數為 12。用十進制表示就是,111 ÷ 10 = 11 余 1。
你自己嘗試去實現這個長除法。你應該去寫一個函數 DivideU32 ,其中 r0 是被除數,而 r1 是除數,在 r0 中返回結果,在 r1 中返回余數。下面,我們將完成一個有效的實現。
function DivideU32(r0 is dividend, r1 is divisor)set shift to 31set result to 0while shift ≥ 0if dividend ≥ (divisor << shift) thenset dividend to dividend - (divisor << shift)set result to result + 1end ifset result to result << 1set shift to shift - 1loopreturn (result, dividend) end function這段代碼實現了我們的目標,但卻不能用于匯編代碼。我們出現的問題是,我們的寄存器只能保存 32 位,而 divisor << shift 的結果可能在一個寄存器中裝不下(我們稱之為溢出)。這確實是個問題。你的解決方案是否有溢出的問題呢?
幸運的是,有一個稱為 clz( 計數前導零(count leading zeros))的指令,它能計算一個二進制表示的數字的前導零的個數。這樣我們就可以在溢出發生之前,可以將寄存器中的值進行相應位數的左移。你可以找出的另一個優化就是,每個循環我們計算 divisor << shift 了兩遍。我們可以通過將除數移到開始位置來改進它,然后在每個循環結束的時候將它移下去,這樣可以避免將它移到別處。
我們來看一下進一步優化之后的匯編代碼。
.globl DivideU32 DivideU32: result .req r0 remainder .req r1 shift .req r2 current .req r3clz shift,r1 lsl current,r1,shift mov remainder,r0 mov result,#0divideU32Loop$:cmp shift,#0blt divideU32Return$cmp remainder,currentaddge result,result,#1subge remainder,currentsub shift,#1lsr current,#1lsl result,#1b 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 r3clz shift,r1 clz r3,r0 subs shift,r3 lsl current,r1,shift mov remainder,r0 mov result,#0 blt divideU32Return$divideU32Loop$:cmp remainder,currentblt divideU32LoopContinue$add result,result,#1subs remainder,currentlsleq result,shiftbeq divideU32Return$ divideU32LoopContinue$:subs shift,#1lsrge current,#1lslge result,#1bge 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 ≥ 0then return UnsignedString(value, dest, base)otherwiseif dest > 0 thensetByte(dest, '-')set dest to dest + 1end ifreturn UnsignedString(-value, dest, base) + 1end if end functionfunction UnsignedString(r0 is value, r1 is dest, r2 is base)set length to 0doset (value, rem) to DivideU32(value, base)if rem > 10then set rem to rem + '0'otherwise set rem to rem - 10 + 'a'if dest > 0then setByte(dest + length, rem)set length to length + 1while value > 0if dest > 0then ReverseString(dest, length)return length end functionfunction ReverseString(r0 is string, r1 is length)set end to string + length - 1while end > startset temp1 to readByte(start)set temp2 to readByte(end)setByte(start, temp2)setByte(end, temp1)set start to start + 1set end to end - 1end while end function上述代碼實現在一個名為 text.s 的匯編文件中。記住,如果你遇到了困難,可以在下載頁面找到完整的解決方案。
4、格式化字符串
我們繼續回到我們的字符串格式化方法。因為我們正在編寫我們自己的操作系統,我們根據我們自己的意愿來添加或修改格式化規則。我們可以發現,添加一個 a % b 操作去輸出一個二進制的數字比較有用,而如果你不使用空終止符字符串,那么你應該去修改 %s 的行為,讓它從另一個參數中得到字符串的長度,或者如果你愿意,可以從長度前綴中獲取。我在下面的示例中使用了一個空終止符。
實現這個函數的一個主要的障礙是它的參數個數是可變的。根據 ABI 規定,額外的參數在調用方法之前以相反的順序先推送到棧上。比如,我們使用 8 個參數 1、2、3、4、5、6、7 和 8 來調用我們的方法,我們將按下面的順序來處理:
現在,我們必須確定我們的函數確切需要的參數。在我的案例中,我將寄存器 r0 用來保存格式化字符串地址,格式化字符串長度則放在寄存器 r1 中,目標字符串地址放在寄存器 r2 中,緊接著是要求的參數列表,從寄存器 r3 開始和像上面描述的那樣在棧上繼續。如果你想去使用一個空終止符格式化字符串,在寄存器 r1 中的參數將被移除。如果你想有一個最大緩沖區長度,你可以將它保存在寄存器 r3 中。由于有額外的修改,我認為這樣修改函數是很有用的,如果目標字符串地址為 0,意味著沒有字符串被輸出,但如果仍然返回一個精確的長度,意味著能夠精確的判斷格式化字符串的長度。
如果你希望嘗試實現你自己的函數,現在就可以去做了。如果不去實現你自己的,下面我將首先構建方法的偽代碼,然后給出實現的匯編代碼。
function StringFormat(r0 is format, r1 is formatLength, r2 is dest, ...)set index to 0set length to 0while index < formatLengthif readByte(format + index) = '%' thenset index to index + 1if readByte(format + index) = '%' thenif dest > 0then setByte(dest + length, '%')set length to length + 1otherwise if readByte(format + index) = 'c' thenif dest > 0then setByte(dest + length, nextArg)set length to length + 1otherwise if readByte(format + index) = 'd' or 'i' thenset length to length + SignedString(nextArg, dest, 10)otherwise if readByte(format + index) = 'o' thenset length to length + UnsignedString(nextArg, dest, 8)otherwise if readByte(format + index) = 'u' thenset length to length + UnsignedString(nextArg, dest, 10)otherwise if readByte(format + index) = 'b' thenset length to length + UnsignedString(nextArg, dest, 2)otherwise if readByte(format + index) = 'x' thenset length to length + UnsignedString(nextArg, dest, 16)otherwise if readByte(format + index) = 's' thenset str to nextArgwhile getByte(str) != '0'if dest > 0then setByte(dest + length, getByte(str))set length to length + 1set str to str + 1loopotherwise if readByte(format + index) = 'n' thensetWord(nextArg, length)end ifotherwiseif dest > 0then setByte(dest + length, readByte(format + index))set length to length + 1end ifset index to index + 1loopreturn length end function雖然這個函數很大,但它還是很簡單的。大多數的代碼都是在檢查所有各種條件,每個代碼都是很簡單的。此外,所有的無符號整數的大小寫都是相同的(除了底以外)。因此在匯編中可以將它們匯總。下面是它的匯編代碼。
.globl FormatString FormatString: format .req r4 formatLength .req r5 dest .req r6 nextArg .req r7 argList .req r8 length .req r9push {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,#0formatLoop$:subs formatLength,#1movlt r0,lengthpoplt {r4,r5,r6,r7,r8,r9,pc}ldrb r0,[format]add format,#1teq r0,#'%'beq formatArg$formatChar$:teq dest,#0strneb r0,[dest]addne dest,#1add length,#1b formatLoop$formatArg$:subs formatLength,#1movlt r0,lengthpoplt {r4,r5,r6,r7,r8,r9,pc}ldrb r0,[format]add format,#1teq r0,#'%'beq formatChar$teq r0,#'c'moveq r0,nextArgldreq nextArg,[argList]addeq argList,#4beq 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,#0x0ldreq nextArg,[argList]addeq argList,#4beq formatLoop$add length,#1teq dest,#0strneb r0,[dest]addne dest,#1add nextArg,#1b formatString$formatSigned$:mov r0,nextArgldr nextArg,[argList]add argList,#4mov r1,destmov r2,#10bl SignedStringteq dest,#0addne dest,r0add length,r0b formatLoop$formatUnsigned$:teq r0,#'u'moveq r2,#10teq r0,#'x'moveq r2,#16teq r0,#'b'moveq r2,#2teq r0,#'o'moveq r2,#8mov r0,nextArgldr nextArg,[argList]add argList,#4mov r1,destbl UnsignedStringteq dest,#0addne dest,r0add length,r0b 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,#16mov r1,r0 ldr r0,=formatEnd mov r2,#0 mov r3,r4cmp 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,#256bl DrawStringadd r4,#16 b loop$.section .data format: .ascii "%d=0b%b=0x%x=0%o='%c'" formatEnd:你能在測試之前推算出將發生什么嗎?特別是對于 r3 ≥ 128 會發生什么?嘗試在樹莓派上運行它,看看你是否猜對了。如果不能正常運行,請查看我們的排錯頁面。
如果一切順利,恭喜你!你已經完成了屏幕04 教程,屏幕系列的課程結束了!我們學習了像素和幀緩沖的知識,以及如何將它們應用到樹莓派上。我們學習了如何繪制簡單的線條,也學習如何繪制字符,以及將數字格式化為文本的寶貴技能。我們現在已經擁有了在一個操作系統上進行圖形輸出的全部知識。你可以寫出更多的繪制方法嗎?三維繪圖是什么?你能實現一個 24 位幀緩沖嗎?能夠從命令行上讀取幀緩沖的大小嗎?
接下來的課程是輸入系列課程,它將教我們如何使用鍵盤和鼠標去實現一個傳統的計算機控制臺。
總結
以上是生活随笔為你收集整理的hdmi 屏幕旋转 树莓派_计算机实验室之树莓派:课程 9 屏幕04的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小强升职记梗概_《小强升职记》读后感
- 下一篇: python中字典的find_pytho