十一.GPT定时器使用
今天我们来学习一种新的定时器——GPT(General Purpose Timer通用定时器)。I.MX6UL可以通过GPT来实现高精度的定时效果。
GPT定时器原理
GPT和前面讲的EPIT的基础有很多相似度地方:
计数器都是32位;
- 时钟源可选择,但是GPT除过关闭时钟源还有5组可选择
- 12位分频器,可选1~4096分频
- 可通过设置比较值触发中断
- 32位计数器
- 有两种运行模式,但是稍微有些区别:分别是restar模式和free-run模式
当然还是有些区别的
- GPT是向上计数,从0开始每个时钟周期增加1
- 可以设置3个比较值进行比较中断
- 计数器溢出后会产生溢出中断
- 除了比较中断和溢出中断,还有两个输入捕获通道,可以产生捕获中断
GPT定时器在《IMX6ULL参考手册.pdf》Chapter 30General Purpose Timer (GPT)有很详细的介绍,首先看一下系统框架图
GPT两种运行模式
GPT有两种运行模式:restart模式和free-run模式,两种模式选择通过寄存器GPTx_CR中的FRR位[bit9]控制
restart模式下,当计数器的值和比较寄存器值相等时计数器直接清零,然后从0x00000000开始重新自增。所以这种模式下只有比较通道1有效,并且在向比较通道1寄存器写入数据时GPT计数器都会复位。
free-run模式,该模式适用于3个比价通道,比较事件触发中断后计数器不会复位而是直接数到0xFFFFFFFF,触发溢出中断后回滚到0x00000000。
GPT的输出模式
GPT的输出模式和EPIT差不多,直接从手册上截图,能看出来大致的流程,对应的IO也是可以设置高、低电平或反转模式
GPT的寄存器
I.MX6UL有两套GPT,先看下内存映射表
我们只对一套GPT寄存器分析,可以看出来,GPT跟EPIT的寄存器构成差不多, 一组控制,一组分频器(GPT把分频器单独定义了一个寄存器设置),3个输出比较寄存器用来存放对应的比较值,2个捕获输入通道,还有一个计数器寄存器。
控制寄存器
GPTx_CR状态寄存器ControlRegister,32位,寄存器结构如下表
这个寄存器内容有些多,按位说明有些事重复的我就省略了
下面我们挑几个比较重要的讲一下
bit[31:16]高16位都是设置输出、捕获等功能,我们暂时用不到。
SWR[bit15]SoftWareReset 软件复位,通过对该位写1即可复位定时器,复位后该位自动清零
EN_24M([bit10]) 24MHz时钟源使能,这个24MHz时钟源的分频器和另外一组时钟源的分频器是一组
FRR(bit[9]) 运行模式,是为Free-Run mode
CLKSRC(bit[8:6]) 时钟源选择
ENMOD(bit[1]) 启动初始值,1时从0开始计数,0时从上一次disable时的计数器值开始计数
EN(bit[0]) GPT使能
回顾一下EPIT,可以发现使用方法都差不多。
分频寄存器
分频寄存器GPTx_PR,Prescaler Register,32位,高16位无效,bit[15:12]对24MHz时钟源进行最高16分频,最低12位对时钟源进行分频
状态寄存器
状态寄存器GPTx_SR Status Register,只有5位可以用
ROV是回滚标志位Rollover Flag,当计数器从0x00000000累加到0xFFFFFFFF后该位置1,在Restar和Free-Run模式下都有效。(Restar模式应该是到达最大值。)
IF1、2对应两个捕获事件发生
OF3/2/1是三个比较事件发生。
注意5个位都是w1c的,也就是如果我们需要用中断处理事件时要对该bit写1清除。
中断寄存器
GPTx_IR,5位可以用,跟前面状态寄存器的结构一样
需要哪个时间可以触发中断,对该位置1即可。
还有比较值寄存器1、2、3,捕获寄存器1、2,计数器寄存器都是32位的,保存对应的值就行了。没什么可讲的。
GPT定时器使用
gpt定时器可以用作中断触发另外的服务,也可以定制成高精度的延时定时器。
GPT定时中断
GPT用作中断的时候跟EPIT的方法基本一致,先初始化,再注册服务函数,最后使能定时器。
这里跟教程上不太一样,我做了;一个通用的GPT定时器使用,直接放代码
/** * @file bsp_gpt.c * @author your name (you@domain.com) * @brief GPT定时器 * @version 0.1 * @date 2022-01-14 * * @copyright Copyright (c) 2022 * */ #include "bsp_gpt.h" /** * @brief 通用GPT定时器初始化 * * @param num 定时器编号1/2 * @param flac 分频器0~4095 * @param value 终止值 */ void gpt_init(int num,unsigned int flac,unsigned int value) { volatile GPT_Type *gpt; volatile unsigned int iRn; if(num ==1) //判定选择哪个GPT { gpt = GPT1; iRn = GPT1_IRQn; } else if(num ==2) { gpt = GPT2; iRn = GPT2_IRQn; } else{} gpt->CR = 0; //清除GPT gpt->CR |= (1<<15); //复位GPT while((gpt->CR >>15) & 0x01); //复位结束,SWR回0,跳出while循环 gpt->CR |= (1<<1) | (1<<6); //初始化 gpt->PR = flac; //分频器 gpt->OCR[0] = value; //计数器中值 gpt->IR = 1<<0; //使能中断 GIC_EnableIRQ(iRn); //GIC使能GPT system_register_irqHandler(iRn,gpt_irqhandler,NULL); //注册中断服务 gpt->CR |= 1<<0; //使能GPT } /** * @brief GPT中断服务函数 * * @param gicciar 中断ID号 * @param param 参数 */ void gpt_irqhandler(unsigned int gicciar,void *param) { if(gicciar == GPT1_IRQn) { gpt1_irqhandler(); //GPT1中断调用实际服务函数 GPT1->SR |= (1<<0); //清除GPT1_SR中断标志位 } else if(gicciar == GPT2_IRQn) { gpt2_irqhandler(); //GPT2中断调用实际服务函数 GPT2->SR |= (1<<0); //清除GPT2_SR中断标志位 } } void gpt1_irqhandler(void) { static unsigned char status=OFF; status = !status; beep_switch(status); } void gpt2_irqhandler(void) { static unsigned char status=OFF; status = !status; led_switch(LED0,status); }
这个确实也没什么可讲到,这是个低版本的定时中断功能,GPT还有很多其他的作用这里没写,就是那几个寄存器,直到怎么用就可以了。初始化函数在头文件里声明一下就可以用了。
想要用哪个定时器,在main.c里直接初始化就可以
gpt_init(1,65,500000); gpt_init(2,65,1000000);
下面是头文件
#ifndef __BSP_GPT_H #define __BSP_GPT_H #include "imx6ul.h" #include "bsp_int.h" /*调用底层硬件驱动*/ #include "bsp_beep.h" #include "bsp_led.h" /*声明终端处理函数*/ void gpt_irqhandler(unsigned int gicciar,void *param); void gpt1_irqhandler(void); void gpt2_irqhandler(void); #endif
其实还可以解藕一下,初始化的函数一被调用,定时器就启动了,其实应该把初始化、定时器启动、定时器停止分开模块化。这里不再细分了。
高精度定时器
我们最开始闪烁LED的时候写了一个延时函数,那个函数是通过一个变量自减实现的,也就是个软件延时,而软件延时跟CPU主频什么的关系比较大,再更改CPU主频后肯定就不准了。而GPT是个定时器,我们可以利用定时器的特性实现延时功能。
/* * @description :基于GPT1的延时功能初始化 * @param :None * @return :None */ void delay_init(void) { /*先对通过对SWR标志位写1复位*/ GPT1->CR = 0; GPT1->CR |= (1<<15); while((GPT1->CR >>15) & 0x01); //复位结束,SWR回0,跳出while循环 GPT1->CR |= 1<<1 | 1<<6; //设置ENMOD=1,时钟源为IPG_CLK GPT1->PR = 65; //66分频,周期为1M GPT1->OCR[0] = 0xFFFFFFFF; //输出比较1寄存器 GPT1->CR |= 1<<0; //使能GPT1定时器 } /** * @brief 微秒精度定时器 * * @param us 定时微秒值 */ void delay_us(unsigned int us) { unsigned long oldcnt,newcnt; unsigned long tcntvalue = 0; oldcnt = GPT1->CNT; //函数启动是GPT1计数值 while(1) { newcnt = GPT1->CNT; //当前计数值 if(newcnt != oldcnt) { if(newcnt>oldcnt) //未溢出 { tcntvalue += newcnt-oldcnt; //newcnt-oldcnt=1,tcntvalue加1 } else //溢出 { // tcntvalue += 0xFFFFFFFF -oldcnt + newcnt; //正点原子教程上的代码 tcntvalue += 0xFFFFFFFF -oldcnt + newcnt +1; } oldcnt = newcnt; if(tcntvalue >= us) //延时值超过指定微秒值 { break; } } } } /** * @brief 毫秒精度定时器 * * @param ms 定时毫秒值 */ void delay_ms(unsigned int ms) { int i=0; for(i=0;i<ms;i++) {delay_us(1000);} }
初始化过程中值定义了时钟源、分频器和OCR1的值(也可以不定义OCR1,设定工作模式为free-run模式),这样,GPT的计数器就可以从0跑到0xFFFFFFFF,分频器值为65,对应66分频,时钟源是外设时钟源(IPG_CLK),时钟周期66MHz,对其进行66分频刚好计数器加1时间为1us。我们只需要计算counter的值就行了。函数里定义了3个变量,分别表示这个指令周期counter的值,上个指令周期counter的值以及总数。这里有个点要注意,GPT的计数器在跑到0xFFFFFFFF是会溢出后回到0的,要注意溢出时那个周期,oldcnd值为0xFFFFFFFF,newcnt的值为0,那么这时候再用newcnt-oldcnt值就跑远了,这里要修正一下。根据正点原子的教程上提供的代码,是这样的
tcntvalue += 0xFFFFFFFF -oldcnt + newcnt;
我觉得这里应该是少加了个1,因为溢出后newcnt值应该是0,oldcnt值为0xFFFFFFFF,结果这个指令周期tcntvalue累加值为0。应该是不对的。
我用示波器对这个延时函数进行了测量,这种延时的用法要比中断精度差一些,但是精确到ms还是可以的。还有一点,也是个延时函数,也就是说以旧需要占用CPU资源的,(us的是阻塞在while循环里,ms的是阻塞在for循环里)。