DTM sample's dtm_wait() exposes a race condition in nrfx_timer_enable()

We incorporated the basic functionality of the NCS 2.0's DTM sample app inside an larger application with other threads running.

In our case, for what maps to the dtm_wait(), we experienced a race condition in nrfx_timer_enable() leading to an assert in that routine. Here is the relevant code in the DTM sample and the nrfx timer routines:

void dtm_wait(void)
{
    int err;
    nrfx_timer_enable(&dtm_inst.wait_timer);
    err = k_sem_take(&dtm_inst.wait_sem, K_FOREVER);
  etc.
}

static void wait_timer_handler(nrf_timer_event_t event_type, void *context)
{
    nrfx_timer_disable(&dtm_inst.wait_timer);
    nrfx_timer_clear(&dtm_inst.wait_timer);
    k_sem_give(&dtm_inst.wait_sem);
}

void nrfx_timer_enable(nrfx_timer_t const * p_instance)
{
    NRFX_ASSERT(m_cb[p_instance->instance_id].state == NRFX_DRV_STATE_INITIALIZED);
    nrf_timer_task_trigger(p_instance->p_reg, NRF_TIMER_TASK_START);
    m_cb[p_instance->instance_id].state = NRFX_DRV_STATE_POWERED_ON;
    NRFX_LOG_INFO("Enabled instance: %d.", p_instance->instance_id);
}

void nrfx_timer_disable(nrfx_timer_t const * p_instance)
{
    NRFX_ASSERT(m_cb[p_instance->instance_id].state != NRFX_DRV_STATE_UNINITIALIZED);
    nrf_timer_task_trigger(p_instance->p_reg, NRF_TIMER_TASK_SHUTDOWN);
    m_cb[p_instance->instance_id].state = NRFX_DRV_STATE_INITIALIZED;
    NRFX_LOG_INFO("Disabled instance: %d.", p_instance->instance_id);
}


You can see the potential race condition by supposing the following sequence occurs in the DTM sample app code.

  1. dtm_wait() / nrfx_timer_enable() has just completed the call to nrf_timer_task_trigger() (lines 4 and 19), and then the thread of that execution gets preempted.
  2. While the thread of dtm_wait() is preempted, the nrfx_timer expires and calls wait_timer_handler().
  3. The wait_time_handler() disables the timer by calling nrfx_timer_disable() (line 11), which sets the timer state to NRFX_DRV_STATE_INITIALIZED (line 28).
  4. The thread of dtm_wait() resumes at setting the timer state to NRFX_DRV_STATE_POWERED_ON (line 20).
  5. The dtm_wait() takes the semaphore (line 5), which was already given since the timer handler already expired (line 13).
  6. At the next call to dtm_wait()/nrfx_timer_enable(), we fail the assert (line 18) because the state is wrong since step 3 and step 4 are not the expected order.

We fixed the race condition by putting an irq_lock() in dtm_wait() around the call to nrfx_time_enable().

It seems that the race condition could be fixed alternatively by swapping the timer trigger and the state change in your routine, as follows (lines 4 and 5 swapped):

void nrfx_timer_enable(nrfx_timer_t const * p_instance)
{
    NRFX_ASSERT(m_cb[p_instance->instance_id].state == NRFX_DRV_STATE_INITIALIZED);
    m_cb[p_instance->instance_id].state = NRFX_DRV_STATE_POWERED_ON;
    nrf_timer_task_trigger(p_instance->p_reg, NRF_TIMER_TASK_START);
    NRFX_LOG_INFO("Enabled instance: %d.", p_instance->instance_id);
}

Is that change appropriate for you to make in the NCS?  If not, should there be a note somewhere for nrfx_timer_enable() that the caller should ensure the call is protected?

Thank you in advance.

Parents Reply Children
Related