计算机实验室之树莓派：课程 4 OK04
树莓派 计时器
![](/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/) 荣誉推出


OK
Raspberry Pi

# Lesson 4 OK04

The OK04 lesson builds on OK03 by teaching how to use the timer to flash the 'OK' or 'ACT'
LED at precise intervals. It is assumed you have the code for the [
Lesson 3: OK03](ok03.html) operating system as a basis.

## 1 A New Device

The timer is the only way the Pi can keep time. Most computers have a battery powered clock to keep time when off.

So far, we've only looked at one piece of hardware on the Raspberry Pi, namely the GPIO Controller. I've simply told you what to do, and it happened. Now we're going to look at the timer, and I'm going to lead you through understanding how it works.

Just like the GPIO Controller, the timer has an address. In this case, the timer
is based at 2000300016. Reading the manual, we find the following table:

Address | Size / Bytes | Name | Description | Read or Write |
---|---|---|---|---|

20003000 | 4 | Control / Status | Register used to control and clear timer channel comparator matches. | RW |

20003004 | 8 | Counter | A counter that increments at 1MHz. | R |

2000300C | 4 | Compare 0 | 0th Comparison register. | RW |

20003010 | 4 | Compare 1 | 1st Comparison register. | RW |

20003014 | 4 | Compare 2 | 2nd Comparison register. | RW |

20003018 | 4 | Compare 3 | 3rd Comparison register. | RW |


![Flowchart of the system timer's operation](images/systemTimer.png)

This table tells us a lot, but the descriptions in the manual of the various fields tell us the most. The manual explains that the timer fundamentally just increments the value in Counter by 1 every 1 micro second. Each time it does so, it compares the lowest 32 bits (4 bytes) of the counter's value with the 4 comparison registers, and if it matches any of them, it updates Control / Status to reflect which ones matched.

For more information about bits, bytes, bit fields, and data sizes expand the box below.

Our goal is to implement a function that we can call with an amount of time as an input that will wait for that amount of time and then return. Think for a moment about how we could do this, given what we have.

I see there being two options:

- Read a value from the counter, and then keep branching back into the same code until the counter is the amount of time to wait more than it was.
- Read a value from the counter, add the amount of time to wait, store this in one of the comparison registers and then keep branching back into the same code until the Control / Status register updates.

Issues like these are called concurrency problems, and can be almost impossible to fix.

Both of these strategies would work fine, but in this tutorial we will only implement the first. The reason is because the comparison registers are more likely to go wrong, as during the time it takes to add the wait time and store it in the comparison register, the counter may have increased, and so it would not match. This could lead to very long unintentional delays if a 1 micro second wait is requested (or worse, a 0 microsecond wait).

## 2 Implementation

Large Operating Systems normally use the Wait function as an opportunity to perform background tasks.

I will largely leave the challenge of creating the ideal wait method to you. I suggest you put all code related to the timer in a file called 'systemTimer.s' (for hopefully obvious reasons). The complicated part about this method, is that the counter is an 8 byte value, but each register only holds 4 bytes. Thus, the counter value will span two registers.

The following code blocks are examples.

ldrd r0,r1,[r2,#4]

ldrd regLow,regHigh,[src,#val] loads 8 bytes from the address given by the number in src plus val into regLow and regHigh .

An instruction you may find useful is the ldrd
instruction above. It loads 8 bytes of memory across 2 registers. In this case,
the 8 bytes of memory starting at the address in r2
would be copied into r0 and
r1. What is slightly complicated about this arrangement is that
r1 actually holds the highest 4 bytes. In other words, if the counter
had a value of 999,999,999,99910 = 11101000110101001010010100001111111111112,
r1 would contain 111010002 and r0 would contain 110101001010010100001111111111112.

The most sensible way to implement this would be to compute the difference between the current counter value and the one from when the method started, and then to compare this with the requested amount of time to wait. Conveniently, unless you wish to support wait times that were 8 bytes, the value in r1 in the example above could be discarded, and only the low 4 bytes of the counter need be used.

When waiting you should always be sure to use higher comparisons not equality comparisons, as if you try to wait for the gap between the time the method started and the time it ends to be exactly the amount requested, you could miss the value, and wait forever.

If you cannot figure out how to code the wait function, expand the box below for a guide.

## 3 Another Blinking Light

Once you have what you believe to be a working wait function, change 'main.s' to use it. Alter everywhere you wait to set the value of r0 to some big number (remember it is in microseconds) and then test it on the Raspberry Pi. If it does not function correctly please see our troubleshooting page.

Once it is working, congratulations you have now mastered another device, and with
it, time itself. In the next and final lesson in the OK series, [
Lesson 5: OK05](ok05.html) we shall use all we have learned to flash out a pattern on
the LED.