Example Path: examples/timers/ArmM7_DwtCounterTimer
ArmM7_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.
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 300 MHz.
The numbers (MODM7AE70 @ 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 – it is ~49.7 days on the RT parts too.
- DWT's window SHRINKS as the clock rises: 14.32 s @ 300 MHz, 8.13 s @ 528 MHz (SOMRT1061), 5.37 s @ 800 MHz (MODRT1171).
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 below are produced automatically and stay correct even if the clock is changed at runtime (e.g. via SetPLL / SetMCKDivider).
SUPPORTED PLATFORMS
DWT/CYCCNT is a Cortex-M7 core feature, so this example runs on every M7-based NetBurner module. It is NOT available on the Coldfire (MCF5441X) parts, and the makefile rejects those (and any other non-listed platform) with a clear error before compiling.
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
SOMRT1061 NXP i.MX RT1061 (M7) 528 MHz 1.894 ns ~8.134 s
MODRT1171 NXP i.MX RT1171 (M7) 800 MHz 1.250 ns ~5.369 s
Notes:
- Higher clock = finer resolution but a SHORTER wrap window. Even the shortest (~5.4 s on MODRT1171) is far longer than the microsecond-scale intervals this example is meant to time.
- Clock rates above are the standard NetBurner operating clocks; the code reads the actual CPU_CLOCK at run time, so the printed resolution / wrap reflect the real configured clock.
WHAT THE PROGRAM DOES
- Initializes the system, enables diagnostics, waits for the network.
- Enables the cycle counter (DEMCR.TRCENA, zero CYCCNT, CTRL.CYCCNTENA).
- Prints CPU_CLOCK, the resolution (ns/count), and the 32-bit wrap time.
- 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).
- One-shot: times a 10,000-iteration busy loop with the CycleTimer class.
- 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;
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
uint32_t start = DWT->CYCCNT;
uint32_t cycles = DWT->CYCCNT - start;
double usec = (double)cycles * 1e6 / CPU_CLOCK;
SAMPLE OUTPUT (MODM7AE70)
ARM M7 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 it is implemented. 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 (a few M7 implementations gate DWT behind a lock-access register; these NetBurner parts do not).
- StopWatch is unaffected for normal millisecond-and-up timing; DWT is the recommendation specifically for sub-millisecond, timing-critical work where the rare backward-step artifact matters.