前言:
本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM(MX6U)裸机篇”视频的学习笔记,在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。
引用:
正点原子IMX6U仓库 (GuangzhouXingyi) - Gitee.com
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》
正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档
正文:
本文是 “正点原子[第二期]Linux之ARM(MX6U)裸机篇--第18 讲” 的读书笔记。第87讲主要是介绍I.MX6U处理器的的GPT(通用目的定时器)来实现高精度延时。本节将参考正点原子的视频教程第18讲和配套的正点原子开发指南文档进行学习。
0. 概述
延时函数是很常见的API函数,在前面的实验中我们使用循环来实现延时函数,但是使用循环来实的延时函数不准确,误差会很大。虽然使用到延时函数的地方精度要求都不会很严格(要求严格的话就使用硬件定时器了),但是延时函数肯定是越精确越好,这样延时函数就可以使用在某些对时序要求严格的场合。本章我们就来学习一下如何使用硬件定时器来实现高精度延时。
1. 高精度延时简介
1.1 GPT定时器简介
学过STM32的同学应该知道,在使用STM32的时候可以使用 SYSTICK 来实现高精度延时。I.MX6U 没有 SYSTICK 定时器,但是I.MX6U有其他定时器啊,比如第十八章讲解的EPIT定时器。本章我们使用 I.MX6U 的 GPT (General Purpose Timer)来四坏心眼高精度延时,顺便学习一下GPT定时器,GPT定时器全程为 Gerernal Purpose Timer。
GPT定时器是一个32位向上定时器(也就是说从0x0000_0000向上递增计数),GPT定时器也可以跟一个值进行比较,当计数器值和这个值相等的话就发生比较事件,产生比较中断。GPT定时器有一个12位分频器,可以对GPT定时器的时钟源进行分频,GPT定时器特性如下:
- 一个可选时钟源的32位向上计数器
- 两个输入捕获通道,可以设置触发方式
- 三个比较输出通道,可以设置输出方式
- 可以生成捕获中断,比较中断和溢出中断
- 计数器可以运行在重新启动(restart)和(自由运行)free-run模式
GPT定时器的可选时钟源如下图所示
从上图中可以看出一共有五个时钟源,分别为:ipg_clk_24M,GPT_CLK(外部时钟),ipg_clk,ipg_clk_32k,ip_clk_higrfreq。本例程选择ipg_clk作为GPT的使用源,ipg_clk=66MHz。
GPT定时器的结构如下图所示:
其中各部分含义如下
序号 | 描述 |
1 | 此部分为GPT定时器的时钟源,前面已经说过了,本章例程选择ipg_clk作为GPT定时器的时钟源。ipg_clk=66MHz |
2 | 此部分为12位分频器,对时钟源进行分频处理,可设置0~4095,分别对应1~4096分频。 |
3 | 经过分频的时钟源进入到GPT定时器内部的32位计数器。GPT定时器的计数器是32位向上计数器(即从0x00000000向上递增) |
4,5 | 这两部分是GPT的两路输入捕获通道,本章不讲解GPT的输入捕获 |
6 | 此部分为输出比较寄存器,一共有三路输出比较,因此有三个输出比较寄存器,输出比较寄存器是32位的 |
7 | 此部分为输出比较中断,三路输出比较中断,当计数器里面的值和比较寄存器里面的值相等就会触发输出比较中断。 |
GPT定时器有两种工作模式:重新启动(restart)模式和自由运行(free-run)模式,这两个工作模式区别如下:
- 重新启动(restatr)模式:当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,此寄存器结构如下图:
bit位 | 描述 |
SWR bit[15] | 复位GPT定时器,向此位写1就可以复位GPT定时器,当GPT复位完成以后此位会自动清零。 |
FRR bit[9] | 运行模式选择,当此位为0时比较通道1工作在重新启动模式。当此位为1的时候所有的三个比较通道均工作在自由运行模式(free-run) |
CLK_SRC bit[8: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 bit[1] | GPT使能模式,此位为0的时候如果关闭GPT定时器,计数器寄存器保存定时器关闭时候的计数值,为1的时候如果关闭GPT定时器,计数器寄存器就会清零。 |
EN bit[0] | GPT使能位,为1的时候使能GPT定时器,为0的时候关闭GPT定时器。 |
接下来看一下GPT定时器的分频寄存器GPTx_PR,此寄存器的结构如下图所示:
寄存器GPTx_PR我们用到的重要位就一个:PRESCALER (bit[11:0]),这就是12位分频值,可设置0~4095,分别对应1~4096分频。
接下来看一下GPT定时器的状态寄存器 GPTx_SR,此寄存器的结构如下图所示:
寄存器GPTx_SR重要位如下:
位 | 描述 |
ROV bit[5] | 回滚标志位,当计数值从 0xFFFFFFFF 回滚到 0x00000000 的时候此位置1 |
IF2~IF1 bit[4:3] | 输入捕获标志位,当输入捕获事件发生以后此位置1,一共有两路输入捕获通道。如果使用输入捕获中断的话需要再中断处理函数中清除此位。 |
OF3~OF1 bit[2:0] | 输出比较中断标志位,当输出比较中断事件发生以后此位置1,一共有三路输出比较通道。如果使用输出比较中断的话需要再中断处理函数中清除此位。 |
接下来看一下GPT定时器的计数寄存器GPTx_CNT,这个寄存器保存着GPT定时器的当前计数值,最后看一下GPT定时器的输出比较寄存器 GPTx_OCR ,每个输出比较通道对应一个输出比较寄存器,因此一个GPT定时器有三个OCR寄存器,它们的作用是相同的。以输出比较通道1为例,器输出比较寄存器为 GPTx_OCR1 ,这是一个32为寄存器,用于存放32位的比较值。当计数值和寄存器 GPTx_OCR1 中的值相等就会产生比较事件,如果省内了比较中断的话就会触发相应的中断。
关于 GPT 的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6ULL 参考手册》第 1432 页的 30.6 小节。
1.2 定时器实现高精度延时的原理
高精度延时函数的实现肯定是要借助硬件定时器,前面说了本章实验使用GPT定时器来实现高精度延时。如果设置GPT定时器的时钟源为 ipg_clk=66MHz,设置66分频,那么进入GPT定时器的最终时钟频率就是 66MHz/66=1MHz,周期为 1us(微秒)。GTP的计数器每计一个数就表示“过去”了1us。如果10个计数就表示“过去”了10us。通过读取寄存器GPTx_CNT中的值就知道计数个数,比如现在要延时100us,那么进入延时函数以后记录下寄存器GPTx_CNT的值为200,当GPTx_CNT中的值为300的时候就表示100us过去了,也就是延时结束。GPTx_CNT是个32为寄存器,如果时钟为1MHz的话,GPTx_CNT最多可以实现
0xFFFFFFFF=4,294,967,295 us = 4294s = 72min。
也就是说72分钟以后 GPTx_CNT 寄存器就会回滚到 0x00000000,也就是溢出,所以在延时函数中需要处理溢出的情况。关于定时器实现高精度延时的原理就讲解到这里,原理韩式很简单的,高精度延时的实现步骤如下:
- 设置GPT1定时器
首先设置GPT1_CR寄存器的 SWR(bit15) 位来复位寄存器 GPT1。复位完成以后设置寄存器 GPT1_CR 寄存器的 CLKSRC (bit8:6) ,选择 GPT1 的时钟源为 ipg_clk。设置定时器 GPT1的工作模式。- 设置GPT1的分频值
设置寄存器 GPT1_PR 寄存器的 PRESCALAR (bit:11:0)位,设置分频值。- 设置GPT1的比较值
如果要使用GPT1的输出比较中断,那么GPT1的输出比较寄存器 GPT1_OCR1 的值可以根据所需的中断时间来设置。本例程不使用比较输出中断,所以将 GPT1_COR1 设置为最大值,即: 0xFFFFFFFF。- 使能 GPT1 定时器
设置好GPT1定时器以后就可以使能了,设置GPT1_CR的EN(bit0)位为1来使能 GPT1定时器。- 编写延时函数
GPT1定时器已经开始运行了,可以根据前面介绍的高精度延时函数原理来编写延时函数,针对us 和 ms 延时分别编写两个延时函数。
1.3 硬件原理分析
本实验用到的资源如下
- 一个LED灯:LED0
- 定时器GPT1
本实验通过高精度延时函数来控制LED0的闪烁,可以通过示波器来观察LED0的控制IE输出波形,通过波形的频率或者周期来判断延时函数的精度是否正常。
2. 高精度延时函数程序编写
基于GPT定时器的高精度延时函数程序源码如下,参考正点原子的示例程序:
bsp/bsp_dealy.c
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_int.h"void short_delay(volatile unsigned int n)
{while(n--){;}
}/* sleep 1ms */
void delay(volatile unsigned int m){while(m--){short_delay(0x7ff);}
}#include "bsp_delay.h"
#include "bsp_int.h"
#include "bsp_led.h"
#include "bsp_beep.h"/** @description : 延时初始化* @param : 无* @return : 无*/
void delay_init(void)
{/* GPT1_CR 清零 */GPT1->CR = 0;/* GPT1->CR 软件复位 */GPT1->CR |= (1 << 15);while(((GPT1->CR >> 15) & 0x01));GPT1->CR |= (1 << 6); /* CLKSRC bit[8:6], 1 时钟源选择ipg_clk */GPT1->CR |= (1 << 1); /* ENMODE bit[1], 1 reset mode *//* FRR bit[9], 0 reset mode *//* GPT1_PR 分频 */GPT1->PR &= ~(0xFFF << 0);GPT1->PR |= (65 << 0); /* 66Mhz/66=1MHz *///GPT1->OCR[0] = 1000000/10; /* GPT1输出比较通道1 */GPT1->OCR[0] = 0xFFFFFFFF; /* GPT1输出比较通道1 */#if 0GPT1->IR &= ~(1 << 0);GPT1->IR |= (1 << 0); /* 使能GPT1输出比较通道1中断 */GIC_EnableIRQ(GPT1_IRQn);system_irqhandler_register(GPT1_IRQn, gpt_system_irq_handler_t, NULL);gpt_irq_enable(GPT1); /* GPT1 输出比较中断使能 */
#endifgpt_enable(GPT1); /* GPT1 enable*/
}/** @description : 使能GPT输出比较通道1中断。* @param – base : GPT寄存器* @return : 无*/
void gpt_enable(GPT_Type *base)
{base->CR |= (1 << 0);
}/** @description : 使能GPT输出比较通道1中断。* @param – base : GPT寄存器* @return : 无*/
void gpt_disable(GPT_Type *base)
{base->CR &= ~(1 << 0);
}/** @description : 使能GPT输出比较通道1中断。* @param – base : GPT寄存器* @return : 无*/
void gpt_irq_enable(GPT_Type *base)
{base->IR |= (1 << 0);
}/** @description : 禁用GPT输出比较通道1中断。* @param – base : GPT寄存器* @return : 无*/
void gpt_irq_disable(GPT_Type *base)
{base->IR &= ~(1 << 0);
}/** @description : 清除中断标志位。* @param – base : GPT寄存器* @return : 无*/
void gpt_cleanIrqFlag(GPT_Type *base)
{base->SR |= (1 << 0);
}/** @description : 清除中断标志位。* @param – base : GPT寄存器* @return : 无*/
void gpt_restart_timer(GPT_Type *base, int value)
{base->CR &= ~(1 << 0); /* 关闭GPT定时器 */base->OCR[0] = value;base->CR |= (1 << 0); /* 启用GPT定时器 */
}/** @description : GPT中断处理函数。* @param – irq : 中断ID* @param – userparam : 用户注册参数* @return : 无*/
void gpt_system_irq_handler_t(IRQn_Type irq, void *userparam)
{static int led_state = 0;if(GPT1->SR & (1 << 0)){led_state = !led_state;led_switch(LED_0, led_state);beep_switch(led_state);//gpt_restart_timer(GPT1, 1000000/10);/* 清除中断标志位 */gpt_cleanIrqFlag(GPT1);}
}/** @description : 高精度延时函数微秒,基于GPT定时器* @param – us : 延时微秒数* @return : 无*/
void delay_us(int time_us)
{int oldvalue = 0;int newvalue = 0;int value = 0;while(1){newvalue = GPT1->CNT;if(newvalue != oldvalue){if(oldvalue < newvalue){value += 0xFFFFFFFF - oldvalue + newvalue;}else{value += newvalue - oldvalue;}oldvalue = newvalue;if(value >= time_us)break;}}
}/** @description : 高精度延时函数毫秒,基于GPT定时器* @param – us : 延时毫秒数* @return : 无*/
void delay_ms(int time_ms)
{int i = 0;for(i=0; i<time_ms; i++){delay_us(1000);}
}
定义了 delay_us() 和 dalay_ms() 分别来实现微秒(us) 和 毫秒(ms)级别的延时。
3. 编译烧写SD卡验证实验结果
译修改主频后源码烧录SD卡验证本节的基于GPT定时器的高精度延时函数是否生效。预期烧录SD卡后正点原子I.MX6ULL ALPHA/Mini 开发板后,开发板按照指定的延时时间周期性的控制蜂鸣器的鸣叫和LED的闪亮。
我本地验证的结果是基于GPT定时器的高精度延时实验结果正常,开发板按照指定的延时时间周期性的控制蜂鸣器的鸣叫和LED的闪亮。
4. 总结和实验遇到的问题记录
4.1 问题1:高精度延时函数delay_us() 微秒延时函数的源码需要优化,减少不必要的指令和对寄存器的读取。
如图是3个版本的 dealy_us() 微秒延时延时函数的源码版本,第一个版本源码V1是我自己写的,V2 版本和 V3 版本是参考正点原子的示例源码和视频教程进行优化的两个版本,V3版本是我们最终需要的delay_us()实现微秒级延时函数的最终版本。
下面分析一下 V1,V2,V3版本的 delay_us() 函数优化了哪些?以及为什么需要做这样的优化。
- V1->V2版本的优化,主要是减少了每次循环是对寄存器GPTx->CNT寄存器的读取次数。
优化v1->v2:每次循环的时候只读一次寄存器GPT->CNT 的值,读取到中间变量newcnt 里面,这样可以减少读取寄存器次数,并且防止在运行过程中CPT->CNT计数器值发生了增长。- V2->V3的优化,主要是减少了每次循环时非必要的加减数学运算以及对oldcnt的更新。
优化V2->V3:每次执行到循环的时候判断newcnt 是否等于oldcnt。因为CPU的执行速度要快于GPT定时器的主频,每次CPU再次执行循环的时候,很可能GPTx->CNT 计数器的值并没有修改。所以每次循环的时候先判断 newcnt 是否等于 oldcnt。
需要注意这里实现高精度延时函数时源码的写法,这里需要优化掉不必要的寄存器读取操作,和不必要的加减数学运算和更新。