在Linux应用程序中,常用的延时函数包括sleep()、usleep()、select()等,这几个延时函数函数的执行机制,都是将当前线程挂起,由操作系统做延时,然后再恢复当前线程。这意味着其延时的最小间隔是两次线程切换时间。经测试,在单一线程情况下,两次线程的切换时间在150us左右。大多数情况下,应用程序会有多个线程在运行,这时线程恢复有可能在下一个时间片,而Linux系统缺省的线程轮片时间为10ms,这意味着只有当延时在大于10ms情况时,常规的延时函数才有意义。在工控领域,我们常常碰到需要微秒级的延时需求,例如实现某种读写时序等,这时Linux系统的常规延时函数难于满足其需求。针对这样的应用需求,我们设计了采用内存映射的方法操作主板的硬件定时器和GPIO,从而产生出具有微秒精度的脉冲波形来。下面就详细介绍如何在用户进程实现这样的精确延时的操作。
以EM335x工控主板为例,用其内部的定时器来实现精确延时的功能,EM335x内部定时器的输入时钟为24MHz,单位时间为41.6ns,通过将Linux系统的mem设备文件和mmap()函数结合起来使用,可直接对EM335x内部定时器的寄存器进行操作,再通过同样的方式控制GPIO,实现:(1)设置GPIO,(2)启动定时器,当检测到定时器计数完毕,(3)再设置GPIO,共三个步骤,就可产生精确时间间隔的脉冲。
Linux系统中的/dev/mem设备文件,是专门用来读写物理地址用的,里面的内容是所有物理内存的地址以及内容信息。只要我们使用mmap()函数将/dev/mem设备文件映射到进程地址空间,实现对内存物理地址的读写,就能够通过这种方式快速的对GPIO和定时器进行操作,而mmap操作提供了一种机制,让用户程序直接访问设备内存,这样就相当于直接对硬件进行操作,从而避开了驱动程序,如果调用驱动就需要在用户空间和内核空间互相拷贝数据,还会涉及到系统调度等机制,效率将会变低。
将/dev/mem/设备文件中定时器的地址映射到用户进程空间的代码:
void *timer_em335x_pin_config(unsigned int BASE)
{
int mem_fd;
void *base;
mem_fd = open('/dev/mem', O_RDWR|O_SYNC);
printf('mem_fd is %d\n', mem_fd);
/* mmap Timer */
base = mmap(
NULL, // 起始地址
DMTIMER_DEV_SIZE, // 映射的文件内容的大小
PROT_READ|PROT_WRITE, // 映射区域可读可写
MAP_SHARED, // 映射区域的写入数据会写回到原来的文件
mem_fd,
BASE // 被映射的硬件地址
);
close(mem_fd);
return base;
}
将/dev/mem/设备文件中GPIO的地址映射到用户进程空间的代码:
void *GPIO_MMAP::gpio_em335x_pin_config(unsigned int BASE)
{
int mem_fd;
void *base;
mem_fd = open('/dev/mem', O_RDWR|O_SYNC);
printf('mem_fd is %d\n', mem_fd);
/* mmap GPIO */
base = mmap(
NULL, // 起始地址
GPIO_DEV_SIZE, // 映射的文件内容的大小
PROT_READ|PROT_WRITE, // 映射区域可读可写
MAP_SHARED, // 映射区域的写入数据会写回到原来的文件
mem_fd,
BASE // 被映射的硬件地址
);
close(mem_fd);
return base;
}
成功执行时,mmap()函数返回被映射区的指针。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。只需要使用返回的地址指针在对应的寄存器的偏移地址赋值,就可以完成操作。在例程中已经将函数接口引出(详细的代码请参考例程):
ptr=Timer_Init(); // 初始化,将定时器地址映射到用户进程
Timer_Start(ptr, GPIO0, 0xfffffffa); // 启动定时器,并设置时间和哪一位GPIO
定时器是从0计数到0xffffffff,需要实现定时功能,我们就要改变定时器的初值,上面的程序中0xfffffffa为定时器的初值,前面提到过由于EM335x定时器时钟为24MHZ,所以定时器单位时间为1/24000000=41.6ns,假设程序访问寄存器还需要花费时间T0,在计算初值的时候,就需要加上这一部分时间才能保证准确性,因此定时器取值的计算公式为:
T=0xffffffff-(目标延时/41.6ns)+T0
经过测试,执行一次程序访问寄存器所需花费的时间大约为T0=800ns。举个例子,比如目标延时为2μs,那么定时器初值为:0xffffffff-(2000/41.6)+800,也就是0xffffffe2,测试的时候带入这个值,再进行微调,即可得到想要的结果。
使用英创工控主板运行例程测试,分别测试延时1μs,1.5μs,2μs,5μs,10μs时的精度,结果如下:
目标延时 | 定时器取值 | 实际延时 | |
Min | Max | ||
1us | 0xfffffffa | 1.14us | 1.20us |
1us的测试波形
目标延时 | 定时器取值 | 实际延时 | |
Min | Max | ||
1.5us | 0xffffffee | 1.46us | 1.52us |
1.5μs的测试波形
目标延时 | 定时器取值 | 实际延时 | |
Min | Max | ||
2us | 0xffffffe2 | 1.90us | 2.08us |
2μs的测试波形
目标延时 | 定时器取值 | 实际延时 | |
Min | Max | ||
5us | 0xffffff9a | 4.92us | 5.04us |
5μs的测试波形
目标延时 | 定时器取值 | 实际延时 | |
Min | Max | ||
10us | 0xffffff22 | 9.90us | 10.10us |
10μs的测试波形
可以看到,在1μs时,误差范围在±200ns左右,超过1μs,其余的取值,误差都在±100ns以内,随着延时的增加,精确度将越来越高,在10μs的时候,误差已经非常小了。
通过以上方案实现了在用户进程对精确延时的操作,详细的操作代码请参考例程。
关于这一方法在EM9x60系列工控主板上的实现可阅读下文:英创嵌入式主板支持精确延时操作之二
注意事项:我们推荐客户直接使用例程中引出的接口进行操作,不推荐客户对硬件访问这一部分代码进行修改,以免在操作的时候出现无法预估的错误。
全部0条评论
快来发表一下你的评论吧 !