计算机实验室之树莓派：课程 4 OK04
OK04 课程在 OK03 的基础上进行构建，它教你如何使用定时器让 OK 或 ACT LED 灯按精确的时间间隔来闪烁。
树莓派 计时器
OK04 课程在 OK03 的基础上进行构建，它教你如何使用定时器让 OK 或 ACT LED 灯按精确的时间间隔来闪烁。
![](/data/attachment/album/201902/10/234202zj838rljquf9zy52.jpg)


OK04 课程在 OK03 的基础上进行构建，它教你如何使用定时器让 OK 或 ACT LED 灯按精确的时间间隔来闪烁。假设你已经有了 [课程 3：OK03](/article-10519-1.html) 的操作系统，我们将以它为基础来构建。


### 1、一个新设备



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


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


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


表 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](/data/attachment/album/201902/10/234424z0ded7vccy4zz8y2.png)


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


关于<ruby> 位 <rt>  bit </rt></ruby>、<ruby> 字节 <rt>  byte </rt></ruby>、<ruby> 位字段 <rt>  bit field </rt></ruby>、以及数据大小的更多内容如下：



> 
> 一个位是一个单个的二进制数的名称。你可能还记得，一个单个的二进制数既可能是一个 1，也可能是一个 0。
> 
> 
> 一个字节是一个 8 位集合的名称。由于每个位可能是 1 或 0 这两个值的其中之一，因此，一个字节有 2<sup> 8</sup> = 256 个不同的可能值。我们一般解释一个字节为一个介于 0 到 255（含）之间的二进制数。
> 
> 
> ![Diagram of GPIO function select controller register 0.](/data/attachment/album/201902/10/234240m932lb2ajbbgb8zj.png)
> 
> 
> 一个位字段是解释二进制的另一种方式。二进制可以解释为许多不同的东西，而不仅仅是一个数字。一个位字段可以将二进制看做为一系列的 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 字节到寄存器 `regLow` 和 `regHigh` 中。
> 
> 
> 


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


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


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


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



> 
> 借鉴 GPIO 控制器的创意，第一个函数我们应该去写如何取得系统定时器的地址。示例如下：
> 
> 
> 
> ```
> .globl GetSystemTimerBase
> GetSystemTimerBase:
> ldr r0,=0x20003000
> mov pc,lr
> ```
> 
> 另一个被证明非常有用的函数是返回在寄存器 `r0` 和 `r1` 中的当前计数器值：
> 
> 
> 
> ```
> .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`，这个函数将会返回寄存器 `r0` 和 `r1` 中的当前计数器值。接着复制计数器值的低位 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](https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok05.html)，我们将使用我们已经学习过的知识让 LED 按我们的模式进行闪烁。




---


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


作者：[Robert Mullins](http://www.cl.cam.ac.uk/%7Erdm34) 选题：[lujun9972](https://github.com/lujun9972) 译者：[qhwdw](https://github.com/qhwdw) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出

