烤派宝典第三章之OK03

#烤派宝典第三章之OK03 OK03 这一章基于OK02,将教会你在汇编语言中如何使用函数,函数的引入将使得汇编代码可用性大大提升,并提升代码的可读性。开始本章之前,我们假定你已经有了烤派宝典第二章之OK02的代码作为基础。

内容
1 可重用的代码
2 第一个函数
3 一个大函数
4 另一个函数
5 新的开始
6 前瞻

##1 可重用的代码 迄今为止我们写操作系统时代码都是硬生生敲进去的。对于小程序而言无所谓,但倘若我们要把整个系统用这种方式写出来,代码将变得完全不可读。解决方案是我们将使用函数。
有关函数的解释:

函数是一小段用于计算出特定答案的、可重用的代码,或者它可以执行一个特定的动作。你可能听过它的不同叫法,例如procedures, routines, subroutines。 这些叫法没有本质上的差别,也没有特定的标准叫法。 你可能早就在数学课上接触过函数的概念了。比如余弦函数将根据给定的角度计算出一个介于-1和1之间的值。我们用标号cos(x)来表明这是参数为x的函数。在代码中,函数能拥有多个输入值(也可以无输入值),这可能会有点副作用。比如一个函数可能能完成多个功能:在文件系统上创建一个文件,读入标准输入,保存命名之。

在高级语言譬如C或C++中,函数是语言本身的概念。但是在汇编语言中,函数就是我们所拥有的想法。

functions

理想情况下,我们设想给寄存器某些输入值,而后程序分支跳转到另一地址,执行完该地址的代码后将跳转回代码分支执行的地方,这时候寄存器中会得到输出值。这就是汇编语言中的函数执行流程。难点在于我们如何系统化组织和设置寄存器。如果我们使用我们所能想象到的方式来组织,那么每个程序员所撰写的代码都会变得难以理解。对于编译器而言,如何组织设置寄存器是无所谓的话题,因为它们压根就不知道如何使用函数。为了避免混淆,一种叫ABI(Application Binary Interface,二进制程序接口)的标准被引入,这是任何一种汇编语言中的约定俗成,规定了函数应该如何被运行。如果每个程序员都按照同样的方式来创建函数,那么所有的函数都可以被别人使用。在这里我会讲解这一标准,而且从现在开始我敲入的所有函数代码都将适应这一标准。

标准定义如下:寄存器r0,r1,r2,r3将被依次被用作函数的输入。如果函数无输入值,这些寄存器值就都可以被忽略。如果需要一个参数,则把参数放入r0中。如果需要两个参数,则第一个参数放入r0中,第二个参数放入r1中,依此类推。输出值将被放入r0中。如果函数无输出值,那就可以忽略r0里的值。

此外,标准还要求,函数运行完毕后,r4到r12必须保持函数运行前的取值。这意味着当你调用一个函数时,可以确认的是r4到r12的值不会发生变化,但是对r0到r3的值,我们就没法确保了。

函数通常被称之为“黑盒子”,我们输入值,然后得到输出值。但是我们不知道黑盒子内部是如何工作的。

函数执行完毕后需要回到函数被调用的起始点。这意味着我们必须知道代码是从何处被调用的。为方便起见,有一个特殊寄存器,名为lr(link register,链接寄存器),它总是用于记住指令调用该函数后的地址。

  <th>概述</th>

  <th>是否保留</th>

  <th>规则</th>
</tr>
  <td>参数和结果</td>

  <td>No</td>

  <td rowspan="2">r0 和 r1被用作传递第一个和第二个参数给函数,并存储在函数中返回的值。如果函数无返回值,则他们的值可为任意值。
  </td>
</tr>

<tr class="highlightRow1">
  <td>r1</td>

  <td>参数和结果</td>

  <td>No</td>
</tr>

<tr>
  <td>r2</td>

  <td>参数</td>

  <td>No</td>

  <td rowspan="2">r2 和r3被用于传递第3个参数起的两个参数给函数。当函数被运行了,它们可以被改变为其他任意值。
  </td>
</tr>

<tr>
  <td>r3</td>

  <td>参数</td>

  <td>No</td>
</tr>

<tr class="highlightRow1">
  <td>r4</td>

  <td>通用</td>

  <td>Yes</td>

  <td rowspan="9">r4 到 r12被用作工作值,他们的值在函数调用前后保持一致。
  </td>
</tr>

<tr class="highlightRow1">
  <td>r5</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r6</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r7</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r8</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r9</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r10</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r11</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr class="highlightRow1">
  <td>r12</td>

  <td>通用</td>

  <td>Yes</td>
</tr>

<tr>
  <td>lr</td>

  <td>返回值</td>

  <td>No</td>

  <td>lr 中存储了函数完成时需要跳转回的地址,这个值在函数完成前保持不变.
  </td>
</tr>

<tr class="highlightRow1">
  <td>sp</td>

  <td>栈指针</td>

  <td>Yes</td>

  <td>sp 是栈指针寄存器,描述如下。它的值需要保持到函数执行完后。
  </td>
</tr>

通常函数运行时需要更多的寄存器,而不是仅仅局限于r0到r3,因为r4到r12的值需要在函数运行前后保持一直,在使用前我们就必须将它们保存到某个特定地方,这个特定的地方被称之为栈(Stack)。

栈是我们在计算机中用于存储数值的一个比喻。就像一叠盘子,你只能在这一堆盘子的顶部取走盘子,或者添加新的盘子到顶部。 在函数运行时候使用栈来存储寄存器数据是一个绝佳的主意。比如如果我有一个函数需要存储寄存器r4和r5的值,那我就可以直接将这些寄存器的值放到栈的顶部。调用完函数后再从栈中将其送回。更聪明的如果我在函数里又调用了另一个函数以便完成某些操作,我仍然可以将当前的寄存器值继续送到栈中,运行完再取回。反复的压栈和取栈操作并不会影响到r4和r5所存储位置的值,每次新的压栈都会被添加到栈的顶部,而后从同样的位置被移除。 我们用来在栈里存放数据的方法用术语来讲就叫“stack frame(堆栈桢)”。并不是每个函数都需要用到堆栈桢,有的函数压根儿就不需要保存数值。

stack

由于栈如此有用,在ARMv6指令集中我们可以直接设置它。一个特殊的寄存器叫sp(stack pointer,栈指针)用于专门存放栈地址。当有新的条目被添加到栈中时,栈指针寄存器将更新其内容,以确保永远指向第一个条目的地址。push{r4,r5}这条指令将会把r4和r5放入到栈的顶部,pop{r4,r5}将把这两个条目从栈中取回(按正确的顺序取回)。

##2 第一个函数 现在我们已经知道函数是如何工作的了,让我们来写一个函数。作为第一个最基础的例子,我们要写一个没有输入的函数,输出则是GPIO地址。在上一章里,我们已经写入了这个值,但最好还是用函数来实现,因为在一个操作系统中我们需要频繁设置GPIO口的值,但我们不可能每一次都记得该GPIO口的物理地址。
拷贝下列代码到一个新文件中,命名为’gpio.s’。新文件应该在’source'文件夹中,和’main.s'同一级。我们将把所有与GPIO控制器相关的函数都放在这个文件中,以方便我们查找。

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

这是一个很简单而完整的函数。.global GetGpioAddress命令是传递给汇编器的消息,这使得GetGpioAddress在所有的文件里都可见。也意味着在main.s文件里我们可以跳转到GetGpioAddress这个符号,尽管在当前文件中没有定义。

.globl lbl使得标号lbl可以在其他文件中被调用。

你应该已经认识ldr r0,=0x20200000命令了,这条命令将GPIO控制器地址加载入r0寄存器中。这里我们用函数的形式实现,需要把输出放入r0中,所以我们不再像以前一样可以自由使用任何寄存器了。

mov pc,lr将lr寄存器中存储的值拷贝到pc。如以前所说,lr中存储的地址永远是我们执行完函数后需要返回的地址。pc是一个特殊的寄存器,它含有下一条需要运行的指令的地址。正常的跳转命令将改变这个寄存器的值。通过把lr的值拷贝到pc,我们确保了函数执行完后依然可以返回当初调用该函数的下一行执行。

mov reg1,reg2将寄存器reg2中的值拷贝到寄存器reg1中。

一个合理的问题是:实际上我们是如何运行函数中的代码的?我们用一个特殊的跳转指令bl来完成这一点。它跳转到一个标签,就像是正常的分支指令,但是调用这条指令会更新lr的值,使得lr指向分支指令后的紧跟的代码行。更新完lr的值意味着当函数完成时,需要返回的行将是bl指令的下一行。这使得函数的调用和其他命令一样,它被调用,做完需要完成的事情后,继续执行下一行。这是思考函数工作方式时,一个非常有用的思考方式。使用函数时,我们把函数看作是‘黑盒子’,我们不需要考虑它们内部的运行逻辑和工作方式,我们只需要知道其所需要的输入和它们能给出的输出。
现在,别担心使用函数会带来什么坏处了,我们将在下一小节开始使用它们。
##3 一个大函数 接下来我们来实现一个大函数。我们要完成的第一个任务是使能GPIO16管脚的输出。用函数来实现它是一种优雅的方式。我们的代码将调用一个函数和给出一个管脚号,函数将设置该管脚的值。这样以来,我们将可以使用代码来控制任意GPIO口,而不仅仅是LED。

拷贝下列命令到gpio.s中,在GetGpioAddress函数下:

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

后缀ls使得该条指令的执行条件限定在:仅仅在第一个数字小于或是等于第二个数字。这两个数字都应该是无符号数字.
后缀hi使得该条命令的执行条件限定在:仅仅在第一个数字大于第二个数字。 这两个数字都应该是无符号数。

我们需要时刻铭记于心的是:既然函数里的代码都是手动输入的,如果它们运行错误该怎么办?在这个例子里,我们的唯一输入是GPIO管脚数字,所以其输入值应该在0到53之间,因为一共只有54个管脚。每个管脚有8个函数,所以0到7是函数代码所能取值的范围。我们可以假定输入都是正确的,但不加检查和限定的代码很危险,因为在实际的硬件上工作时,错误的输入值将导致可能灾难性的后果。因此,在这个例子中,我们希望能对输入的值做安全检查,使得它们都在正确的范围内。

我们需要检查r0<=53和r1<=7。首先,我们需要使用之前我们已经使用过的比较功能,对r0的值和53做比较。就下来,我们调用cmpls,这是一条通用的比较指令,只有在r0小于和等于53的时候才会被运行。如果这一步被执行了,我们接着比较r1和7的值,否则比对结果依然是上一次比对的结果。如果历经比较后,最后一次比对的值大于给定值(r0大于53或者r1大于7),我们将返回到调用函数的起始地址。

这正好是我们想要的结果,如果r0大于53,那么之后的cmpls命令就不会被执行。如果r0小于等于53,cmpls命令才被执行,之后用r1和7作比较,如果r1大于7,movhi会被执行,函数终结。否则movhi不会被执行,这样我们就知道了r0<=53, r1<=7。

ls(小于或相同)和le(小于或相等)之间有微小的差别, hi(大于)和 gt(大于)指令之间也存在细微的差别,我会在后面的章节中提到这一问题。
接着拷贝下列代码:

	push {lr}
	mov r2,r0
	bl GetGpioAddress

这三条命令将用于调用我们之前的第一个方法。push {lr}命令将lr的值拷贝入栈的顶部,以便我们在稍后取回它。这一步是必须的,因为我们需要调用GetGpioAddress函数,调用前,我们需要使用lr寄存器用于存储调用完毕后以便程序返回的地址。

push {reg1,reg2,…}将列表reg1,reg2….中的寄存器拷贝到栈的顶部。只有通用寄存器和lr才可以执行push指令。

如果我们不知道不了解GetGpioAddress的实现细节,那么我们必须假定调用函数会改变r0,r1,r2和r3,那么我们可以将这些值暂存入r4和r5中,因为这些寄存器的值在函数调用的前后不会发生变化。幸运的是,这里我们已经知道GetGpioAddress的实现细节了,我们知道它仅仅改变了r0寄存器的值,而不会影响到r1,r2和r3。 因此,我们只需要将r0的值移出即可,这样就不会在函数调用中被覆盖,我们可以把它暂存入r2中,因为GetGpioAddress函数并不会改动r2的值。

最后我们使用bl指令来运行GetGpioAddress。 通常我们使用名词'调用(call)‘来运行一个函数。之前我们说过,bl在调用函数前会把lr更新为下一条指令的地址后,才分支跳转到函数中去。

bl lbl将lr寄存器指向下一条指令地址,然后跳转执行到标号lbl所示的代码。

当函数调用完毕后,我们可以说函数'返回'了。当GetGpioAddress返回时,我们知道r0寄存器中包含了GPIO地址,r1中包含了函数代码r2包含了GPIO 管脚数。我之前提到过GPIO函数以10的block大小存储,所以首先我们需要确定我们所得到的是在哪一个块中。听起来我们需要完成一个除法,但是除法在处理器中执行起来很慢,这里我们最好使用重复减法。

在上面代码的基础上,拷贝入下列代码行:

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

add reg,#val 将数字val加到reg寄存器所包含的值中去。

这个简单的循环代码比较了管脚号和9, 如果管脚号大于9, 那它将自动减少10,然后把地址在GPIO控制器的基础上加上4后,重新运行检查程序。

这样做的效果是,r2将包含0~9之间的数字,用于表示管脚除以10后的余数。 r0现在则包含了GPIO控制器中关于管脚的地址。效果等同于GPIO控制器基准地址 + 4 x (GPIO管脚号/10)。

最后,在上述代码的基础上,拷贝入下列代码行:

	add r2, r2,lsl #1
	lsl r1,r2
	str r1,[r0]
	pop {pc}

这几行代码完成了这个函数。第一行实际上是将数字乘以3. 乘法在汇编代码中执行起来很慢,因为电路需要很长时间才能运算出答案。有时候,使用某些指令能更快的得到结果。 在这个例子中,我知道r2x3的结果相当于r2x2+r2。 将r2乘以2的操作实际上就是将二进制表示的数字左移一位。

reg,lsl #val将二进制表示的reg中的值左移val个位。这个移位操作在使用它之前就会做完。

ARMv6汇编语言的一个很有用的特性是能够在使用一个数字前将其做偏移。在这个例子中,我将r2加上了r2二进制表示下左移一行以后的结果。在汇编代码中,我们可以使用这样的技巧以便使得更容易得到计算结果,如果你觉得这样实现起来不舒服,你仍然可以使用传统的方式来实现,比如: mov r3,r2; add r2,r3; add r2,r3.

lsl reg,amt 将二进制表示的reg中的值左移amt个位置。

现在我们将r1的值左移r2的位置。大多数诸如add和sub的指令有一个使用寄存器而不是使用数值的变体。我们运行这条shift指令是因为我们需要设置管脚值所对应的比特位, 每个管脚有3个比特位。

我们接下来将计算出的函数地址存储到GPIO控制器中,我们已经在循环中计算出了地址,所以我们不再需要像在OK01和OK02中使用便移了。

str reg,[dst]和str reg,[dest,#0]作用效果相同。

最后,我们从这个函数调用中返回。 因为我们以前曾把lr放入了栈中,如果我们将pc取出, 它将拷贝上一次我们放入栈中的lr值。这和mov pc,lr这条指令执行的效果是一样的, 执行完这一行后,函数调用就返回了。

pop {reg1,reg2,…} 将值从栈顶按顺序取出,仅通用寄存器和pc才可以被pop出。

细心的家伙可以会注意到这个函数实际上是不能正运行的。尽管它设置了正确的数值和GPIO管脚给函数, 但是它导致同一个块中的10个函数的值都变成了0!因为频繁使用了GPIO管脚设置函数,会给系统带来很重的负担。这里我把练习留给你,让你来修正这个错误,使得我们在设置某一个管脚时候,不会影响到别的管脚状态,所做的改动仅限于该管脚对应的3个bit位上。这个练习的解决方案可以在本章的下载页面中找到。可能对你有用的函数包括:计算布尔值的and,用于计算布尔的与,not,计算布尔值的或,orr用于计算布尔值的或。
##4 另一个函数 现在,我们已经有了一个可以用于设置GPIO管脚函数的函数。我们现在需要一个可以用于设置GPIO管脚导通和断开的函数。与其写两个函数来完成这两个功能,不如用一个函数来控制两种状态。

我们将创建一个叫SetGpio的函数,这个函数的第一个输入参数是r0,用于记录GPIO的管脚号,第二个参数用于记录该管脚的取值。如果取值为0,我们将管脚断开,如果是非0值,我们将这个管脚导通。

在文件’gpio.s'的末尾拷贝和粘贴进下列代码:

	.globl SetGpio
	SetGpio:
	pinNum .req r0
	pinVal .req r1

alias .req reg 设置了寄存器的别名为reg。

我们又一次使用了.globl命令用于将这个函数设为全局可访问(可以从其他文件中访问)。这次我们要设置一个寄存器的别名。寄存器别名允许我们使用一个更容易记忆的名字,而不是直接使用晦涩难懂的r0或是r1。现在看起来设置别名不重要,但是在写大的函数体的时候你会发现这一点很有用。现在开始你可以试着使用别名了。 pinNum .reg r0意思是pinNum现在在代码中表示r0。

在上述代码的基础上,拷贝和粘贴下列代码:

	cmp pinNum,#53
	movhi pc,lr
	push {lr}
	mov r2,pinNum
	.unreq pinNum
	pinNum .req r2
	bl GetGpioAddress
	gpioAddr .req r0

.unreq alias 将移除别名alias.

和SetGpioFunction类似,函数开始的第一件事情就是用来检查管脚是否是一个有效值。 我们将首先比较pinNum(r0)值和53, 如果管脚值大于53,函数将立刻返回。 既然我们需要调用GetGpioAddress函数,我们需要在栈中保存lr的值,并将pinNum(r0)的值保存到r2。接着我们使用.unreq声明来移除关于r0的别名。因为管脚取值现在被存储在r2中,我们需要别名映射到它,所以我们在移除r0的同时,新建了一个指向r2的别名。一旦我们不再使用变量别名,第一件事就是将它移除掉,这样在之后的代码中你就不会引用到一个不再使用的别名了。

接着我们调用GetGpioAddress,并创建一个指向r0的别名来表示它。

在上述代码的基础上,拷贝和粘贴下列代码:

	pinBank .req r3
	lsr pinBank,pinNum,#5
	lsl pinBank,#2
	add gpioAddr,pinBank
	.unreq pinBank

lsr dst,src,#val 将二进制形式表示的数字src右移val个位置,并将结果保存在dst中。

GPIO控制器有两个4比特的序列用于控制管脚的导通与截至。第一个序列控制着前32个管脚的状态,第二个序列则控制着剩下的22个。为了确定我们需要哪个序列,我们需要将管脚除以32。除法在这里非常容易,就等同于将二进制格式表示的管脚值右移5个位。 因此,在这个例子中将r3命名为pinBank,并将pinNum/32的值存入其中。因为序列的大小是4个Byte,要得到地址,则需要将pinBank的值乘以4。乘以4这个操作等同于将二进制表示的值左移两个位,这就是lsl pinBank,#2这条指令做的事。你可能会奇怪,为什么我们非要先右移后左移,先右5后左2干嘛不直接右3?答案是如果直接右移3位将不会工作,因为在执行/32的时候,有些值会被截断。而单纯除以8则达不到这个效果。

现在就诶过是gpioAddr会被设置为,如果管脚是0-31时值为2020000016, 如果管脚是32-53时则是2020000416。 这意味着如果我们加上2810,我们将会把管脚导通,加上4010则会把管教截至。我们现在可以不用使用pinBank了,所以用.unreq将其注释掉。

在上述代码的基础上,拷贝和粘贴入下列代码:

and pinNum,#31
setBit .req r3
mov setBit,#1
lsl setBit,pinNum
.unreq pinNum

and reg,#val 计算出reg中数字和val的布尔与运算的结果。

函数的下一部分是生成正确的含有位偏移的数字。为了让GPIO控制器打开和关闭一个管脚,我们需要用该管脚值除以32后得到的余数去设置对应的值。例如,要设置第16个管脚,我们需要在数字的第16位设置位1.为了设置管脚45为1,我们需要将第13位设置为1,原因是因为45/32的余数是13。

与运算指令and可用于计算余数。对两个数进行and运算的结果是只有当两个数的某个位都为1才为1,否则就为0. 这是一个很基本的二进制运算,速度非常快。这里我们给出的输入值是pinNum and 31(10进制的31) = 11111(二进制)。这意味着只有最后5个位置的值会被计入最终结果,因而所得的结果肯定在0~31之间。具体来说,只有为1的位会被计入结果,和除以32的结果是一样的,这不是巧合,因为31=32-1。

binary

接下来的代码使用这个值来将1左移相应的位置。这就创建出了我们所需要的特定的二进制值。

在现有代码的基础上拷贝和粘贴上以下代码:

teq pinVal,#0
.unreq pinVal
streq setBit,[gpioAddr,#40]
strne setBit,[gpioAddr,#28]
.unreq setBit
.unreq gpioAddr
pop {pc}

teq reg,#val 检查reg中的数字是否等于val。

上几行代码是函数的结束。正如前面指明的,如果pinVal的值为0,我们就将管脚断开了,否则意味着管教导通了。teq( test equal,测试相等)是另一条比较指令,只能用于测试相等。它和cmp相似,但是不用用于比较哪个数字更大。如果你要做的只是比较两个数字是否相等,你可以直接使用 teq指令。

如果 pinVal的值为0,我们将调用setBit在GPIO基准地址40, 这能将管脚设为断开状态。否则我们将其放置于GPIO基准地址28的地址,这能将管教置为导通。最后,我们通过从栈中压出之前保存的值,从而返回。
##5 新的开始 最后,我们终于拥有了自己的GPIO函数。现在我们修改’main.s'以使用它。因为’main.s'现在变得越来越大,结构也越来越复杂,我们最好将它分成两个部分。‘init'部分我们已经使用过,保持它越小越好。以便我们将来修改的方便,修改完立马就可以看到结果。

将下列代码插入到main.s文件中_start: 之后:

b main

.section .text
main:
mov sp,#0x8000

我们这里的主要修改在于引入了.text部分。在我的makefile和链接脚本中,.text部分(即默认部分)中的代码会被放置在.init部分之后,位置在800016。这是默认的加载地址,给了我们用于存储栈的空间。因为栈存在于内存中,它需要有一个地址。栈在内存中从上向下增长, 这样每一个新插入的值都拥有最低的地址,这就是栈的"栈顶”, 越靠近栈顶,地址越低。

oslayout

图中的 ‘ATAGs'部分存储了Raspberry Pi硬件相关的信息,譬如,板载内存大小,默认的屏幕分辨率,等等。

将上一章中用于设置GPIO管脚的代码换成下列代码:

	pinNum .req r0
	pinFunc .req r1
	mov pinNum,#16
	mov pinFunc,#1
	bl SetGpioFunction
	.unreq pinNum
	.unreq pinFunc

以上代码调用了SetGpioFunction函数,参数为管脚号16,管脚函数名为1.这使得该管脚置为输出状态。

把上一章中用于点亮OK LED的代码替换为下列代码:

pinNum .req r0
pinVal .req r1
mov pinNum,#16
mov pinVal,#0
bl SetGpio
.unreq pinNum
.unreq pinVal

上述代码调用SetGpio以关闭GPIO管脚16。,从而点亮OK LED。如果我们使用mov pinVal, #1,那么LED将熄灭。把你之前用于点亮LED的代码换成上述代码。
##6 前瞻 希望到现在,你可以在Raspberry Pi上测试完上面写完的代码。这一章我们写了很大一块代码,免不了我们会碰到各种乱七八糟的错误。如果你遇到了,请参考troubleshooting页面。

如果你使用本章代码成功点亮了你的OK LED,恭喜你!对比于烤派宝典2之OK02而言,我们所做的事情效果一样,但是我们接触到了许多新的概念,譬如函数和格式,我们还学会了如何写出更块的函数的特性。现在稍微修改修改我们代码,指向不同的GPIO寄存器,就能让我们的操作系统更为完善,通过控制不同的GPIO寄存器,我们可以自由控制任何想控制的硬件。

在下一章OK04中,我们将引入更为精确的定时函数,这样我们可以更好的控制LED,最终我们将玩转所有的GPIO口。

烤派宝典第二章之OK02

#烤派宝典第二章之OK02 OK02 这一章建立在OK01的基础上,将反复点亮/熄灭’OK’或’ACT’ LED。我们假定你已经有了第一章烤派宝典第一章之OK01提到过的操作系统知识作为基础。

内容
1 等待
2 整合

###等待 出乎意料的是,等待在操作系统开发里是一个非常有用的特性。通常操作系统发现自己无事可做时,会选择等待。在我们的例子里,我们需要让LED一闪一灭。如果你只是把管脚状态置为on或是off,那么LED在我们视线里依然是可见的,因为计算机在一秒内会成千上万次的开关该GPIO端口。在以后的章节里我们会涉及到精准定时,但是现在为了效率起见,我们可以简单的把CPU的时间浪费掉。

	mov r2,#0x3F0000
	wait1$:
	sub r2,#1
	cmp r2,#0
	bne wait1$

上述的代码简单的通过计算一个大数的减法创建了一个延迟,得益于每一块Raspberry Pi基本上都相同,上面得到的延迟时间也差不多。上述代码可以简单解释为,使用mov指令把十六进制数0x3F0000传递给r2,然后对此数执行减1运算,直到变为0。这里引入了3个新指令分别是sub,cmp和bne。

sub是减法指令(substract)的简写,从第一个参数中减第二个参数的值。

sub reg,#val 意思是从reg存储的值中减掉val大小。

cmp是一条有意思得多的指令。它比较第一个参数和第二个参数的值,将比较的结果存入到一个特殊的寄存器中,这个寄存器叫CPSR寄存器,意思是当前处理器状态寄存器(Current Processor Status Register)。你不需要太担心这个,简单的说,她就是能记住两个数字谁比谁大或是谁比谁小,或是相等[^1]。

cmp reg,#val 意思是把寄存器reg中的值和数值val做比较。比较的结果在CPSR中。

bne事实上是分支命令的变种。在ARM汇编语言家族里,所有的指令都可以条件执行。这也就是说,指令只有在上一条比较指令有确切的结果才会被执行到。以后的教程中我们将频繁的在有趣的场合使用到它,但现在我们在b指令后加上ne代表只有上一次比较的结果不相等时候才会执行bne后的指令。ne后缀可以在任意命令后被使用,有多达16个条件可以供选择,比较常用的有eq(用于判断是否相等),和lt(小于)。

ne后缀使得该条命令只会在上一次比较的结果不等时才会被执行到。

###整合 我以前提起过LED状态可以通过写入GPIO控制器后28位移的地址置为开启状态,也可以通过写入GPIO控制器后40位移的地址置为关闭状态。据此你可以修改OK01中的代码,首先把LED点亮,而后运行等待代码,再将其关闭,再次运行等待代码,而后调用分支代码回到初始状态,以继续循环。需要注意的是,我们不需要再次激活GPIO16口,激活一次就足够了。如果你追求高效的话,我强烈建议你重用r1的值。在所有的课程里,你都可以在下载页面找到完整的解决方案。注意你需要保证在你的代码里,标号都是唯一的。如果你已经定义了wait1$,那么你不能再将另一行也定义为wait1$。

在我的Raspberry Pi上,闪烁频率大约是一秒钟两次。闪烁频率可以很方便的通过改变r2的初始值来调节。然而,不幸的是我们现在还不能非常精确的控制LED灯的闪烁频率。如果你没有得到预期的结果,请翻阅troubleshooting页,如果你成功了,恭喜你!

本章中我们学习了两个汇编指令,sub和cmp,还领略了ARM中的条件执行方法。

下一章烤派宝典之OK03中,,我们将评估我们编程的方式,我们将建立我们的代码规则,以便我们可以重用代码,如果可以的话,我们将同时使用使用C/C++代码来工作。

烤派宝典第零章-介绍

#第零章 介绍 本章为介绍环节,不包含实战环节,主要用于介绍一个操作系统所涉及到的基本概念、什么是汇编语言,以及其他重要的基础性概念。如果你想一步到位直接进入实战环节,你可以忽略掉这一章。

内容
1 操作系统
2 汇编语言

##1 操作系统 操作系统是非常复杂的程序。它负责协调计算机上的程序,包括共享计算机时间、内存、硬件和其他资源。你可能听说过的计算机上的大型桌面操作系统包括GNU/Linux, Mac OS X和Microsoft Windows。 其他设备,譬如手机上可能也使用操作系统,譬如Android, iOS和Windows Phone等1

既然操作系统需要直接和计算机系统的硬件打交道,那它就必须了解系统的硬件配置。为了让系统能运行于不同的计算机平台上,驱动程序的概念被引入。所谓驱动,就是一小段程序,可以被操作系统所调用和挪除,以使得操作系统可以直接和特定的硬件进行“对话”。 在本课程中,我们不会涉及到太多如何创建这种可卸载的驱动程序的话题,我们将专注于Raspberry Pi平台本身。

操作系统设计博大精深,市面上已经有多种不同的设计理念,这门课程只能触及其表面。本门课程里我们只关注于让操作系统通过各种特定的硬件位来直接操作硬件,这通常是最棘手的做法,因为参考文档只能来自于有限的文档,网上也很难找到现成的帮助。

##2 汇编语言 本课程仅使用汇编语言。汇编语言是最接近机器底层的语言,也最容易被机器所理解。计算机工作的原理其实是源自于存在一个叫处理器的小设备用于时刻不停地执行简单的工作,譬如加数操作,同时有一系列或多系列的被称之为RAM(内存)的集成电路用于存储运算数字。计算机加电后,处理器运行传给它的一系列指令,这一系列指令将导致内存中存储的数字发生变化,以便和连接的其他硬件相交互。汇编代码就是被翻译成人类可读格式的这些指令的文字。

通常我们使用编程语言来编程,譬如C++,Java,C#,Basic等语言,程序员撰写的程序通过编译器被转换为汇编语言,接着被转换为二进制代码2。二进制代码是机器能理解的语言,但是不具备可读性,无法被人类理解。汇编语言就好多了,但令人沮丧的是它只有有限的条数,很难被扩展。我们需要记住一个原则:任何一个你输入的汇编指令都能被处理器直接理解,因为汇编指令设计时遵循了精炼的原则,物理电路必须理解并处理每一条指令。

compiler

和普通编程语言一样,有许多不同的汇编语言。而与普通编程语言不同的是,汇编语言的多样性是因为存在很多种不同架构的处理器,每种处理器只能理解特定的语言。因而用一种汇编语言代码编写出的程序在另一种机器体系上就完全不能被执行。在大多数情况下,不能重用代码简直是一场灾难,因为为该种机器体系编写的程序都需要被重写,但引入操作系统后问题就变得容易多了,很多操作系统都是用C/C++编写的,它可以很容易被移植到不同的硬件平台上,只有完全用汇编语言编写的程序部分才需要完全重写。

现在你已经完成了第0课,可以挪步到第一章: OK01了。


  1. 详细的操作系统列表可以参阅: List of operating systems - Wikipedia, the free encyclopaedia ↩︎

  2. 这里我简化了关于通用编程的解释,事实上编程重度依赖于编程语言和机器平台。如果你感兴趣,你可以参考: [Compiler - Wikipedia, the free encyclopedia](Compiler - Wikipedia, the free encyclopedia)。 ↩︎

烤派宝典(序)

#烤派宝典-操作系统开发 欢迎来到烤派宝典之操作系统开发!这门课由Alex Chadwick撰写,当前版本为V1.0c(2013年7月)。

本宝典将指引你在Raspberry Pi上开发出一个非常基本的操作系统!本宝典主要针对16岁以上读者,当然对未满16岁的小朋友我们也未作访问限制,你可以在助手的引导下访问本网站。适当时候本课程将增加更多的内容。

本宝典将指引你用汇编语言构建出一个基本的操作系统。我们假定手持本宝典的诸位大侠都是初出江湖之辈,之前从未接触过操作系统开发和汇编语言的知识。有编程经验固然可以让你如虎添翼,但是本宝典的设计门槛应该是针对一穷二白完全没有经验的读者。Raspberry Pi论坛中藏龙卧虎,遍布着热心之极的人们,一个个望穿秋水般等着随时给予你以援手,所以当你碰到问题时,尽管去提问好了。本宝典被划分为一系列“章节”,按顺序列举如下。每一“章节”里在介绍理论的同时也将给出一些实际的练习,双管齐下助你事半功倍多快好省的掌握武功秘籍。

本宝典不会涉及到过于细节的内容,也不会顾及到关于创建操作系统知识的方方面面,本宝典中的一系列指南将关注于完成一系列小的任务。我们希望读者在通读并完成本宝典练习的最后,能融汇贯通自己所学,最终具备独立创建一个个性化的操作系统的“神功”。尽管本宝典的章节大多关注于特定的功能实现,但其中也留有大片空间供读者自行发挥。比如,在学习完关于函数的章节后,你可以构思出一种更好的汇编代码组织方式。比如,在阅读完有关图形的章节后,你可以设想下如何开发一个3D操作系统。因为本宝典是讲述操作系统的牛逼内容,你完全可以拥有设计出你喜爱的事情的能力。如果你有了灵光一闪的瞬间,抓住并实现它!计算机科学仍然是一门新兴学科,有的是空间任君遨游。

内容

1 要求
2 章节

1 要求

你需要具备下列物品以完成本宝典:一块附带有SD卡的Raspberry Pi及电源, 一台运行Linux, Microsoft Windows或Mac OS X的开发机, SD卡读写器及读写软件。如果你的Raspberry Pi能连接屏幕就更好了,当然这不是必需条件。

软件方面,你需要准备基于ARMv6的GNU编译工具链。在下载页面里你可以找到下载链接,该页面中也提供了所有练习的参考答案。

2 章节

  <th>名称</th>

  <th>描述</th>
</tr>
  <td><a href="introduction.html">介绍</a></td>

  <td>本章节不包含实战环节,主要用于介绍一个操作系统所涉及到的基本概念、什么是汇编语言,以及其他重要的基础性概念。如果你想一步到位直接进入实战环节,你可以忽略掉这一章。
  </td>
</tr>

<tr>
  <th colspan="3">OK LED 系列 (初级)</th>
</tr>

<tr>
  <td>1</td>

  <td><a href="ok01.html">OK01</a></td>

  <td>OK01 这一章内容涉及了如何起步,并将教会你如何点亮Raspberry PI开发板上的'OK'或是'ACT' LED灯,这个LED灯靠近RCA和USB口。
  </td>
</tr>

<tr>
  <td>2</td>

  <td><a href="ok02.html">OK02</a></td>

  <td>OK02 这一章建立在OK01的基础上,将反复点亮/熄灭'OK'或'ACT' LED。
  </td>
</tr>

<tr>
  <td>3</td>

  <td><a href="ok03.html">OK03</a></td>

  <td>OK03 这一章基于OK02,将教会你在汇编语言中如何使用函数,函数的引入将使得汇编代码可用性大大提升,并提升代码的可读性。
  </td>
</tr>

<tr>
  <td>4</td>

  <td><a href="ok04.html">OK04</a></td>

  <td>OK04 这一章基于OK03,将教会你如何使用定时器(timer)来精确控制'OK'或'ACT'灯的闪烁频率。
  </td>
</tr>

<tr>
  <td>5</td>

  <td><a href="ok05.html">OK05</a></td>

  <td>OK05这一章基于OK04, 将教会你如何根据摩尔斯码来控制LED的闪烁。LED的闪烁将发出SOS信号,此信号的格式如下:(...---...).</td>
</tr>

<tr>
  <th colspan="3">屏幕系列 (高阶)</th>
</tr>

<tr>
  <td>6</td>

  <td><a href="screen01.html">Screen01</a></td>

  <td>Screen01这一章讲述了一些基本的图像处理理论,我们将在这一章里,在屏幕上或是电视机(TV)上显示出一个渐变颜色的格式。
  </td>
</tr>

<tr>
  <td>7</td>

  <td><a href="screen02.html">Screen02</a></td>

  <td>Screen02这一章基于Screen01,将教会你如何在屏幕上画直线,还将教会你一个如何生成随机数的技巧。
  </td>
</tr>

<tr>
  <td>8</td>

  <td><a href="screen03.html">Screen03</a></td>

  <td>Screen03这一章基于Screen02,本章将教会你如何在屏幕上画出字符,还将介绍给你内核命令行的概念。
  </td>
</tr>

<tr>
  <td>9</td>

  <td><a href="screen04.html">Screen04</a></td>

  <td>Screen04这一章基于Screen03,将教会你如何操纵字符,并将计算出来的值显示在屏幕上。
  </td>
</tr>

<tr>
  <th colspan="3">Input 系列(高阶)</th>
</tr>

<tr>
  <td>10</td>

  <td><a href="input01.html">Input01</a></td>

  <td>Input01这一章将教给你关于设备驱动的理论,如何链接驱动程序库,比如键盘。本章过后你将可以在屏幕上看到输入的字符。</td>
</tr>

<tr>
  <td>11</td>

  <td><a href="input02.html">Input02</a></td>

  <td>Input02这一章基于Input01,将教会你如何创建一个和操作系统打交道的命令行接口。
  </td>
</tr>

Things to be done(5)

Following things should be done in recent days:

  • RaspberryPI’s Operating System make
  • How to change the default theme of octopress
  • Translation of RaspberrPI Cambridge lessons
  • Setup a repository on github which holds raspberryPI relasted code
  • How to view sound card parameters