This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Direct manipulation of peripherals (TIMER, GPIOTE, PPI)?

Hi,

I'm porting code from Simblee/Arduino to Laird's BL651/Nordic SDK (nRF52810, SDK 15.3, S112). (Simblee discontinued.) For accurate timing of a GPIO pin, I use TIMER1, TIMER2, GPIOTE and PPI. GPIOTE watches an input pin for transition LOTOHI and HITOLO. PPI is used to start TIMER2 (timer mode) on input pin transition LOTOHI, then stop TIMER2 on transition HITOLO. PPI is also used to increment (COUNT) TIMER1 (counter mode) each time TIMER2 overflows. GPIOTE triggers interrupt call on input pin transition LOTOHI where code captures TIMER1 and TIMER2 values and calculates time between LOTOHI and HITOLO transitions of input pin and sends to BLE Central.

Here's the current code that works well on Simblee:

// Find three free PPI channels.

uint32_t i;

// Initialize Programmable Peripheral Interconnect
// Find first free PPI Channel in the range 0-7. Note the channels 8-15 occupied by SoftDevice
for (i = 0; i < 8; i++) {
    if (!(NRF_PPI->CHEN & (1 << i))) {
        chan_0 = i;
        // Find second free PPI Channel in the range 0-7.
        for (i = (chan_0 + 1); i < 8; i++) {
            if (!(NRF_PPI->CHEN & (1 << i))) {
                chan_1 = i;
                // Find third free PPI Channel in the range 0-7.
                for (i = (chan_1 + 1); i < 8; i++) {
                    if (!(NRF_PPI->CHEN & (1 << i))) {
                        chan_2 = i;
                        break;
                    }
                }
                break;
            }
        }
        break;
    }
}

// The approach here is to implement TIMER1 and TIMER2 as a real time counter, start the counter
// when input pin goes LoToHi and stop counter when input pin goes HiToLo. TIMER1 counts the number of times
// TIMER2 overflows, so we multiply the overflow count by 65535 and add it to the TIMER2 value to get
// the total number of ticks between beam break and restore. Then calculate milliseconds
// according to prescaler.
//
// The incrementing of TIMER1 on overflow of TIMER2, as well as the starting and stoping of TIMER2 on
// input pin LoToHi and HiToLo all happen via GPIOTE, TIMER1, TIMER2 and PPI, so it is accurate and not
// subject to CPU usage. GPIOTE calls interrupt routine to notify of LoToHi input pin transition.


// Configure TIMER1 as counter
NRF_TIMER1->TASKS_STOP = 1; // Stop timer
NRF_TIMER1->MODE = TIMER_MODE_MODE_Counter; // Set to counter mode
NRF_TIMER1->TASKS_CLEAR = 1; // Clear the counter
NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_16Bit; // Set to 16 bit 
NRF_TIMER1->TASKS_START = 1; // Start counter

// Configure TIMER2 as timer
NRF_TIMER2->TASKS_STOP = 1; // Stop timer
NRF_TIMER2->MODE = TIMER_MODE_MODE_Timer; // Set to timer mode
NRF_TIMER2->PRESCALER = 9; // 9 is highest prescaler; gives us enough accuracy and minimizes overflows
NRF_TIMER2->TASKS_CLEAR = 1; // Clear the timer
NRF_TIMER2->BITMODE = TIMER_BITMODE_BITMODE_16Bit; // Set to 16 bit
NRF_TIMER2->CC[0] = 0;

// Configure GPIOTE channel 0 as event that occurs when input pin changes from digital
// low to hi.
NRF_GPIOTE->CONFIG[0] = (GPIOTE_CONFIG_POLARITY_LoToHi << GPIOTE_CONFIG_POLARITY_Pos)
    | (inputPin << GPIOTE_CONFIG_PSEL_Pos)
    | (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos);

// Configure GPIOTE channel 1 as event that occurs when input pin changes from digital
// hi to low.
NRF_GPIOTE->CONFIG[1] = (GPIOTE_CONFIG_POLARITY_HiToLo << GPIOTE_CONFIG_POLARITY_Pos)
    | (inputPin << GPIOTE_CONFIG_PSEL_Pos)
    | (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos);

// Interrupt only on input pin LoToHi transition.
NRF_GPIOTE->INTENCLR = GPIOTE_INTENCLR_IN0_Msk | GPIOTE_INTENCLR_IN2_Msk | GPIOTE_INTENCLR_IN3_Msk;
NRF_GPIOTE->INTENSET = GPIOTE_INTENSET_IN1_Msk;

// Clear all events.
NRF_GPIOTE->EVENTS_IN[0] = 0;
NRF_GPIOTE->EVENTS_IN[1] = 0;
NRF_GPIOTE->EVENTS_IN[2] = 0;
NRF_GPIOTE->EVENTS_IN[3] = 0;

// Attach interrupt handler.
dynamic_attachInterrupt(GPIOTE_IRQn, beamEventHandler);

// Enable interrupts.
NVIC_EnableIRQ(GPIOTE_IRQn);

// Configure PPI channel 0 to increment TIMER1 when TIMER2 overflows.
simblee_ppi_channel_assign(chan_0, &NRF_TIMER2->EVENTS_COMPARE[0], &NRF_TIMER1->TASKS_COUNT);

// Configure PPI channel 1 to start TIMER2 when GPIOTE channel 0 event occurs.
simblee_ppi_channel_assign(chan_1, &NRF_GPIOTE->EVENTS_IN[0], &NRF_TIMER2->TASKS_START);

// Configure PPI channel 2 to stop TIMER2 when GPIOTE channel 1 event occurs.
simblee_ppi_channel_assign(chan_2, &NRF_GPIOTE->EVENTS_IN[1], &NRF_TIMER2->TASKS_STOP);

As you can see, I access all peripherals directly rather than using TIMER, GPIOTE and PPI drivers. I read somewhere that it's better to use the peripheral drivers rather than direct manipulation (this is understandable). So I did so in porting to Nordic SDK (15.3 with SoftDevice S112. However, I've discovered that you cannot use the GPIOTE driver (routines like nrfx_gpiote_init(), etc.) to watch for more than one event per pin. I cannot use it to watch for both LoToHi and HiToLo. I could use toggle (NRF_GPIOTE_POLARITY_TOGGLE polarity), but would need to check transition in the interrupt routine to know which transition triggered the interrupt.

My questions are:

1) Is it critical to use the peripheral drivers rather than accessing peripherals directly?

2) Given the above limitation in GPIOTE, is it okay to manipulate GPIOTE directly?

The code that is working (using nRF52 dev kit) is below, but manipulates GPIOTE directly:

// GPIOTE interrupt routine
void GPIOTE_IRQHandler(void) {

    // Clear the events.
    NRF_GPIOTE->EVENTS_IN[0] = 0;
    NRF_GPIOTE->EVENTS_IN[1] = 0;

    // Capture current timer1 value into capture register 1
    nrfx_timer_capture(&m_timer1,1);
    uint32_t numberOfOverflows = nrfx_timer_capture_get(&m_timer1,1);
    printf("numberOfOverflows %u\n",numberOfOverflows);

    nrfx_timer_capture(&m_timer2,1);
    uint32_t timerValue = nrfx_timer_capture_get(&m_timer2,1);
    printf("timerValue %u\n",timerValue);

    // Factor in number of times TIMER2 overflowed.
    unsigned long durationInTicksToSend = timerValue + (numberOfOverflows * 65535);

    // Calculate duration in milliseconds * 100 to preserve accuracy.
    double durationMillisecondsTimes100Double = durationInTicksToSend * 3.2; // based on prescaler of 9
    double durationMillisecondsDouble = durationMillisecondsTimes100Double / 100;
    unsigned long durationMilliseconds = durationMillisecondsDouble; // round to milliseconds

    // Clear the timers to get ready for next events
    nrfx_timer_clear(&m_timer1);
    nrfx_timer_clear(&m_timer2);

    printf("durationMilliseconds %u\n",durationMilliseconds);

}

// TIMER interrupt
void timer_event_handler(nrf_timer_event_t event_type, void* p_context) {
    // never called.
}


// Initialize timers
void timers_init() {

    // Configure TIMER1 as counter
    nrfx_timer_config_t timer1_cfg = NRFX_TIMER_DEFAULT_CONFIG;
    timer1_cfg.mode = NRF_TIMER_MODE_COUNTER;
    timer1_cfg.bit_width = NRF_TIMER_BIT_WIDTH_16;
    nrfx_timer_init(&m_timer1, &timer1_cfg, timer_event_handler);
    nrfx_timer_clear(&m_timer1);
    nrfx_timer_enable(&m_timer1);

    // Configure TIMER2 as timer
    nrfx_timer_config_t timer2_cfg = NRFX_TIMER_DEFAULT_CONFIG;
    timer2_cfg.mode = NRF_TIMER_MODE_TIMER;
    timer2_cfg.bit_width = NRF_TIMER_BIT_WIDTH_16;
    timer2_cfg.frequency = NRF_TIMER_FREQ_31250Hz;
    nrfx_timer_init(&m_timer2, &timer2_cfg, timer_event_handler);
    nrfx_timer_clear(&m_timer2);
    nrfx_timer_compare(&m_timer2,NRF_TIMER_CC_CHANNEL0,0,false);

}

// Initialize GPIOTE
void gpiote_init(void) {

    // Configure GPIOTE channel 0 as event that occurs when input pin changes from digital
    // hi to low.
    NRF_GPIOTE->CONFIG[0] = (GPIOTE_CONFIG_POLARITY_HiToLo << GPIOTE_CONFIG_POLARITY_Pos)
        | (inputPin << GPIOTE_CONFIG_PSEL_Pos)
        | (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos);

    // Configure GPIOTE channel 1 as event that occurs when input pin changes from digital
    // low to hi.
    NRF_GPIOTE->CONFIG[1] = (GPIOTE_CONFIG_POLARITY_LoToHi << GPIOTE_CONFIG_POLARITY_Pos)
        | (inputPin << GPIOTE_CONFIG_PSEL_Pos)
        | (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos);

    // Interrupt only on on LoToHi transition.
    NRF_GPIOTE->INTENCLR = GPIOTE_INTENCLR_IN0_Msk | GPIOTE_INTENCLR_IN2_Msk | GPIOTE_INTENCLR_IN3_Msk;
    NRF_GPIOTE->INTENSET = GPIOTE_INTENSET_IN1_Msk;

    // Clear all events.
    NRF_GPIOTE->EVENTS_IN[0] = 0;
    NRF_GPIOTE->EVENTS_IN[1] = 0;
    NRF_GPIOTE->EVENTS_IN[2] = 0;
    NRF_GPIOTE->EVENTS_IN[3] = 0;

    // Enable interrupts.
    NVIC_EnableIRQ(GPIOTE_IRQn);

}

// Initialize PPI
void ppi_init(void) {

    nrfx_err_t err_code = NRF_SUCCESS;

    // PPI channels
    uint8_t chan_0 = 255;
    uint8_t chan_1 = 255;
    uint8_t chan_2 = 255;

    err_code = nrfx_ppi_channel_alloc(&chan_0);
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_alloc(&chan_1);
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_alloc(&chan_2);
    APP_ERROR_CHECK(err_code);

    // Configure PPI channel 0 to increment TIMER1 when TIMER2 overflows.
    err_code = nrfx_ppi_channel_assign(chan_0, nrfx_timer_event_address_get(&m_timer2,NRF_TIMER_EVENT_COMPARE0), nrfx_timer_task_address_get(&m_timer1,NRF_TIMER_TASK_COUNT));
    APP_ERROR_CHECK(err_code);

    // Configure PPI channel 1 to start TIMER2 when GPIOTE channel 0 event occurs.
    err_code = nrfx_ppi_channel_assign(chan_1, &NRF_GPIOTE->EVENTS_IN[0], nrfx_timer_task_address_get(&m_timer2,NRF_TIMER_TASK_START));
    APP_ERROR_CHECK(err_code);

    // Configure PPI channel 2 to stop TIMER2 when GPIOTE channel 1 event occurs.
    err_code = nrfx_ppi_channel_assign(chan_2, &NRF_GPIOTE->EVENTS_IN[1], nrfx_timer_task_address_get(&m_timer2,NRF_TIMER_TASK_STOP));
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_enable(chan_0);
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_enable(chan_1);
    APP_ERROR_CHECK(err_code);

    err_code = nrfx_ppi_channel_enable(chan_2);
    APP_ERROR_CHECK(err_code);

}


Thanks for any help!

Tim

Parents
  • 1) Is it critical to use the peripheral drivers rather than accessing peripherals directly?

     No, you can access the registers directly, that should be fine.

     

    2) Given the above limitation in GPIOTE, is it okay to manipulate GPIOTE directly?

     Yes, I believe that should be okay.

  • Thank you Hakon. This post recommends using API calls rather than direct access to peripheral registers (at least for PPI).

    With SoftDevice (S112) enabled, I presume care is necessary to not use PPI channels reserved for the SD. I see equivalent calls for all nrfx driver PPI API calls in the SD API (e.g. "sd_ppi_channel_assign" for "nrfx_ppi_channel_assign") except for nrfx_ppi_channel_alloc, which I presume is the important SD routine for obtaining an available PPI channel.

    The only peripheral I need to access directly, it seems, is GPIOTE. I will do so in order to have two events for a single input pin.

    A related question (being new to Nordic SDK). My understanding is there are legacy drivers with prefix "nrf_dvr_.." and current(?) drivers with prefix "nrfx_...". I presume I should be using the current "nrfx_..." drivers.

    Many thanks!

    -Tim

  • Tim said:
    A related question (being new to Nordic SDK). My understanding is there are legacy drivers with prefix "nrf_dvr_.." and current(?) drivers with prefix "nrfx_...". I presume I should be using the current "nrfx_..." drivers.

    In the current SDK nrf_drv_ is just a glue layer for nrfx_, so you can use both, but nrfx_ is what you're supposed to use and not nrf_drv.

Reply
  • Tim said:
    A related question (being new to Nordic SDK). My understanding is there are legacy drivers with prefix "nrf_dvr_.." and current(?) drivers with prefix "nrfx_...". I presume I should be using the current "nrfx_..." drivers.

    In the current SDK nrf_drv_ is just a glue layer for nrfx_, so you can use both, but nrfx_ is what you're supposed to use and not nrf_drv.

Children
Related