NetBurner 3.5.8
PDF Version
DWT Counter Timer

Example Path: examples/PlatformSpecific/SAME70/Timers/DwtCounterTimer

Location: examples/PlatformSpecific/SAME70/Timers/DwtCounterTimer

Supported Platforms: MODM7AE70 SBE70LC

DwtCounterTimer - Cortex-M7 DWT cycle-counter timing example

OVERVIEW

This example demonstrates a strictly monotonic, high-resolution stopwatch built on the Cortex-M7 Data Watchpoint & Trace (DWT) cycle counter (DWT->CYCCNT).

It is intended as a drop-in alternative to the SDK StopWatch class for timing very short, timing-critical operations – on the SAME70 only (see the platform limitation below).

PLATFORM LIMITATION – SAME70 ONLY (MODM7AE70, SBE70LC)

DWT->CYCCNT counts CORE CLOCK cycles – it advances only while the CPU is being clocked. The NetBurner RTOS idle task issues WFI whenever no task is ready to run (see arch/cortex-m7/source/nbrtosmain.cpp).

  • On the i.MX RT10xx (SOMRT1061) and RT11xx (MODRT1171), that idle WFI gates the core clock (low-power WAIT/STOP). Because CYCCNT lives in the core clock domain, it FREEZES for the entire idle period and never counts the time spent asleep. In a running system the idle task runs constantly between task time-slices, so any DWT interval that is not one tight, uninterrupted code path silently loses all of the idle/sleep time – the measurement is wrong. DWT is therefore NOT safe for timing on these parts; use the SDK StopWatch instead (it is reconstructed from the always-running system tick and is unaffected by sleep).
  • On the SAME70 (MODM7AE70, SBE70LC) the cycle counter keeps running through the idle WFI, so DWT stays reliable.

That is why this example builds for the two SAME70 boards only (enforced by SUPPORTED_PLATFORMS in the makefile). The MCF5441X Coldfire parts are excluded for a different reason – they are not Cortex-M7 and have no DWT at all.

WHY DWT INSTEAD OF StopWatch

On the M7 parts the SDK StopWatch does NOT read a single free-running counter. Its timestamp is reconstructed from the 1 ms system tick plus the SysTick down-counter, with a couple of rollover/pending-tick correction steps. Those corrections can, very occasionally, make the "now" value read at Stop() come out a hair BELOW the value captured at Start(). Stop() then performs an unsigned 64-bit subtraction (now - start), so that tiny backward step wraps to a number just under 2^64 (~6.1e13 ms). It is rare, intermittent, and is most easily triggered when the timed region straddles a system-tick boundary.

DWT->CYCCNT is a single free-running 32-bit register clocked at the CPU clock. One read, no reconstruction, no corrections – so it can never step backward and can never produce that 2^64 outlier.

RESOLUTION IS THE SAME – MAXIMUM INTERVAL IS NOT

A common misconception is that DWT is "higher resolution" than StopWatch. It is not: on the M7 parts BOTH resolve to a single CPU cycle. The real – and large – difference is the maximum single interval each can measure before its counter wraps. The trade-off is range vs. robustness, not resolution.

Why they wrap at different points – both ultimately resolve to CPU cycles, but each is bounded by a 32-bit counter; the difference is the UNIT that 32-bit counter ticks in:

DWT - DWT->CYCCNT is a 32-bit register that ticks once per CPU cycle,
so it wraps after 2^32 CYCLES.
StopWatch - its coarse counter is the system TimeTick (a 32-bit millisecond
counter). StopWatch::GetNow() reconstructs cycles as
TimeTick * (cycles-per-ms) + (within-ms SysTick offset), so it
is correct only across one TimeTick wrap -- a limit of 2^32
MILLISECONDS.
(see arch/cortex-m7/cpu/SAME70/source/stopwatch.cpp)

So the ratio of the two maximum intervals is exactly cycles-per-ms = CPU_CLOCK / 1000 = 300,000x at the SAME70's 300 MHz.

The numbers (SAME70 @ 300 MHz):

Method Counter that wraps Tick unit Max single interval
--------- ------------------- -------------- -------------------------
DWT 32-bit CYCCNT 1 CPU cycle 2^32 / 300 MHz = ~14.32 s
(3.333 ns)
StopWatch 32-bit TimeTick 1 ms 2^32 ms = ~49.7 days
High-resolution stopwatch timer for precise event timing and performance measurement.
Definition stopwatch.h:138
  • StopWatch's ~49.7 day limit is CLOCK-INDEPENDENT, because TimeTick counts milliseconds.
  • DWT's window shrinks as the clock rises (it is set by 2^32 / CPU_CLOCK). On the SAME70 that is ~14.32 s @ 300 MHz. The higher-clocked RT parts would give shorter windows (~8.13 s @ 528 MHz, ~5.37 s @ 800 MHz) – but DWT is not usable there anyway; see PLATFORM LIMITATION.

The catch (the whole point of this example):

  • Resolution is identical. StopWatch::CountResolution() returns 1 / CPU_CLOCK – one CPU cycle, the same 3.333 ns as DWT. StopWatch is NOT coarser; it reconstructs down to the cycle.
  • DWT trades range for robustness. StopWatch buys its huge range (49.7 days) by RECONSTRUCTING the cycle count from two registers (TimeTick + SysTick->VAL) with rollover / pending-tick corrections. Those corrections are what can occasionally step backward and produce the ~2^64 outlier. DWT does a single register read – no reconstruction – so it cannot step backward, but it pays with the ~14.3 s wrap window.

Bottom line: for the sub-millisecond, timing-critical work this example targets, the ~14.3 s DWT window is plenty and you get guaranteed-monotonic single reads. If you genuinely need to time something lasting seconds-to-days in one shot, StopWatch's range is the reason it exists – you would just have to tolerate the rare reconstruction artifact.

RESOLUTION AND MAXIMUM MEASURABLE TIME

The counter increments once per CPU clock cycle, so:

resolution = 1 / CPU_CLOCK (one CPU cycle)
max single = 2^32 / CPU_CLOCK (the 32-bit counter's wrap period)
interval

"Max single interval" is how long ONE Start..Stop region may last and still be measured correctly. The elapsed value is computed as an unsigned subtraction (now - start), which is correct across at most one 32-bit wrap. Keep any single timed interval shorter than the wrap period below, and do not store the absolute counter value across a wrap.

All timing math in this example divides by the SDK's live CPU_CLOCK global, so the numbers are produced automatically and stay correct even if the clock is changed at runtime (e.g. via SetPLL / SetMCKDivider).

SUPPORTED PLATFORMS

This example builds for the SAME70 boards only:

Platform CPU Clock Resolution Max single
(per count) interval (32-bit wrap)
----------- ------------------------ -------- ------------- ----------------------
MODM7AE70 Microchip SAME70 (M7) 300 MHz 3.333 ns ~14.317 s
SBE70LC Microchip SAME70 (M7) 300 MHz 3.333 ns ~14.317 s

The makefile's SUPPORTED_PLATFORMS list restricts the build to these two boards; the NetBurner build system declines to build (with a warning) for any other platform.

Excluded parts:

  • SOMRT1061 (i.MX RT1061) and MODRT1171 (i.MX RT1171): the DWT cycle counter exists, but it freezes during the RTOS idle WFI – see PLATFORM LIMITATION.
  • MOD5441X and other MCF5441X Coldfire parts: not Cortex-M7, no DWT at all.

The code reads the actual CPU_CLOCK at run time, so the printed resolution / wrap reflect the real configured clock.

WHAT THE PROGRAM DOES

  1. Initializes the system, enables diagnostics, waits for the network.
  2. Enables the cycle counter (DEMCR.TRCENA, zero CYCCNT, CTRL.CYCCNTENA).
  3. Prints CPU_CLOCK, the resolution (ns/count), and the 32-bit wrap time.
  4. Runs a startup sanity check that DWT->CYCCNT is actually incrementing (this is the runtime confirmation that the cycle counter is implemented and enabled on the specific silicon – CYCCNT is architecturally optional).
  5. One-shot: times a 10,000-iteration busy loop with the CycleTimer class.
  6. Continuous: repeatedly times a placeholder operation and prints ONLY when a new maximum is seen – the same "print on new max" pattern that exposed the StopWatch 2^64 wrap. With DWT the reported maximum stays sane.

Replace the marked placeholder loop in main.cpp with the operation you actually want to time.

KEY CODE (CycleTimer)

extern uint32_t CPU_CLOCK; // SDK global: CPU clock in Hz
// one-time enable:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
// measure:
uint32_t start = DWT->CYCCNT;
// ... operation ...
uint32_t cycles = DWT->CYCCNT - start; // unsigned wrap is correct
double usec = (double)cycles * 1e6 / CPU_CLOCK;

SAMPLE OUTPUT (MODM7AE70)

DWT Counter Timer Example
CPU_CLOCK = 300000000 Hz
DWT resolution = 3.333 ns/count
32-bit wrap = 14.317 s
DWT counter running (12345 -> 12362)
Busy loop of 10000 counts: 30012 cycles = 100.040 us
New max: 412 cycles = 1.373 us (0.001373 ms) [iter 0]
New max: 631 cycles = 2.103 us (0.002103 ms) [iter 4]
...

CAVEATS

  • CYCCNT is architecturally optional on ARMv7-M; the DWT_CTRL.NOCYCCNT bit reports if it is absent. On all four NetBurner M7 parts CYCCNT is implemented (though on the RT parts it is unusable for timing because the idle WFI freezes it – see PLATFORM LIMITATION). The startup "is it counting?" check is the practical confirmation – if it ever prints the "did not advance" warning, the counter was not enabled on that silicon.
  • StopWatch is unaffected for normal millisecond-and-up timing; DWT is the recommendation specifically for sub-millisecond, timing-critical work on the SAME70 where the rare backward-step artifact matters.