Timer and Counter

  1. General Information
  2. Configuring the Timer
  3. Timer Counter Full Code Example
  4. Configuring the Timer for PWM usage
  5. Timer PWM Full Code Example

General Information

The Timer and Counter modules keep track of timing and count events. It can generate output waveforms and trigger timed actions in other peripherals. Timers are beneficial when activities need to be timed precisely with as little CPU usage as possible.

The XDK’s MCU features the following timer peripherals, which are used for specific tasks:

  • Four 16 Bit Timer/Counter module
  • One 24 Bit and one 32 Bit Real-Time Counter
  • Three 8/16 Bit Pulse Counter
  • One Watchdog Timer

As can be seen, timers are also used in the watchdog and the real time counter module, as well as in the pulse counter module.

A timer can primarily be used for activities like up/down counting, input capture, output comparison and Pulse Width Modulation (PWM).

  • Up-Count - The timer counts up until he reaches the configured maximal value. After that, the timer is reset to 0 before counting up again. As soon as the timer switches from the maximal value to 0, an interrupt can occur (as per configuration).
  • Down-Count - The timer counts down from the maximal value to 0. After that, the timer is reloaded and restarts counting down from the maximal value. When the timer switches from 0 to the maximal value, an interrupt can occur (as per configuration).
  • Up/Down-Count - The timer starts at 0 and counts up to the maximal configured value. Then it counts down to 0 and starts counting up again. Every time the value switches from the maximal value to the value (maximal value - 1), an overflow occurs. An underflow occurs, when the value goes from 0 to 1. Both events can trigger an interrupt.
  • Input capture - This mode will capture the value of the counter based on a trigger event on the input pin. This can be used to measure pulse width or period.
  • Output compare - This mode is used to compare an output value with the counter value. It is also possible to trigger a configured interrupt on a compare match.
  • PWM - The timer has a separate mode for PWM generation, and it’s only supported by up-count and down-count events.

Configuring the Timer

This section shows how to configure the timer module to be used as counter. The EMLib API for the timer module will be used, since it offers a lot of functionality which is currently not covered on higher API levels.

#include "em_timer.h"

The essential functions that are used in this guide are described in the table below. For a full description, please refer to the corresponding API documentation.

FunctionDescription
TIMER_Init()This function is called to initialize the Timer module of the XDK. This involves passing a struct into it with configurations for the module itself.
TIMER_IntEnable()This function is called to enable a specific interrupt of the timer module.
TIMER_IntClear()This function is called to clear the interrupt flag of the timer module.
TIMER_TopSet()This function is called to set the maximal counting value for the timer module.
TIMER_CompareBufSet()This function is called to set buffered compare match value for the timer module. It will be set after the next counting session.
TIMER_InitCC()This function is called to initialize the PWM output functionality for the timer module. Please note that it does not cover a specific pin configuration for the used output pins.

In addition to the em_timer.h interface, the following interface will be used, as well.

#include "em_cmu.h"
#include "em_gpio.h"

The interface em_cmu.h will be used to enable the clock for the timer module.

The code below shows how to configure and initialize the timer module for counting to a set top level and then trigger an interrupt for the overflow event.

// Enable clock for TIMER0 module
CMU_ClockEnable(cmuClock_TIMER0, true);

TIMER_Init_TypeDef timerInit = TIMER_INIT_DEFAULT;
timerInit.prescale = timerPrescale1024;
timerInit.mode = timerModeUp;

TIMER_IntEnable(TIMER0, TIMER_IF_OF);
NVIC_EnableIRQ(TIMER0_IRQn);

TIMER_TopSet(TIMER0, 10);

TIMER_Init(TIMER0, &timerInit);

The first function call of the code above, CMU_ClockEnable() enables the clock for the timer module. Afterwards, a struct of the type TIMER_Init_TypeDef is declared and filled with default configuration parameters provided by the define TIMER_INIT_DEFAULT . Only specific configurations are made for the pre scaler and the timer mode. In this case the pre scaler is set up to 1024, that divides the used clock of 48 MHz down to approximately 48 kHz.

The mode timerModeUp configures the timer for up counting up. Furthermore, the overflow interrupt of the timer module is enabled by calling the function Timer_IntEnable() and passing the parameter TIMER_IF_OF to it. Additionally, the timer interrupt is also enabled in the device specific interrupt controller of the XDK’s MCU by calling the function NVIC_EnableIRQ().

Afterwards the maximal value is set by the function TIMER_TopSet() to ten, that the timer counts from 0 to 10 upwards and then triggers an overflow interrupt. Please note that the maximal value is not limited and can be set to any value to count up to.

Now that all configurations are made, the timer module can be initialized by calling the function TIMER_Init(). Please note that the timer directly starts counting after the initialization, because the enable parameter was already set by the default settings when making usage of TIMER_INIT_DEFAULT.

The last step is to define an interrupt handler for this particular timer. If none is defined, the application will not process the interrupt correctly.

The implementation of it is shown in the code snippet below and needs to implemented before attempting to start the timer.

void TIMER0_IRQHandler(void) {
  TIMER_IntClear(TIMER0, TIMER_IF_OF);

  // Do something
  portYIELD_FROM_ISR(pdTRUE);
}

The signature for the interrupt handler should always be the same as it is shown in the code snippet above. The function call of TIMER_IntClear() inside the function is used to clear the interrupt overflow flag. If the flag is not cleared, no new counting up is started. Please note that it is essential to clear this flag within the interrupt handler. Additional functionality can be added before or after the call of TIMER_IntClear().

In most use cases, it is recommended to clear the flag first before executing any other functionality. Additionally, please note that the functionality should be kept short and simple, since an interrupt event occurs periodically in very short time intervals. As such, long processing chains should be avoided. Additionally, it is necessary to call the macro portYIELD_FROM_ISR() at the end of the interrupt handler too. This macro informs FreeRTOS that the interrupt is finished, so that execution can return to the context of the task that was interrupted.

This is necessary, because the interrupt from the timer module is a hardware interrupt with the highest priority and automatically interrupts the currently running operating task. To avoid issues with FreeRTOS, it is mandatory to return to the initial task after the interrupt was processed.

Afterwards, no further implementation or configuration needs to be done.

Timer Counter Full Code Example

Note: The full code example is intended for XDK-Workbench versions 3.4.0 and higher.

/* --------------------------------------------------------------------------- |
 * INCLUDES & DEFINES ******************************************************** |
 * -------------------------------------------------------------------------- */

/* own header files */
#include "XdkAppInfo.h"
#undef BCDS_MODULE_ID  /* Module ID define before including Basics package*/
#define BCDS_MODULE_ID XDK_APP_MODULE_ID_APP_CONTROLLER

/* own header files */
#include "AppController.h"

/* system header files */
#include <stdio.h>

/* additional interface header files */
#include "BCDS_CmdProcessor.h"
#include "FreeRTOS.h"
#include "task.h"

#include "em_timer.h"
#include "em_cmu.h"

/* --------------------------------------------------------------------------- |
 * HANDLES ******************************************************************* |
 * -------------------------------------------------------------------------- */

static CmdProcessor_T * AppCmdProcessor;

/* --------------------------------------------------------------------------- |
 * EXECUTING FUNCTIONS ******************************************************* |
 * -------------------------------------------------------------------------- */

void TIMER0_IRQHandler(void) {
  TIMER_IntClear(TIMER0, TIMER_IF_OF);

  // Do something
  portYIELD_FROM_ISR(pdTRUE);
}

/* --------------------------------------------------------------------------- |
 * BOOTING- AND SETUP FUNCTIONS ********************************************** |
 * -------------------------------------------------------------------------- */

static void AppControllerEnable(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);

    TIMER_Init_TypeDef timerInit = TIMER_INIT_DEFAULT;
    timerInit.prescale = timerPrescale1024;
    timerInit.mode = timerModeUp;

    TIMER_IntEnable(TIMER0, TIMER_IF_OF);
    NVIC_EnableIRQ(TIMER0_IRQn);
    TIMER_TopSet(TIMER0, 10);
    TIMER_Init(TIMER0, &timerInit);
}

static void AppControllerSetup(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);
    Retcode_T retcode = RETCODE_OK;

    CMU_ClockEnable(cmuClock_TIMER0, true);

    retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerEnable, NULL, UINT32_C(0));

    if (RETCODE_OK != retcode){
        printf("AppControllerSetup : Failed \r\n");
        Retcode_RaiseError(retcode);
        assert(0);
    }

}

void AppController_Init(void * cmdProcessorHandle, uint32_t param2)
{
    BCDS_UNUSED(param2);

    Retcode_T retcode = RETCODE_OK;

    if (cmdProcessorHandle == NULL)
    {
        printf("AppController_Init : Command processor handle is NULL \r\n");
        retcode = RETCODE(RETCODE_SEVERITY_ERROR, RETCODE_NULL_POINTER);
    }
    else
    {
        AppCmdProcessor = (CmdProcessor_T *) cmdProcessorHandle;
        retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerSetup, NULL, UINT32_C(0));
    }

    if (RETCODE_OK != retcode)
    {
        Retcode_RaiseError(retcode);
        assert(0);
    }
}

Configuring the Timer for PWM usage

This chapter introduces how to use the PWM functionality of the timer module. This means that the output state of a pin, which can be controlled via the timer module, is changed during the interrupt events.

To do this, additional configuration needs to be applied to the pin and to the specific timer channel which controls that particular pin.

The code-snippet below shows which additional configuration need to be made.

GPIO_PinModeSet(gpioPortA, 0, gpioModePushPull, 0);
GPIO_PinOutClear(gpioPortA, 0);

TIMER_InitCC_TypeDef timerCCInit = {
    .eventCtrl  =   timerEventEveryEdge,
    .edge       =   timerEdgeBoth,
    .prsSel     =   timerPRSSELCh0,
    .cufoa      =   timerOutputActionNone,
    .cofoa      =   timerOutputActionNone,
    .cmoa       =   timerOutputActionToggle,
    .mode       =   timerCCModePWM,
    .filter     =   false,
    .prsInput   =   false,
    .coist      =   false,
    .outInvert  =   false
};

TIMER_InitCC(TIMER0, 0, &timerCCInit);

TIMER0->ROUTE |= (TIMER_ROUTE_CC0PEN | TIMER_ROUTE_LOCATION_LOC4);

As done in the previous sections, the pin needs to be configured via the em_gpio.h interface first, to act as an output with additional configuration such as push pull output. Afterwards, the timer channel itself needs to be configured. In this example, the GPIO pin PA0 on the extension bus is used.

To ensure that this pin is used, the route parameter is set to location four, which indicates that PA0, PC0 and PC1 can be used to drive three phased actuators via the extension bus. Furthermore, the parameter TIMER_ROUTE_CC0PEN, enables that PA0 is used by the timer module.

For the timer channel itself multiple configurations can be made. For this the parameters of the configuration struct TIMER_InitCC_TypeDef are used. The essential configuration parameter are eventCtrl, edge, cmoa and mode.

The parameter event control eventCtrl is used to configure when a change to the output pin should be applied. Here, it is configured to apply on every event edge. That means that the state is changed on every triggered timer interrupt. Thus, the parameter edge configures if the state change is applied to the rising or falling edge of the triggered interrupt signal.

The parameter cmoa stands for counter match output action and is configured to toggle the output pin. The parameter mode allows to configure the used mode. It can be configured for PWM, compare or capture. Here, it is configured for PWM.

Additionally, the same settings need to be made as in the previous chapter for the timer module itself.

The code-snippet below gives a short overview of this settings.

// Please add this code in appinitSystem() below BCDS_UNUSED(param2)
CMU_ClockEnable(cmuClock_TIMER0, true);

// Paste the content of the PWM pin configuration here

TIMER_TopSet(TIMER0, 48000);
TIMER_CompareBufSet(TIMER0, 0, 24000);

TIMER_Init_TypeDef timerInit = {
    .enable     = true,
    .debugRun   = true,
    .prescale   = timerPrescale1024,
    .clkSel     = timerClkSelHFPerClk,
    .fallAction = timerInputActionNone,
    .riseAction = timerInputActionNone,
    .mode       = timerModeUp,
    .dmaClrAct  = false,
    .quadModeX4 = false,
    .oneShot    = false,
    .sync       = false
};

TIMER_IntEnable(TIMER0, TIMER_IF_OF);
NVIC_EnableIRQ(TIMER0_IRQn);
TIMER_Init(TIMER0, &timerInit);

The configuration of the timer module is similar as in Code 16. The necessary configuration for PWM usage is to configure up or down counting in the TIMER_Init_TypeDef struct. The function TIMER_TopSet() is used to configure the period for PWM the function and TIMER_CompareBufSet() is used for the relative turn on time.

The first parameter of the function TIMER_CompareBufSet() indicates the timer module itself. The second parameter sets the timer channel and the last parameter the compare value, here 24000. This defines a duty cycle of 50 percent. Additionally, the function buffers the configured duty cycle and executes it after the next interrupt event occurs.

Please note that an interrupt handler as shown in code 17 is still needed. In that function, changes to the relative turn on time can be applied with the function TIMER_CompareBufSet() , as well. Additionally, please note that the timer module is also able to use two additional GPIO pins to drive 3 phase output actuators. For that, two additional channels on the route location 4 would need to be configured as shown in the previous code-snippet.

Timer PWM Full Code Example

Note: The full code example is intended for XDK-Workbench versions 3.4.0 and higher.

/* --------------------------------------------------------------------------- |
 * INCLUDES & DEFINES ******************************************************** |
 * -------------------------------------------------------------------------- */

/* own header files */
#include "XdkAppInfo.h"
#undef BCDS_MODULE_ID  /* Module ID define before including Basics package*/
#define BCDS_MODULE_ID XDK_APP_MODULE_ID_APP_CONTROLLER

/* own header files */
#include "AppController.h"

/* system header files */
#include <stdio.h>

/* additional interface header files */
#include "BCDS_CmdProcessor.h"
#include "FreeRTOS.h"
#include "task.h"

#include "em_timer.h"
#include "em_cmu.h"
#include "em_gpio.h"

/* --------------------------------------------------------------------------- |
 * HANDLES ******************************************************************* |
 * -------------------------------------------------------------------------- */

static CmdProcessor_T * AppCmdProcessor;

/* --------------------------------------------------------------------------- |
 * EXECUTING FUNCTIONS ******************************************************* |
 * -------------------------------------------------------------------------- */

void TIMER0_IRQHandler(void) {
    TIMER_IntClear(TIMER0, TIMER_IF_OF);

    // Do something
    portYIELD_FROM_ISR(pdTRUE);
}

/* --------------------------------------------------------------------------- |
 * BOOTING- AND SETUP FUNCTIONS ********************************************** |
 * -------------------------------------------------------------------------- */

static void AppControllerEnable(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);

    TIMER_Init_TypeDef timerInit = {
        .enable     = true,
        .debugRun   = true,
        .prescale   = timerPrescale1024,
        .clkSel     = timerClkSelHFPerClk,
        .fallAction = timerInputActionNone,
        .riseAction = timerInputActionNone,
        .mode       = timerModeUp,
        .dmaClrAct  = false,
        .quadModeX4 = false,
        .oneShot    = false,
        .sync       = false
    };

    TIMER_TopSet(TIMER0, 48000);
    TIMER_CompareBufSet(TIMER0, 0, 24000);

    TIMER_IntEnable(TIMER0, TIMER_IF_OF);
    NVIC_EnableIRQ(TIMER0_IRQn);
    TIMER_Init(TIMER0, &timerInit);

}

static void AppControllerSetup(void * param1, uint32_t param2)
{
    BCDS_UNUSED(param1);
    BCDS_UNUSED(param2);
    Retcode_T retcode = RETCODE_OK;

    GPIO_PinModeSet(gpioPortA, 0, gpioModePushPull, 0);
    GPIO_PinOutClear(gpioPortA, 0);

    TIMER_InitCC_TypeDef timerCCInit = {
        .eventCtrl  =   timerEventEveryEdge,
        .edge       =   timerEdgeBoth,
        .prsSel     =   timerPRSSELCh0,
        .cufoa      =   timerOutputActionNone,
        .cofoa      =   timerOutputActionNone,
        .cmoa       =   timerOutputActionToggle,
        .mode       =   timerCCModePWM,
        .filter     =   false,
        .prsInput   =   false,
        .coist      =   false,
        .outInvert  =   false
    };

    TIMER_InitCC(TIMER0, 0, &timerCCInit);
    TIMER0->ROUTE |= (TIMER_ROUTE_CC0PEN | TIMER_ROUTE_LOCATION_LOC4);

    CMU_ClockEnable(cmuClock_TIMER0, true);

    retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerEnable, NULL, UINT32_C(0));

    if (RETCODE_OK != retcode){
        printf("AppControllerSetup : Failed \r\n");
        Retcode_RaiseError(retcode);
        assert(0);
    }

}

void AppController_Init(void * cmdProcessorHandle, uint32_t param2)
{
    BCDS_UNUSED(param2);

    Retcode_T retcode = RETCODE_OK;

    if (cmdProcessorHandle == NULL)
    {
        printf("AppController_Init : Command processor handle is NULL \r\n");
        retcode = RETCODE(RETCODE_SEVERITY_ERROR, RETCODE_NULL_POINTER);
    }
    else
    {
        AppCmdProcessor = (CmdProcessor_T *) cmdProcessorHandle;
        retcode = CmdProcessor_Enqueue(AppCmdProcessor, AppControllerSetup, NULL, UINT32_C(0));
    }

    if (RETCODE_OK != retcode)
    {
        Retcode_RaiseError(retcode);
        assert(0);
    }
}