nrf52832 利用app_timer 产生精确的1ms 心跳

一、我的需求

应用层的调度依赖于一个周期为1ms的滴答心跳(SysTick),并且对这个心跳的精确度要求比较高。

二、存在的问题

初来乍到,对nordic 的sdk 并不熟悉,发现app 定时器 用起来挺方便,直接用它实现一个周期为1ms的周期定时器,然后在定期处理函数中进行SysTick 计数。
用了一段时间才发现,这种方式实现的1ms 中断,误差非常大,实测只有976 us,对应用层的影响比较大,需要更精确的心跳。

三、解决历程

3.1 利用 Systick Timer 实现心跳?

第一时间想到的就是用 Systick Timer 实现,因为之前用stm32 单片机,就是用它实现的。nrf52832 采用的也是arm 内核,应该也是有Systick Timer 。
从nordic sdk 的nrfx_systick.c 中可知,默认情况下,Systick timer 并没有启动。即使启动 Systick timer , sdk 中也是利用Systick Timer 进行阻塞的延时处理,并没有开启中断响应。
在网上查了一下,一般的说法是考虑到蓝牙产品的低功耗特性,sdk 默认不使用Systick Timer。暂时先放弃它。

3.2 利用RTC 时钟实现心跳?

3.2.1 为什么用rtc ?

nrf52832 有一个32.768k的外部晶振作为rtc的时钟源,功耗不高,精确度高。

3.2.2 片上rtc 资源

片上一共有三个rtc 资源。协议栈利用rtc0 进行调度,app 定时器利用rtc1,空闲的只有rtc2 。

3.2.3 利用rtc2 实现


static void rtc2_handler(nrfx_rtc_int_type_t int_type)
{
    if ( int_type == NRFX_RTC_INT_COMPARE0)
    {
	_ms_tick++;
	nrfx_rtc_counter_clear(&rtc2); 
    }
}

static void rtc2_config(void)
{
    uint32_t err_code;
    //定义rtc 初始化配置结构体,并使用默认参数初始化
    nrfx_rtc_config_t config = NRFX_RTC_DEFAULT_CONFIG;

    //freq = 32768/(prescaler + 1)
    config.prescaler = 0;
    err_code = nrfx_rtc_init(&rtc2,&config,rtc2_handler);
    APP_ERROR_CHECK(err_code);

    //设置rtc 通道0的比较值
    //tick : 1000000/32768 = 30.5175us
    //count: 1000/tick = 1000/30.5175 = 72.768
    // 32* 30.517 = 976
    // 33* 30.517 = 1007
    err_code = nrfx_rtc_cc_set(&rtc2,0,32,true);
    APP_ERROR_CHECK(err_code);

    nrfx_rtc_enable(&rtc2);
    
}

RTC 产生事件后,会进入中断服务函数irq_handler(), 而中断服务函数会禁止RTC 比较事件和比较事件中断。我们为了能周期性地产生中断,需要将这两个禁止屏蔽。

static void irq_handler(NRF_RTC_Type * p_reg,
                        uint32_t       instance_id,
                        uint32_t       channel_count)
{
    uint32_t i;
    uint32_t int_mask = (uint32_t)NRF_RTC_INT_COMPARE0_MASK;
    nrf_rtc_event_t event = NRF_RTC_EVENT_COMPARE_0;

    for (i = 0; i < channel_count; i++)
    {
        if (nrf_rtc_int_is_enabled(p_reg,int_mask) && nrf_rtc_event_pending(p_reg,event))
        {
            /* nrf_rtc_event_disable(p_reg,int_mask); */
            /* nrf_rtc_int_disable(p_reg,int_mask); */
            nrf_rtc_event_clear(p_reg,event);
            NRFX_LOG_DEBUG("Event: %s, instance id: %lu.", EVT_TO_STR(event), instance_id);
            m_handlers[instance_id]((nrfx_rtc_int_type_t)i);
        }
        int_mask <<= 1;
        event    = (nrf_rtc_event_t)((uint32_t)event + sizeof(uint32_t));
    }
    event = NRF_RTC_EVENT_TICK;
    if (nrf_rtc_int_is_enabled(p_reg,NRF_RTC_INT_TICK_MASK) &&
        nrf_rtc_event_pending(p_reg, event))
    {
        nrf_rtc_event_clear(p_reg, event);
        NRFX_LOG_DEBUG("Event: %s, instance id: %lu.", EVT_TO_STR(event), instance_id);
        m_handlers[instance_id](NRFX_RTC_INT_TICK);
    }

    event = NRF_RTC_EVENT_OVERFLOW;
    if (nrf_rtc_int_is_enabled(p_reg,NRF_RTC_INT_OVERFLOW_MASK) &&
        nrf_rtc_event_pending(p_reg, event))
    {
        nrf_rtc_event_clear(p_reg,event);
        NRFX_LOG_DEBUG("Event: %s, instance id: %lu.", EVT_TO_STR(event), instance_id);
        m_handlers[instance_id](NRFX_RTC_INT_OVERFLOW);
    }
}

3.3 重新回归app timer定时器

虽然用rtc2 实现了精确的1毫秒的中断,但是由于多启用了一个定时器,功耗高了,心理不爽。回来分析一下为什么app timer 实现的1ms 定时器误差那么大

3.3.1 io 口翻转辅助分析

利用io 口翻转查看了1ms 中断的时间间隔,发现这个时间稳定为976us。看起来这个调度本身是挺稳定的。

3.3.2 额外添加n个tick,凑足1ms,是否可行?

实际算了一下APP_TIMER_TICKS(1) 换算出来的 ticks 数是16 。其中APP_TIMER_CONFIG_RTC_FREQUENCY 默认为1,也就是16384 hz。1000000/16384 * 16 算出来的值刚好是976 。说明本身app 定时器的定时时间是很准确的。

3.3.3 APP_TIMER_TICKS 运算精度差

查看APP_TIMER_TICKS 的函数实现,发现APP_TIMER_TICKS(1) 理论上算出来是16.884,由于都是整形数据参与运算,返回的结果就截掉了后面的小数部分,返回基本整形16 ,造成最后的误差大。

3.3.4 提高时钟分频系数,降低误差

APP_TIMER_CONFIG_RTC_FREQUENCY 由1 调整 为0,时钟频率由16384 提到到32768,每个tick 的时间61.11 变成30.52 。也就是说,当误差为1个tick时,之前的最大误差是61.11 us,现在变成了30.52us。产生1ms 中断,理论上的中断周期是1007us,误差是7us。

上一篇:基于 Web 引擎扩展技术的 RTC 混合开发框架实践


下一篇:X86汇编语言从实模式到保护模式08:中断和动态时钟显示