烤派宝典第五章之OK05

#烤派宝典第五章之OK05 OK05这一章基于OK04, 将教会你如何根据摩尔斯码来控制LED的闪烁。LED的闪烁将发出SOS信号,此信号的格式如下:(…—…).我们假设你之前已经拥有了烤派宝典第四章里的所有代码作为基础。

内容
1 数据
2 你享受乐趣时,光阴似箭
###数据
迄今为止,我们所传递给Raspberry Pi的都是单纯的指令。 然而某些时候,指令只是故事的一般。我们的操作系统需要数据。

有些早期的操作系统不允许在文件中包含数据,这让使用者觉得很受限制。现代的方式让应用程序有更大的灵活性。

通常来说数据指的是重要的数值。你大可把数据想象成一个给定的类型,比如,一个包含有文字的文本文件,一幅含有图像信息的图像文件,等等。事实上这只是概念上的差别而已。计算机中的所有数据都是二进制数,我们和它们打交道的方式就是通过数数字。在本章的例子中,我们要储存的数据是一段用于控制闪烁节奏的数据。

在’main.s'文件的最后拷贝入下列代码:

	.section .data
	.align 2
	pattern:
	.int 0b11111111101010100010001000101010

数据和代码的区别在于,我们把所有的代码都放在.data段。我已经在操作系统内存布局图中描述了这一点。这里我把数据段(data)放在代码的最结尾位置。把代码和指令分开放置,以便于我们最终能在操作系统中实现安全机智,我们需要知道代码的哪些部分是可以执行的,哪些是不可以被执行的。

我在这里使用了两个., .align和.inig。 .align确保下列字节以2的2次方对齐。在这个例子中,我使用的就是2的2次方对齐。.align 2意味着数据肯定是在2的2次方,即可整除4的内存位置。我们需要特别注意这一点,因为用于从内存中读取内容的ldr指令之工作在能整除4的内存位置。

.align num确保下一行地址可以整除2的num次方

.int命令把其后带的常量直接拷贝到输出。这以为者11111111101010100010001000101010将会被放入到输出,因而标签形式事实上把这部分数据标注成了标签。

.int val输出数字val

正如我以前所提及的,数字可以包含任何你想要的东西。在本例中我们把摩尔斯电码所表示的SOS序列,即…—…用这些方式表达出来。我使用0以代表LED熄灭的时间,用1代表LED点亮的时间。这样一来,我们就可以写出用于表示时间序列的代码,接下来我们要的就是改变数据以显示不同的显示方式了。这是一个很简单的例子,可以用来说明操作系统在任何时候应该做什么;交互和显示数据。

这里有个挑战就是找出有效而游泳的表达方式。我们这种用于表达开/关状态的存储序列的方式运行起来很简单,但是很难编辑,因为我们无法在0和1的组合中体现出摩尔斯代码的-或.的含义。

拷贝下面的代码行到’main.s'中,代码应该放置在 loop$ 标签之前:

	ptrn .req r4
	ldr ptrn,=pattern
	ldr ptrn,[ptrn]
	seq .req r5
	mov seq,#0

上面的代码将闪烁的样式加载到寄存器r4中,同时把r5的值加载为0. r5将用于表征我们的序列位置,以便我们可以跟踪我们已经运行完了多少个样式。

下面的代码则是将一个非0值加载到了r1中,仅限于当前的样式部分为1时。

	mov r1,#1
	lsl r1,seq
	and r1,ptrn

代码作用于你调用SetGpio时候,这时候必须给定一个非0值以熄灭LED,给定0值以点亮LED。

现在修改你代码中的’main.s’,以便代码根据当前序列的值设置LED的亮/灭,而后等待250000微秒(或是其他你觉得合适的延迟时间),接着运行下一个序列中的样式。当序列记数达到32时,需要返回到0. 你可以试一下,看你是否可以自己实现之,另外再添加一点额外的挑战,看你是否能用仅仅一条指令实现之(解决方案在下载页面中)。

###你享受乐趣时,光阴似箭 现在你可以在Raspberry Pi上测试这一章的例子了。它应该先短闪烁3次,然后再来3次长闪烁,接着再来3次短闪烁。再经过一段时间后,闪烁格式会重复。如果它没有工作,请参阅troubleshooting页面。

如果它正常工作的话,恭喜你,你已经完成了OK系列指南的所有部分!

在这个系列中,我们主要学习了汇编语言、GPIO控制器、系统定时器。我们学习了函数和ABI的概念,我们还杰出了许多基本的操作系统的概念,最后还接触了数据的概念。

现在你可以移步到更为高阶的话题中了。

  • 下面我们要提到的Screen系列将教会你如何在汇编语言中使用屏幕。
  • Input系列将教会你如何使用键盘和鼠标。

现在你已经活得了很多关于操作系统相关的消息,也学习到了和GPIO口打交道的一种最基本也是最直接的方法。如果你手头有机器人制作套件,你可以试着写一个机器人上的操作系统,用它来控制机器人的GPIO口!

烤派宝典第六章之Screen01

#烤派宝典第六章之Screen01 欢迎来到Screen教程系列。在这一系列教程中,你将学会如何在汇编语言中控制Raspberry Pi的屏幕, 如何开始显示随机数据,然后我们将学会如何显示一个固定的图像,显示文字,如何将数字转换为文字。 我们假设你已经完成了之前的OK系列,OK系列里提到过的基本概念在本教程中不会再被提及。
Screen01这一章讲述了一些基本的图像处理理论,我们将在这一章里,在屏幕上或是电视机(TV)上显示出一个渐变颜色的格式。

内容
1 开始
2 计算机图形
3 邮递员编程
4 我最心爱的图像处理器
5 在一个框架内的连续像素
6 曙光初现

###1. 开始 我们希望你已经完成了OK系列,OK系列中已经完成的’gpio.s'和’systemTimer.s'文件中所撰写的函数会在本系列中被重复调用。如果你还没有这些文件,或者说你更愿意使用标准的参考文件,你可以从下载页面中下载到OK05的解决方案模板。 ‘main.s'文件同样是有用的,但是你需要把mov sp,#0x8000之后的内容删除。
###2. 计算机图形 正如你所希望看到的,在很原始的层面上来看,计算机其实是很愚蠢的。它们的指令集是有限的,几乎不会解数学题目。但是不知为何它们拥有近乎无所谓不为的能力。我们目前所需要了解的,计算机如何在屏幕上显示出图像?我们如何将问题翻译成二进制而后扔给计算机去解决?答案非常简单,对于每一种颜色,我们为它编上一个唯一的编号,然后我们在计算机屏幕上的每一个点都储存上这个编号中的一个值。像素就是你屏幕上的这样一个小点,如果你凑近了看,你可能会在自己的屏幕上看到这样的点,你可以看到电脑上的每一幅图像,都是由这样的点的集合所组成的。

随着计算机时代的演进,人们需要越来越复杂的图形,所以显卡被发明出来。显卡可以看作是你的计算机的第二个处理器,它仅仅负责在屏幕上画图。它的主要职责是将像素值信息转换为被送到屏幕上的光强度信息。 在现代计算机中,显卡能干的活儿远远不止这些,比如绘制3D图像也是它的拿手好戏。然而在本教程中,我们之关注于显卡的初级使用;从内存中获取到像素信息,输出到屏幕上。

有很多种用数字来表达颜色的系统,这里我们使用RGB系统,但是HSL是另一种广泛被使用的系统。

要注意到的一点是我们系统所使用的色彩编号系统。有很多种选择,每种选择将输出不同质量的图像。为了有助于你的完整理解,我在这里列举出了一些选择。

尽管这里的图像有很多种颜色,它们都使用了一种被称作空间抖动的技术。这使得它们能用很少的颜色表现出绝佳的图像效果。许多早期的操作系统中都运用了这种技术。

  <th>单色</th>

  <th>描述</th>

  <th>例子</th>
</tr>
  <td>2</td>

  <td>使用一个位来存储每个像素点,1代表白色,0代表黑色
  </td>

  <td><img alt="鸟-单色" src=
  "/images/colour1bImage.png"></td>
</tr>

<tr>
  <td>灰阶</td>

  <td>256</td>

  <td>使用1byte来表示每个像素点,有255中表示白色的方式,0代表黑色,0~255之间的任意数字都代表两者的线性组合
  </td>
  <td><img alt="鸟-灰阶" src=
  "/images/colour8gImage.png"></td>
</tr>

<tr>
  <td>8位色</td>

  <td>8</td>

  <td>使用3个位来存储每个像素点,第1个位代表红色通道的值,第2个位代表绿色通道的值,第3个位代表蓝色通道的值。
  </td>

  <td><img alt="鸟-八位色" src=
  "/images/colour3bImage.png"></td>
</tr>

<tr>
  <td>低彩色</td>

  <td>256</td>

  <td>使用8个位来存储每个像素点的信息,前3个位代表红色通道的强度,接下来的3个位代表绿色通道的强度,最后2个位代表蓝色通道的强度。
  </td>

  <td><img alt="鸟- 256色(低彩色)" src=
  "/images/colour8bImage.png"></td>
</tr>

<tr>
  <td>高彩色</td>

  <td>65,536</td>

  <td>使用16个位来存储每个像素点的信息,前5个位用于表示红色通道的强度,接下来的6个位用于表示绿色通道的强度,最后的5个位用来表示蓝色通道的强度。
  </td>

  <td><img alt="鸟- 高彩色" src=
  "/images/colour16bImage.png"></td>
</tr>

<tr>
  <td>真彩色</td>

  <td>16,777,216</td>

  <td>使用24个位来存储每个像素点的信息,前8个位用于表示红色通道的强度,接下来的8个位用于表示绿色通道强度,最后的8个位用于表示蓝色通道的强度。
  </td>

  <td><img alt="鸟 - 真彩色" src=
  "/images/colour24bImage.png"></td>
</tr>

<tr>
  <td>RGBA32</td>

  <td>16,777,216(256个透明度级别)</td>

  <td colspan="2">使用32个位来存储每个像素点的信息,前8个位用于表示红色通道的强度,接下来的8个位用于表示绿色通道的强度,第3组8个位用于表示蓝色通道的强度,最后的8个位用于表示透明度通道。同名读通道通常用于在一幅图片上绘制另一幅图片,0用于代表后置图片的颜色,255代表当前图片的颜色,所有其之间的值代表两者的组合。
  </td>

  <td></td>
</tr>

在本指南中我们将使用高彩色。从上面的图片中我们可以看到,高彩彩色已经相当清晰,图片质量也不错,对比于真彩色,它不会占据太大的空间。也就是说,显示一张800x600像素大小的图片,它只会占据大概1M byte的空间。它还有个优势是,它是2的幂次方的大小,对比于真彩图片,很容易得到其信息。

Raspberry Pi上的图形处理器有一点点怪异。在Raspberry Pi上,图形显示器最先运行,并负责启动主处理器(中央处理器)。对比于正常的操作系统,这有点“不太正常”。 虽然最终这种启动方式不会造成任何差别,但总给人感觉就是中央处理器被沦落为了第二处理器,图形处理器有喧宾夺主的感觉。Raspberry Pi的中央处理器和图形处理器之间的通信方式被称之为“邮箱(mailbox)”. 这两个处理器都可以给另一个处理器发送邮件,而对段处理器在收到邮件后将在可能的时间点处理之。我们可以使用邮箱机制从图形处理器取得一个地址。这个地址就是我们需要将色彩像素信息写在屏幕上的位置,被称为Frame Buffer, 显卡通常会检查这个位置,并从这里取得信息用于直接更新屏幕。

存储Frame buffer会给计算机带来沉重的内存负载。基于这个原因,早期的计算机经常会使用一些欺诈的伎俩,比如,储存一个满是文字的屏幕,但是每次仅仅更新单个需要更新的字符。
###3. 邮递员编程 有关图形编程我们第一个要接触的就是‘邮箱’这个概念。邮箱只有两个方法需要注意,一个是MailboxRead, 用于从位于r0的邮箱通道里读出来一条消息。MailboxWrite,用于将r0中的前28个二进制位直接写入到位于r1的邮箱通道。Raspberry Pi有7个邮箱通道,可用于连接图像处理器,我们只能使用第一个通道,因为它直接和Frame Buffer交互。

下面的表格和图标描述了有关邮箱的操作。

  <th>大小/ Bytes</th>

  <th>名称</th>

  <th>描述</th>

  <th>读/ 写</th>
</tr>
  <td>4</td>

  <td>Read</td>

  <td>接收邮件.</td>

  <td>R</td>
</tr>

<tr>
  <td>2000B890</td>

  <td>4</td>

  <td>Poll</td>

  <td>接收邮件但不取回.</td>

  <td>R</td>
</tr>

<tr>
  <td>2000B894</td>

  <td>4</td>

  <td>Sender</td>

  <td>发送信息.</td>

  <td>R</td>
</tr>

<tr>
  <td>2000B898</td>

  <td>4</td>

  <td>Status</td>

  <td>信息.</td>

  <td>R</td>
</tr>

<tr>
  <td>2000B89C</td>

  <td>4</td>

  <td>Configuration</td>

  <td>配置.</td>

  <td>RW</td>
</tr>

<tr>
  <td>2000B8A0</td>

  <td>4</td>

  <td>Write</td>

  <td>发送邮件.</td>

  <td>W</td>
</tr>

消息传递是组件间通信的一种很常见的方式。一些操作系统使用虚拟的消息传递机制以允许应用程序之间的相互通信.

发送消息给特定的邮箱步骤如下:

  1. 发送端必须等待状态寄存器的最高位为0.
  2. 发送端写入Write寄存器, 第4个二进制位代表要写入的位置,高28个二进制位代表要写入的信息。

从邮箱读取一条信息的步骤如下:

  1. 接收端等待状态寄存器的第30个二进制位变为0.
  2. 接收端从Read寄存去读取。
  3. 接收端确认消息是从正确的邮箱读出的,如果不是,则重试。

如果你足够自信的话,你现在已经拥有足够的信息可以用于写出邮箱读/写函数了,如果你觉得还不够,那么请接着往下读。

还是那句话,我推荐你一开始就把邮箱区域的取寄存器地址函数实现掉: .globl GetMailboxBase GetMailboxBase: ldr r0,=0x2000B880 mov pc,lr 发送流程最为简单,所以我们一开始就把它实现掉。随着你的函数越来越复杂,你需要开始计划如何组织它们。一个好的方式是实现将其实现步骤列举出来,越详细越好,比如下面的:

  1. 我们的输出需要写入的寄存器是(r0), 邮箱需要写入的寄存器是(r1)。 我们需要检查邮箱地址是否有效,低4个二进制位的值是否是0,永远不要忘记校验输入值。
  2. 使用GetMailboxBase函数取得mailbox的地址。
  3. 读取状态寄存器的位。
  4. 检查最高位是否是0,如果不为0,则返回第3步。
  5. 组合值,写入邮箱通道。
  6. 写入Write寄存器。

让我们挨个来实现之:
1.

	.globl MailboxWrite
	MailboxWrite: 
	tst r0,#0b1111
	movne pc,lr
	cmp r1,#15
	movhi pc,lr

以上代码实现了关于r0和r1寄存器的校验。 tst用于比较两个数字,通过对两个数字的逻辑与(and)运算, 并将结果与0作比较。 在这个例子这哦你哦个我们检查寄存器r0的低4位是否全为0。

tst reg,#val计算reg和#val作与运算的值,将其结果与0做比较。

2.

	channel .req r1
	value .req r2
	mov value,r0
	push {lr}
	bl GetMailboxBase
	mailbox .req r0

上述代码确保调用函数时我们不会覆盖已有的寄存器的值和lr寄存器的值, 之后我们开始调用GetMailboxBase。
3.

	wait1$:
	status .req r3
	ldr status,[mailbox,#0x18]

上述代码将加载入当前的状态。
4.

	tst status,#0x80000000
	.unreq status
	bne wait1$

上述代码将检查状态寄存器的最高位是否为0,如果不是,则返回步骤3.
5.

	add value,channel
	.unreq channel

上述代码将通道值和值做组合。
6.

	str value,[mailbox,#0x20]
	.unreq value
	.unreq mailbox
	pop {pc}

上述代码将结果写入到write寄存器中。

MailboxRead的代码也是类似的。

  1. 我们的输入值为需要读取的邮箱值(r0)。 我们需要实现校验之,以确定它是真实的邮箱地址。永远记得在函数调用前要校验输入值。
  2. 调用GetMailboxBase函数用于取回地址。
  3. 读取状态寄存器标志位。
  4. 检查第30个二进制位是否是0,如果不为0,则分支跳转回3.
  5. 从Read寄存器对应的地址开始读取数据。
  6. 检查mailbox是否是我们需要的那个,如果不是,则返回第3步继续执行。
  7. 返回结果。

让我们按次序来实现之:
1.

	.globl MailboxRead
	MailboxRead: 
	cmp r0,#15
	movhi pc,lr

上述代码用于校验r0,是否是通道0~16之间,事实上我们只能使用一个mailbox通道。
2.

	channel .req r1
	mov channel,r0
	push {lr}
	bl GetMailboxBase
	mailbox .req r0

上述代码确保我们在调用GetMailboxBase函数时不会覆盖通用寄存器和lr寄存器值。
3.

	rightmail$:
	wait2$:
	status .req r2
	ldr status,[mailbox,#0x18]

上述代码将载入当前的状态寄存器值,现在你可以在r2中看到当前的状态寄存器值。
4.

	tst status,#0x40000000
	.unreq status
	bne wait2$

上述代码用于检查状态寄存器的第30个二进制位是否为0,如果不是,则返回到第3步继续等待。
5.

	mail .req r2
	ldr mail,[mailbox,#0]

上述代码从邮箱中读入下一个条目。
6.

	inchan .req r3
	and inchan,mail,#0b1111
	teq inchan,channel
	.unreq inchan
	bne rightmail$
	.unreq mailbox
	.unreq channel

上述代码检查我们刚才读取的通道就是我们所提供的通道,如果不是,我们将返回步骤3继续执行。因为低4位包含了地址,所以将其与输入值作对比就知道我们读取的是否是正确的,一般来说,这里可以看作是校验过程。
7.

	and r0,mail,#0xfffffff0
	.unreq mail
	pop {pc}

上述代码将答案(mail的高28位)写入r0寄存器中。 ###4. 我最心爱的图像处理器 有了上面写的两个函数充当邮递员,我们就拥有了往显卡发送消息的能力了。我们应该发送什么消息过去?这个答案很难回答,在所有线上文档中我没发现有确切的答案。 然而通过查阅Raspberry Pi的GNU/Linux文档,我们可以随心所欲的发送任何我们想要发送的东西。

消息非常简单,我们描述好我们希望创建的framebuffer, 显卡要么同意我们的请求,要么拒绝。如果显卡同意我们开辟出如此大小的framebuffer,那它会返回0, 并填充我们所需要的内存区域;如果显卡拒绝我们的请求,将返回给我们一个非0值, 这代表可能分配了一小块或者根本就没分配。 不幸的是,我不知道这些非0值的真实含义是什么,我们只能以为如果显卡送回0时,它表示很高兴接收我们赋予它的任务。幸运的是,没有特殊情况的话,显卡总是乐呵呵的返回给我们0值,所以在返回值这个问题上,你不要有过多的担心。

因为Pi上的内存被中央处理起和显示处理器所共享,我们只需要发送何处可以找到我们消息的地址就可以了, 这种技术叫做DMA,许多复杂的设备使用它以加速访问时间。

为简单起见,我们将提前实现我们的需求,将其存储在.data段中。 在文件’framebuffer.s'中我们输入下列代码:

	section .data
	.align 4
	.globl FrameBufferInfo 
	FrameBufferInfo:
	.int 1024 /* #0 Physical Width */
	.int 768 /* #4 Physical Height */
	.int 1024 /* #8 Virtual Width */
	.int 768 /* #12 Virtual Height */
	.int 0 /* #16 GPU - Pitch */
	.int 16 /* #20 Bit Depth */
	.int 0 /* #24 X */
	.int 0 /* #28 Y */
	.int 0 /* #32 GPU - Pointer */
	.int 0 /* #36 GPU - Size */

上面就是我们需要传递给显示处理器的消息格式。前两个字描述了物理屏幕的宽度和高度。第二对字则是虚拟屏幕的的宽度和高度。framebuffer的宽度和高度属于虚拟宽度和高度,GPU会自动将其适配到物理的宽度和高度上。接下来的字用于表示GPU是否可以满足我们的请求,如果能则会被设置为framebuffer每一行的字节数,在这里是2x1024=2048。 接下来的字是每一个像素需要分配多少个二进制位用于表征色彩信息。使用16代表GPU支持高彩色,如果是24则代表真彩色,32则代表RGBA32。接下来的两个字分别代表x和y坐标,代表将framebuffer拷贝到屏幕上时需要忽略掉的像素值,x和y是相对于屏幕左上方而言。最后的两个字会被显示处理器所填充,第一个代表指向framebuffer的地址,第二个值代表framebuffer的实际大小,单位是byte。

这里我很谨慎地使用了一个.align 4。 以前我们提起过,这条指令将保证下一条指令地址的低4个二进制为全0. 这样以来,我们就可以确保FrameBufferInfo放在可以直接传递给图形处理起的地址,因为我们的mailbox只能传送低4位全为0的地址。

当和使用DMA的设备交互时,对齐限制变得非常重要。GPU希望消息以16个byte对齐。

现在我们已经构建好了我们的消息结构,接下来我们写出发送这些消息的代码。消息通信将遵循一下流程:

  1. 将地址FrameBufferInfo + 0x40000000写入邮箱通道1。
  2. 读取邮箱通道1的结果,如果它为非0,这代表我们没有请求到合适的frame buffer。
  3. 拷贝图像到GPU的指针地址,它就会直接在屏幕上显示出来。

在步骤1中,我们需要在FrameBufferInfo地址上加上偏移0x40000000后才发送之。这条指令是一条特殊信号,用于告诉GPU在何处写入结构。如果我们只传输地址,GPU将返回它的应答信息,而不会立即更新缓存。缓存是一段内存,被处理器用于记载其当前正在处理的值,以便稍后放入内存中的对应位置。如果加上0x40000000,我们将告诉GPU不用写入缓存,保证了我们可以直接看到更改。

因为这里我们需要做很多事,最好是将其实现为一个函数,我们将写一个名为InitialiseFrameBuffer的函数,这个函数完成诊断和协调工作,并返回包含有frame buffer信息数据结构的指针。 为简单起见,我们应该把宽度,高度和frame buffer的颜色深度都作为这个函数的输入,这样在main.s中我们就很容易更改值,而不用关注到函数的实现细节。

还是老路子,我们先把实现的步骤逐个写下来。如果你有足够的自信,你可以跳开这一步,直接进入到函数的编写阶段。

  1. 检验输入值。
  2. 将输入值写入到frame buffer中。
  3. 将地址frame buffer + 0x40000000写入到mailbox地址。
  4. 收到mailbox的应答信息。
  5. 如果应答信息非0,表示方法失败。我们将返回0代表失败。
  6. 如果成功,我们将返回表征frame buffer信息的指针。

现在我们将撰写一个巨大的实现函数,下面的代码是一个实现方案。
1.

	.section .text
	.globl InitialiseFrameBuffer
	InitialiseFrameBuffer:
	width .req r0
	height .req r1
	bitDepth .req r2
	cmp width,#4096
	cmpls height,#4096
	cmpls bitDepth,#32
	result .req r0
	movhi result,#0
	movhi pc,lr

上述代码检查width和height是否在4096的范围内,颜色深度是否小于32。我们在这里又用到了条件执行的计谋。 请仔细检查你的代码是否能检查到输入值正确的取值。
2.

	fbInfoAddr .req r4
	push {r4,lr}
	ldr fbInfoAddr,=FrameBufferInfo
	str width,[r4,#0]
	str height,[r4,#4]
	str width,[r4,#8]
	str height,[r4,#12]
	str bitDepth,[r4,#20]
	.unreq width
	.unreq height
	.unreq bitDepth

上面的代码用于直接加载预先定义过的frame buffer 结构。这里我还将r4和链接寄存器的值保存在了栈中,因为我们需要将framebuffer的值保存在r4中。
3.

	mov r0,fbInfoAddr
	add r0,#0x40000000 
        mov r1,#1
	bl MailboxWrite

MailboxWrite函数的输入值分别是:r0中存储需要写入的值,r1中存储需要写入的通道值。
4.

	mov r0,#1
	bl MailboxRead

MailboxRead的输入值是寄存器r0, 代表要读取的通道,输出值是我们读出的值。
5.

	teq result,#0
	movne result,#0
	popne {r4,pc}

上述代码检查MailboxRead的返回值是否为0,如果不是0,代表我们读取失败,函数将返回0(本函数返回0代表失败, MailboxRead返回0却代表成功,这两者容易混淆,需要注意)。
6.

	mov result,fbInfoAddr
	pop {r4,pc}
	.unreq result
	.unreq fbInfoAddr

上述代码是函数的结束,成功返回frame buffer 的地址。
###5. 在一个框架内的连续像素 现在我们来创建一个可以和显示处理器通信的函数。它将给出来一个指向frame buffer的指针(地址),以便我们可以在该地址上绘图。现在我们可以开始画图了。

在第一个例子中,我们将绘制一个持续颜色渐变的屏幕。它看起来不怎么美观,但是至少它代表了我们的程序是可以正常工作的。 实现的原理是我们将framebuffer中的每个像素点设置为一个持续渐变的值,并不停的更新它。

将下列代码拷贝到’main.s'文件中的mov sp,#0x8000代码后。

	mov r0,#1024
	mov r1,#768
	mov r2,#16
	bl InitialiseFrameBuffer

以上代码调用了InitialiseFrameBuffer函数,创建出一个宽1024,高768的frame buffer, 颜色深度为16。 你可以将这些值改成你所期待的数字, 只要你能让它运行起来就没问题。 如果图形处理器无法给出我们一个正确的frame buffer,这个函数将返回0, 我们最好检查这个函数的返回值,如果函数执行失败,我们将点亮OKLED等,以表示函数运行有误。

	teq r0,#0
	bne noError$
	
	mov r0,#16
	mov r1,#1
	bl SetGpioFunction
	mov r0,#16
	mov r1,#0
	bl SetGpio
	
	error$:
	b error$
	
	noError$:
	fbInfoAddr .req r4
	mov fbInfoAddr,r0

现在我们已经拥有了frame buffer info的地址,我们需要从中得到frame buffer的指针地址,即frame buffer point, 以便我们可以直接绘制图形到屏幕上。 我们使用两个循环函数来完成绘制过程,一个循环绘制成行的像素点,另一个函数绘制成列的像素点。 在Raspberry Pi上,事实上,在几乎所有的应用程序中,图像的存储方式都是从左至右,从上而下的,我们的函数就是按照这样的方式来绘制图形的。

	render$:
	fbAddr .req r3
	ldr fbAddr,[fbInfoAddr,#32]
	
	colour .req r0
	y .req r1
	mov y,#768
	drawRow$:
	x .req r2
	mov x,#1024
	drawPixel$:
	strh colour,[fbAddr]
	add fbAddr,#2
	sub x,#1
	teq x,#0
	bne drawPixel$
	
	sub y,#1
	add colour,#1
	teq y,#0
	bne drawRow$
	
	b render$
	
	.unreq fbAddr
	.unreq fbInfoAddr

这里有很多代码,loop里嵌套的loop里还有loop, 为了让你更便于浏览这些代码,我在这里使用了适当的缩进,用以标明不同的loop级别。 在高级编程语言里,使用缩进很常见,汇编代码中也支持缩进,因为编译器会自动去除tab缩进符。 我们看到代码中首先先我们往frame buffer地址中加载入frame buffer消息结构,然后循环设置一行的像素点,也就是说,给这一行里的每一个像素点分别赋值。在每个像素点,我们使用一条strh(存储高位字)指令用于存储当前的颜色信息,而后对我们当前所写入的地址执行加1操作。写完一行以后,我们对绘制的颜色执行加1操作。写完整个屏幕后,我们将跳转回开始点重新执行。

strh reg,[dest]将寄存器reg中的低半字存入dest给定的地址中。

###6. 曙光初现 现在你可以在Raspberry Pi上测试你的代码了。你将看到一个不断变化的渐变色屏幕。注意:在发送第一个消息给mailbox前,Raspberry Pi在四个角上显示一个静止的倾斜样式。如果你碰到问题,请参阅troubleshooting页面。

如果你成功运行了本章例程,恭喜你!现在你学会控制屏幕了!你可以修改代码以便在屏幕上画出任何你想要的样式。你可以自己定义出一些好看的显示样式,你可以直接计算每个像素的值,因为y包含了y坐标,x包含了x坐标,你定义的点可以直接显示在屏幕上。 下一课中也就是烤派宝典第7章之Screen02中,我们将学习一种常见的绘制方法,画直线。

烤派宝典第四章之OK04

#烤派宝典第四章之OK04 OK04 这一章基于OK03,将教会你如何使用定时器(timer)来精确控制’OK’或’ACT’灯的闪烁频率。我们假设你已经拥有了烤派宝典第三章之OK03中的代码和知识储备作为基础。

内容
1 新的设备
2 实现
3 另一个闪烁灯版本

###新的设备 到现在为止,我们已经杰出倒了Raspberrry Pi中的一个硬件,也就是GPIO控制器。在前面的章节中,我只是告诉了你现成的答案,需要做什么,里头的主要原理。现在我们来看看一个新的设备,定时器,这回我将手把手教会你如何从硬件手册中理解其工作方式。

和GPIO控制器一样,定时器也有一个地址。在我们的例子中,定时器的地址在2000300016, 通过阅读手册,我们能找到下面的表格:

定时器是Raspberry Pi上唯一可以保存时间的方式。大多数计算机在主板上都有电池驱动的RTC模块用于在掉电时保持时间。但是Raspberry Pi为了降低成本,去掉了这一部分电路。

  <th>大小 / Bytes</th>

  <th>名字</th>

  <th>描述</th>

  <th>读/写</th>
</tr>
  <td>4</td>

  <td>控制/状态</td>

  <td>用于控制和清除定时器通道比较位
  </td>

  <td>RW</td>
</tr>

<tr>
  <td>20003004</td>

  <td>8</td>

  <td>定时器</td>

  <td>一个以1MHz递加的定时器。</td>

  <td>R</td>
</tr>

<tr>
  <td>2000300C</td>

  <td>4</td>

  <td>比较值 0</td>

  <td>第0个比较值寄存器</td>

  <td>RW</td>
</tr>

<tr>
  <td>20003010</td>

  <td>4</td>

  <td>比较值 1</td>

  <td>第一个比较值寄存器.</td>

  <td>RW</td>
</tr>

<tr>
  <td>20003014</td>

  <td>4</td>

  <td>比较值 2</td>

  <td>第二个比较值寄存器.</td>

  <td>RW</td>
</tr>

<tr>
  <td>20003018</td>

  <td>4</td>

  <td>比较值 3</td>

  <td>第三个比较值寄存器.</td>

  <td>RW</td>
</tr>

timer

表格里包含了很多信息,但是手册里关于每个域的解释更为详尽。手册里的说明显示定时器每一微秒增加1. 每次增加1时,它将比较自身的值和最低的32位(4 byte)中的4个比较寄存器, 如果附和它们中的任何一个,就将更新控制/状态寄存器以表征时间匹配。

更详细的关于bits/bytes/bit field和数据大小的解释如下:

一个bit是单个的二进制数字位,回忆一下,单个二进制位只能有两种取值,0或者1。 一个byte是我们取给8个bit集合的名称。因为每个bit只能有两种取值,一共有2的八次方,一共256个不同的取值。我们经常理解为0到255之间的任一值。 下图是GPIO函数选择控制器0的表征。一个bit域可被理解为二进制位,除了可以被理解为数字外,二进制可以表征更多的事物。比如,我们可以用二进制来表征开关的开(1)/关(0)状态。 我们已经在GPIO控制器中接触到了比特域表征的值,用于描述一个管脚的开/关。 有时候我们需要表征更多的状态,我们就可以将很多个比特位链接起来,如图所示。比如GPIO控制函数的设置,图中所示,在图中每一个3字节的位对应控制一个GPIO的管脚函数。

gpioControllerFunctionSelect

我们的目的是实现一个可以输入等待时间值的函数,函数读取等待时间值后将在等待完相应的时间间隔后而后返回。 在实现前根据我们现有的材料,思考一下该如何实现它。

在我看来有两种方法可以实现:

  1. 从计数器中读入数值,然后通过分支跳转到同样的代码中,等待,直到计数器中的值大于设定的值。
  2. 从计数器中读入数值, 增加已经等待过的时间,将其存放在某一比较寄存器中,而后分支跳转回同样的代码中,直到控制/状态寄存器更新。

两种策略都可以工作得很好,但是在本章中我们只实现第一种方法。原因在于,比较寄存器很容易出错,因为在将等待时间储存到比较寄存器的过程中时,计数器可能会增加值,以至于不能匹配。这可能引发会在请求1微s(microsecond)的等待时间无意中引发出过长的等待间隙(或者,更糟的是,0微s, 0 microsecond的等待时间)。

这种情况被称之为并发行问题,几乎是不可被修复的。

###实现 这里我将实现wait方法的挑战留给你。 我建议你在名为’systemTimer.s'的文件中放入所有你操作定时器的代码。 这个挑战中的难点在于计数器是8byte长度的,但是每个寄存器只能储存4个byte, 因而我们需要把寄存器的值分为两部分。
下列的代码供参考:

	ldrd r0,r1,[r2,#4]

ldrd regLow,regHigh,[src,#val] 从src地址加上val的内存中取出来8个byte,将其分别放入regLow和regHigh中。

上面这条指令对你很有用处。它将一个8byte的值分配到两个寄存器中。在这个例子中,r2寄存器中存储的地址起始的8个byte的内存将被分别拷贝给r0和r1。 有点儿复杂的是这种分配方式中,r1中将存储到高的4个byte,距离来说,如果一个计数器的值是十进制的999,999,999,999 = 也就是二进制的1110100011010100101001010000111111111111, 那么r1中的值会是 111010002, r0 则包含剩下的 110101001010010100001111111111112.

大型操作系统通常在执行等待函数时,在后台运行程序以充分利用CPU。

最明智的方式是计算出当前计数器的值,减掉函数调用时的值,用这个结果和我们设定的值作比较,以确定等待的结束。为了更方便使用,除非我们支持8个byte长度的等待时间,否则我们可以把例子中r1存储的值忽略,只考虑低4个byte。

等待的过程中你应该总是记得比较大于值,而不是相等的值。因为你可能等不到那个刚好的值,错过了那个时间点,你就只能一直等在那里了。

如果你不知道如何编写wait函数代码,请点击一下链接:
waitfunction ###另一个闪烁灯版本 当你觉得自己的等待函数可以工作时,更改’main.s'来使用它。更改r0的值为一个很大的数字(记住这个值表征的是微秒), 然后在你的Raspberry Pi上验证之。如果有任何问题,请参阅troubleshooting页面。
如果你成功了,恭喜你已经掌握了另一种设备,定时器。在下一章,也就是OK系列的最后一章,烤派宝典第五章之OK05中我们将学会如何在LED按预设的模式进行闪烁。

Table in Octopress

###Table in Markdown Table in markdown can be represent like following:

	| Tables        | Are           | Cool  |
	| ------------- |:-------------:| -----:|
	| col 3 is      | right-aligned | $1600 |
	| col 2 is      | centered      |   $12 |
	| zebra stripes | are neat      |    $1 |

Or we can use the pure html code:

	<table>
	    <tr>
	        <td>Foo</td>
	    </tr>
	</table>

By both method we can create our own table. ###Display Table in Octopress By default, table won’t be displayed in octopress, this is because we don’t provide the corresponding css file to table. we have first to add the css file under source/sytlesheets/data-table.css:

* + table {
  border-style:solid;
  border-width:1px;
  border-color:#e7e3e7;
}
 
* + table th, * + table td {
  border-style:Trustyed;
  border-width:1px;
  border-color:#e7e3e7;
  padding-left: 3px;
  padding-right: 3px;
}
 
* + table th {
  border-style:solid;
  font-weight:bold;
  background: url("/images/noise.png?1330434582") repeat scroll left top #F7F3F7;
}
 
* + table th[align="left"], * + table td[align="left"] {
  text-align:left;
}
 
* + table th[align="right"], * + table td[align="right"] {
  text-align:right;
}
 
* + table th[align="center"], * + table td[align="center"] {
  text-align:center;
}

Then we can add the following line in source/_includes/head.html:

	  <link href="/stylesheets/data-table.css" media="screen, projection" rel="stylesheet" type="text/css" />

By now we can see the tables as following:

Table 1

Table Processing in vim

Install htmltidy via:

	$ pacman -S tidyhtml

Add following lines into your own ~/.vimrc

	:vmap ,x :%!tidy -q -i --show-errors 0<CR>
	:command Thtml :%!tidy -q -i --show-errors 0
	:command Txml  :%!tidy -q -i --show-errors 0 -xml

Open your vim and type in:

	:Thtml

by now you can get tidy html.

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