正点原子 发表于 2020-6-3 18:35:04

【正点原子Linux连载】第二十章高精度延时实验--摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南

本帖最后由 正点原子 于 2020-10-24 16:02 编辑

1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
4)本章实例源码下载:
5)对正点原子Linux感兴趣的同学可以加群讨论:876919289 6)关注正点原子公众号,获取最新资料




第二十章高精度延时实验

      延时函数是很常用的API函数,在前面的实验中我们使用循环来实现延时函数,但是使用循环来实现的延时函数不准确,误差会很大。虽然使用到延时函数的地方精度要求都不会很严格(要求严格的话就使用硬件定时器了),但是延时函数肯定是越精确越好,这样延时函数就可以使用在某些对时许要求严格的场合。本章我们就来学习一下如何使用硬件定时器来实现高精度延时。
20.1高精度延时简介
20.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 GPT时钟源
      从图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定时器结构图
      图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所示:

图20.1.1.3寄存器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)。
ENMODE(bit1):GPT使能模式,此位为0的时候如果关闭GPT定时器,计数器寄存器保存定时器关闭时候的计数值。此位为1的时候如果关闭GPT定时器,计数器寄存器就会清零。
EN(bit):GPT使能位,为1的时候使能GPT定时器,为0的时候关闭GPT定时器。
接下来看一下GPT定时器的分频寄存器GPTx_PR,此寄存器结构如图20.1.1.4所示:

图20.1.1.4寄存器GPTx_PR寄存器
      寄存器GPTx_PR我们用到的重要位就一个:PRESCALER(bit11:0),这就是12位分频值,可设置0~4095,分别对应1~4096分频。
      接下来看一下GPT定时器的状态寄存器GPTx_SR,此寄存器结构如图20.1.1.5所示:

图20.1.1.5 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小节。
20.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延时分别编写两个延时函数。
20.2硬件原理分析
      本试验用到的资源如下:
、一个LED灯:LED0。
、定时器GPT1。
      本实验通过高精度延时函数来控制LED0的闪烁,可以通过示波器来观察LED0的控制IO输出波形,通过波形的频率或者周期来判断延时函数精度是否正常。
20.3 实验程序编写
      本实验对应的例程路径为:开发板光盘-> 1、裸机例程->12_highpreci_delay。
      本章实验在上一章例程的基础上完成,更改工程名字为“delay”,直接修改bsp_delay.c和bsp_delay.h这两个文件,将bsp_delay.h文件改为如下所示内容:
示例代码20.3.1 bsp_delay.h文件代码
1#ifndef __BSP_DELAY_H
2#define __BSP_DELAY_H
3/***************************************************************
4Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
5文件名    : bsp_delay.h
6作者      : 左忠凯
7版本      : V1.0
8 描述      : 延时头文件。
9其他      : 无
10论坛      : www.openedv.com
11日志      : 初版V1.0 2019/1/4 左忠凯创建
12
13         V2.0 2019/1/15 左忠凯修改
14      添加了一些函数声明。
15 ***************************************************************/
16 #include "imx6ul.h"
17
18/* 函数声明 */
19void delay_init(void);
20void delayus(unsignedint usdelay);
21void delayms(unsignedint msdelay);
22void delay(volatileunsignedint n);
23void gpt1_irqhandler(void);
24
25 #endif
      bsp_delay.h文件就是一些函数声明,很简单。在文件bsp_delay.c中输入如下内容:
示例代码20.3.2 bsp_delay.c文件代码
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名   : bsp_delay.c
作者   : 左忠凯
版本   : V1.0
描述   : 延时文件。
其他   : 无
论坛   : www.openedv.com
日志   : 初版V1.0 2019/1/4 左忠凯创建

         V2.0 2019/1/15 左忠凯修改
使用定时器GPT实现高精度延时,添加了:
         delay_init 延时初始化函数
         gpt1_irqhandler gpt1定时器中断处理函数
         delayus us延时函数
         delayms ms延时函数
***************************************************************/
1   #include "bsp_delay.h"
2
3/*
4    * @description      : 延时有关硬件初始化,主要是GPT定时器
5          GPT定时器时钟源选择ipg_clk=66Mhz
6    * @param               : 无
7    * @return            : 无
8    */
9void delay_init(void)
10{
11      GPT1->CR =0;                /* 清零                        */
12      GPT1->CR =1<<15;                /* bit15置1进入软复位      */
13while((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=0XFFFFFFFF;
38      GPT1->CR |=1<<0;/* 使能GPT1 */
39
40/* 一下屏蔽的代码是GPT定时器中断代码,
41       * 如果想学习GPT定时器的话可以参考一下代码。
42       */
43#if0
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=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#if0
74/* 中断处理函数 */
75void gpt1_irqhandler(void)
76{
77staticunsignedchar state =0;
78      state =!state;
79/*
80       * GPT的SR寄存器,状态寄存器
81       * bit2: 1 输出比较1发生中断
82       */
83if(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 – value      : 需要延时的us数,最大延时0XFFFFFFFFus
94   * @return                : 无
95   */
96void delayus(unsignedint usdelay)
97{
98unsignedlong oldcnt,newcnt;
99unsignedlong tcntvalue =0;/* 走过的总时间*/
100
101   oldcnt = GPT1->CNT;
102while(1)
103{
104         newcnt = GPT1->CNT;
105if(newcnt != oldcnt)
106{
107if(newcnt > oldcnt)      /* GPT是向上计数器,并且没有溢出 */
108               tcntvalue += newcnt - oldcnt;
109else                /* 发生溢出                                        */
110               tcntvalue +=0XFFFFFFFF-oldcnt + newcnt;
111             oldcnt = newcnt;
112if(tcntvalue >= usdelay)      /* 延时时间到了                                        */
113break;                /*跳出                                                */
114}
115}
116}
117
118/*
119* @description             : 毫秒(ms)级延时
120* @param - msdelay         : 需要延时的ms数
121* @return                  : 无
122*/
123void delayms(unsignedint msdelay)
124{
125int i =0;
126for(i=0; i<msdelay; i++)
127{
128         delayus(1000);
129}
130}
131
132/*
133* @description         : 短时间延时函数
134* @param - n         : 要延时循环次数(空操作循环次数,模式延时)
135* @return            : 无
136*/
137void delay_short(volatileunsignedint n)
138{
139while(n--){}
140}
141
142/*
143* @description         : 延时函数,在396Mhz的主频下
144*                  延时时间大约为1ms
145* @param - n         : 要延时的ms数
146* @return            : 无
147*/
148void delay(volatileunsignedint n)
149{
150while(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就是按照我们在20.1.2小节讲解的高精度延时原理编写的,delayus函数处理GPT1计数器溢出的情况。函数delayus只有一个参数usdelay,这个参数就是要延时的us数。delayms函数很简单,就是对delayus(1000)的多次叠加,此函数也只有一个参数msdelay,也就是要延时的ms数。
      最后修改mian.c文件,内容如下:
示例代码20.3.3 main.c文件代码
/**************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名   :    mian.c
作者   : 左忠凯
版本   : V1.0
描述   : I.MX6U开发板裸机实验12 高精度延时实验
其他   : 本实验我们学习如何使用I.MX6U的GPT定时器来实现高精度延时,
以前的延时都是靠空循环来实现的,精度很差,只能用于要求
不高的场合。使用I.MX6U的硬件定时器就可以实现高精度的延时,
最低可以做到20us的高精度延时。
论坛   : www.openedv.com
日志   : 初版V1.0 2019/1/15 左忠凯创建
**************************************************************/
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*/
14int main(void)
15{
16      unsignedchar 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      return0;
33}
      main.c函数很简单,在第20行调用delay_init函数进行延时初始化,最后在while循环中周期性的点亮和熄灭LED0,调用函数delayms来实现延时。
20.4编译下载验证
20.4.1编写Makefile和链接脚本
      因为本章例程并没有新建任何文件,所以只需要修改Makefile中的TARGET为delay即可,链接脚本报纸不变。
20.4.2编译下载
      使用Make命令编译代码,编译成功以后使用软件imxdownload将编译完成的delay.bin文件下载到SD卡中,命令如下:
chmod 777 imxdownload                        //给予imxdownload可执行权限,一次即可
./imxdownload delay.bin /dev/sdd                //烧写到SD卡中
      烧写成功以后将SD卡插到开发板的SD卡槽中,然后复位开发板。程序运行正常的话LED0会以500ms为周期不断的亮、灭闪烁。可以通过肉眼观察LED亮灭的时间是否为500ms。但是肉眼观察肯定不准确,既然本章号称高精度延时实验,那么就得经得住专业仪器的测试。我们将“示例代码20.3.3”中第29行,也就是mian函数while循环中的延时改为“delayus(20)”,也就是LED0亮灭的时间各为20us,那么一个完整的周期就是20+20=40us,LED0对应的IO频率就应该是1/0.00004=25000Hz=25KHz。使用示波器测试LED0对应的IO频率,结果如图20.4.3.1所示:

图20.4.3.120us延时波形
      从图20.4.3.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.3.2所示:

图20.4.3.2修改while循环后的波形
      从图20.4.3.2可以看出,此时while循环执行一次的时间是41.8us,那么一次delayus(20)的时间就是41.8/2=20.9us,很接近我们的20us理论值。但是还是因为有其他程序开销存在,在加上示波器测量误差,所以不可能测量出绝对的20us。但是其已经非常接近了,基本可以证明我们的高精度延时函数是成功的、可以用的。

页: [1]
查看完整版本: 【正点原子Linux连载】第二十章高精度延时实验--摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南