一、我的需求
应用层的调度依赖于一个周期为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。