Skip to content

Commit 17be03c

Browse files
committed
Add clocks module to HostTests
1 parent 505352e commit 17be03c

File tree

3 files changed

+386
-4
lines changed

3 files changed

+386
-4
lines changed

tests/HostTests/app/modules.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
XX(json5) \
55
XX(json6) \
66
XX(files) \
7-
XX(rational)
7+
XX(rational) \
8+
XX(clocks)

tests/HostTests/app/test-clocks.cpp

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
/*
2+
* Tests optimised time conversion code use for hardware timer.
3+
* We do this by evaluating various time values and comparing against floating-point
4+
* calculation.
5+
*/
6+
7+
#include "common.h"
8+
#include <Platform/Timers.h>
9+
10+
template <class Clock, typename TimeType> class ClockTestTemplate : public TestGroup
11+
{
12+
public:
13+
using Micros = NanoTime::TimeSource<Clock, NanoTime::Microseconds, TimeType>;
14+
15+
ClockTestTemplate()
16+
: TestGroup(F("TimeSource: ") + Micros::toString()), refCycles("Reference cycles"),
17+
calcCycles("Calculation cycles")
18+
{
19+
#if DEBUG_VERBOSE_LEVEL == DBG
20+
verbose = true;
21+
#endif
22+
}
23+
24+
void execute() override
25+
{
26+
if(Clock::frequency() == 160000000) {
27+
System.setCpuFrequency(eCF_160MHz);
28+
} else {
29+
System.setCpuFrequency(eCF_80MHz);
30+
}
31+
32+
printLimits();
33+
34+
for(unsigned i = 0; i < 2000; ++i) {
35+
auto value = os_random();
36+
check<NanoTime::Milliseconds>(value);
37+
check<NanoTime::Microseconds>(value);
38+
check<NanoTime::Nanoseconds>(value);
39+
}
40+
41+
printStats();
42+
}
43+
44+
template <NanoTime::Unit unit> void check(TimeType value)
45+
{
46+
using TimeSource = NanoTime::TimeSource<Clock, unit, TimeType>;
47+
48+
// Serial.print("HwTimer::maxTime = ");
49+
// Serial.println(HwTimer::maxTime());
50+
51+
this->timeunit = unit;
52+
53+
noInterrupts();
54+
55+
valueIsTime = true;
56+
this->value = value % (TimeSource::maxCalcTime() + 1);
57+
refCycles.start();
58+
ref = timeToTicksRef();
59+
refCycles.update();
60+
61+
calcCycles.start();
62+
calc = TimeSource::timeToTicks(this->value);
63+
calcCycles.update();
64+
65+
if(calc != ref) {
66+
calc = TimeSource::timeToTicks(this->value);
67+
}
68+
69+
compare();
70+
71+
valueIsTime = false;
72+
this->value = value % (TimeSource::maxCalcTicks() + 1);
73+
refCycles.start();
74+
ref = ticksToTimeRef();
75+
refCycles.update();
76+
77+
calcCycles.start();
78+
calc = TimeSource::ticksToTime(this->value);
79+
calcCycles.update();
80+
compare();
81+
82+
interrupts();
83+
}
84+
85+
void printStats()
86+
{
87+
Serial.println(refCycles);
88+
Serial.println(calcCycles);
89+
}
90+
91+
template <NanoTime::Unit unit> void printMaxTicks(bool isTimeTicks)
92+
{
93+
NanoTime::TimeSource<Clock, unit, TimeType> src;
94+
timeunit = unit;
95+
TimeType maxTicks;
96+
maxTicks = isTimeTicks ? src.maxCalcTicks() : src.maxTicks();
97+
TimeType maxTime = isTimeTicks ? src.ticksToTime(maxTicks) : src.maxClockTime().time();
98+
99+
Serial.print(" ");
100+
Serial.print(maxTicks);
101+
Serial.print(" ticks = ");
102+
Serial.print(NanoTime::time(timeunit, maxTime));
103+
Serial.print(" = ");
104+
Serial.println(NanoTime::TimeValue(timeunit, maxTime).toString());
105+
};
106+
107+
template <NanoTime::Unit unit> void printMaxTime()
108+
{
109+
NanoTime::TimeSource<Clock, unit, TimeType> source;
110+
timeunit = unit;
111+
auto maxTime = source.maxCalcTime();
112+
auto maxTicks = source.timeToTicks(maxTime);
113+
114+
Serial.print(" ");
115+
Serial.print(maxTime.toString());
116+
Serial.print(" = ");
117+
Serial.print(maxTicks);
118+
Serial.print(" ticks = ");
119+
Serial.println(maxTicks);
120+
};
121+
122+
void printLimits()
123+
{
124+
m_puts("Limits:\r\n");
125+
126+
m_puts(" clock ticks:\r\n");
127+
valueIsTime = true;
128+
auto pmt = [this](bool isTimeTicks) {
129+
// printMaxTicks<NanoTime::Days>(isTimeTicks);
130+
// printMaxTicks<NanoTime::Hours>(isTimeTicks);
131+
// printMaxTicks<NanoTime::Minutes>(isTimeTicks);
132+
// printMaxTicks<NanoTime::Seconds>(isTimeTicks);
133+
printMaxTicks<NanoTime::Milliseconds>(isTimeTicks);
134+
printMaxTicks<NanoTime::Microseconds>(isTimeTicks);
135+
printMaxTicks<NanoTime::Nanoseconds>(isTimeTicks);
136+
};
137+
pmt(false);
138+
139+
m_puts(" ticks -> time:\r\n");
140+
pmt(true);
141+
142+
m_puts(" time -> ticks:\r\n");
143+
valueIsTime = true;
144+
145+
// printMaxTime<NanoTime::Days>();
146+
// printMaxTime<NanoTime::Hours>();
147+
// printMaxTime<NanoTime::Minutes>();
148+
// printMaxTime<NanoTime::Seconds>();
149+
printMaxTime<NanoTime::Milliseconds>();
150+
printMaxTime<NanoTime::Microseconds>();
151+
printMaxTime<NanoTime::Nanoseconds>();
152+
}
153+
154+
private:
155+
const char* get_tag(bool is_time)
156+
{
157+
return is_time ? "time" : "ticks";
158+
}
159+
160+
void print()
161+
{
162+
String value_tag = get_tag(valueIsTime);
163+
String result_tag = get_tag(!valueIsTime);
164+
auto diff = calc - ref;
165+
166+
Serial.print(" ");
167+
Serial.print(value_tag);
168+
Serial.print(": ");
169+
Serial.print(value);
170+
Serial.print(" (");
171+
Serial.print(unitToString(timeunit));
172+
Serial.print("), ref ");
173+
Serial.print(result_tag);
174+
Serial.print(": ");
175+
Serial.print(ref);
176+
Serial.print(", calc ");
177+
Serial.print(result_tag);
178+
Serial.print(": ");
179+
Serial.print(calc);
180+
Serial.print(", diff: ");
181+
Serial.println(diff);
182+
}
183+
184+
void compare()
185+
{
186+
// Tolerate a +/- 1 in result to account for FP rounding
187+
int64_t diff = calc - ref;
188+
bool ok = abs(diff) == 0; //1;
189+
if(verbose || calc != ref) {
190+
print();
191+
}
192+
TEST_ASSERT(ok);
193+
};
194+
195+
#define USE_FP_CALC
196+
197+
uint64_t timeToTicksRef()
198+
{
199+
auto unitTicks = NanoTime::unitTicks[timeunit];
200+
#ifdef USE_FP_CALC
201+
return round(double(value) * Clock::frequency() * unitTicks.den / unitTicks.num);
202+
#else
203+
return value / Ratio64 r(unitTicks.num, Clock::frequency() * unitTicks.den);
204+
#endif
205+
}
206+
207+
uint64_t ticksToTimeRef()
208+
{
209+
auto unitTicks = NanoTime::unitTicks[timeunit];
210+
#ifdef USE_FP_CALC
211+
return round(double(value) * unitTicks.num / (Clock::frequency() * unitTicks.den));
212+
#else
213+
return value * Ratio64 r(unitTicks.num, Clock::frequency() * unitTicks.den);
214+
#endif
215+
}
216+
217+
private:
218+
NanoTime::Unit timeunit = NanoTime::Seconds;
219+
TimeType value = 0; // ticks or time as input to calculation
220+
bool valueIsTime = false;
221+
uint64_t ref = 0; // Reference result
222+
TimeType calc = 0; // Calculated result
223+
bool verbose = false;
224+
CycleTimes refCycles;
225+
CycleTimes calcCycles;
226+
};
227+
228+
template <hw_timer_clkdiv_t clkdiv, NanoTime::Unit unit, typename TimeType>
229+
struct Timer1TestSource : public Timer1Clock<clkdiv> {
230+
static TimeType timeToTicks_test1(const TimeType& time)
231+
{
232+
/*
233+
* Refactorise to eliminate overflow when scaling down and avoid division by
234+
* using pre-defined constant values.
235+
*
236+
* Original code:
237+
*
238+
* if(us > 0x35A)
239+
* return (us / 4) * (frequency / 250000) + (us % 4) * (frequency / 1000000);
240+
*
241+
* This only works for /16 prescale. It's also un-necessary since the ratio reduces to 5:1,
242+
* i.e. it's just a x5 multiplication.
243+
*
244+
* However, with /256 prescale it's a little tricker. This code is used in the
245+
* `ets_timer_arm_new` function for converting milliseconds into ticks:
246+
*
247+
* if(ms > 13743)
248+
* return (ms / 4) * (frequency / 250) + (ms % 4) * (frequency / 1000)
249+
*
250+
* In this case the ratio is 625:2 which limits the range to 1'54"31.947, but this
251+
* calculation offers an improvement to 3'49"25.92. It is probably slightly faster
252+
* as well.
253+
*
254+
* Converting from microseconds the ratio is 16:5.
255+
*
256+
* The advantage of muldiv is that it is generic and will work with any ratio.
257+
* In most cases it's just as fast, offers overflow detection and a greater range by
258+
* using 64-bit calculations when necessary.
259+
*
260+
* Ideally all time conversions should be pre-calculated so that time-critical code
261+
* (e.g. within ISRs) operates using the timer tick values.
262+
*
263+
*/
264+
constexpr uint32_t prediv = 4;
265+
constexpr auto unitTicks = NanoTime::unitTicks[unit];
266+
constexpr auto frequency = Timer1TestSource::frequency();
267+
constexpr uint32_t mul = frequency * unitTicks.den / (unitTicks.num / prediv);
268+
constexpr uint32_t div = frequency * unitTicks.den / unitTicks.num;
269+
270+
if(clkdiv == TIMER_CLKDIV_16) {
271+
// debug_i("prediv = %u, frequency = %u, mul = %u, div = %u", prediv, frequency, mul, div);
272+
return (time / prediv) * mul + (time % prediv) * div;
273+
} else {
274+
using R = std::ratio<frequency * unitTicks.den, unitTicks.num>;
275+
return muldiv<R::num, R::den>(time);
276+
}
277+
}
278+
279+
TimeType timeToTicks_test2(const TimeType& time)
280+
{
281+
/*
282+
* Evaluate performance using full muldiv64 all the time.
283+
* In practice, this is un-necessary and the implemented solution is to
284+
* only use it when an overflow would occur.
285+
*/
286+
return uint64_t(time) * Timer1TestSource::ticksPerUnit(unit); // 116
287+
// return muldiv(time, uint32_t(TPU::num), uint32_t(TPU::den)); // 47
288+
}
289+
};
290+
291+
template <hw_timer_clkdiv_t clkdiv, typename TimeType> void testTimer1()
292+
{
293+
using Clock = Timer1TestSource<clkdiv, NanoTime::Microseconds, uint32_t>;
294+
using TimeSource = NanoTime::TimeSource<Clock, NanoTime::Microseconds, TimeType>;
295+
Clock timer1;
296+
297+
// Timer1TestSource<NanoTime::Microseconds, uint32_t, TIMER_CLKDIV_16> timer1;
298+
299+
CycleTimes m1("ticks"), m2("ticks1"), m3("ticks2");
300+
301+
for(unsigned i = 0; i < 5000; ++i) {
302+
TimeType time = os_random(); //0x7fffffff;
303+
if(sizeof(time) == sizeof(uint64_t)) {
304+
// time = (time << 32) | os_random();
305+
}
306+
307+
time %= TimeSource::maxCalcTime();
308+
309+
m1.start();
310+
volatile TimeType ticks = TimeSource::timeToTicks(time);
311+
m1.update();
312+
313+
m2.start();
314+
volatile TimeType ticks1 = timer1.timeToTicks_test1(time);
315+
m2.update();
316+
317+
m3.start();
318+
volatile TimeType ticks2 = timer1.timeToTicks_test2(time);
319+
m3.update();
320+
321+
TimeType refticks = round(double(time) * TimeSource::TicksPerUnit::num / TimeSource::TicksPerUnit::den);
322+
323+
// uint64_t refticks = timer1.timeToTicksRef(time);
324+
325+
auto check = [time, refticks](const char* tag, TimeType ticks) {
326+
if(refticks != ticks) {
327+
int64_t diff = int64_t(ticks) - int64_t(refticks);
328+
329+
Serial.print("time = ");
330+
Serial.print(time);
331+
Serial.print(", refticks = ");
332+
Serial.print(refticks);
333+
Serial.print(", ");
334+
Serial.print(tag);
335+
Serial.print(" = ");
336+
Serial.print(ticks);
337+
Serial.print(", diff = ");
338+
Serial.println(diff);
339+
}
340+
};
341+
342+
check("ticks", ticks);
343+
check("ticks1", ticks1);
344+
check("ticks2", ticks2);
345+
346+
// if(refticks != ticks || refticks != ticks1 || refticks != ticks2) {
347+
// m_printf("time = %u (0x%08x), ticks = %u, ticks1 = %u, ticks2 = %u\r\n", time, time, ticks, ticks1, ticks2);
348+
// }
349+
}
350+
351+
Serial.println(m1);
352+
Serial.println(m2);
353+
Serial.println(m3);
354+
}
355+
356+
class TimerCalcTest : public TestGroup
357+
{
358+
public:
359+
TimerCalcTest() : TestGroup(_F("Timer calculations"))
360+
{
361+
}
362+
363+
void execute() override
364+
{
365+
testTimer1<TIMER_CLKDIV_16, uint32_t>();
366+
testTimer1<TIMER_CLKDIV_256, uint32_t>();
367+
}
368+
};
369+
370+
void REGISTER_TEST(clocks)
371+
{
372+
registerGroup<TimerCalcTest>();
373+
374+
registerGroup<ClockTestTemplate<Timer1Clock<TIMER_CLKDIV_16>, uint32_t>>();
375+
registerGroup<ClockTestTemplate<Timer1Clock<TIMER_CLKDIV_256>, uint32_t>>();
376+
377+
registerGroup<ClockTestTemplate<Timer2Clock, uint32_t>>();
378+
379+
registerGroup<ClockTestTemplate<CpuCycleClockNormal, uint32_t>>();
380+
registerGroup<ClockTestTemplate<CpuCycleClockFast, uint64_t>>();
381+
}

0 commit comments

Comments
 (0)