计算机实验室之树莓派:课程 4 OK04

2019-02-10
6分钟阅读时长

OK04 课程在 OK03 的基础上进行构建,它教你如何使用定时器让 OK 或 ACT LED 灯按精确的时间间隔来闪烁。假设你已经有了 课程 3:OK03 的操作系统,我们将以它为基础来构建。

1、一个新设备

定时器是树莓派保持时间的唯一方法。大多数计算机都有一个电池供电的时钟,这样当计算机关机后仍然能保持时间。

到目前为止,我们仅看了树莓派硬件的一小部分,即 GPIO 控制器。我只是简单地告诉你做什么,然后它会发生什么事情。现在,我们继续看定时器,并继续带你去了解它的工作原理。

和 GPIO 控制器一样,定时器也有地址。在本案例中,定时器的基地址在 20003000 16。阅读手册我们可以找到下面的表:

表 1.1 GPIO 控制器寄存器

地址大小 / 字节名字描述读或写
200030004Control / Status用于控制和清除定时器通道比较器匹配的寄存器RW
200030048Counter按 1 MHz 的频率递增的计数器R
2000300C4Compare 00 号比较器寄存器RW
200030104Compare 11 号比较器寄存器RW
200030144Compare 22 号比较器寄存器RW
200030184Compare 33 号比较器寄存器RW

Flowchart of the system timer’s operation

这个表只告诉我们一部分内容,在手册中描述了更多的字段。手册上解释说,定时器本质上是按每微秒将计数器递增 1 的方式来运行。每次它是这样做的,它将计数器的低 32 位(4 字节)与 4 个比较器寄存器进行比较,如果匹配它们中的任何一个,它更新 Control/Status 以反映出其中有一个是匹配的。

关于bit 字节 byte 位字段 bit field 、以及数据大小的更多内容如下:

一个位是一个单个的二进制数的名称。你可能还记得,一个单个的二进制数既可能是一个 1,也可能是一个 0。

一个字节是一个 8 位集合的名称。由于每个位可能是 1 或 0 这两个值的其中之一,因此,一个字节有 2 8 = 256 个不同的可能值。我们一般解释一个字节为一个介于 0 到 255(含)之间的二进制数。

Diagram of GPIO function select controller register 0.

一个位字段是解释二进制的另一种方式。二进制可以解释为许多不同的东西,而不仅仅是一个数字。一个位字段可以将二进制看做为一系列的 1(开) 或 0(关)的开关。对于每个小开关,我们都有一个意义,我们可以使用它们去控制一些东西。我们已经遇到了 GPIO 控制器使用的位字段,使用它设置一个针脚的开或关。位为 1 时 GPIO 针脚将准确地打开或关闭。有时我们需要更多的选项,而不仅仅是开或关,因此我们将几个开关组合到一起,比如 GPIO 控制器的函数设置(如上图),每 3 位为一组控制一个 GPIO 针脚的函数。

我们的目标是实现一个函数,这个函数能够以一个时间数量为输入来调用它,这个输入的时间数量将作为等待的时间,然后返回。想一想如何去做,想想我们都拥有什么。

我认为这将有两个选择:

  1. 从计数器中读取一个值,然后保持分支返回到相同的代码,直到计数器的等待时间数量大于它。
  2. 从计数器中读取一个值,加上要等待的时间数量,将它保存到比较器寄存器,然后保持分支返回到相同的代码处,直到 Control / Status 寄存器更新。

这两种策略都工作的很好,但在本教程中,我们将只实现第一个。原因是比较器寄存器更容易出错,因为在增加等待时间并保存它到比较器的寄存器期间,计数器可能已经增加了,并因此可能会不匹配。如果请求的是 1 微秒(或更糟糕的情况是 0 微秒)的等待,这样可能导致非常长的意外延迟。

像这样存在被称为“并发问题”的问题,并且几乎无法解决。

2、实现

我将把这个创建完美的等待方法的挑战基本留给你。我建议你将所有与定时器相关的代码都放在一个名为 systemTimer.s 的文件中(理由很明显)。关于这个方法的复杂部分是,计数器是一个 8 字节值,而每个寄存器仅能保存 4 字节。所以,计数器值将分到 2 个寄存器中。

大型的操作系统通常使用等待函数来抓住机会在后台执行任务。

下列的代码块是一个示例。

ldrd r0,r1,[r2,#4]

ldrd regLow,regHigh,[src,#val]src 中的数加上 val 之和的地址加载 8 字节到寄存器 regLowregHigh 中。

上面的代码中你可以发现一个很有用的指令是 ldrd。它加载 8 字节的内存到两个寄存器中。在本案例中,这 8 字节内存从寄存器 r2 中的地址 + 4 开始,将被复制进寄存器 r0r1。这种安排的稍微复杂之处在于 r1 实际上只持有了高位 4 字节。换句话说就是,如果如果计数器的值是 999,999,999,999 10 = 1110100011010100101001010000111111111111 2 ,那么寄存器 r1 中只有 11101000 2,而寄存器 r0 中则是 11010100101001010000111111111111 2

实现它的更明智的方式应该是,去计算当前计数器值与来自方法启动后的那一个值的差,然后将它与要求的等待时间数量进行比较。除非恰好你希望的等待时间是占用 8 字节的,否则上面示例中寄存器 r1 中的值将会丢弃,而计数器仅需要使用低位 4 字节。

当等待开始时,你应该总是确保使用大于比较,而不是使用等于比较,因为如果你尝试去等待一个时间,而这个时间正好等于方法开始的时间与结束的时间之差,那么你就错过这个值而永远等待下去。

如果你不明白如何编写等待函数的代码,可以参考下面的指南。

借鉴 GPIO 控制器的创意,第一个函数我们应该去写如何取得系统定时器的地址。示例如下:

.globl GetSystemTimerBase
GetSystemTimerBase:
ldr r0,=0x20003000
mov pc,lr

另一个被证明非常有用的函数是返回在寄存器 r0r1 中的当前计数器值:

.globl GetTimeStamp
GetTimeStamp:
push {lr}
bl GetSystemTimerBase
ldrd r0,r1,[r0,#4]
pop {pc}

这个函数简单地使用了 GetSystemTimerBase 函数,并像我们前面学过的那样,使用 ldrd 去加载当前计数器值。

现在,我们可以去写我们的等待方法的代码了。首先,在该方法启动后,我们需要知道计数器值,我们可以使用 GetTimeStamp 来取得。

delay .req r2
mov delay,r0
push {lr}
bl GetTimeStamp
start .req r3
mov start,r0

这个代码复制了我们的方法的输入,将延迟时间的数量放到寄存器 r2 中,然后调用 GetTimeStamp,这个函数将会返回寄存器 r0r1 中的当前计数器值。接着复制计数器值的低位 4 字节到寄存器 r3 中。

接下来,我们需要计算当前计数器值与读入的值的差,然后持续这样做,直到它们的差至少是 delay 的大小为止。

loop$:

bl GetTimeStamp
elapsed .req r1
sub elapsed,r0,start
cmp elapsed,delay
.unreq elapsed
bls loop$

这个代码将一直等待,一直到等待到传递给它的时间数量为止。它从计数器中读取数值,减去最初从计数器中读取的值,然后与要求的延迟时间进行比较。如果过去的时间数量小于要求的延迟,它切换回 loop$

.unreq delay
.unreq start
pop {pc}

代码完成后,函数返回。

3、另一个闪灯程序

你一旦明白了等待函数的工作原理,修改 main.s 去使用它。修改各处 r0 的等待设置值为某个很大的数量(记住它的单位是微秒),然后在树莓派上测试。如果函数不能正常工作,请查看我们的排错页面。

如果正常工作,恭喜你学会控制另一个设备了,会使用它,则时间由你控制。在下一节课程中,我们将完成 OK 系列课程的最后一节 课程 5:OK05,我们将使用我们已经学习过的知识让 LED 按我们的模式进行闪烁。


via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok04.html

作者:Robert Mullins 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出