烤派宝典第一章之OK01

#烤派宝典第一章之OK01 OK01 这一章内容涉及了如何起步,并将教会你如何点亮Raspberry PI开发板上的’OK’或是’ACT’ LED灯,这个LED灯靠近RCA和USB口。这个LED灯最开始被命名为OK,在Raspberry Pi第二版时它被改名为ACT。

内容
1 起步
2 开始
3 第一行代码
4 使能输出
5 生命的迹象
6 从此过上幸福的生活
7 Pi登台

###1 起步 此时此刻我假设你已经访问了下载页面,并架设好了必要的GNU工具链。下载页面上有一个名为OS Template的文件,请下载并解压到一个新的目录。

###2 开始 现在你已经解压开了模板,接下来请在’source'文件夹下创建一个名为’main.s'的新文件。这个文件将包含这个操作系统的代码。特别指明一下,现在你的目录结构看起来应该如下:

	build/
	   (empty)
	source/
	   main.s
	kernel.ld
	LICENSE
	Makefile

‘.s'文件扩展名可以在所有的汇编语言格式中被使用,但是我们要时刻记住现在是在ARMv6架构中写程序。

在文字编辑器中打开’main.s'文件,开始敲入汇编代码。Rapberry Pi使用的汇编代码类型被成为ARMv6, ARMv6这就是我们需要写入的代码类型。

粘贴以下代码:

.section .init
.globl _start
_start:

敲入的这几行代码不能让Raspberry Pi做任何事,它们只是传递给汇编器的指令而已。汇编器是一个用于在人类可理解的汇编代码和Raspberry Pi能理解的二进制代码之间做转换的一个程序。在灰白你代码中,每一行都是一个新的命令。第一行告诉汇编器1在哪里放置我们的代码。我提供的模板使得.init部分的代码被放置到输出文件的起始部分。这很重要,是因为我们需要确保能控制哪一段代码应该首先被执行。如果我们不指定代码的执行顺序,以字母顺序排列的代码将第一个被运行!.section命令用于告诉汇编器将代码放入哪一部分,从这一行起直到文件结尾。

接下来的两行用于移除警告信息,完全可以忽略。2 ###3 第一行代码 现在我们真正来编点儿代码。在汇编代码中,除非被特别告知,否则计算机将一行一行的执行代码,严格按顺序执行每条指令。每条指令以一个新行开始。
粘贴入以下代码:

	ldr r0,=0x20200000

在汇编代码中,你可以插入空行,也可以在行首或行尾加上任意多个空格,这将增强代码的可读性。

这是我们输入的第一条命令。它告诉处理器,将数字0x20200000加载入寄存器r0。这里我需要回答两个问题,什么是寄存器,0x20200000是怎样的一个数字?
寄存器是位于处理器中的一小片内存,用于存储处理器当前用于工作所需的数字。处理器中有很多寄存器,它们中的很多都有着特殊的含义,之后我们将慢慢见识到。最重要的寄存器有13个(分别被命名为r0,r1,r2,…,r9,r10,r11,r12),它们被称之为通用寄存器,你可以用它们来执行任何你想让其执行的运算。由于这是我们的第一个程序,这里我使用了r0,其实我可以使用r0~12之中的任意一个。只要你愿意,你用哪个都可以。
0x20200000实际上就是个数字。然而它被写成了十六进制记数法的格式。如果你想了解更多的关于十六进制的知识,请点开下面的参考:
十六进制解释

ldr reg,=val用于将数字val加载入名为reg的寄存器中。
在Raspberry Pi上,单个寄存器可以存储0到4,294,967,295之间的任意数字,这个数看起来很巨大了,事实上不过区区32个二进制位而已。

我们的第一条命令将十六进制数0x20200000加载入了r0寄存器。这听起来似乎没什么多大用处,但事实上用处大大。计算机中,有着意想不到的大块大块的内存和设备可以用于访问。为了可以访问到这些硬件,我们需要给它们每个一个地址。就好比邮寄地址或是互联网上的域名一样,我们用地址来标明计算机可访问到的每一个硬件设备或是每一块内存。地址在计算机中就是数字,20200000这个十六进制地址恰好是Raspberry Pi的GPIO控制器的硬件地址。它由硬件制造商所决定,硬件制造商可以指定出任何一个可能的地址(前提是这个地址和别的设备地址不冲突)。我知道0x20200000这个地址是因为我阅读了硬件手册3,在地址这个话题上,没有特殊的系统(任何一个系统上它们都是大块大块的十六进制数字)。 ###4 使能输出 读完手册里的对应部分后,我们知道现在应该传递两条消息给GPIO控制器。我们需要用它的语言和它交谈,只要它理解了,它就能乖乖的照我们所指示的去做,点亮板子上的OK LED。幸运的是,GPIO控制器是个很简单的芯片,只需要几个数字就能让它明白自己该做什么。

gpio controller

	mov r1,#1
	lsl r1,#18
	str r1,[r0,#4]

这几行代码激活了第16个GPIO口的输出。首先我们往r1中存入一个值,而后将它传递给GPIO控制器。这里我们用了两条指令用于设置r1中的值,其实我们可以像前面一样使用ldr指令来一步到位设置好。但是为了以后的课程中我们能随心所欲的设置/控制任意给定的GPIO口值,我们这里稍走几步弯路,使用公式来直接推导出相应的值,而不是直接写上。OK LED连接在第16个GPIO口上,因而我们需要传送命令以激活第16个GPIO口的输出。

mov reg,#val 用于将数字val放入名为reg的寄存器中。
lsl reg,#val 用于将寄存器reg中二进制表示的数字左移val个位置。
str reg,[dest,#val] 用于将reg中的数字存储在由dest+val指定的地址中。

r1中存储的数值是用来激活LED口的。第一行将十进制的1放入寄存器r1中。mov指令要比ldr指令快得多,因为它不会涉及到与内存交互,而ldr则需要从内存中加载我们需要传递给寄存器的值。但是,mov只能被用来加载常值4。在ARM汇编语言中,几乎所有的指令都以三个字母开头。这种命名规则被称之为助记符,可用于提示该条指令的实际用途。比如mov是move(移动)的缩写,ldr则是load(加载)寄存器的缩写。mov移动第二个参数#1到第一个参数r1寄存器中。通常来说,#必须被用于表示数字,但是我们已经见到过反例了。

第二条指令lsl,被称之为逻辑左移(logical shift left)。它表示将二进制表示的第一个参数逻辑左移,移动的参数由第二个参数指定。在这里我们逻辑左移十进制表示的1(二进制表示要是1) 18个位置(结果是1000000000000000000,也就是十进制的262144)。
如果你对二进制不熟悉,你可以参考这里:
二进制解释
再提一次,我知道18这个数值,还是通过阅读硬件手册得来的3。手册上说,由GPIO控制器中共24比特的比特集合的值,以决定GPIO管脚的配置。前4个比特集与前10个GPIO口相关,第二个4比特集则与后10个GPIO口相关,依此类推。Raspberry Pi上存在一共54个GPIO口。既然我们需要定位到第16个GPIO口,我们就需要使用第二个4比特集合,因为我们所需要处理的GPIO口落在10-19这个范围内,我们需要这个范围内的第6个值,再乘以3个位(bits),所以结果就是18(18==6*3),这就解释了为什么在上面的代码中我们需要设置#18的值给r1。

最后一条指令str ‘store register'代表存储寄存器值,它将第一个参数中存储的值,即寄存器r1里的值,存储到第二个需要通过计算才得来的表达式中。这个表达式可以是一个寄存器,在我们的例子中是r0,我们知道这个值是GPIO控制器的地址,和另一个数值相加所得出的结果,在我们的例子中,这个数值是#4.这意味着我们对GPIO控制器的地址加4,并将r1中存储的数值写入了这个地址。这个被写入的地址恰恰是我在上一段中提到的,第二个4比特集合中对应的位置,就这样我们把第一条消息传递给了GPIO控制器,告诉它把第16个GPIO管脚设置为输出(output)状态。

###5 生命的迹象 现在LED已经准备好,只差一步就能点亮了,让我们来点亮它!完成这一步意味着我们需要发送一条消息给GPIO控制器以将16管脚关闭。是的,关闭。芯片制造商决定了5要将LED点亮就需要将GPIO口置为关闭(off)状态。硬件工程师似乎经常我行我素的做乱七八糟的决定,似乎只是为了让操作系统开发人员跟在他们屁股后头闻尾气。这里你需要特别注意下,千万别弄错了。

	mov r1,#1
	lsl r1,#16
	str r1,[r0,#40]

希望这时候你已经能对上面的命令熟视无睹了,不用考虑他们的值。第一条指令用于将1放入寄存器r1中。第二条指令将1以二进制的表示形式左移16位。因为我们需要将16口置为关闭(off)状态,我们需要在下一条消息的第16个位上置1,其他的值则是用于控制别的管脚。然后我们将它写入到GPIO控制器以后加上40的地址上,这个地址可以被用于写入,以控制管脚的关闭(GPIO控制器后28的地址可以将管脚置为接通状态)。

###6 从此过上幸福的生活 现在该试着结束了,但是不幸的是处理器并不知道我们该结束了。事实上,处理器是永远不会停摆的。只要它被加电,它就一直工作。既然如此,我们就需要给它一个持续执行的空任务,否则Raspberry Pi就会崩溃(在这个例子中问题不大,反正OK灯打开了就一直会亮着)。

	loop$:
	b loop

代码的第一行在这里不是指令,只是个标号而已。它标明下一行的名称为loop$。这标明在以后的代码中,我们就可以使用这个标号来引用该行了。这个标号被称之为label。标号在变成二进制代码时候会被省略,但它能方便我们随时引用需要引用的行数,或者数字(常用于标识地址)。按照惯例,我们使用$符号用于表示标号,作用范围也仅仅在当前文件中,对全局程序不会构成影响。b(branch分支)命令用于指示下一行后执行标号指定一行的内容,而不是b的当前行位置的后一行。由于使用了b,这一行代码会被反复执行,直到天长地久海枯石烂。这样处理器就被卡在一个完美的无限循环状态,直到被安全断电为止。
代码块后的新行需要特别注意。GNU工具链希望每一个代码文件都以空行结束,以确保你真正写完了改写的代码,文件也不应该被截断。如果你不多加个新行,你可能会在编译时收到汇编器给出的警告信息。

命名:标识下一行的名字为name.
b label使得下一行被执行到label所在的行数。

###7 Pi 登台 好了,代码写完了,接下来我们请出来Pi。在你的开发机上打开一个终端程序,切换到当前工作目录的父目录下,敲入make,回车。如果一切顺利,你将得到一个kernel.img文件。如果你遇到任何错误,请参考troubleshooting章节。kernel.list文件是你刚才写入的汇编代码经过编译后实际得到的行数。在将来它可以被用来检查到底你做对了哪些事情。kernel.map文件则包含了所有label的起始位置,可用于跟踪变量。
要安装你的操作系统,首先你应该准备一张已经烧写好Raspberry PI操作系统的SD卡。如果你浏览SD卡中的文件,你将看到一个名为kernel.img的文件。将它重命名为另一个名字。然后拷贝我们生成的kernel.img到SD卡的对应位置下。这意味着我们把Raspberry Pi上的操作系统换成我们刚刚写出来的操作系统了。如果要用回原来的操作系统,我们只需要删除自己的kernel.img文件,换回刚才备份好的那个文件就好。我发现随时保留一个原始 Raspberry Pi操作系统的镜像是很有必要的,这样我们随时可以回滚到安全的状态下。
将SD卡插入Raspberry Pi,加电。OK LED灯现在应该亮起来了。如果没有亮,请查阅troubleshooting页面。如果成功了,恭喜你,你已经写出了你的第一个操作系统。接下来你可以学习烤派宝典第2章之OK02,这一课将教会你如何让LED一闪一灭。


  1. 好吧,关于汇编器这里我撒了点小谎,事实上应该是linker,链接器,链接器是用于将诸多汇编文件链接为一个可执行文件的一段程序。但这里撒个小谎无伤大雅。 ↩︎

  2. 既然你点开了这个,说明了解这个Warning信息对你很重要咯!由于GNU工具链主要用于创建程序,它希望标签_start始终是程序的切入点。在我们正在创建的这个系统中,_start总是被我们认为是至高无上的入口点。如果我们不显式指出程序的入口点,编译链可能会觉得失落,并给出来一点小小的抱怨(warning信息)。因此第一行我们定义一个名为_start的符号,并使其全局可见(globally),第二行指明_start的符号的地址实际上是指向下一行,我们很快就能到达这个地址。 ↩︎

  3. 本指南意在减轻你阅读硬件手册的痛苦,然而你非要自讨苦吃的话,你可以在 SoC-Perpherals.pdf这个pdf中找到这个地址。可能是为了增加读者的疑惑程度吧,手册中使用了一套不同的寻址系统。手册里的0x7E200000在我们的操作系统中事实上是0x20200000。译者注:从0x7e000000到0x20000000的映射其实是Broadcom所定义的memory map. ↩︎

  4. Only values which have a binary representation which only has 1s in the first 8 bits of the representation. In other words, 8 1s or 0s followed by only 0s. ↩︎

  5. 一位好心的硬件工程师作出了以下的解释:原因在于现代的芯片大多采用CMOS工艺制造,CMOS的全称是(Complementary Metal Oxide Semiconductor,互补式金属氧化物半导体)。互补意味着每一个信号被连接到两个晶体管上,其中一个的制作材料是N型半导体,主要用于拉低至低电压,而另一个则由P型半导体制作而成,用于拉升到高电压。同一时刻这一对晶体管只能有一个能处于导通状态,否则我们就会得到一个短路电路。P型的材质和N型不同,P型导通需要3倍大的电流。这就是为什么LED经常被接到拉低的一端而不是提升的一端,因为N型拉低比P型拉高要容易得多。还有另外一个原因。回到1970年代,芯片几乎全部都是由N型材料(‘NMOS’)制作而成,P型则被替代为晶体管。这意味着一个信号被拉低时芯片才开始消耗能量(并且会发热),尽管没做任何事情,只要拉低它就开始耗能。设想下如果你的手机放在兜里啥也不做就开始耗电、发热一定会搞得你很不爽。因而信号都被设置为“低电平有效”,因此它们平时处于高位,不过被激活时是绝对不会消耗电能的。即便工程师们后来不再使用NMOS了,拉低电平通常也比提升电平要容易得多。通常一个“低电平有效”的信号会在名字上方加一横杠,或者写成SIGNAL_n或是/SIGNAL的表达方式。。即便这样,也依然很容易让人混淆,即便是硬件工程师有时也会被绕晕! ↩︎

烤派宝典第三章之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>