第4课OK4
这一课是建立在上一节课的基础上,主要任务是教你如何利用定时器以一个精确的时间来实现OK LED的亮灭。我认为你已经完成了第3课的编程。
1一个新的device
定时器(timer)是PI保存时间的唯一方式。大多数电脑会有一个电池来给时钟供电,保证在关机后,时间不丢失。
目前,我们只是玩了玩PI的GPIO控制器,其它的硬件基本没有涉及到。(译者注:感觉作者花了好多时间,讲了一些过于简单的东西,但考虑到这个教程是针对16岁以上的,没有编程经验的孩子,这也是很正常的。不过上一课中讲的一些关于编程的方法,ABI标准等还是值得学习的)。
现在,让我们来看看timer(定时器),我将会让你理解他是怎么工作的。(译者注:对于学过单片机的,都知道,定时器最基本的用法就是设置好的定时的方式和初值后,开启定时器就行了,然后利用查询或是中断的方法来查看定时时间是否到来。)
就像GPIO的控制器一样,定时器也有自己的地址。在这个例子中,定时器的基地址是 0X20003000。通过阅读manual,你可以发现下面的表:
这个表告诉我们很多,但是在手册manual中,关于每个寄存器的每一位都有详细讲解。手册中是这样解释的:每1us后,定时器counter寄存器中的数值加1,然后counter中低32位与4个compares寄存器进行比较,如果与其中的任何一个相等。就会更新control/status寄存中的相关位域。更新位域就是为了反映这一事实。
更多有关bits,bytes,bit fields,和data sizes的解释如下:
1bit就是只能存放一个二进制的数字(0或1)。 1byte就是8个bit的集合。因为1个bit可以每2个values,所以8个bits就可以放2的8次=256个不同的值。我们经常解释1个byte的值的范围是0~255(包括0,255)。 a bit filed是对二进制的另一种说法。不是把二进制解释成数。二进制可以解释成很多不现的东西。a bit filed把进制看成一系列不同的开关。开是1(on),关是0(off)。如果这些开关都是和其它的物理硬件对应的,我们就可以用这些开关了控制这些硬件了。实际上我们已经看到过GPIO控制器的bit filed。当设置pin是on 或是off时,就是把相应的bit filed置1。 有时候我们可能需要更多的选择,而不仅仅是开或是关。所以我们把向个bit组合在一起,例如GPIO控制器中设置GPIO功能的寄存器中(如下图),在下面这个寄存器中,每3bits组合成一个bit filed,用来控制GPIO引脚的功能。
我们的目的是实现一个函数,我们把定时器定时的时间长度作为输入参数输入,然后他就会定时那么长的时间,时间到了之后就返回。想一会我们应该怎么做。想想我们有什么。
(译者注:应试是这样做的吧:1:把输入值给counter.2:启动好相应的控制位3:启动定时器,不停的查询是否等于某个值,如果不等,接着查,如果相等,返回。。这是用查询的功能)
我知道有两种选择可以完成这个功能:
从counter中读数据,作为起始时间start,然后跳转到loop中读取当前conter的值与start做差,把结果后我输入的delay时间进行比较,如时结果小于delay,说明时间没有到,继续到到loop。直到做差的结果比delay大时,就说明延时的时间到了。
从counter中读数据,把读到数据与我们要等的时间相加,然后保存到comparison寄存器中,然后等着,真到Control/Status产生了更新。
上面的两种方法都是可以的,但是在这里,我们只实现第一种方法。原因是comparison寄存器很容易出错,因为在你去把counter中数据取出来与需要wait的相加时,然后保存到comparison时,counter中数据都已加了好几次了。可能就不与comparison中的值匹配了。这样会导致一个我们不愿意看到delays。
2定时器的实现
大的操作系统经常用调用wait函数的机会去处理后台任务。
我将把创建一个理想的wait函数的任务留给你,这个任务很具有挑战性。我建议你把与定时器有关的所以代码放到一个systemTimer.s的文件中。关于这个函数最复杂的部分是,counter这个寄存器保存的是8字节的数据(64位),但是在arm中每个寄存器只能保存4字节的数据。因此我们要把counter分成两个寄存器(和51单片机是一样的TH0 TL0)。
下面的指令是一个例子:
ldrd r0,r1,[r2,#4]
上面这个指令你会发现很有用,他会从memory中加载8个字节的数据到2个寄存吕。在这个例子中,8个字节的数据的起始地址是r2。有点复杂的是,r1保存了最高地址的4个字节。
换句话说:如果counter有一个值=999,999,999,999(10进制的)
=1110100011010100101001010000111111111111(2进制的)
因为r1保存高32位,所以r1=11101000
r0保存低32位,所以r0=11010100101001010000111111111111
实现定时最容易的方法就是把当前counter中数值与我们输入的数据做差。然后把这个数据与我们要等的时间做比较。通常为了方便我们会把高32位的数据丢掉,因为低32位的等待时间就足够了。
在等待的过程中,我们要确保用的时比较是于高于,不要用比较是否相等。因为比较相等时要求太高了,可能你就在运行其它指令的时候会错过个相等的值,从而进入了死循环(译者注:从这里可以推测出作者用的是查询的方法,而且counter还会自动重载)。
如果你还是不会写wait函数,看下面的向导。
从GPIO控制器写函数思想一样,第一个函数我们应该得到定时器地址。例子如下:
.globl GetSystemTimerBase
GetSystemTimerBase:
ldr r0,=0x20003000
mov pc,lr
另一个函数是很有用的,返回当前counter寄存器的值,到r0,r1:
.globl GetTimeStamp
GetTimeStamp:
push {lr}
bl GetSystemTimerBase
ldrd r0,r1,[r0,#4]
pop {pc}
上面的函数用了GetSystemTimerBase 函数,得到counter中的值,是用ldrd指令实现的。
现在我们要编写我们的wait函数了。首先,我们需要知道counter的值当调用wait函数时。所以我们下面要用GetTimeStamp函数。
delay .req r2
mov delay,r0
push {lr}
bl GetTimeStamp
start .req r3
mov start,r0
这个代码得到我们输入,我们需要延时的时间,保存到r2,然后调用GetTimeStamp得到counter的当前时间。我们知道当前时间保存在r0,r1中。然后把最低位复制到r3中 。
下面我们需要要计算出当前counter的值与刚才我们读到counter的值的差值,一直这么做,直到这个差值要大于我们输入的delay值。
loop$:
bl GetTimeStamp
elapsed .req r1
sub elapsed,r0,start
cmp elapsed,delay
.unreq elapsed
bls loop$
上面的函数一直循环,直到定时器走过的时间大于我们输入的delay时间。不停的读取counter中的值,然后与start做减法后,得到的结果与delay值进行比较。如果结果小于delay就说明延时时间没有到,接着回到loop$。
3另一种方式实现灯的闪烁。
一旦你相信你的wait函数可以工作了,在main.s里面调用试试。改变r0的值就可以改变定时器等待的时间了,(记住定时器是微秒级的)。如果你的定时器函数工作了,祝贺你,你又掌握了一个device。
在下一课中,也是OK系列的最后一课。
第5课中,我们会让led以一种编码闪烁。
(译者注:因为寝室没有显示器,所以显示器部分实验的翻译可能我不做了,如果谁能借我一个显示器用下那也不错啊,邮到华科,用完了再给你邮回去,呵呵,开玩笑了。)
下面是剑桥大学官网的本课答案:
http://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/downloads.html#solution4