原文地址:https://devzone.nordicsemi.com/nordic/b/blog/posts/wireless-timer-synchronization-among-nrf5-devices
简介:
有一些情况需要很多设备同步时钟。
一些无线协议如蓝牙对底层的射频硬件实现了优秀的抽象。这使得顶层的开发者无需关心底层的具体实现。直接调用send函数就可以把数据发到指定的位置,而无需关心环境噪声。
蓝牙是是可信赖的协议的一个例子。顶层数据应用发送的数据会在底层被重复发送,直到对方返回一个接收成功回应。应用也不需要考虑底层究竟尝试发送了多少次,这是在时间同步中的一个很大的问题。纠正数据可以通过后续发送多次来完成。在本篇文章中,实现了一个更简单(或者说是精确?)的同步方法:在 in a proprietary radio mode下,通过softDevice的时间间隙中准确地发送和接收定时信标信号 ;
概要:
网络中每个节点都保持一个带有16位计数器的自由运行16 MHz定时器,这意味着定时器将溢出,大约以224Hz的频率循环。定时器可以通过GPIOTE+PPI的方式触发,通过定时器来翻转一个GPIO,然后通过逻辑分析仪来测量这个GPIO将得到以下结果:
这个自由运行的定时器将是同步的基础,网络中的一个节点将作为时间主机,目的是使用该节点的时间来同步其他节点中自由运行的定时器。然后通过示波器或者逻辑分析仪来查看GPIO的输出,可以验证一个同步网络中彼此时间的接近程度。
时间主机将会按找配置好的间隔发射同步包(主机主动发送??),这些数据包将包含一个值,该值指示无线数据包相对于自由运行的16 MHz定时器的传输时间,当其他节点接收到该同步包时,他们可以使用包里的时间值,加上一个正向或者负向的偏移来更新自己的时间。
一个信标的例子:
struct
{
int32_t timer_val;
int32_t rtc_val;
} sync_pkt;
取决于想要的精度和功耗,可以使用RTC而非定时器来实现低精确度和低功耗的时间同步,在下面的代码示例中,仅使用16 MHz定时器。
通常情况下,传输速率越慢,时钟漂移的时间就越长,因此,应权衡好无线传输活动(功率/共存)与准确性的关系。
注意,因为PPI和定时器都是运行在16M的时钟上,这意味着本次设计的时间基本单位为62.5ns(一个时钟周期)。
一致性
为了实现精确的时间同步保持,保持所有与时序相关的因素尽可能一致是很重要的,包括:
- 让定时主机每次发送信标同步包,在定时器捕获和无线电传输之间具有相同的偏移
- 使用最精确的振荡器进行计时,即使用外部的高速和低速晶振,不要使用内部RC振荡器
- 使用硬件定时器和触发器(而不是CPU)进行以下操作:
- 自由运行计时器捕获
- 自由定时器更新
- 同步包发送
- 捕获数据包接收的时间
特别是最后一条非常重要(即使用硬件定时器和触发器),如果CPU用于定时器操作和数据包传输触发,还有许多其他因素会引入抖动和偏移:其他高优先级中断,编译器设置,高速缓存未命中(nRF52),内存总线时钟域抖动(nRF52)。
时间间隙(Time slot)
SoftDevice时隙API允许顶级应用程序请求在BLE活动之间访问无线电硬件。 在该示例中,使用时间间隙使得正常BLE活动可以与时间同步功能同时运行。
此示例的代码实现以下时隙行为:
- 默认情况下,每个节点将请求用于在RX模式下运行无线电的时隙,监听时间信标。 所有可用的无线电时间都将用于此(效率不是很高)。
- 当按下nRF52-DK上的按钮1时,该设备将充当定时主机并以可配置的间隔(默认为100 Hz)开始发送时间信标。
- 该代码基于SDK HTS示例,在同步活动期间,设备可通过BLE连接
完整代码获取请访问: https://github.com/nordic-auko/nRF5-ble-timesync-demo
以下为代码片段:
time_sync.h实现以下API:
typedef struct
{
uint8_t rf_chn; /** RF Channel [0-80] */
uint8_t rf_addr[5]; /** 5-byte RF address */
uint8_t ppi_chns[3]; /** PPI channels */
uint8_t ppi_chhg; /** PPI Channel Group */
NRF_TIMER_Type * high_freq_timer[2]; /** 16 MHz timer (e.g. NRF_TIMER2) */
NRF_RTC_Type * rtc;
} ts_params_t;
/**@brief SoftDevice system event handler. Must be called when a system event occurs */
void ts_on_sys_evt(uint32_t sys_evt);
/**@brief Initialize time sync library
*
* @param[in] p_params Parameters
*
* @retval NRF_SUCCESS if successful
*/
uint32_t ts_init(const ts_params_t * p_params);
/**@brief Enable time sync library. This will enable reception of sync packets.
*
* @retval NRF_SUCCESS if successful
*/
uint32_t ts_enable(void);
/**@brief Disable time sync library.
*
* @retval NRF_SUCCESS if successful
*/
uint32_t ts_disable(void);
/**@brief Start sync packet transmission (become timing master).
*
* @note @ref ts_enable() must be called prior to calling this function
* @note Expect some jitter depending on BLE activity.
*
* @param[in] sync_freq_hz Frequency of transmitted sync packets.
*
* @retval NRF_SUCCESS if successful
*/
uint32_t ts_tx_start(uint32_t sync_freq_hz);
/**@brief Stop sync packet transmission (become timing slave again).
*
* @retval NRF_SUCCESS if successful
*/
uint32_t ts_tx_stop(void);
如 ts_params_t 所示,代码运行需要以下资源:
- 3 个PPI 通道
- 1个PPI组
- 2个16Mhz的定时器
其中一个16 MHz定时器是自由运行的定时器,另一个定时器用于准确触发主无线电传输。 请注意,这可以简化为仅使用一个额外的计时器,因为TIMER0可以在时隙内使用来触发无线发送。
使用的无线电参数非常基本。 请注意,此代码包括使用nRF52改进,更快的无线电加速时间。 除此之外,代码也可以在nRF51上运行(期望相同的结果)(即博主觉得可以在51上运行。。。)。以下为配置参数:
static void update_radio_parameters()
{
// TX power
NRF_RADIO->TXPOWER = RADIO_TXPOWER_TXPOWER_0dBm << RADIO_TXPOWER_TXPOWER_Pos;
// RF bitrate
NRF_RADIO->MODE = RADIO_MODE_MODE_Ble_1Mbit << RADIO_MODE_MODE_Pos;
// Fast startup mode
NRF_RADIO->MODECNF0 = RADIO_MODECNF0_RU_Fast << RADIO_MODECNF0_RU_Pos;
// CRC configuration
NRF_RADIO->CRCCNF = RADIO_CRCCNF_LEN_Two << RADIO_CRCCNF_LEN_Pos;
NRF_RADIO->CRCINIT = 0xFFFFUL; // Initial value
NRF_RADIO->CRCPOLY = 0x11021UL; // CRC poly: x^16+x^12^x^5+1
// Packet format
NRF_RADIO->PCNF0 = (0 << RADIO_PCNF0_S0LEN_Pos) | (0 << RADIO_PCNF0_LFLEN_Pos) | (0 << RADIO_PCNF0_S1LEN_Pos);
NRF_RADIO->PCNF1 = (RADIO_PCNF1_WHITEEN_Disabled << RADIO_PCNF1_WHITEEN_Pos) |
(RADIO_PCNF1_ENDIAN_Big << RADIO_PCNF1_ENDIAN_Pos) |
(4 << RADIO_PCNF1_BALEN_Pos) |
(sizeof(m_sync_pkt) << RADIO_PCNF1_STATLEN_Pos) |
(sizeof(m_sync_pkt) << RADIO_PCNF1_MAXLEN_Pos);
NRF_RADIO->PACKETPTR = (uint32_t)&m_sync_pkt;
// Radio address config
NRF_RADIO->PREFIX0 = m_params.rf_addr[0];
NRF_RADIO->BASE0 = (m_params.rf_addr[1] << 24 | m_params.rf_addr[2] << 16 | m_params.rf_addr[3] << 8 | m_params.rf_addr[4]);
NRF_RADIO->TXADDRESS = 0;
NRF_RADIO->RXADDRESSES = (1 << 0);
NRF_RADIO->FREQUENCY = m_params.rf_chn;
NRF_RADIO->TXPOWER = RADIO_TXPOWER_TXPOWER_Pos4dBm << RADIO_TXPOWER_TXPOWER_Pos;
NRF_RADIO->EVENTS_END = 0;
NRF_RADIO->INTENCLR = 0xFFFFFFFF;
NRF_RADIO->INTENSET = RADIO_INTENSET_END_Msk;
NVIC_EnableIRQ(RADIO_IRQn);
}
下面是代码的第一个时序关键部分,它确保定时主机在实际传输数据包时以一致的时间增量捕获自由运行的定时器值:(来自timeslot_begin_handler()的片段):
update_radio_parameters();
ppi_chn = m_params.ppi_chns[0];
ppi_chn2 = m_params.ppi_chns[1];
// Use PPI to create fixed offset between timer capture and packet transmission
// Compare event #0: Capture timer value for free running timer
// Compare event #1: Trigger radio transmission
NRF_PPI->CH[ppi_chn].EEP = (uint32_t) &m_params.high_freq_timer[1]->EVENTS_COMPARE[0];
NRF_PPI->CH[ppi_chn].TEP = (uint32_t) &m_params.high_freq_timer[0]->TASKS_CAPTURE[1];
NRF_PPI->CHENSET = (1 << ppi_chn);
NRF_PPI->CH[ppi_chn2].EEP = (uint32_t) &m_params.high_freq_timer[1]->EVENTS_COMPARE[1];
NRF_PPI->CH[ppi_chn2].TEP = (uint32_t) &NRF_RADIO->TASKS_START;
NRF_PPI->CHENSET = (1 << ppi_chn2);
m_params.high_freq_timer[1]->PRESCALER = 4; // 1 us resolution
m_params.high_freq_timer[1]->MODE = TIMER_MODE_MODE_Timer << TIMER_MODE_MODE_Pos;
m_params.high_freq_timer[1]->SHORTS = TIMER_SHORTS_COMPARE1_STOP_Msk | TIMER_SHORTS_COMPARE1_CLEAR_Msk;
m_params.high_freq_timer[1]->TASKS_STOP = 1;
m_params.high_freq_timer[1]->TASKS_CLEAR = 1;
m_params.high_freq_timer[1]->CC[0] = 40; // Matches 40 us radio rampup time
m_params.high_freq_timer[1]->CC[1] = 50; // Margin for timer readout
m_params.high_freq_timer[1]->EVENTS_COMPARE[0] = 0;
m_params.high_freq_timer[1]->EVENTS_COMPARE[1] = 0;
NRF_RADIO->SHORTS = RADIO_SHORTS_END_DISABLE_Msk;
NRF_RADIO->TASKS_TXEN = 1;
m_params.high_freq_timer[1]->TASKS_START = 1;
while (m_params.high_freq_timer[1]->EVENTS_COMPARE[0] == 0)
{
// Wait for timer to trigger
__NOP();
}
m_radio_state = RADIO_STATE_TX;
m_sync_pkt.timer_val = m_params.high_freq_timer[0]->CC[1];
m_sync_pkt.rtc_val
第二个时序关键部分是接收器在收到同步信标数据包时如何更新其本地自由运行定时器。 请注意以下代码中的魔术值“TX_CHAIN_DELAY”:
static inline void sync_timer_offset_compensate(void)
{
uint32_t chn0, chn1, chg;
int32_t peer_timer;
int32_t local_timer;
int32_t timer_offset;
peer_timer = m_sync_pkt.timer_val;
peer_timer += TX_CHAIN_DELAY;
local_timer = m_params.high_freq_timer[0]->CC[1];
if (local_timer > peer_timer)
{
timer_offset = TIMER_MAX_VAL - local_timer + peer_timer;
}
else
{
timer_offset = peer_timer - local_timer;
}
if (timer_offset == 0 ||
timer_offset == TIMER_MAX_VAL)
{
// Already in sync
return;
}
chn0 = m_params.ppi_chns[0];
chn1 = m_params.ppi_chns[1];
chg = m_params.ppi_chhg;
// Use a timer compare register to reset the timer according to the offset value
// PPI channel 0: clear timer when offset value is reached
NRF_PPI->CHENCLR = (1 << chn0);
NRF_PPI->CH[chn0].EEP = (uint32_t) &m_params.high_freq_timer[0]->EVENTS_COMPARE[2];
NRF_PPI->CH[chn0].TEP = (uint32_t) &m_params.high_freq_timer[0]->TASKS_CLEAR;
// PPI channel 1: disable PPI channel 0 such that the timer is only reset once.
NRF_PPI->CHENCLR = (1 << chn1);
NRF_PPI->CH[chn1].EEP = (uint32_t) &m_params.high_freq_timer[0]->EVENTS_COMPARE[2];
NRF_PPI->CH[chn1].TEP = (uint32_t) &NRF_PPI->TASKS_CHG[chg].DIS;
// Use PPI group for PPI channel 0 disabling
NRF_PPI->TASKS_CHG[chg].DIS = 1;
NRF_PPI->CHG[chg] = (1 << chn0);
// Write offset to timer compare register
m_params.high_freq_timer[0]->CC[2] = (TIMER_MAX_VAL - timer_offset);
// Enable PPI channels
NRF_PPI->CHENSET = (1 << chn0) | (1 << chn1);
}
结果
通过在示波器或逻辑分析仪上测量GPIO切换可以看到自由运行的定时器的效果。 在此测试中使用了两个nRF52-DK,每个都非常靠近,以便使用逻辑分析仪探头连接。 在现实世界的情况下,距离会更大,这将增加一些抖动(每16 MHz时钟周期光传播约19米,因此设备之间每19米的抖动预计至少有1个时钟周期)。
下图显示了未传输同步时间包时的两个设备:
一旦其中一个设备采用定时主机角色,使用100 Hz传输速率,自由运行的定时器就会排队。 请注意,不强制执行GPIO极性。 重要的是GPIO同时切换:
同步信号的特写镜头。 在这种情况下,逻辑分析仪报告两个设备之间的20纳秒偏移:
在分析切换时间时,我们可以生成一些统计信息。 这是在运行2个设备测试30分钟后:
- 在合理范围内发现切换439445次
- 通道0的总时间:1800.0027646 s
- otal time in channel 1: 1800.00276466 s
- 最大差异:切换时为220.0 ns#54446(223.010816秒)
- 439445次切换的平均差异= 65.6412747898 ns
- 439445次切换的标准偏差= 41.0020747807 ns
这些结果表明,在理想条件下,可以保持在一个或两个16MHz的同步时钟周期内。 当然,在现实世界中,将会出现数据包丢失,传播延迟(取决于距离)以及可能会降低时序性能的反射。