TranslateProject/published/201902/20120204 Computer Laboratory - Raspberry Pi- Lesson 4 OK04.md
2019-03-01 10:19:51 +08:00

10 KiB
Raw Permalink Blame History

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

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

1、一个新设备

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

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

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

表 1.1 GPIO 控制器寄存器

地址 大小 / 字节 名字 描述 读或写
20003000 4 Control / Status 用于控制和清除定时器通道比较器匹配的寄存器 RW
20003004 8 Counter 按 1 MHz 的频率递增的计数器 R
2000300C 4 Compare 0 0 号比较器寄存器 RW
20003010 4 Compare 1 1 号比较器寄存器 RW
20003014 4 Compare 2 2 号比较器寄存器 RW
20003018 4 Compare 3 3 号比较器寄存器 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,99910 = 11101000110101001010010100001111111111112 ,那么寄存器 r1 中只有 111010002,而寄存器 r0 中则是 110101001010010100001111111111112

实现它的更明智的方式应该是,去计算当前计数器值与来自方法启动后的那一个值的差,然后将它与要求的等待时间数量进行比较。除非恰好你希望的等待时间是占用 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 系列课程的最后一节 课程 5OK05,我们将使用我们已经学习过的知识让 LED 按我们的模式进行闪烁。


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

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

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