延时函数是很常用的 API
函数,在前面的实验中我们使用循环来实现延时函数,但是使用循环来实现的延时函数不准确,误差会很大。虽然使用到延时函数的地方精度要求都不会很严格(
要求严格的话就使用硬件定时器了
)
,但是延时函数肯定是越精确越好,这样延时函数就可以使用在某些对时序要求严格的场合。本章我们就来学习一下如何使用硬件定时器来实现高精度延时。
1、 高精度延时简介
1.1 GPT 定时器简介
学过 STM32
的同学应该知道,在使用
STM32
的时候可以使用
SYSTICK
来实现高精度延时。I.MX6U
没有
SYSTICK
定时器,但是
I.MX6U
有其他定时器啊,比如第十五章讲解的
EPIT定时器。本章我们使用 I.MX6U
的
GPT
定时器来实现高精度延时,顺便学习一下
GPT
定时器, GPT 定时器全称为
General Purpose Timer
。
GPT 定时器是一个
32
位向上定时器
(
也就是从
0X00000000
开始向上递增计数
)
,
GPT
定时器也可以跟一个值进行比较,当计数器值和这个值相等的话就发生比较事件,产生比较中断。GPT 定时器有一个
12
位的分频器,可以对
GPT
定时器的时钟源进行分频,
GPT
定时器特性如下:
- ①、一个可选时钟源的 32 位向上计数器。
- ②、两个输入捕获通道,可以设置触发方式。
- ③、三个输出比较通道,可以设置输出模式。
- ④、可以生成捕获中断、比较中断和溢出中断。
- ⑤、计数器可以运行在重新启动(restart)或(自由运行)free-run 模式。
- GPT 定时器的可选时钟源如图 20.1.1.1 所示:
从图 20.1.1.1
可以看出一共有五个时钟源,分别为:
ipg_clk_24M
、
GPT_CLK(
外部时钟
)
、ipg_clk、
ipg_clk_32k
和
ipg_clk_highfreq
。本例程选择
ipg_clk
为
GPT
的时钟源,
ipg_clk=66MHz
。
GPT
定时器结构如图
20.1.1.2
所示:
图 20.1.1.2
中各部分意义如下:
- ①、此部分为 GPT 定时器的时钟源,前面已经说过了,本章例程选择 ipg_clk 作为 GPT 定时器时钟源。
- ②、此部分为 12 位分频器,对时钟源进行分频处理,可设置 0~4095,分别对应 1~4096 分频。
- ③、经过分频的时钟源进入到 GPT 定时器内部 32 位计数器。
- ④和⑤、这两部分是 GPT 的两路输入捕获通道,本章不讲解 GPT 定时器的输入捕获。
- ⑥、此部分为输出比较寄存器,一共有三路输出比较,因此有三个输出比较寄存器,输出比较寄存器是 32 位的。
- ⑦、此部分位输出比较中断,三路输出比较中断,当计数器里面的值和输出比较寄存器里面的比较值相等就会触发输出比较中断。
GPT 定时器有两种工作模式:重新启动
(restart)
模式和自由运行
(free-run)
模式,这两个工作模式的区别如下:
重新启动(restart)
模式
:当
GPTx_CR(x=1
,
2)
寄存器的
FRR
位清零的时候
GPT
工作在此 模式。在此模式下,当计数值和比较寄存器中的值相等的话计数值就会清零,然后重新从 0X00000000 开始向上计数,只有比较通道
1
才有此模式!向比较通道
1
的比较寄存器写入任何 数据都会复位 GPT
计数器。对于其他两路比较通道(通道
2
和
3
),当发生比较事件以后不会复位计数器。
自由运行(free-run)
模式
:当
GPTx_CR(x=1
,
2)
寄存器的
FRR
位置
1
时候
GPT
工作在此模 式下,此模式适用于所有三个比较通道,当比较事件发生以后并不会复位计数器,而是继续计 数,直到计数值为 0XFFFFFFFF
,然后重新回滚到
0X00000000
。
接下来看一下 GPT
定时器几个重要的寄存器,第一个就是
GPT
的配置寄存器
GPTx_CR
, 此寄存器的结构如图 20.1.1.3
所示:
寄存器
GPTx_CR
我们用到的重要位如下:
SWR(bit15):复位
GPT
定时器,向此位写
1
就可以复位
GPT
定时器,当
GPT
复位完成以后此为会自动清零。
FRR(bit9):
运行模式选择,当此位为
0
的时候比较通道
1
工作在重新启动
(restart)
模式。当此位为 1
的时候所有的三个比较通道均工作在自由运行模式
(free-run)
。
CLKSRC(bit8:6):
GPT
定时器时钟源选择位,为
0
的时候关闭时钟源;为
1
的时候选择ipg_clk 作为时钟源;为
2
的时候选择
ipg_clk_highfreq
为时钟源;为
3
的时候选择外部时钟为时钟源;为 4
的时候选择
ipg_clk_32k
为时钟源;为
5
的时候选择
ip_clk_24M
为时钟源。本章例程选择 ipg_clk
作为
GPT
定时器的时钟源,因此此位设置位
1(0b001)
。
ENMOD(bit1):
GPT
使能模式,此位为
0
的时候如果关闭
GPT
定时器,计数器寄存器保存定时器关闭时候的计数值。此位为 1
的时候如果关闭
GPT
定时器,计数器寄存器就会清零。
EN(bit):
GPT
使能位,为
1
的时候使能
GPT
定时器,为
0
的时候关闭
GPT
定时器。
接下来看一下 GPT
定时器的分频寄存器
GPTx_PR
,此寄存器结构如图
20.1.1.4
所示:
寄存器
GPTx_PR
我们用到的重要位就一个:
PRESCALER(bit11:0)
,这就是
12
位分频值,可设置 0~4095
,分别对应
1~4096
分频。
接下来看一下
GPT
定时器的状态寄存器
GPTx_SR
,此寄存器结构如图
20.1.1.5
所示:
寄存器 GPTx_SR
重要的位如下:
ROV(bit5):
回滚标志位,当计数值从
0XFFFFFFFF
回滚到
0X00000000
的时候此位置
1
。
IF2~IF1(bit4:3):
输入捕获标志位,当输入捕获事件发生以后此位置
1
,一共有两路输入捕 获通道。如果使用输入捕获中断的话需要在中断处理函数中清除此位。
OF3~OF1(bit2:0):输出比较中断标志位,当输出比较事件发生以后此位置
1
,一共有三路 输出比较通道。如果使用输出比较中断的话需要在中断处理函数中清除此位。接着看一下 GPT
定时器的计数寄存器
GPTx_CNT
,这个寄存器保存着
GPT
定时器的当前计数值。最后看一下 GPT
定时器的输出比较寄存器
GPTx_OCR
,每个输出比较通道对应一个输出比较寄存器,因此一个 GPT
定时器有三个
OCR
寄存器,它们的作都是相同的。以输出比较通道 1
为例,其输出比较寄存器为
GPTx_OCR1
,这是一个
32
位寄存器,用于存放
32
位的比较值。当计数器值和寄存器 GPTx_OCR1
中的值相等就会产生比较事件,如果使能了比较中断的话就会触发相应的中断。
关于 GPT
的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《
I.MX6ULL
参考手册》第 1432
页的
30.6
小节。
1.2 定时器实现高精度延时原理
高精度延时函数的实现肯定是要借助硬件定时器,前面说了本章实验使用 GPT
定时器来实现高精度延时。如果设置 GPT
定时器的时钟源为
ipg_clk=66MHz
,设置
66
分频,那么进入
GPT定时器的最终时钟频率就是 66/66=1MHz
,周期为
1us
。
GPT
的计数器每计一个数就表示“过去”了 1us
。如果计
10
个数就表示“过去”了
10us
。通过读取寄存器
GPTx_CNT
中的值就知道计了个数比如现在要延时100us
,那么进入延时函数以后纪录下寄存器
GPTx_CNT
中的值为
200
, 当 GPTx_CNT
中的值为
300
的时候就表示
100us
过去了,也就是延时结束。
GPTx_CNT
是个 32 位寄存器,如果时钟为
1MHz
的话,
GPTx_CNT
最多可以实现
0XFFFFFFFFus=4294967295us ≈4294s
≈
72min
。也就是说
72
分钟以后
GPTx_CNT
寄存器就会回滚到
0X00000000
,也就是溢 出,所以需要在延时函数中要处理溢出的情况。关于定时器实现高精度延时的原理就讲解到这 里,原理还是很简单的,高精度延时的实现步骤如下:
1、设置
GPT1
定时器
首先设置 GPT1_CR
寄存器的
SWR(bit15)
位来复位寄存器
GPT1
。复位完成以后设置寄存器 GPT1_CR
寄存器的
CLKSRC(bit8:6)
位,选择
GPT1
的时钟源为
ipg_clk
。设置定时器
GPT1
的工作模式,
2、设置
GPT1
的分频值
设置寄存器 GPT1_PR
寄存器的
PRESCALAR(bit111:0)
位,设置分频值。
3、设置
GPT1
的比较值
如果要使用 GPT1
的输出比较中断,那么
GPT1
的输出比较寄存器
GPT1_OCR1
的值可以 根据所需的中断时间来设置。本章例程不使用比较输出中断,所以将 GPT1_OCR1
设置为最大 值,即:0XFFFFFFFF
。
4、使能
GPT1
定时器
设置好 GPT1
定时器以后就可以使能了,设置
GPT1_CR
的
EN(bit0)
位为
1
来使能
GPT1
定 时器。
5、编写延时函数
GPT1定时器已经开始运行了,可以根据前面介绍的高精度延时函数原理来编写延时函数, 针对 us
和
ms
延时分别编写两个延时函数。
2、 硬件原理分析
本试验用到的资源如下:
①、一个
LED
灯:
LED0
。
②、定时器
GPT1
。
本实验通过高精度延时函数来控制
LED0
的闪烁,可以通过示波器来观察
LED0
的控制
IO
输出波形,通过波形的频率或者周期来判断延时函数精度是否正常。
3、 实验程序编写
本章实验在上一章例程的基础上完成,更改工程名字为“
delay
”,直接修改
bsp_delay.c
和
bsp_delay.h
这两个文件,将
bsp_delay.h
文件改为如下所示内容:
#ifndef __BSP_DELAY_H
#define __BSP_DELAY_H
#include "imx6ul.h"
/* 函数声明 */
void delay_init(void);
void delayus(unsigned int usdelay);
void delayms(unsigned int msdelay);
void delay(volatile unsigned int n);
void gpt1_irqhandler(void);
#endif
bsp_delay.h
文件就是一些函数声明,很简单。在文件
bsp_delay.c
中输入如下内容:
#include "bsp_delay.h"
/*
* @description : 延时有关硬件初始化,主要是GPT定时器
GPT定时器时钟源选择ipg_clk=66Mhz
* @param : 无
* @return : 无
*/
void delay_init(void)
{
GPT1->CR = 0; /* 清零,bit0也为0,即停止GPT */
GPT1->CR = 1 << 15; /* bit15置1进入软复位 */
while((GPT1->CR >> 15) & 0x01); /*等待复位完成 */
/*
* GPT的CR寄存器,GPT通用设置
* bit22:20 000 输出比较1的输出功能关闭,也就是对应的引脚没反应
* bit9: 0 Restart模式,当CNT等于OCR1的时候就产生中断
* bit8:6 001 GPT时钟源选择ipg_clk=66Mhz
* bit
*/
GPT1->CR = (1<<6);
/*
* GPT的PR寄存器,GPT的分频设置
* bit11:0 设置分频值,设置为0表示1分频,
* 以此类推,最大可以设置为0XFFF,也就是最大4096分频
*/
GPT1->PR = 65; /* 设置为65,即66分频,因此GPT1时钟为66M/(65+1)=1MHz */
/*
* GPT的OCR1寄存器,GPT的输出比较1比较计数值,
* GPT的时钟为1Mz,那么计数器每计一个值就是就是1us。
* 为了实现较大的计数,我们将比较值设置为最大的0XFFFFFFFF,
* 这样一次计满就是:0XFFFFFFFFus = 4294967296us = 4295s = 71.5min
* 也就是说一次计满最多71.5分钟,存在溢出
*/
GPT1->OCR[0] = 0XFFFFFFFF;
GPT1->CR |= 1<<0; //使能GPT1
/* 一下屏蔽的代码是GPT定时器中断代码,
* 如果想学习GPT定时器的话可以参考一下代码。
*/
#if 0
/*
* GPT的PR寄存器,GPT的分频设置
* bit11:0 设置分频值,设置为0表示1分频,
* 以此类推,最大可以设置为0XFFF,也就是最大4096分频
*/
GPT1->PR = 65; //设置为1,即65+1=66分频,因此GPT1时钟为66M/66=1MHz
/*
* GPT的OCR1寄存器,GPT的输出比较1比较计数值,
* 当GPT的计数值等于OCR1里面值时候,输出比较1就会发生中断
* 这里定时500ms产生中断,因此就应该为1000000/2=500000;
*/
GPT1->OCR[0] = 500000;
/*
* GPT的IR寄存器,使能通道1的比较中断
* bit0: 0 使能输出比较中断
*/
GPT1->IR |= 1 << 0;
/*
* 使能GIC里面相应的中断,并且注册中断处理函数
*/
GIC_EnableIRQ(GPT1_IRQn); //使能GIC中对应的中断
system_register_irqhandler(GPT1_IRQn, (system_irq_handler_t)gpt1_irqhandler, NULL); //注册中断服务函数
#endif
}
#if 0
/* 中断处理函数 */
void gpt1_irqhandler(void)
{
static unsigned char state = 0;
state = !state;
/*
* GPT的SR寄存器,状态寄存器
* bit2: 1 输出比较1发生中断
*/
if(GPT1->SR & (1<<0))
{
led_switch(LED2, state);
}
GPT1->SR |= 1<<0; /* 清除中断标志位 */
}
#endif
/*
* @description : 微秒(us)级延时
* @param - value : 需要延时的us数,最大延时0XFFFFFFFFus
* @return : 无
*/
void delayus(unsigned int usdelay)
{
unsigned long oldcnt,newcnt;
unsigned long tcntvalue = 0; /* 走过的总时间 */
oldcnt = GPT1->CNT;
while(1)
{
newcnt = GPT1->CNT;
if(newcnt != oldcnt)
{
if(newcnt > oldcnt) /* GPT是向上计数器,并且没有溢出 */
tcntvalue += newcnt - oldcnt;
else /* 发生溢出 */
tcntvalue += 0XFFFFFFFF-oldcnt + newcnt;
oldcnt = newcnt;
if(tcntvalue >= usdelay)/* 延时时间到了 */
break; /* 跳出 */
}
}
}
/*
* @description : 毫秒(ms)级延时
* @param - msdelay : 需要延时的ms数
* @return : 无
*/
void delayms(unsigned int msdelay)
{
int i = 0;
for(i=0; i<msdelay; i++)
{
delayus(1000);
}
}
/*
* @description : 短时间延时函数
* @param - n : 要延时循环次数(空操作循环次数,模式延时)
* @return : 无
*/
void delay_short(volatile unsigned int n)
{
while(n--){}
}
/*
* @description : 延时函数,在396Mhz的主频下
* 延时时间大约为1ms
* @param - n : 要延时的ms数
* @return : 无
*/
void delay(volatile unsigned int n)
{
while(n--)
{
delay_short(0x7ff);
}
}
文件 bsp_delay.c
中一共有
5
个函数,分别为:
delay_init
、
delayus
、
delayms
、
delay_short 和 delay
。除了
delay_short
和
delay
以外,其他三个都是新增加的。函数
delay_init
是延时初始化函数,主要用于初始化 GPT1
定时器,设置其时钟源、分频值和输出比较寄存器值。第
43
到 68 行被屏蔽掉的程序是
GPT1
的中断初始化代码,如果要使用
GPT1
的中断功能的话可以参考此部分代码。第 73
到
89
行被屏蔽掉的程序是
GPT1
的中断处理函数
gpt1_irqhandler
,同样的, 如果需要使用 GPT1
中断功能的话可以参考此部分代码。
函数 delayus
和
delayms
就是
us
级和
ms
级的高精度延时函数,函数
delayus
就是按照我们
在
20.1.2
小节讲解的高精度延时原理编写的,
delayus
函数处理
GPT1
计数器溢出的情况。函数delayus 只有一个参数
usdelay
,这个参数就是要延时的
us
数。
delayms
函数很简单,就是对 delayus(1000)的多次叠加,此函数也只有一个参数
msdelay
,也就是要延时的
ms
数。
最后修改 main.c
文件,内容如下:
#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_keyfilter.h"
/*
* @description : main函数
* @param : 无
* @return : 无
*/
int main(void)
{
unsigned char state = OFF;
int_init(); /* 初始化中断(一定要最先调用!) */
imx6u_clkinit(); /* 初始化系统时钟 */
delay_init(); /* 初始化延时 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */
beep_init(); /* 初始化beep */
while(1)
{
state = !state;
led_switch(LED0, state);
delayms(500);
}
return 0;
}
main.c
函数很简单,在第
20
行调用
delay_init
函数进行延时初始化,最后在
while
循环中 周期性的点亮和熄灭 LED0
,调用函数
delayms
来实现延时。
4、 编写 Makefile 和链接脚本
因为本章例程并没有新建任何文件,所以只需要修改
Makefile
中的
TARGET
为
delay
即
可,链接脚本保持不变。
5、编译下载
使用 Make 命令编译代码,编译成功以后使用软件 imxdownload2 将编译完成的 bsp.bin 文件生成可执行的img文件,命令如下:
make
./imxdownload2 delay.bin
如果 imxdownload2无权限,可用以下命令添加权限
chmod 777 imxdownload2
利用Win32DiskImager软件将load.img执行文件写入SD卡,SD卡插入开发板上即可正常运行。如果代码运行正常的话 LED0 会以以 500ms 为周期不断的亮、灭闪烁。可以通过肉眼观察 LED 亮灭的时间是否为 500ms。
但是肉眼观察肯定不准确,既然本章号称高精度延时实验,那么就得经得住专业仪器的测试。我们
率就应该是
1/0.00004=25000Hz=25KHz
。使用示波器测试
LED0
对应的
IO
频率,结果如图
20.4.2.1
所示:
从图
20.4.2.1
可以看出,
LED0
对应的
IO
波形频率为
22.3KHz
,周期是
44.9us
,那么
main 函数中 while
循环执行一次的时间就是
44.9/2=22.45us
,大于我们设置的
20us
,看起来好像是延 时不准确。但是我们要知道这 22.45us
是
main
函数里面
while
循环总执行时间,也就是下面代
码的总执行时间:
while(1)
{
state = !state;
led_switch(LED0, state);
delayus(20);
}
在上面代码中不止有
delayus(20)
延时函数,还有控制
LED
灯亮灭的函数,这些代码的执行也需要时间的,即使是 delayus
函数,其内部也是要消耗一些时间的。假如我们将
while
循环里面的代码改为如下形式:
while(1)
{
GPIO1->DR &= ~(1<<3);
delayus(20);
GPIO1->DR |= (1<<3);
delayus(20);
}
上述代码我们通过直接操作寄存器的方式来控制
IO
输出高低电平,理论上
while
循环执行时间会更小,并且 while
循环里面使用了两个
delayus(20)
,因此执行一次
while
循环的理论时间应该是 40us
,和上面做的实验一样。重新使用示波器测量一下,结果如图
20.4.2.2
所示:
从图
20.4.2.2
可以看出,此时
while
循环执行一次的时间是
41.8us
,那么一次
delayus(20)
的时间就是 41.8/2=20.9us
,很接近我们的
20us
理论值。但是还是因为有其他程序开销存在,在加上示波器测量误差,所以不可能测量出绝对的 20us
。但是其已经非常接近了,基本可以证明我们的高精度延时函数是成功的、可以用的。