Baking Pi - Operating Systems Development Lesson 3

06 Dec 2012

OK03的课程在OK02的基础上继续,主要介绍如何通过使用函数,来提高代码的可复用性和可读性。这节课假设你已经有了OK02的代码。

1 可复用的代码

之前,我们一直都是根据自己的思路按照顺序将代码写出来的。这对于很小的程序来说没有问题,不过如果我们将整个系统都这么写出来,代码基本就没法让人看了。因此需要引入函数,来帮助我们。

像c和c++这些上层语言,函数是程序语言的一部分。在汇编语言里,本身没有提供函数这一语法,但是我们可以通过良好的代码组织来实现这一概念。

一般来说我们会希望能够给寄存器设定值,然后跳转到了某个地址继续执行,然后在未来的某个时候还可以跳回到我们之前设定寄存器的代码那里。这就是函数在汇编语言上的体现。当然如何保证我们设定寄存器的规则的一致性是难点之一。因为如果我们随便用这些寄存器来设值,那么别的程序员在读这段代码的时候就会很痛苦了。更一进步,编译器也没法正常工作了,因为他们不知道如何使用函数。为了防止这种情况发生,一个应用程序二进制接口(简称ABI)的标准就显得十分必要了,这一标准就是在说明不同汇编语言的情况下如何使用函数。如果所有人都遵循这一标准,那么函数就可以被所有人都共享了。下面就会介绍这一标准,而且之后的代码都会严格按照这一标准来编写。

标准要求r0,r1,r2和r3寄存器,应该按照顺序作为函数的输入。不需要输入参数的函数,就不需要在意这些寄存器了。对于需要一个输入参数的,那么输入参数就应该放在r0上,两个的话,就按照顺序r0,r1存放,以此类推。输出的数据则应该一直放在r0上。如果没有输出,当然就无所谓了。

同时,标准也要求,函数运行后,r4到r12这些寄存器的值,应该与函数运行前保持一致。也就是说,你调用一个函数的时候,可以确定r4到r12的寄存器不会有变化,而r0到r3则不一定。

函数结束后就要返回原来的位置,那么函数就需要记录下调用自己的位置。为此,会有专门的一个寄存器,叫做lr(link register),用于记录函数被调用的代码位置。

表1.1 ARM ABI寄存器使用

寄存器 作用 是否会变化 规则
r0 输入参数和返回值 变化 r0,r1用于传递函数的头两个参数,和函数返回值。如果函数无返回值,则可能是任何数字
r1 输入参数和返回值 变化 同上
r2 输入参数 变化 r2,r3用于传递函数后两个参数,返回时,可能是任意值
r3 输入参数 变化 同上
r4-r12 通用 不变化 r4-r12作用为给用于一般使用的值,返回时,值必须恢复到和原来一致
lr 返回地址 变化 函数结束时返回执行的地址,这在函数完成返回前都不能被改变
sp 栈地址 不变化 栈地址,下面就解释。函数返回前后时的值必须一致

因为函数一般不会只用r0-r3这几个寄存器,而r4和r12又必须前后保持值的一致,所以就需要将他们保存起来。而保存这些地址的地方就被叫做栈。

由于栈十分有用,在ARMv6的指令集中有专门的指令操作栈。sp寄存器就专门用来保存栈的地址。一旦有值被加入到栈中,寄存器也会跟着变化,保证寄存器中的地址指向栈的第一项。 push {r4,r5} 就会将r4和r5的值压入栈,而 pop {r4,r5} 则会在重新放回原来的位置(按照正确顺序)。

2 第一个函数的实现

现在我们了解了函数的工作原理,然后我们就实现一个。作为开始,我们的函数比较简单,没有输入,输出则是GPIO的地址。上节课中,我们是直接写出这个值的,而通过函数实现则更好,因为这一值要经常使用,而我们有时候又记不住这个值。

拷贝如下代码到新建的一个名为“gpoi.s”的文件中。文件和“main.s”一起放在source目录下就可以。我们将会将所有和GPIO控制器相关的函数都放在一个文件里,方便查找。

.globl GetGpioAddress
GetGpioAddress:
ldr r0,=0x20200000
mov pc,lr

非常简单的一个函数。 .globl GetGpioAddress 用于告知汇编器标签 GetGpioAddress 全局可见。这样即使我们在main.s文件中,也可跳转到 GetGpioAddress ,而不在需要非得在同一文件内才能跳转。

ldr r0,=0x20200000 这句代码会很熟悉,就是保存GPIO控制器的地址在r0上。因为这是一个函数,我们需要将输出放在r0上,这不像我们之前那样可以随便使用寄存器了。

move pc,lrlr 的值拷贝到 pc 中。之前提到过, lr 一直保存着函数结束后需要返回的地址。 pc 则是一个特殊的寄存器,一直都保存下一个指令的地址。一般的跳转都是通过修改它的值来实现。通过将 lr 的值拷贝到 pc 上,我们就将下条要执行的指令位置指定成为了函数执行完成后需要返回的指令位置。

现在的一个问题就是,如何来执行这段代码?一个特别的跳转指令 bl 实现了这一功能。它就要其他跳转一样,跳转了某个标签的位置上,但是在跳转前,会将跳转代码后面那行地址保存到 lr 中。也就是说函数结束的时候,就需要回到 bl 指令后面的那条指令上。这样函数的运行就像是其他普通的指令一样,只管运行,完成自己的功能,然后继续函数后面的代码。这是一种关于函数的非常有用的思考方式。我们将函数当做“黑盒”来处理,我们使用的时候,不需要知道它具体如何工作,我们只需要知道他的输入和输出。

现在,不要急着用,下一小节我们就会用到它。

3 DONE 一个大函数

Now we're going to implement a bigger function. Our first job was to enable output on GPIO pin 16. It would be nice if this was a function. We could simply specify a pin and a function as the input, and the function would set the function of that pin to that value. That way, we could use the code to control any GPIO pin, not just the LED.

Copy the following commands below the GetGpioAddress function in gpio.s. 现在开始我们要完成一个大点的函数。第一项工作就是打开GPIO pin16的输出。这个功能比较时候写成一个函数。这样我们就可以指定一个pin的位置和函数的输入,TODO

复制以下的GetGpioAddress函数到gpio.s中去

.globl SetGpioFunction
SetGpioFunction:
cmp r0,#53
cmpls r1,#7
movhi pc,lr

One of the first things we should always think about when writing functions is our inputs. What do we do if they are wrong? In this function, we have one input which is a GPIO pin number, and so must be a number between 0 and 53, since there are 54 pins. Each pin has 8 functions, numbered 0 to 7 and so the function code must be too. We could just assume that the inputs will be correct, but this is very dangerous when working with hardware, as incorrect values could cause very bad side effects. Therefore, in this case, we wish to make sure the inputs are in the right ranges. 写函数第一件事情就是要考虑函数的输入。如果输入错误应该如何处理?在这个函数中,我们只有输入,就是GPIO的pin的编号,而且必须是0到53之间的数值,因为总共只有54个pin。每个pin,有8个functions,从0到7编号,所以参数也应该在这个范围内。我们可以简单的假设输入是正确的,但是与硬件交互的代码中出现这样的假设是比较危险的,因为错误的值可能会产生很严重的副作用。所以,这里我们需要确保数值都在正确范围内。

To do this we need to check that r0 <= 53 and r1 <= 7. First of all, we can use the comparison we've seen before to compare the value of r0 with 53. The next instruction, cmpls is a normal comparison instruction that will only be run if r0 was lower than or the same as 53. If that was the case, it compares r1 with 7, otherwise the result of the comparison is the same as before. Finally we go back to the code that ran the function if the result of the last comparison was that the register was higher than the number.

为此,我们需要检查r0 <= 53 而 r1 <= 7. 首先我们可以用之前提到过的比较操作来将r0和53比较。下一个指令 cmpls 一个常用的比较指令,其只有在r0小于等于53的条件下才会运行。满足条件的情况下,它会比较r1和7,否则保持之前的比较的结果不变。最后我们会返回到处理最后的比较中寄存器值比数值大的情况下的代码。

这一代码的结果就满足我们之前的意图。如果r0比53大,那么 cmpls 就不会运行,但是 movhi 会运行。如果 r0 <= 53 ,那么 cmpls 就会运行,然后比较r1和7,如果比7大, movhi 运行,函数结束,否则 movhi 则不会运行,我们也就完成了 r0<=53 而且r1<=7的检查。

对于 ls (lower or same) 和 le (less or qual)后缀有一些小差别,我们随后会细说这一问题, hi (high) 和 gt (greater)后缀也一样。

继续拷贝如下代码:

push {lr}
mov r2,r0
bl GetGpioAddress

接下来的这三个指令主要用于调用我们的第一个方法。 push {lr} 命令拷贝 lr 中的值到栈顶,这样我们随后才能在此找到值。之所以我们要做这一步,是因为我们调用GetGpioAddress的时候,我们需要使用 lr 来保存函数的返回值1

如果我们不清楚GetGpioAddress函数,我们应该假定他会修改r0-r3的寄存器,需要将值移动到r4和r5以保证执行完毕后还能保存下值来。不过,我们很清楚GetGpioAddress的内部,知道它值改动r0,不影响r1-r3的寄存器。所以,我们就只需要将r0的值保存下来以免被覆盖掉。其实我们直接将值移动到r2中了,因为GetGpioAddress不改变r2的值。

最后我们使用 bl 指令来运行GetGpioAddress。一般来说我们使用‘调用’一次来表示运行一个函数,我们从现在开始就使用这一名词了。之前就说过, bl 会通过改变lr的值,将其设置为下一个要执行的地址来调用一个函数,然后跳转到函数处执行。

函数执行完成后,就会‘返回’。到调用的GetGpioAddress地址返回后,我们知道r0保存的是GPIO的地址,r1保存的是功能编码(function code),r2保存的是GPIO的pin值。之前我们提到过[OK1中],GPIO的功能编码都是10个10个的组织在一起的,我们首先就需要考虑是应该会落到第几个10个的区域。这里看起来需要一个除法,但是除法的操作很慢,所以这种小数值可以通过重复的减法来实现除法。

拷贝代码:

functionLoop$:
    cmp r2,#9
    subhi r2,#10
    addhi r0,#4
    bhi functionLoop$

这里的循环代码的判断条件就是不断的将pin编号和0对比,如果比9大,就从中减去10,然后在GPIO控制器的地址上增加4,然后继续循环。 The effect of this is that r2 will now contain a number from 0 to 9 which represents the remainder of dividing the pin number by 10. r0 will not contain the address in the GPIO controller of this pin's function settings. This would be the same as GPIO Controller Address + 4 × (GPIO Pin Number ÷ 10). 运行的结果就是r2会保存一个0-9的数值,代表的是pin值除以10的余数,

4 TODO 另一个函数

5 TODO 新的开始

Footnotes:

1

原文有个ot是什么东西?