一、Timer 原理
定时器是单片机的重要外设之一,可用于定时、精确延时、计数等。而且Time在运行时不占用CPU时间,在配置好后,可以和CPU并行工作,实现精确的定时和计数。并可以通过软件控制其来产生中断,使用起来方便灵活。
在NRF51822中共有3个定时器,对应的编号为TIMER0、TIMER1、TIMER2,都可以分别工作在定时模式和计数模式下。其原理结构体如下:
Timer模块从PCLK1M/PCLK16M处获取时钟源,然后经分频后的到时钟作为Timer的时钟(即图中的fTIMER),接着选择相对于的模式。当处于定时模式时,Counter会在fTIMERr的每个tick 计数一次,当计数值与CC[n](n为0,1,2,3)寄存器中的值相等时就会触发对应的Compare[n]事件,如果我们设置了Compare[n]事件产生时触发中断,那么在Counter计数到与 CC[n]寄存器中的值相等时触发中断,从而实现定时器功能了。而当处于计数模式时,TIMER内部计数器在COUNT引脚上每个脉冲来临的时候会计数一次。
- Timer时钟源
Timer使用的时钟来自PCLK1M或PCLK16M,系统会根据设置的Timer时钟频率fTIMER来决定用哪一个时钟源(即无需软件来操作时钟源的设置)。当我们设置好了Timer时钟频率后,系统会根据时钟频率自动选择时钟源,选择如下:
-
- 当fTIMER > 1MHz时,定时器模块会选择PCLK16M为时钟源。
- 当fTIMER <= 1MHz时,定时器模块会选择PCLK1M为时钟源,从而降低功耗。
- Timer时钟频率
Timer时钟频率fTIMER是以16MHz为基准,计算公式:fTIMER = 16MHz / (2PRESCALER),其中PRESCALER是分频系数,其可设置的范围是0~9。而分频系数、时钟频率和时钟源的关系如下:
- Timer位宽(位数)
nRF51822共有3个定时器,可配置各自的寄存器BITMODE将定时器的位宽设置为8/16/24/32位,但需注意的是,TIMER1和TIMER2只能设置为8位或16位,寄存器如下:
- Timer启动和停止
Timer可以处于定时或计数模式下,但无论哪种模式下都是通过START任务寄存器来启动Timer,通过触发STOP认为寄存器来停止Timer的。在Timer停止后,触发START任务寄存器可以让其再次启动,启动后,定时器将继续从之前被停止的值开始计数。相关的3个任务定时器:
-
- START:启动定时/计数器
- STOP:停止定时/计数器
- SHURDOWN:让定时/计数器掉电,后续无法通过START来启动该定时/计数器,除非重新复位。
- 定时和计数
- 定时模式:Timer内部计数器的值在每个时钟脉冲加1,此模式下,触发任务寄存器COUNT不会影响到计数寄存器的值。
- 计数模式:每触发一次任务寄存器COUNT,Timer内部计数器寄存器的值加1,此模式下,Timer的时钟频率和分频系数没有使用。
注意:设置分频系数(PRESCALER寄存器)和位宽(BITMODE寄存器)时,必须要停止Timer后再设置,否则将会导致不可预知的后果。
- 溢出和清零
当Timer的计数寄存器的值增大到最大值时,计数寄存器溢出,此时计数寄存器的数值将会清零并自动从零开始计数。或者触发任务寄存器CLEAR,计数寄存器的值也会被清零。
- 捕获
Timer中的每个COMPAER[n]比较寄存器对应一个捕获TASKS_CAPTURE[n],每次触发任务i寄存器CAPTURE[n],捕获值会被存储到CC[n]寄存器中。
二、寄存器介绍
nRF51822的3个定时器(TIMER0~TIMER2)基址及相关寄存器如下:
- SHORTS:该寄存器可以把TASK和EVENT联系起来。例如COMPARE[n]_CLEAR可以在内部计数器相当于CC[n]的时候清除内部计数器的计数值。
- INTENSET:中断允许设定寄存器,当设定为"1"的时候,对应的COMPARE[n]事件产生的时候,会产生COMPARE[n]中断。
三、相关的API函数
- Timer初始化函数 nrf_drv_timer_init(p_instance, p_config, timer_event_handler) ;
- Timer扩展比较模式下通通道设置函数 nrf_drv_timer_extended_compare( p_instance, cc_channel, cc_value, timer_short_mask, enable_int) :扩展比较模式下设置Timer通道。
- Timer启动函数 nrf_drv_timer_enable(p_instance);
- Timer关闭函数 nrf_drv_timer_disable(p_instance):关闭Timer,注意是关闭而不是停止,函数中可以触发任务TASKS_SHUTDOWN关闭Timer。
- 毫秒转Ticks函数 nrf_drv_timer_ms_ti _ticks(p_instance, time_ms) :输入的毫秒转换成Ticks。
四、应用流程
Timer可以用于定时和计数两种模式,定时模式下要设置捕获/比较寄存器的比较值,这个比较值对应的就是定时的时间。可以同时设置多个捕获/比较寄存器的值。Timer启动后,计数值在每个时钟频率递增,当计数值达到比较值的时候(即定时时间到了),产生比较匹配事件,在事件处理函数中加入相应的处理代码即可实现定时处理任务的目的。常用的一般步骤为:
- 设置工作模式。
- 设定预分频(计数器不适用)。
- 设定CC[n]寄存器的值。
- 使能中断(中断模式)。
- 启动START任务。
- COMPARE EVENT到来,清除内部计数器的值,清楚中断(中断模式)。
以Timer作为定时器的应用流程为例(库函数):
1、初始化Timer
首先要定义一个驱动程序实例,定义驱动程序实例时,可以命名为实例名称,这样使用起来比较直观,例如定义一个用于定时驱动LED指示灯的驱动程序实例TIMER_LED。驱动程序实例的ID号对应的是Timer的编码,如NRF_DRV_TIMER_INSEANCE(1)对应的是Timer1,而nRF51822有三个定时器,因此可定义的驱动程序实例的ID是0~2:
const nrf_drv_timer_t TIMER_LED = NRF_DRV_TIMER_INSEANCE(1);
接着定义一个定时器配置结构体饼对其成员变量进行赋值,其结构体为:
typedef struct { nrf_timer_frequency_t frequency; //时钟频率:可配置10种时钟频率(参考表格) nrf_timer_mode_t mode; //模式:定时或计数模式 nrf_timer_bit_width_t bit_width; //位宽:8/16/24/32位(PS:TIMER1 和 TIMER2 只能设置8或16位) uint8_t interrupt_priority; //中断优先级 void * p_context; //传递给中断函数的参数 } nrf_drv_timer_config_t
当然也可以直接调用库函数中的默认配置,如:nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
定时器配置结构体定义好之后,调用nrf_drv_timer_init()函数使用配置结构体中的设置的数值来初始化Timer。在初始化前,还需注册事件的回调函数,当产生比较事件是会调用该回调函数:
nrf_drv_timer_init(&TIMER_LED,&timer_cfg, timer_led_even_handler);
2、计算定时时间对应的ticks
对于Timer定时来说,定时时间实际对应的是Timer的ticks(时钟滴答)数,但tisks和时间的对应关系并不明显。如果每次修改时间都要计算一次ticks数会比较麻烦,因此在程序中我们会直接用一个函数来计算对应的ticks:
time_ticks = nrf_drv_timer_ms_to_ticks(&TIMER_LED, time_ms);
这样每当我们需要计算时间对应的ticks时,将时间值传给函数的参数time_ms(单位是毫秒),函数的返回值就是时间对应的ticks。
3、设置定时器通道和比较值
每个Timer都有各自的捕获/比较通道,一般的定时使用一个通道就可以。以Timer0为例,设置定时器的捕获比较通道0和它的比较值:
nrf_drv_timer_extened_compare( &TIMER_LED, NRF_TIMER_CC_CHANNEL2, time_ticks, NRF_TIMER_SHORT_CPMPARE2_CLEAR_MASK, true, );
4、启动Timer
定时器配置完成后,调用nrf_drv_timer_enable函数即可启动定时器开始计时:
nrf_drv_timer_enable(&TIMER_LED);
五、实例操作
1.定时驱动LED闪烁(库函数)
1.创建工程
创建工程和配置基本配置在前两节已做说明,此章不再陈述。
2.工程实验
接着配置好相对应的头文件目录,且定义好相对应所需的宏定义,(PS: 添加头文件的方式 :缺啥补啥,编译显示缺少什么就添加什么),并添加相应的实现函数,以下为我的配置:
注意:应记得勾选C99 Mode,否则可能出现不必要的错误。如:main.c(34): error: #29: expected an expression const nrf_drv_timer_t TIMER_LED = RF_DRV_TIMER_INSTANCE(0)等.....
除了配置基本的设置外,还需在配置文件“sdk_config.h”中启用Timer程序模块和Timer0,将宏定义TIMER_ENABLED和TIMER0_ENABLED设置为“1”,如图所示:
PS:可能有的sdk_config.h找不到TIMER_ENABLED,SDK配置文件可以从SDK中其他例子拷贝过来修改(我用的是SDK 12.3.0B版本的)
3.实验代码
#include "boards.h" #include "nrf_gpio.h" #include "nrf_drv_timer.h" const nrf_drv_timer_t TIMER_LED = NRF_DRV_TIMER_INSTANCE(0); //定义Tiemr0的驱动实例。驱动实例的ID对应Timer的ID void timer_led_even_handler(nrf_timer_event_t event_type, void * p_context) //回调函数 { switch(event_type) { case NRF_TIMER_EVENT_COMPARE0: nrf_gpio_pin_toggle(LED_1); break; default: break; } } int main(void) { uint32_t timer_ms = 500; //定时0.5s uint32_t timer_ticks; //ticks数 uint32_t err_code = NRF_SUCCESS; nrf_gpio_cfg_output(LED_1); //配置LED_1的GPIO为输出 nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG; //配置TIMER的配置为默认配置 err_code = nrf_drv_timer_init(&TIMER_LED, &timer_cfg, timer_led_even_handler); //初始化定时器 APP_ERROR_CHECK(err_code); timer_ticks = nrf_drv_timer_ms_to_ticks(&TIMER_LED, timer_ms); //将定时时间转换为ticks数 nrf_drv_timer_extended_compare( &TIMER_LED, NRF_TIMER_CC_CHANNEL0, timer_ticks, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, true); //设置定时器捕获/比较通道及该通道的比较值,使能通达的中断 nrf_drv_timer_enable(&TIMER_LED); //使能启动定时器 while(true) { } }