[#]: collector: (lujun9972) [#]: translator: (qhwdw) [#]: reviewer: (wxy) [#]: publisher: (wxy) [#]: url: (https://linux.cn/article-10526-1.html) [#]: subject: (Computer Laboratory – Raspberry Pi: Lesson 4 OK04) [#]: via: (https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok04.html) [#]: author: (Robert Mullins http://www.cl.cam.ac.uk/~rdm34) 计算机实验室之树莓派:课程 4 OK04 ====== OK04 课程在 OK03 的基础上进行构建,它教你如何使用定时器让 OK 或 ACT LED 灯按精确的时间间隔来闪烁。假设你已经有了 [课程 3:OK03][1] 的操作系统,我们将以它为基础来构建。 ### 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][2] 这个表只告诉我们一部分内容,在手册中描述了更多的字段。手册上解释说,定时器本质上是按每微秒将计数器递增 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.][3] > > 一个位字段是解释二进制的另一种方式。二进制可以解释为许多不同的东西,而不仅仅是一个数字。一个位字段可以将二进制看做为一系列的 1(开) 或 0(关)的开关。对于每个小开关,我们都有一个意义,我们可以使用它们去控制一些东西。我们已经遇到了 GPIO 控制器使用的位字段,使用它设置一个针脚的开或关。位为 1 时 GPIO 针脚将准确地打开或关闭。有时我们需要更多的选项,而不仅仅是开或关,因此我们将几个开关组合到一起,比如 GPIO 控制器的函数设置(如上图),每 3 位为一组控制一个 GPIO 针脚的函数。 我们的目标是实现一个函数,这个函数能够以一个时间数量为输入来调用它,这个输入的时间数量将作为等待的时间,然后返回。想一想如何去做,想想我们都拥有什么。 我认为这将有两个选择: 1. 从计数器中读取一个值,然后保持分支返回到相同的代码,直到计数器的等待时间数量大于它。 2. 从计数器中读取一个值,加上要等待的时间数量,将它保存到比较器寄存器,然后保持分支返回到相同的代码处,直到 `Control / Status` 寄存器更新。 这两种策略都工作的很好,但在本教程中,我们将只实现第一个。原因是比较器寄存器更容易出错,因为在增加等待时间并保存它到比较器的寄存器期间,计数器可能已经增加了,并因此可能会不匹配。如果请求的是 1 微秒(或更糟糕的情况是 0 微秒)的等待,这样可能导致非常长的意外延迟。 > 像这样存在被称为“并发问题”的问题,并且几乎无法解决。 ### 2、实现 我将把这个创建完美的等待方法的挑战基本留给你。我建议你将所有与定时器相关的代码都放在一个名为 `systemTimer.s` 的文件中(理由很明显)。关于这个方法的复杂部分是,计数器是一个 8 字节值,而每个寄存器仅能保存 4 字节。所以,计数器值将分到 2 个寄存器中。 > 大型的操作系统通常使用等待函数来抓住机会在后台执行任务。 下列的代码块是一个示例。 ```assembly 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,99910 = 11101000110101001010010100001111111111112 ,那么寄存器 `r1` 中只有 111010002,而寄存器 `r0` 中则是 110101001010010100001111111111112。 实现它的更明智的方式应该是,去计算当前计数器值与来自方法启动后的那一个值的差,然后将它与要求的等待时间数量进行比较。除非恰好你希望的等待时间是占用 8 字节的,否则上面示例中寄存器 `r1` 中的值将会丢弃,而计数器仅需要使用低位 4 字节。 当等待开始时,你应该总是确保使用大于比较,而不是使用等于比较,因为如果你尝试去等待一个时间,而这个时间正好等于方法开始的时间与结束的时间之差,那么你就错过这个值而永远等待下去。 如果你不明白如何编写等待函数的代码,可以参考下面的指南。 > > 借鉴 GPIO 控制器的创意,第一个函数我们应该去写如何取得系统定时器的地址。示例如下: > > ```assembly > .globl GetSystemTimerBase > GetSystemTimerBase: > ldr r0,=0x20003000 > mov pc,lr > ``` > > 另一个被证明非常有用的函数是返回在寄存器 `r0` 和 `r1` 中的当前计数器值: > > ```assembly > .globl GetTimeStamp > GetTimeStamp: > push {lr} > bl GetSystemTimerBase > ldrd r0,r1,[r0,#4] > pop {pc} > ``` > > 这个函数简单地使用了 `GetSystemTimerBase` 函数,并像我们前面学过的那样,使用 `ldrd` 去加载当前计数器值。 > > 现在,我们可以去写我们的等待方法的代码了。首先,在该方法启动后,我们需要知道计数器值,我们可以使用 `GetTimeStamp` 来取得。 > > ```assembly > delay .req r2 > mov delay,r0 > push {lr} > bl GetTimeStamp > start .req r3 > mov start,r0 > ``` > > 这个代码复制了我们的方法的输入,将延迟时间的数量放到寄存器 `r2` 中,然后调用 `GetTimeStamp`,这个函数将会返回寄存器 `r0` 和 `r1` 中的当前计数器值。接着复制计数器值的低位 4 字节到寄存器 `r3` 中。 > > 接下来,我们需要计算当前计数器值与读入的值的差,然后持续这样做,直到它们的差至少是 `delay` 的大小为止。 > > ```assembly > loop$: > > bl GetTimeStamp > elapsed .req r1 > sub elapsed,r0,start > cmp elapsed,delay > .unreq elapsed > bls loop$ > ``` > > 这个代码将一直等待,一直到等待到传递给它的时间数量为止。它从计数器中读取数值,减去最初从计数器中读取的值,然后与要求的延迟时间进行比较。如果过去的时间数量小于要求的延迟,它切换回 `loop$`。 > > ```assembly > .unreq delay > .unreq start > pop {pc} > ``` > > 代码完成后,函数返回。 > ### 3、另一个闪灯程序 你一旦明白了等待函数的工作原理,修改 `main.s` 去使用它。修改各处 `r0` 的等待设置值为某个很大的数量(记住它的单位是微秒),然后在树莓派上测试。如果函数不能正常工作,请查看我们的排错页面。 如果正常工作,恭喜你学会控制另一个设备了,会使用它,则时间由你控制。在下一节课程中,我们将完成 OK 系列课程的最后一节 [课程 5:OK05][4],我们将使用我们已经学习过的知识让 LED 按我们的模式进行闪烁。 -------------------------------------------------------------------------------- via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok04.html 作者:[Robert Mullins][a] 选题:[lujun9972][b] 译者:[qhwdw](https://github.com/qhwdw) 校对:[wxy](https://github.com/wxy) 本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 [a]: http://www.cl.cam.ac.uk/~rdm34 [b]: https://github.com/lujun9972 [1]: https://linux.cn/article-10519-1.html [2]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/systemTimer.png [3]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/images/gpioControllerFunctionSelect.png [4]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok05.html