9 min read

Infineon STM

The Infineon AURIX TriCore family of microprocessors equip a very useful system timer peripheral function module called the System Timer Module or STM (Infineon 2024) for short. The STM peripheral is a general-purpose timer that can be used for various timing and counting applications, such as measuring time intervals, generating periodic interrupts, and implementing delay functions.

Quite useful! Especially so in automotive applications where precise timing is a crucial requirement.

STM Peripheral Model

The TC3xx series has a single STM peripheral with three timer blocks, each with a 64-bit free-running up-counter. See Figure 1 below for a visual representation of the STM peripheral on the TC37x.

The STM peripheral model for a TC37x consists of three timer units. Each timer unit has a 64-bit free-running up-counter register that increments at a fixed frequency. The STM peripheral naturally includes control and status registers that allow configuration of the timer blocks and monitoring of their status.

Figure 1: The STM peripheral model for a TC37x consists of three timer units. Each timer unit has a 64-bit free-running up-counter register that increments at a fixed frequency. The STM peripheral naturally includes control and status registers that allow configuration of the timer blocks and monitoring of their status.

Key components:

  • Timer Registers, TIM0-TIM6: different views of the 64-bit counter
  • Compare and Capture: CMP0 and CMP1 compare registers, CAP capture register, CMCON control
  • Interrupts: ICR (control) and ISCR (status and clear)
  • Second View: TIM0SV and CAPSV for atomic reads
  • Control and Debug: clock control, reset, OCDS, and access enable registers

Architecture

In short, the peripheral comprises three STM instances (STM0, STM1, STM2) at base addresses \(F0001000_{16}\), \(F0001100_{16}\), and \(F0001200_{16}\), respectively, each with a 64-bit free-running counter and dual compare channels.

The diagram in Figure 1 shows a more-or-less complete register map with offsets for all registers, including register type definitions used by the Infineon low-level drivers (e.g., Ifx_STM_CLC, Ifx_STM_TIM0) and their corresponding offsets for each STM instance. The diagram also sorts the registers by function group, such as the compare-capture registers CMP0,1 and CAP; the interrupt control-status registers ICR and ISCR; and the control-debug registers (clock control, reset, OCDS1, access enable).

Low-Level Driver API

Figure 2 depicts the “class” diagram for the Infineon low-level driver API for the STM peripheral. I place the word class in quotes because the low-level drivers implement a C-style functional interface—so not strictly object-oriented in a mechanical sense. C is a procedural language: functions operating on data. C can, however, represent objects conceptually, and the Infineon low-level drivers do just so using structures to encapsulate register maps and functions to operate as if methods on the peripheral bus, following the common ‘objectesque’ design pattern in C programming.

Infineon's IfxStm utility class

Figure 2: Infineon’s IfxStm utility class

Infineon’s API effectively wraps the STM hardware model into a single monolith with multiple offset functions, one for each TIM register.

Clocks and Timers

Such a timing peripheral is ideal for measuring time at high resolution and simultaneously over very long periods, or indefinitely for all practical purposes. Clocked at \(300\) MHz, the STM counter increments every \(3.33\) nanoseconds, so it can measure very short durations with high precision; two back-to-back timer reads will never give the same sample, assuming that the counter does not halt in debug mode and that reads bypass the cache. The width of the 64-bit counter allows it to measure long durations without overflow, making it suitable for applications that require long-term timing.

\[ 300 \text{ MHz} = 300 \times 10^6 \text{ ticks per second} \\ \text{Tick period} = \frac{1}{300 \times 10^6 \text{ seconds per tick}} \approx 3.33 \text{ nanoseconds per tick} \\ \text{Time to overflow} \approx 2^{64} \text{ ticks} \times 3.33 \text{ nanoseconds per tick} \approx 1,949 \text{ years} \]

64 bits incremented every \(3.33\) nanoseconds means the counter will wrap around after approximately \(1,949\) years, which is more than sufficient for most, if not all, applications — more than a human lifetime, and even more than the expected lifetime of a typical device itself.

One could imagine a clock abstraction.

“Come with me and you’ll be, In a world of pure imagination.”—Willy Wonka, Charlie and the Chocolate Factory

Imagine a hierarchy of 64-bit abstract clocks driven by an STM block.

struct clock64_impl;

struct clock64 {
  struct cons_node node;
  const struct clock64_impl *impl;
  void (*tick)(struct clock64 *clock);
  uint64_t ticks;
};

struct clock64_impl {
  uint64_t (*now)(void);          /*!< Function to latch the current time. */
  uint64_t (*ticks_per_us)(void); /*!< Function to access the number of ticks per microsecond. */
};

/*
 * Clock API with the following functions:
 *
 * - clock64_src: Get the source clock of a given clock.
 * - clock64_select: Select a source clock for a given clock.
 *
 * - clock64_now: Get the current time in ticks from a given clock.
 * - clock64_now_us: Get the current time in microseconds from a given clock.
 * - clock64_now_ms: Get the current time in milliseconds from a given clock.
 * - clock64_ticks_per_us: Get the number of ticks per microsecond.
 *
 * - clock64_ticks: Get the number of ticks since the last synchronisation.
 * - clock64_ticks_us: Get the number of microseconds since the last synchronisation.
 * - clock64_ticks_ms: Get the number of milliseconds since the last synchronisation.
 *
 * - clock64_tick: Register a tick function to be called on each tick of the clock.
 * - clock64_sync: Synchronise the clock with its source.
 */

struct clock64 *clock64_src(struct clock64 *clock);
void clock64_select(struct clock64 *clock, struct clock64 *src);

uint64_t clock64_now(struct clock64 *clock);
uint64_t clock64_now_us(struct clock64 *clock);
uint64_t clock64_now_ms(struct clock64 *clock);
uint64_t clock64_ticks_per_us(struct clock64 *clock);

uint64_t clock64_ticks(struct clock64 *clock);
uint64_t clock64_ticks_us(struct clock64 *clock);
uint64_t clock64_ticks_ms(struct clock64 *clock);

void clock64_tick(struct clock64 *clock, void (*tick)(struct clock64 *clock));
void clock64_sync(struct clock64 *clock);

Every clock has a source clock, except for the root clock, which is the STM itself. The cons_node structure encapsulates the tree construct; the details of which are beyond the scope of this article. Each clock synchronises to its source clock, and a tick function can be registered to be called on each tick of the clock.

A “tick of the clock” occurs when a clock synchronisation sees a change in the source clock’s current ticks. Synchronisation runs cyclically for deterministic applications, or otherwise periodically for more non-deterministic application designs, when a core has nothing else to do; the tick function will be called whenever the source clock ticks. Ideal for a timer peripheral!

The complete interface appears on GitHub at clock64.h and its implementation at clock64.c.

STM-based 64-bit clock

Connecting the abstract clock64 to the hardware becomes simple: just define a root clock with hardware implementations for “now” and “ticks per microsecond.”

The following code snippet defines three stm{0,1,2}_tim_impl structures that implement the clock64_impl interface for the three STM peripheral blocks. The stm{0,1,2}_tim_now functions retrieve the current time from the STM counters, and the stm_tim_ticks_per_us function returns the number of ticks per microsecond based on the STM’s clock frequency.

#include <stm_clock64.h>

/*
 * for Infineon System Timer Module (STM) Low-Level (iLLD) Drivers
 */
#include "IfxStm.h"

/*!
 * \brief The number of ticks per microsecond for the STM timers.
 * \details This value is based on the clock frequency of the STM timers
 * and is used to convert the raw tick counts from the timers into
 * microseconds. The STM timers typically run at a frequency of 300 MHz,
 * which results in 300 ticks per microsecond. This constant is used in
 * the implementation of the clock64 functions to provide accurate time
 * measurements based on the STM timers. It is important to ensure that
 * this value matches the actual clock frequency of the STM timers being
 * used, as an incorrect value could lead to inaccurate time
 * measurements.
 */
#define STM_TIM_TICKS_PER_US 300ULL

static uint64_t stm0_tim_now(void) { return IfxStm_get(&MODULE_STM0); }
static uint64_t stm1_tim_now(void) { return IfxStm_get(&MODULE_STM1); }
static uint64_t stm2_tim_now(void) { return IfxStm_get(&MODULE_STM2); }

static uint64_t stm_tim_ticks_per_us(void) { return STM_TIM_TICKS_PER_US; }

static const struct clock64_impl stm0_tim_impl = {.now = stm0_tim_now, .ticks_per_us = stm_tim_ticks_per_us};
static const struct clock64_impl stm1_tim_impl = {.now = stm1_tim_now, .ticks_per_us = stm_tim_ticks_per_us};
static const struct clock64_impl stm2_tim_impl = {.now = stm2_tim_now, .ticks_per_us = stm_tim_ticks_per_us};

static struct clock64 stm0_tim = {.impl = &stm0_tim_impl};
static struct clock64 stm1_tim = {.impl = &stm1_tim_impl};
static struct clock64 stm2_tim = {.impl = &stm2_tim_impl};

struct clock64 *STM0_TIM_clock64(void) { return &stm0_tim; }

struct clock64 *STM1_TIM_clock64(void) { return &stm1_tim; }

struct clock64 *STM2_TIM_clock64(void) { return &stm2_tim; }

The IfxStm_get() function performs the atomic read: capturing the timer’s high word when reading the low word. Why is this important? Because the STM counter is a 64-bit free-running up-counter, reading it requires an atomic operation to ensure that the value is consistent and not affected by concurrent updates. Reading the timer does not stop the counter, so if the counter increments between reading the low and high parts, it could lead to an incorrect 64-bit counter value if the increment overflows the first word. The IfxStm_get() function ensures that the high and low parts of the counter are read atomically, preventing a race condition that exists when the core reads and the counter simultaneously increments.

The core’s main loop only needs to synchronise the clock. It will synchronise the root clock and its entire tree of sub-clocks.

  while (1) {
    clock64_sync(STM0_TIM_clock64());
  }

Having three separate timers allows separating the usage for different cores, e.g. one STM root clock for each core on a three-core TriCore.

Future Work

One could imagine a clock divisor: a special kind of clock that scales down a clock by some prescribed divider. This might not be necessary in 64 bits, since thousands of years is a pretty long time to worry about overflow. But a 32-bit variant could be useful for applications that require shorter time intervals and want to save memory and computation time.

There could be a clock with an expiry time, also known as a “timer.” It acts as a clock that ticks until it reaches a specified expiry time, at which point it stops ticking and can trigger an event or callback. This would be useful for implementing timeouts and delays.

In summary, the Infineon STM peripheral provides a powerful and flexible timing solution for automotive applications. By abstracting it into a clock64 interface, the developer can create a versatile timing system that can be easily integrated into various applications, allowing for precise time measurement and event scheduling.

“Living there you’ll be free, If you truly wish to be.”—Wonka

Infineon. 2024. “AURIX TC3xx System Timer (STM).” 2024. https://documentation.infineon.com/aurixtc3xx/docs/wvt1713281569556.

  1. On-Chip Debug Support. In AURIX microcontrollers, OCDS is a hardware feature that provides low‑level debug access. It allows tools (debuggers and tracers) to interact with the microcontroller while it runs.↩︎