参考:Linux NXP (I.MX6ULL) GPT高精度延时定时器
作者:一只青木呀
发布时间: 2020-09-20 11:50:14
网址:https://blog.csdn.net/weixin_45309916/article/details/108690475
目录
- GPT 定时器简介
- GPT 定时器特性
- GPT 定时器时钟源选择
- GPT 定时器结构
- GPT 定时器的两种工作模式
- GPT 定时器几个重要的寄存器
- 配置寄存器 GPTx_CR
- 分频寄存器 GPTx_PR
- 状态寄存器 GPTx_SR
- 计数寄存器 GPTx_CNT
- 输出比较寄存器 GPTx_OCR
- 定时器高精度延时实现
- 硬件原理分析
- 实验程序编写
- bsp_delay.h
- bsp_delay.c
- 编译下载及示波器验证
GPT 定时器简介
GPT 定时器全称为 General Purpose Timer,GPT 定时器是一个 32 位向上定时器(也就是从 0X00000000 开始向上递增计数), GPT 定时器也可以跟一个值进行比较,当计数器值和这个值相等的话就发生比较事件,产生比较中断。GPT 定时器有一个 12 位的分频器,可以对 GPT 定时器的时钟源进行分频。
GPT 定时器特性
-
①、一个可选时钟源的 32 位向上计数器。
-
②、三个输出比较通道,可以设置输出模式。
-
③、两个输入捕获(配置引脚复用)通道,可以设置触发方式。EPIT定时器只能定时,不能捕获。
-
④、可以生成捕获中断、比较中断和溢出中断。
-
⑤、计数器可以运行在重新启动(restart)或(自由运行)free-run 模式。
GPT 定时器时钟源选择
从上图中可以看出一共有五个时钟源,分别为: ipg_clk_24M、 GPT_CLK(外部时钟)、ipg_clk、ipg_clk_32k 和 ipg_clk_highfreq。本博文选择 ipg_clk 为 GPT 的时钟源, ipg_clk_66MHz。
GPT 定时器结构
GPT 定时器结构中各部分意义如下:
①、此部分为 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 定时器几个重要的寄存器
配置寄存器 GPTx_CR
寄存器 GPTx_CR 我们用到的重要位如下:
位 | 描述 |
---|---|
SWR(bit15) | 软件复位 ,向此位写 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(bit0) | GPT 使能位,为 1 的时候使能 GPT 定时器,为 0 的时候关闭 GPT 定时器。 |
分频寄存器 GPTx_PR
寄存器 GPTx_PR 我们用到的重要位如下(低12位):
位 | 描述 |
---|---|
PRESCALER(bit11:0) | 这就是 12 位分频值,可设置 0~ 4095,分别对应 1~4096 分频。 |
状态寄存器 GPTx_SR
寄存器 GPTx_SR 重要的位如下:
位 | 描述 |
---|---|
ROV(bit5) | 回滚标志位(溢出位),当计数值从 0XFFFFFFFF 回滚到 0X00000000 的时候此位置 1。 |
IF2~IF1(bit4:3) | 输入捕获标志位,当输入捕获事件发生以后此位置 1,一共有两路输入捕获通道。如果使用输入捕获中断的话需要在中断处理函数中清除此位。 |
OF3~OF1(bit2:0) | 输出比较中断标志位,当输出比较事件发生以后此位置 1,一共有三路输出比较通道。如果使用输出比较中断的话需要在中断处理函数中清除此位。 |
计数寄存器 GPTx_CNT
接着看一下 GPT 定时器的计数寄存器 GPTx_CNT,这个寄存器保存着 GPT 定时器的当前计数值。
输出比较寄存器 GPTx_OCR
最后看一下 GPT 定时器的输出比较寄存器 GPTx_OCR,每个输出比较通道对应一个输出比较寄存器,因此一个 GPT 定时器有三个 OCR 寄存器,它们的作都是相同的。以输出比较通道 1 为例,其输出比较寄存器为 GPTx_OCR1,这是一个 32 位寄存器,用于存放 32 位的比较值。当计数器值和寄存器 GPTx_OCR1 中的值相等就会产生比较事件,如果使能了比较中断的话就会触发相应的中断。
定时器高精度延时实现
高精度延时函数的实现肯定是要借助硬件定时器,使用 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 延时分别编写两个延时函数。
硬件原理分析
本试验用到的资源如下:
①、一个LED 灯:LED0。
②、定时器GPT1。
本实验通过高精度延时函数来控制LED0 的闪烁,可以通过示波器来观察LED0 的控制IO输出波形,通过波形的频率或者周期来判断延时函数精度是否正常。
实验程序编写
本实验对应的例程路径为:开发板光盘-> 1、裸机例程-> 12_highpreci_delay。
本章实验在上一章例程的基础上完成,更改工程名字为“delay”,直接修改bsp_delay.c 和bsp_delay.h 这两个文件,将bsp_delay.h 文件改为如下所示内容:
bsp_delay.h
#ifndef __BSP_DELAY_H
#define __BSP_DELAY_H
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名 : bsp_delay.h
作者 : 左忠凯
版本 : V1.0
描述 : 延时头文件。
其他 : 无
论坛 : www.wtmembed.com
日志 : 初版V1.0 2019/1/4 左忠凯创建V2.0 2019/1/15 左忠凯修改添加了一些函数声明。
***************************************************************/
#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 中输入如下内容:
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;//500ms产生中断/** GPT的IR寄存器,使能通道1的比较中断* bit0: 0 使能输出比较中断*/GPT1->IR |= 1 << 0;/** 使能GIC里面相应的中断,并且注册中断处理函数*/ //宏定义里中断ID是87GIC_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 来实现延时。
编译下载及示波器验证
因为本章例程并没有新建任何文件,所以只需要修改Makefile 中的TARGET 为delay 即可,链接脚本保持不变。
使用Make 命令编译代码,编译成功以后使用软件imxdownload 将编译完成的delay.bin 文件下载到SD 卡中,命令如下:
chmod 777 imxdownload //给予imxdownload 可执行权限,一次即可
./imxdownload delay.bin /dev/sdd //烧写到SD 卡中,不能烧写到/dev/sda 或sda1 设备里面!
烧写成功以后将SD 卡插到开发板的SD 卡槽中,然后复位开发板。程序运行正常的话LED0会以500ms 为周期不断的亮、灭闪烁。可以通过肉眼观察LED 亮灭的时间是否为500ms。但是肉眼观察肯定不准确,既然本章号称高精度延时实验,那么就得经得住专业仪器的测试。我们将“示例代码20.3.3”中第29 行,也就是main 函数while 循环中的延时改为“delayus(20)”,也就是LED0 亮灭的时间各为20us,那么一个完整的周期就是20+20=40us,LED0 对应的IO 频率就应该是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。但是其已经非常接近了,基本可以证明我们的高精度延时函数是成功的、可以用的。