PRF:20150616 Computer Laboratory - Raspberry Pi- Lesson 9 Screen04.md

@qhwdw
This commit is contained in:
Xingyu.Wang 2019-03-10 21:03:42 +08:00
parent a96868ee7f
commit 1df9b05a9e

View File

@ -1,73 +1,72 @@
[#]: collector: (lujun9972)
[#]: translator: (qhwdw)
[#]: reviewer: ( )
[#]: reviewer: (wxy)
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Computer Laboratory Raspberry Pi: Lesson 9 Screen04)
[#]: via: (https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen04.html)
[#]: author: (Alex Chadwick https://www.cl.cam.ac.uk)
计算机实验室 树莓派:课程 9 屏幕04
计算机实验室树莓派:课程 9 屏幕04
======
屏幕04 课程基于屏幕03 课程来构建,它教你如何操作文本。假设你已经有了[课程 8屏幕03][1] 的操作系统代码,我们将以它为基础。
### 1、操作字符串
```
变长函数在汇编代码中看起来似乎不好理解,然而 ,它却是非常有用和很强大的概念。
```
能够绘制文本是极好的,但不幸的是,现在你只能绘制预先准备好的字符串。如果能够像命令行那样显示任何东西才是完美的,而理想情况下应该是,我们能够显示任何我们期望的东西。一如既往地,如果我们付出努力而写出一个非常好的函数,它能够操作我们所希望的所有字符串,而作为回报,这将使我们以后写代码更容易。曾经如此复杂的函数,在 C 语言编程中只不过是一个 `sprintf` 而已。这个函数基于给定的另一个字符串和作为描述的额外的一个参数而生成一个字符串。我们对这个函数感兴趣的地方是,这个函数是个变长函数。这意味着它可以带可变数量的参数。参数的数量取决于具体的格式字符串,因此它的参数的数量不能预先确定。
完整的函数有许多选项,而我们在这里只列出了几个。在本教程中将要实现的选项我做了高亮处理,当然,你可以尝试去实现更多的选项
> 变长函数在汇编代码中看起来似乎不好理解,然而 ,它却是非常有用和很强大的概念。
函数通过读取格式字符串来工作,然后使用下表的意思去解释它。一旦一个参数已经使用了,就不会再次考虑它了。函数 的返回值是写入的字符数。如果方法失败,将返回一个负数。
这个完整的函数有许多选项,而我们在这里只列出了几个。在本教程中将要实现的选项我做了高亮处理,当然,你可以尝试去实现更多的选项。
函数通过读取格式化字符串来工作,然后使用下表的意思去解释它。一旦一个参数已经使用了,就不会再次考虑它了。函数的返回值是写入的字符数。如果方法失败,将返回一个负数。
表 1.1 sprintf 格式化规则
| 选项 | 含义 |
| -------------------------- | ------------------------------------------------------------ |
| ==Any character except %== | 复制字符到输出。 |
| ==%%== | 写一个 % 字符到输出。 |
| ==%c== | 将下一个参数写成字符格式。 |
| ==%d or %i== | 将下一个参数写成十进制的有符号整数。 |
| %e | 将下一个参数写成科学记数法,使用 eN 意思是 ×10N。 |
| %E | 将下一个参数写成科学记数法,使用 EN 意思是 ×10N。 |
| %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== | 什么也不输出。而是复制到目前为止被下一个参数在本地处理的字符个数。 |
| ==除了 `%` 之外的任何支付== | 复制字符到输出。 |
| ==`%%`== | 写一个 % 字符到输出。 |
| ==`%c`== | 将下一个参数写成字符格式。 |
| ==`%d` 或 `%i`== | 将下一个参数写成十进制的有符号整数。 |
| `%e` | 将下一个参数写成科学记数法,使用 eN,意思是 ×10<sup>N</sup>。 |
| `%E` | 将下一个参数写成科学记数法,使用 EN,意思是 ×10<sup>N</sup>。 |
| `%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++ 参考][2] 上找到。
下面是调用方法和返回的结果的示例。
表 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" |
|---------------|-------|---------------------|
| `"%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 汇编代码中不能直接实现,因为如果直接实现的话,它得出答案需要花费很长的时间,因此它不是个“简单的”运算。
```
虽然这个函数看起来很强大、也很复杂。但是,处理它的许多情况的最容易的方式可能是,编写一个函数去处理一些非常常见的任务。它是个非常有用的函数,可以为任何底的一个有符号或无符号的数字生成一个字符串。那么,我们如何去实现呢?在继续阅读之前,尝试快速地设计一个算法。
> 除法是非常慢的,也是非常复杂的基础数学运算。它在 ARM 汇编代码中不能直接实现,因为如果直接实现的话,它得出答案需要花费很长的时间,因此它不是个“简单的”运算。
最简单的方法或许就是我在 [课程 1OK01][3] 中提到的“除法余数法”。它的思路如下:
1. 用当前值除以你使用的底。
@ -75,11 +74,10 @@
3. 如果得到的新值不为 0转到第 1 步。
4. 将余数反序连起来就是答案。
例如:
表 2.1 以 2 为底的例子
转换
| 值 | 新值 | 余数 |
@ -100,7 +98,8 @@
我们复习一下长除法
> 假如我们想把 4135 除以 17。
>
>
> ```
> 0243 r 4
> 17)4135
> 0 0 × 17 = 0000
@ -111,9 +110,10 @@
> 55 735 - 680 = 55
> 51 3 × 17 = 51
> 4 55 - 51 = 4
> ```
> 答案243 余 4
>
> 首先我们来看被除数的最高位。 我们看到它是小于或等于除数的最小倍数,因此它是 0。我们在结果中写一个 0。
> 首先我们来看被除数的最高位。我们看到它是小于或等于除数的最小倍数,因此它是 0。我们在结果中写一个 0。
>
> 接下来我们看被除数倒数第二位和所有的高位。我们看到小于或等于那个数的除数的最小倍数是 34。我们在结果中写一个 2和减去 3400。
>
@ -124,16 +124,18 @@
在汇编代码中做除法,我们将实现二进制的长除法。我们之所以实现它是因为,数字都是以二进制方式保存的,这让我们很容易地访问所有重要位的移位操作,并且因为在二进制中做除法比在其它高进制中做除法都要简单,因为它的数更少。
> 1011 r 1
>1010)1101111
> 1010
> 11111
> 1010
> 1011
> 1010
> 1
这个示例展示了如何做二进制的长除法。简单来说就是,在不超出被除数的情况下,尽可能将除数右移,根据位置输出一个 1和减去这个数。剩下的就是余数。在这个例子中我们展示了 1101111<sub>2</sub> ÷ 1010<sub>2</sub> = 1011<sub>2</sub> 余数为 1<sub>2</sub>。用十进制表示就是111 ÷ 10 = 11 余 1。
```
1011 r 1
1010)1101111
1010
11111
1010
1011
1010
1
```
这个示例展示了如何做二进制的长除法。简单来说就是,在不超出被除数的情况下,尽可能将除数右移,根据位置输出一个 1和减去这个数。剩下的就是余数。在这个例子中我们展示了 1101111<sub>2</sub> ÷ 1010<sub>2</sub> = 1011<sub>2</sub> 余数为 1<sub>2</sub>。用十进制表示就是111 ÷ 10 = 11 余 1。
你自己尝试去实现这个长除法。你应该去写一个函数 `DivideU32` ,其中 `r0` 是被除数,而 `r1` 是除数,在 `r0` 中返回结果,在 `r1` 中返回余数。下面,我们将完成一个有效的实现。
@ -155,7 +157,7 @@ end function
这段代码实现了我们的目标,但却不能用于汇编代码。我们出现的问题是,我们的寄存器只能保存 32 位,而 `divisor << shift` 的结果可能在一个寄存器中装不下(我们称之为溢出)。这确实是个问题。你的解决方案是否有溢出的问题呢?
幸运的是,有一个称为 `clz``计数前导零count leading zeros` 的指令,它能计算一个二进制表示的数字的前导零的个数。这样我们就可以在溢出发生之前,可以将寄存器中的值进行相应位数的左移。你可以找出的另一个优化就是,每个循环我们计算 `divisor << shift` 了两遍。我们可以通过将除数移到开始位置来改进它,然后在每个循环结束的时候将它移下去,这样可以避免将它移到别处。
幸运的是,有一个称为 `clz`<ruby>计数前导零<rt>count leading zeros</rt></ruby>的指令,它能计算一个二进制表示的数字的前导零的个数。这样我们就可以在溢出发生之前,可以将寄存器中的值进行相应位数的左移。你可以找出的另一个优化就是,每个循环我们计算 `divisor << shift` 了两遍。我们可以通过将除数移到开始位置来改进它,然后在每个循环结束的时候将它移下去,这样可以避免将它移到别处。
我们来看一下进一步优化之后的汇编代码。
@ -192,11 +194,10 @@ mov pc,lr
.unreq shift
```
```assembly
clz dest,src 将第一个寄存器 dest 中二进制表示的值的前导零的数量,保存到第二个寄存器 src 中。
```
你可能毫无疑问的认为这是个非常高效的作法。它是很好,但是除法是个代价非常高的操作,并且我们的其中一个愿望就是不要经常做除法,因为如果能以任何方式提升速度就是件非常好的事情。当我们查看有循环的优化代码时,我们总是重点考虑一个问题,这个循环会运行多少次。在本案例中,在输入为 1 的情况下,这个循环最多运行 31 次。在不考虑特殊情况的时候,这很容易改进。例如,当 1 除以 1 时,不需要移位,我们将把除数移到它上面的每个位置。这可以通过简单地在被除数上使用新的 `clz` 命令并从中减去它来改进。在 `1 ÷ 1` 的案例中,这意味着移位将设置为 0明确地表示它不需要移位。如果它设置移位为负数表示除数大于被除数因此我们就可以知道结果是 0而余数是被除数。我们可以做的另一个快速检查就是如果当前值为 0那么它是一个整除的除法我们就可以停止循环了。
> `clz dest,src` 将第一个寄存器 `dest` 中二进制表示的值的前导零的数量,保存到第二个寄存器 `src` 中。
你可能毫无疑问的认为这是个非常高效的作法。它是很好,但是除法是个代价非常高的操作,并且我们的其中一个愿望就是不要经常做除法,因为如果能以任何方式提升速度就是件非常好的事情。当我们查看有循环的优化代码时,我们总是重点考虑一个问题,这个循环会运行多少次。在本案例中,在输入为 1 的情况下,这个循环最多运行 31 次。在不考虑特殊情况的时候,这很容易改进。例如,当 1 除以 1 时,不需要移位,我们将把除数移到它上面的每个位置。这可以通过简单地在被除数上使用新的 clz 命令并从中减去它来改进。在 `1 ÷ 1` 的案例中,这意味着移位将设置为 0明确地表示它不需要移位。如果它设置移位为负数表示除数大于被除数因此我们就可以知道结果是 0而余数是被除数。我们可以做的另一个快速检查就是如果当前值为 0那么它是一个整除的除法我们就可以停止循环了。
```assembly
.globl DivideU32
@ -291,17 +292,15 @@ end function
### 4、格式化字符串
我们继续回到我们的字符串格式化方法。因为我们正在编写我们自己的操作系统,我们根据我们自己的意愿来添加或修改格式化规则。我们可以发现,添加一个 `a %b` 操作去输出一个二进制的数字比较有用,而如果你不使用空终止符字符串,那么你应该去修改 `%s` 的行为,让它从另一个参数中得到字符串的长度,或者如果你愿意,可以从长度前缀中获取。我在下面的示例中使用了一个空终止符。
我们继续回到我们的字符串格式化方法。因为我们正在编写我们自己的操作系统,我们根据我们自己的意愿来添加或修改格式化规则。我们可以发现,添加一个 `a % b` 操作去输出一个二进制的数字比较有用,而如果你不使用空终止符字符串,那么你应该去修改 `%s` 的行为,让它从另一个参数中得到字符串的长度,或者如果你愿意,可以从长度前缀中获取。我在下面的示例中使用了一个空终止符。
实现这个函数的一个主要的障碍是它的参数个数是可变的。根据 ABI 规定,额外的参数在调用方法之前以相反的顺序先推送到栈上。比如,我们使用 8 个参数 1、2、3、4、5、6、7 和 8 来调用我们的方法,我们将按下面的顺序来处理:
1. Set r0 = 5、r1 = 6、r2 = 7、r3 = 8
2. Push {r0,r1,r2,r3}
3. Set r0 = 1、r1 = 2、r2 = 3、r3 = 4
4. 调用函数
5. Add sp,#4*4
1. 设置 r0 = 5、r1 = 6、r2 = 7、r3 = 8
2. 推入 {r0,r1,r2,r3}
3. 设置 r0 = 1、r1 = 2、r2 = 3、r3 = 4
4. 调用函数
5. 将 sp 和 #4*4 加起来
现在,我们必须确定我们的函数确切需要的参数。在我的案例中,我将寄存器 `r0` 用来保存格式化字符串地址,格式化字符串长度则放在寄存器 `r1` 中,目标字符串地址放在寄存器 `r2` 中,紧接着是要求的参数列表,从寄存器 `r3` 开始和像上面描述的那样在栈上继续。如果你想去使用一个空终止符格式化字符串,在寄存器 r1 中的参数将被移除。如果你想有一个最大缓冲区长度,你可以将它保存在寄存器 `r3` 中。由于有额外的修改,我认为这样修改函数是很有用的,如果目标字符串地址为 0意味着没有字符串被输出但如果仍然返回一个精确的长度意味着能够精确的判断格式化字符串的长度。
@ -526,13 +525,13 @@ via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen04.html
作者:[Alex Chadwick][a]
选题:[lujun9972][b]
译者:[qhwdw](https://github.com/qhwdw)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://www.cl.cam.ac.uk
[b]: https://github.com/lujun9972
[1]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen03.html
[1]: https://linux.cn/article-10585-1.html
[2]: http://www.cplusplus.com/reference/clibrary/cstdio/sprintf/
[3]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok01.html
[3]: https://linux.cn/article-10458-1.html
[4]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/input01.html