前面的实验中我们使用循环来实现延时函数,但使用循环实现的延时函数不准确,误差会很大。某些对时序要求严格的场合要使用硬件定时器,延时函数越精确越好。本篇学习如何使用硬件定时器实现高精度延时。
我们在使用 STM32 时可使用 SYSTICK(系统滴答定时器) 来实现高精度延时。本章我们使用 I.MX6U 的 GPT 定时器来实现高精度延时,GPT 定时器全称为 General Purpose Timer。
GPT 定时器是一个 32 位向上定时器(也就是从 0X00000000 开始向上递增计数), GPT 定时器也可以跟一个值进行比较,当计数器值和这个值相等的话就产生比较事件。GPT 定时器有一个 12 位的分频器,可以对 GPT 定时器的时钟源进行分频, GPT 定时器特性如下:
GPT 定时器的可选时钟源如下图:
从图中可看出共有五个时钟源,分别为: ipg_clk_24M、 GPT_CLK(外部时钟)、ipg_clk、ipg_clk_32k 和 ipg_clk_highfreq。本实验我们选择 ipg_clk 为 GPT 的时钟源, ipg_clk=66MHz。
GPT 定时器结构如下图:
图 中各部分意义如下:
①部分为 GPT 定时器的时钟源输入,本实验选 ipg_clk(66MHz)作为 GPT 定时器时钟源。
②部分为 12 位分频器,对时钟源进行分频处理,可设置 0~4095,分别对应 1~4096 分频。
③、经过分频的时钟源进入到 GPT 定时器内部 32 位计数器。
④和⑤这两部分是 GPT 的两路输入捕获通道。
⑥部分为输出比较寄存器,一共有三路输出比较,因此有三个输出比较寄存器,输出比较寄存器是 32 位的。
⑦部分位输出比较中断,三路输出比较中断,当计数器里面的值和输出比较寄存器里面的比较值相等就会触发输出比较中断。
GPT 定时器两种工作模式:重新启动(restart)模式和自由运行(free-run)模式,当 GPTx_CR(x=1, 2)寄存器的 FRR 位清零的时候 GPT 工作在重新启动(restart)模式,当 GPTx_CR(x=1, 2)寄存器的 FRR 位置 1 时候 GPT 工作在自由运行(free-run)模式。(常用的是restart模式)
重新启动(restart)模式:在此模式下,当计数值和比较寄存器中的值相等的话计数值就会清零,然后重新从0x00000000 开始向上计数,只有比较通道 1 才有此模式!向比较通道 1 的比较寄存器写入任何数据都会复位 GPT 计数器。而比较通道 2 和 3,当发生比较事件以后不会复位计数器。
自由运行(free-run)模式:此模式适用于所有三个比较通道,当比较事件发生后并不会复位计数器,而是继续计数,直到计数值为 0XFFFFFFFF,然后重新回滚到 0X00000000。
寄存器 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 (001)。
ENMOD(bit1):GPT 使能模式,此位为 0 时如果关闭 GPT 定时器,计数器寄存器保存定时器关闭时候的计数值。此位为 1 的时如果关闭 GPT 定时器,计数器寄存器就会清零。
EN(bit):GPT 使能位,为 1 的时候使能 GPT 定时器,为 0 的时候关闭 GPT 定时器。
寄存器 GPTx_PR 我们用到的位就 PRESCALER(bit11:0),这就是 12 位分频值,可设置0~4095,分别对应 1~4096 分频。
GPT 定时器的状态寄存器 GPTx_SR:
寄存器 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 定时器来实现高精度延时。若设置 GPT 定时器的时钟源为 ipg_clk=66MHz,设置 66 分频,那么进入 GPT定时器的最终时钟频率就是 66/66=1MHz。周期为 1us,即 GPT 的计数器每计一个数就表示“过去”了 1us。计 10 个数就表示“过去”了 10us。通过读取寄存器 GPTx_CNT 中的值就知道计数个数。 GPTx_CNT 是个32 位寄存器,如果时钟为 1MHz 的话, GPTx_CNT 最多可以实现 0XFFFFFFFFus = 4294967295us ≈ 4294s ≈ 72min。也就是说 72 分钟以后 GPTx_CNT 寄存器就会回滚到 0X00000000,计数溢出,所以需在延时函数中要处理溢出的情况。
我们直接修改之前的软件delay函数的 bsp_delay.c 和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.c 中一共有 5 个函数,分别为: delay_init、 delayus、 delayms 、 delay_short和 delay。除了 delay_short 和 delay 以外,其他三个都是新增加的。函数 delay_init 是延时初始化函数,主要用于初始化 GPT1 定时器,设置其时钟源、分频值和输出比较寄存器值。中间一段被屏蔽掉的程序是 GPT1 的中断初始化代码和中断处理函数gpt1_irqhandler,若要使用 GPT1 的中断功能的话可参考此部分代码。
函数 delayus 和 delayms 就是 us 级和 ms 级的高精度延时函数,函数 delayus 就是按照上文中所记录的高精度延时原理编写的, delayus 函数处理 GPT1 计数器溢出的情况。函数delayus 只有一个参数 usdelay,这个参数就是要延时的 us 数。 而delayms 函数就是对delayus(1000)的多次叠加,此函数也只有一个参数 msdelay,也就是要延时的 ms 数。
#include "bsp_delay.h"
void delay_init(void) //延时有关硬件初始化,主要是 GPT 定时器
{
GPT1->CR = 0; /* 清零 */
GPT1->CR = 1 << 15; /* bit15 置 1 进入软复位,复为完成自动置0 */
while((GPT1->CR >> 15) & 0x01); /*等待复位完成 */
/*
* GPT 的 CR 寄存器:GPT 通用设置
* bit22:20 000 输出比较 1 的输出功能关闭,也就是对应的引脚没反应
* bit9: 0 Restart 模式,当 CNT 等于 OCR1 的时候就产生中断
* bit8:6 001 GPT 时钟源选择 ipg_clk=66Mhz
*/
GPT1->CR = (1<<6);
//GPT 的 PR 寄存器, bit11:0 设置分频值
GPT1->PR = 65; /* 66 分频, GPT1 时钟为 66M/(65+1)=1MHz */
/*
* GPT 的 OCR1 寄存器:GPT 的输出比较 1 比较计数值,
* GPT 的时钟为 1Mz,那么计数器每计一个值就是就是 1us。
* 为了实现较大的计数,我们将比较值设置为最大的 0XFFFFFFFF,
* 这样一次计满就是: 0XFFFFFFFFus = 4294967296us = 4295s = 71.5min
*/
GPT1->OCR[0] = 0XFFFFFFFF;
GPT1->CR |= 1<<0; /* 使能 GPT1 */
//以下屏蔽的是 GPT 定时器中断代码,如果用到 GPT 定时中断的话可参考以下代码。
#if 0
//GPT 的 PR 寄存器, GPT 的分频设置,bit11:0 设置分频值,设置为 0 表示 1 分频,
GPT1->PR = 65; /* 66 分频, GPT1 时钟为 66M/(65+1)=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
/* GPT1的中断处理函数 */
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
/************************************各延时函数*************************************/
//微秒(us)级延时。参数:需要延时的 us 数,最大延时 0XFFFFFFFFus
void delayus(unsigned int usdelay)
{
unsigned long oldcnt,newcnt;
unsigned long tcntvalue = 0; /* 走过的总时间 */
oldcnt = GPT1->CNT; //当前GPT1的计数器的值
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; /* 跳出 */
}
}
}
// 毫秒(ms)级延时。参数:需要延时的 ms 数
void delayms(unsigned int msdelay)
{
int i = 0;
for(i=0; i
main.c 函数很简单,在调用delay_init 函数进行延时初始化之后在 while 循环中周期性的点亮和熄灭 LED0,调用函数 delayms 来实现延时。
#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"
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;
}