系列文章目录
I.MX6ULL高精度延时实验
I.MX6ULL高精度延时实验
一、前言
延时函数是很常用的 API 函数,之前的实验中我们使用循环来实现延时函数,但是使用循环来实现的延时函数不准确,误差会很大。虽然使用到延时函数的地方精度要求都不会很严格(要求严格的话就使用硬件定时器了),但是延时函数肯定是越精确越好,这样延时函数就可以使用在某些对时序要求严格的场合。本节我们就来学习一下如何使用硬件定时器来实现高精度延时。
二、高精度延时简介
2.1GPT 定时器简介
在使用 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 定时器的可选时钟源如图所示:
从图中可以看出一共有五个时钟源,分别为:ipg_clk_24M、GPT_CLK(外部时钟)、ipg_clkipg_clk_32k 和 ipg_clk_highfreq。
本例程选择 ipg_clk 为 GPT 的时钟源,ipg_clk=66MHz。
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 定时器几个重要的寄存器,第一个就是 GPT 的配置寄存器 GPTx_CR,此寄存器的结构如图所示:
寄存器 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,此寄存器结构如图所示:
寄存器 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 的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6ULL 参考手册》第 1432 页的 30.6 小节。
2.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 延时分别编写两个延时函数。
三、硬件原理分析
本试验用到的资源如下:
①、一个 LED 灯:LED0。
②、定时器 GPT1。
本实验通过高精度延时函数来控制 LED0 的闪烁,可以通过示波器来观察 LED0 的控制 IO输出波形,通过波形的频率或者周期来判断延时函数精度是否正常。参考硬件原理图
四、实验程序编写
本节实验在上一节例程的基础上完成,上一节例程,更改工程名字为"delay",直接修改 bsp_delay.c 和bsp_delay.h 这两个文件,将 bsp_delay.h 文件改为如下所示内容:
cpp
1 #ifndef __BSP_DELAY_H
2 #define __BSP_DELAY_H
16 #include "imx6ul.h"
17
18 /* 函数声明 */
19 void delay_init(void);
20 void delayus(unsigned int usdelay);
21 void delayms(unsigned int msdelay);
22 void delay(volatile unsigned int n);
23 void gpt1_irqhandler(void);
24
25 #endif
bsp_delay.h 文件就是一些函数声明,很简单。在文件 bsp_delay.c 中输入如下内容:
cpp
1 #include "bsp_delay.h"
2
3 /*
4 * @description : 延时有关硬件初始化,主要是 GPT 定时器
5 GPT 定时器时钟源选择 ipg_clk=66Mhz
6 * @param : 无
7 * @return : 无
8 */
9 void delay_init(void)
10 {
11 GPT1->CR = 0; /* 清零 */
12 GPT1->CR = 1 << 15; /* bit15 置 1 进入软复位 */
13 while((GPT1->CR >> 15) & 0x01); /*等待复位完成 */
14
15 /*
16 * GPT 的 CR 寄存器,GPT 通用设置
17 * bit22:20 000 输出比较 1 的输出功能关闭,也就是对应的引脚没反应
18 * bit9: 0 Restart 模式,当 CNT 等于 OCR1 的时候就产生中断
19 * bit8:6 001 GPT 时钟源选择 ipg_clk=66Mhz
20 */
21 GPT1->CR = (1<<6);
22
23 /*
24 * GPT 的 PR 寄存器,GPT 的分频设置
25 * bit11:0 设置分频值,设置为 0 表示 1 分频,
26 * 以此类推,最大可以设置为 0XFFF,也就是最大 4096 分频
27 */
28 GPT1->PR = 65; /* 66 分频,GPT1 时钟为 66M/(65+1)=1MHz */
29
30 /*
31 * GPT 的 OCR1 寄存器,GPT 的输出比较 1 比较计数值,
32 * GPT 的时钟为 1Mz,那么计数器每计一个值就是就是 1us。
33 * 为了实现较大的计数,我们将比较值设置为最大的 0XFFFFFFFF,
34 * 这样一次计满就是:0XFFFFFFFFus = 4294967296us = 4295s = 71.5min
35 * 也就是说一次计满最多 71.5 分钟,存在溢出。
36 */
37 GPT1->OCR[0] = 0XFFFFFFFF;
38 GPT1->CR |= 1<<0; /* 使能 GPT1 */
39
40 /* 以下屏蔽的代码是 GPT 定时器中断代码,
41 * 如果想学习 GPT 定时器的话可以参考以下代码。
42 */
43 #if 0
44 /*
45 * GPT 的 PR 寄存器,GPT 的分频设置
46 * bit11:0 设置分频值,设置为 0 表示 1 分频,
47 * 以此类推,最大可以设置为 0XFFF,也就是最大 4096 分频
48 */
49
50 GPT1->PR = 65; /* 66 分频,GPT1 时钟为 66M/(65+1)=1MHz */
51 /*
52 * GPT 的 OCR1 寄存器,GPT 的输出比较 1 比较计数值,
53 * 当 GPT 的计数值等于 OCR1 里面值时候,输出比较 1 就会发生中断
54 * 这里定时 500ms 产生中断,因此就应该为 1000000/2=500000;
55 */
56 GPT1->OCR[0] = 500000;
57
58 /*
59 * GPT 的 IR 寄存器,使能通道 1 的比较中断
60 * bit0: 0 使能输出比较中断
61 */
62 GPT1->IR |= 1 << 0;
63
64 /*
65 * 使能 GIC 里面相应的中断,并且注册中断处理函数
66 */
67 GIC_EnableIRQ(GPT1_IRQn); /* 使能 GIC 中对应的中断 */
68 system_register_irqhandler(GPT1_IRQn,
(system_irq_handler_t)gpt1_irqhandler,
NULL);
69 #endif
70
71 }
72
73 #if 0
74 /* 中断处理函数 */
75 void gpt1_irqhandler(void)
76 {
77 static unsigned char state = 0;
78 state = !state;
79 /*
80 * GPT 的 SR 寄存器,状态寄存器
81 * bit2: 1 输出比较 1 发生中断
82 */
83 if(GPT1->SR & (1<<0))
84 {
85 led_switch(LED2, state);
86 }
87 GPT1->SR |= 1<<0; /* 清除中断标志位 */
88 }
89 #endif
90
91 /*
92 * @description : 微秒(us)级延时
93 * @param -- usdelay : 需要延时的 us 数,最大延时 0XFFFFFFFFus
94 * @return : 无
95 */
96 void delayus(unsigned int usdelay)
97 {
98 unsigned long oldcnt,newcnt;
99 unsigned long tcntvalue = 0; /* 走过的总时间 */
100
101 oldcnt = GPT1->CNT;
102 while(1)
103 {
104 newcnt = GPT1->CNT;
105 if(newcnt != oldcnt)
106 {
107 if(newcnt > oldcnt) /* GPT 是向上计数器,并且没有溢出 */
108 tcntvalue += newcnt - oldcnt;
109 else /* 发生溢出 */
110 tcntvalue += 0XFFFFFFFF-oldcnt + newcnt;
111 oldcnt = newcnt;
112 if(tcntvalue >= usdelay) /* 延时时间到了 */
113 break; /* 跳出 */
114 }
115 }
116 }
117
118 /*
119 * @description : 毫秒(ms)级延时
120 * @param - msdelay : 需要延时的 ms 数
121 * @return : 无
122 */
123 void delayms(unsigned int msdelay)
124 {
125 int i = 0;
126 for(i=0; i<msdelay; i++)
127 {
128 delayus(1000);
129 }
130 }
131
132 /*
133 * @description : 短时间延时函数
134 * @param - n : 要延时循环次数(空操作循环次数,模式延时)
135 * @return : 无
136 */
137 void delay_short(volatile unsigned int n)
138 {
139 while(n--){}
140 }
141
142 /*
143 * @description : 延时函数,在 396Mhz 的主频下
144 * 延时时间大约为 1ms
145 * @param - n : 要延时的 ms 数
146 * @return : 无
147 */
148 void delay(volatile unsigned int n)
149 {
150 while(n--)
151 {
152 delay_short(0x7ff);
153 }
154 }
文件 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 就是按照我们讲解的高精度延时原理编写的,delayus 函数处理 GPT1 计数器溢出的情况。函数delayus 只有一个参数 usdelay,这个参数就是要延时的 us 数。delayms 函数很简单,就是对delayus(1000)的多次叠加,此函数也只有一个参数 msdelay,也就是要延时的 ms 数。
最后修改 main.c 文件,内容如下:
cpp
1 #include "bsp_clk.h"
2 #include "bsp_delay.h"
3 #include "bsp_led.h"
4 #include "bsp_beep.h"
5 #include "bsp_key.h"
6 #include "bsp_int.h"
7 #include "bsp_keyfilter.h"
8
9 /*
10 * @description : main 函数
11 * @param : 无
12 * @return : 无
13 */
14 int main(void)
15 {
16 unsigned char state = OFF;
17
18 int_init(); /* 初始化中断(一定要最先调用!) */
19 imx6u_clkinit(); /* 初始化系统时钟 */
20 delay_init(); /* 初始化延时 */
21 clk_enable(); /* 使能所有的时钟 */
22 led_init(); /* 初始化 led */
23 beep_init(); /* 初始化 beep */
24
25 while(1)
26 {
27 state = !state;
28 led_switch(LED0, state);
29 delayms(500);
30 }
31
32 return 0;
33 }
main.c 函数很简单,在第 20 行调用 delay_init 函数进行延时初始化,最后在 while 循环中周期性的点亮和熄灭 LED0,调用函数 delayms 来实现延时。
五、编译下载验证
编写 Makefile 和链接脚本
因为本节例程并没有新建任何文件,所以只需要修改 Makefile 中的 TARGET 为 delay 即可,链接脚本保持不变。参考文章
编译下载
使用 Make 命令编译代码,编译成功以后使用软件 imxdownload 将编译完成的 delay.bin 文件下载到 SD 卡中,命令如下:
cpp
chmod 777 imxdownload //给予 imxdownload 可执行权限,一次即可
./imxdownload delay.bin /dev/sdd //烧写到 SD 卡中,不能烧写到/dev/sda 或 sda1 设备里面!
烧写成功以后将 SD 卡插到开发板的 SD 卡槽中,然后复位开发板。程序运行正常的话 LED0
会以 500ms 为周期不断的亮、灭闪烁。可以通过肉眼观察 LED 亮灭的时间是否为 500ms。但是
肉眼观察肯定不准确,既然本节号称高精度延时实验,那么就得经得住专业仪器的测试。我们
将中第 29 行,也就是 main 函数 while 循环中的延时改为"delayus(20)",也就是 LED0 亮灭的时间各为 20us,那么一个完整的周期就是 20+20=40us
LED0 对应的 IO 频率就应该是1/0.00004=25000Hz=25KHz。使用示波器测试 LED0 对应的 IO 频率,结果如图所示:
从图中可以看出,LED0 对应的 IO 波形频率为 22.3KHz,周期是 44.9us,那么 main函数中 while 循环执行一次的时间就是 44.9/2=22.45us,大于我们设置的 20us,看起来好像是延时不准确。但是我们要知道这 22.45us 是 main 函数里面 while 循环总执行时间,也就是下面代码的总执行时间:
cpp
while(1)
{
state = !state;
led_switch(LED0, state);
delayus(20);
}
在上面代码中不止有 delayus(20)延时函数,还有控制 LED 灯亮灭的函数,这些代码的执行也需要时间的,即使是 delayus 函数,其内部也是要消耗一些时间的。假如我们将 while 循环里面的代码改为如下形式:
cpp
while(1)
{
GPIO1->DR &= ~(1<<3);
delayus(20);
GPIO1->DR |= (1<<3);
delayus(20);
}
上述代码我们通过直接操作寄存器的方式来控制 IO 输出高低电平,理论上 while 循环执行时间会更小,并且 while 循环里面使用了两个 delayus(20),因此执行一次 while 循环的理论时间应该是 40us,和上面做的实验一样。重新使用示波器测量一下,结果如图所示:
从图中可以看出,此时 while 循环执行一次的时间是 41.8us,那么一次 delayus(20)的时间就是 41.8/2=20.9us,很接近我们的 20us 理论值。但是还是因为有其他程序开销存在,在加上示波器测量误差,所以不可能测量出绝对的 20us。但是其已经非常接近了,基本可以证明我们的高精度延时函数是成功的、可以用的。
END