烤派宝典第一章之OK01

TurnToJPG -->


#烤派宝典第一章之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的表达方式。。即便这样,也依然很容易让人混淆,即便是硬件工程师有时也会被绕晕! ↩︎