OK04 课程在 OK03 的基础上进行构建，它教你如何使用定时器让 OK 或 ACT LED 灯按精确的时间间隔来闪烁。假设你已经有了 课程 3：OK03 的操作系统，我们将以它为基础来构建。1、一个新设备定时器是树莓派保持时间的唯一方法。大多数计算机都有一个电池供电的时钟，这样当计算机关机后仍然能保持时间。到目前为止，我们仅看了树莓派硬件的一小部分，即 GPIO 控制器。我只是简单地告诉你做什么，然后它会发生什么事情。现在，我们继续看定时器，并继续带你去了解它的工作原理。和 GPIO 控制器一样，定时器也有地址。在本案例中，定时器的基地址在 20003000 16。阅读手册我们可以找到下面的表：表 1.1 GPIO 控制器寄存器地址大小 / 字节名字描述读或写200030004Control / Status用于控制和清除定时器通道比较器匹配的寄存器RW200030048Counter按 1 MHz 的频率递增的计数器R2000300C4Compare 00 号比较器寄存器RW200030104Compare 11 号比较器寄存器RW200030144Compare 22 号比较器寄存器RW200030184Compare 33 号比较器寄存器RW这个表只告诉我们一部分内容，在手册中描述了更多的字段。手册上解释说，定时器本质上是按每微秒将计数器递增 1 的方式来运行。每次它是这样做的，它将计数器的低 32 位（4 字节）与 4 个比较器寄存器进行比较，如果匹配它们中的任何一个，它更新 Control/Status 以反映出其中有一个是匹配的。关于 位 bit 、 字节 byte 、 位字段 bit field 、以及数据大小的更多内容如下：一个位是一个单个的二进制数的名称。你可能还记得，一个单个的二进制数既可能是一个 1，也可能是一个 0。一个字节是一个 8 位集合的名称。由于每个位可能是 1 或 0 这两个值的其中之一，因此，一个字节有 2 8 = 256 个不同的可能值。我们一般解释一个字节为一个介于 0 到 255（含）之间的二进制数。一个位字段是解释二进制的另一种方式。二进制可以解释为许多不同的东西，而不仅仅是一个数字。一个位字段可以将二进制看做为一系列的 1（开） 或 0（关）的开关。对于每个小开关，我们都有一个意义，我们可以使用它们去控制一些东西。我们已经遇到了 GPIO 控制器使用的位字段，使用它设置一个针脚的开或关。位为 1 时 GPIO 针脚将准确地打开或关闭。有时我们需要更多的选项，而不仅仅是开或关，因此我们将几个开关组合到一起，比如 GPIO 控制器的函数设置（如上图），每 3 位为一组控制一个 GPIO 针脚的函数。我们的目标是实现一个函数，这个函数能够以一个时间数量为输入来调用它，这个输入的时间数量将作为等待的时间，然后返回。想一想如何去做，想想我们都拥有什么。我认为这将有两个选择：从计数器中读取一个值，然后保持分支返回到相同的代码，直到计数器的等待时间数量大于它。从计数器中读取一个值，加上要等待的时间数量，将它保存到比较器寄存器，然后保持分支返回到相同的代码处，直到 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 10 = 1110100011010100101001010000111111111111 2 ，那么寄存器 r1 中只有 11101000 2，而寄存器 r0 中则是 11010100101001010000111111111111 2。实现它的更明智的方式应该是，去计算当前计数器值与来自方法启动后的那一个值的差，然后将它与要求的等待时间数量进行比较。除非恰好你希望的等待时间是占用 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，我们将使用我们已经学习过的知识让 LED 按我们的模式进行闪烁。via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok04.html作者：Robert Mullins 选题：lujun9972 译者：qhwdw 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出