Skip to content

Instantly share code, notes, and snippets.

@sigmdel
Created June 5, 2023 21:19
Show Gist options
  • Save sigmdel/bea3b4065c6fdf2cc2d3c9c7fb1ddca0 to your computer and use it in GitHub Desktop.
Save sigmdel/bea3b4065c6fdf2cc2d3c9c7fb1ddca0 to your computer and use it in GitHub Desktop.
Convert microseconds to 32 bit fraction of a second (for conversion of timeval struct to 64 bit NTP timestamp)
/*
How to convert microseconds to 32 bit fraction of a second. This
calculation is needed when converting a struct timeval to an
NTP 64 bit timestamp.
Four methods are tested with four ESP32 processors. Here
uint32_t y_us is the microseconds input
uint32_t x is the 32 bit fraction of a second ouput
A) x = (uint32_t) ( ((uint64_t) y_us * 4294967296) / 1000000);
B) x = (uint32_t) ( ((uint64_t) y_us << 32) / 1000000 );
C) x = (uint32_t) ((float) y_us * floatfactor);
D) x = (uint32_t) ((double) y_us * doublefactor);
where
const float floatfactor = (float) 4294967296.0/1000000.0;
const double doublefactor = (double) 4294967296.0/1000000.0;
Here is a table of the calculted conversions for 9 microsecond values.
Test A
y_us -> x ; y_us as % x as %
0 -> 0 ; 0.00000000 0.00000000
1 -> 4294 ; 0.00010000 0.00009998
10 -> 42949 ; 0.00100000 0.00099998
100 -> 429496 ; 0.01000000 0.00999998
1000 -> 4294967 ; 0.10000000 0.09999999
10000 -> 42949672 ; 1.00000000 0.99999998
100000 -> 429496729 ; 10.00000000 9.99999999
999999 -> 4294963001 ; 99.99990000 99.99990000
1000000 -> 0 ; 100.00000000 0.00000000
Test A took 43 milliseconds, 43122 microseconds
Test B
y_us -> x ; y_us as % x as %
0 -> 0 ; 0.00000000 0.00000000
1 -> 4294 ; 0.00010000 0.00009998
10 -> 42949 ; 0.00100000 0.00099998
100 -> 429496 ; 0.01000000 0.00999998
1000 -> 4294967 ; 0.10000000 0.09999999
10000 -> 42949672 ; 1.00000000 0.99999998
100000 -> 429496729 ; 10.00000000 9.99999999
999999 -> 4294963001 ; 99.99990000 99.99990000
1000000 -> 0 ; 100.00000000 0.00000000
Test B took 43 milliseconds, 42301 microseconds
Test C
y_us -> x ; y_us as % x as %
0 -> 0 ; 0.00000000 0.00000000
1 -> 4294 ; 0.00010000 0.00009998
10 -> 42949 ; 0.00100000 0.00099998
100 -> 429496 ; 0.01000000 0.00999998
1000 -> 4294967 ; 0.10000000 0.09999999
10000 -> 42949672 ; 1.00000000 0.99999998
100000 -> 429496736 ; 10.00000000 10.00000015
999999 -> 4294962944 ; 99.99990000 99.99989867
1000000 -> 4294967295 ; 100.00000000 99.99999998
Test C took 32 milliseconds, 32182 microseconds
Test D
y_us -> x ; y_us as % x as %
0 -> 0 ; 0.00000000 0.00000000
1 -> 4294 ; 0.00010000 0.00009998
10 -> 42949 ; 0.00100000 0.00099998
100 -> 429496 ; 0.01000000 0.00999998
1000 -> 4294967 ; 0.10000000 0.09999999
10000 -> 42949672 ; 1.00000000 0.99999998
100000 -> 429496729 ; 10.00000000 9.99999999
999999 -> 4294963001 ; 99.99990000 99.99990000
1000000 -> 4294967295 ; 100.00000000 99.99999998
Test D took 42 milliseconds, 42410 microseconds
The last value, 1000000 µs, is out of range since it correspond to
a full second which should be in the other field of the
timeval struct. It is interesting that the floating point
implementations give "better" results in that case, but there
shoud nevertheless be some check to ensure that the
microsecond value is in the range 0..999999 before performing a
conversion.
The rounding errors with the single precision float calculations give
different 32 bit stamps compared to the other three methods. However
the actual error is not very significant when looking at the
32 bit stamp value as a percentage of a second.
The timing results are perhaps surprising. The integer math
implementations are faster than the floating math implementations with
the single core ESP32s. However, for the two core ESP32, the float
implementation is the fastest, followed by the integer implementation
and the double math implementation is the slowest. For the single
core ESP32, the double precision float implementation is faster than
the single precision implementation which seems odd unless all floating
point operations are done in double precision which require extra
casting operation from single to double and then back from double to
single precision if using the float type.
Timing Results
ESP32-C3:
Test A (int mult) time: 2500.000000 ms, 2523008.000000 µs
Test B (int shift) time: 2500.000000 ms, 2407500.000000 µs
Test C (float mult) time: 6000.000000 ms, 6283005.000000 µs
Test D (double mult) time: 4500.000000 ms, 4505004.000000 µs
Test A (int mult) average time: 0.001111 ms, 1.121337 µs
Test B (int shift) average time: 0.001111 ms, 1.070000 µs
Test C (float mult) average time: 0.002667 ms, 2.792447 µs
Test D (double mult) average time: 0.002000 ms, 2.002224 µs
ESP32-S2:
Test A (int mult) time: 1481.000000 ms, 1226958.000000 µs
Test B (int shift) time: 1000.000000 ms, 1082506.000000 µs
Test C (float mult) time: 2000.000000 ms, 2690013.000000 µs
Test D (double mult) time: 1501.000000 ms, 1738998.000000 µs
Test A (int mult) average time: 0.000658 ms, 0.545315 µs
Test B (int shift) average time: 0.000444 ms, 0.481114 µs
Test C (float mult) average time: 0.000889 ms, 1.195561 µs
Test D (double mult) average time: 0.000667 ms, 0.772888 µs
ESP32:
Test A (int mult) time: 1001.000000 ms, 1179501.000000 µs
Test B (int shift) time: 1000.000000 ms, 1067500.000000 µs
Test C (float mult) time: 1000.000000 ms, 632000.000000 µs
Test D (double mult) time: 2000.000000 ms, 1744000.000000 µs
Test A (int mult) average time: 0.000445 ms, 0.524223 µs
Test B (int shift) average time: 0.000444 ms, 0.474444 µs
Test C (float mult) average time: 0.000444 ms, 0.280889 µs
Test D (double mult) average time: 0.000889 ms, 0.775111 µs
ESP32-S3:
Test A (int mult) time: 1500.000000 ms, 1121999.000000 µs
Test B (int shift) time: 1000.000000 ms, 1036000.000000 µs
Test C (float mult) time: 1000.000000 ms, 536000.000000 µs
Test D (double mult) time: 1500.000000 ms, 1680000.000000 µs
Test A (int mult) average time: 0.000667 ms, 0.498666 µs
Test B (int shift) average time: 0.000444 ms, 0.460444 µs
Test C (float mult) average time: 0.000444 ms, 0.238222 µs
Test D (double mult) average time: 0.000667 ms, 0.746667 µs
*/
#include <Arduino.h>
// Conversion factors for floating point calculations
const float floatfactor = (float) 4294967296.0/1000000.0;
const double doublefactor = (double) 4294967296.0/1000000.0;
bool CheckTest = true;
uint32_t y_us = 1; // microseconds
uint32_t x = 0; // NTP timestamp 32-bit fraction of second
#define SIGNIFICANT_DIGITS 8 // 6, 7, 8
void printHeader(char * testname) {
Serial.printf("Test %s\n", (const char*) testname);
#if SIGNIFICANT_DIGITS >= 8
Serial.println(" y_us -> x ; y_us as % x as %"); // 8
#elif SIGNIFICANT_DIGITS == 7
Serial.println(" y_us -> x ; y_us as % x as %"); // 7
#else
Serial.println(" y_us -> x ; y_us as % x as %"); // 6
#endif
}
// Show microsecond and calculated fractions, then show both as a % of a second
void printConversion(void) {
Serial.printf("%7u -> %10u", y_us, x);
double uspercent = (100.0*y_us) / 1000000.0;
double fpercent = (100.0*x) / 4294967296.0;
#if SIGNIFICANT_DIGITS >= 8
Serial.printf(" ; %12.8f %12.8f\n", uspercent, fpercent);
#elif SIGNIFICANT_DIGITS == 7
Serial.printf(" ; %11.7f %11.7f\n", uspercent, fpercent);
#else
Serial.printf(" ; %10.6f %10.6f\n", uspercent, fpercent);
#endif
}
// Accumulated time for each runTest_X(count) run
uint32_t time_A_ms = 0;
uint32_t time_B_ms = 0;
uint32_t time_C_ms = 0;
uint32_t time_D_ms = 0;
uint32_t time_A_us = 0;
uint32_t time_B_us = 0;
uint32_t time_C_us = 0;
uint32_t time_D_us = 0;
uint32_t tcount = 0; // count # of conversions in each test
void clearTimes(void) {
time_A_ms = 0;
time_B_ms = 0;
time_C_ms = 0;
time_D_ms = 0;
time_A_us = 0;
time_B_us = 0;
time_C_us = 0;
time_D_us = 0;
tcount = 0;
}
#define TEST_VALUES_COUNT 9
uint32_t values[9] { 0, 1,
10, 100,
1000, 10000,
100000, 999999,
1000000 }; // last value is overflow!
void runTest_A(int count=1) {
if (CheckTest)
printHeader("A");
unsigned long start_ms = millis();
unsigned long start_us = micros();
while (count) {
for (int i=0; i < TEST_VALUES_COUNT; i++) {
y_us = values[i];
//x = (uint32_t) ( ((unsigned long long) y_us * 4294967296) / 1000000);
x = (uint32_t) ( ((uint64_t) y_us * 4294967296) / 1000000);
if (CheckTest)
printConversion();
else
tcount++;
}
count--;
}
unsigned long runtime_us = micros() - start_us;
unsigned long runtime_ms = millis() - start_ms;
time_A_ms += runtime_ms;
time_A_us += runtime_us;
if (CheckTest)
Serial.printf("Test A took %u milliseconds, %u microseconds\n\n", runtime_ms, runtime_us);
}
void runTest_B(int count=1) {
if (CheckTest)
printHeader("B");
unsigned long start_ms = millis();
unsigned long start_us = micros();
while (count) {
for (int i=0; i< TEST_VALUES_COUNT; i++) {
y_us = values[i];
x = (uint32_t) ( ((unsigned long long) y_us << 32) / 1000000 );
//x = (uint32_t) ( ((uint64_t) y_us << 32) / 1000000 );
if (CheckTest)
printConversion();
}
count--;
}
unsigned long runtime_us = micros() - start_us;
unsigned long runtime_ms = millis() - start_ms;
time_B_ms += runtime_ms;
time_B_us += runtime_us;
if (CheckTest)
Serial.printf("Test B took %u milliseconds, %u microseconds\n\n", runtime_ms, runtime_us);
}
void runTest_C(int count=1) {
if (CheckTest)
printHeader("C");
unsigned long start_ms = millis();
unsigned long start_us = micros();
while (count) {
for (int i=0; i < TEST_VALUES_COUNT; i++) {
y_us = values[i];
x = (uint32_t) (((float) y_us) * floatfactor);
if (CheckTest)
printConversion();
}
count--;
}
unsigned long runtime_us = micros() - start_us;
unsigned long runtime_ms = millis() - start_ms;
time_C_ms += runtime_ms;
time_C_us += runtime_us;
if (CheckTest)
Serial.printf("Test C took %u milliseconds, %u microseconds\n\n", runtime_ms, runtime_us);
}
void runTest_D(int count=1) {
if (CheckTest)
printHeader("D");
unsigned long start_ms = millis();
unsigned long start_us = micros();
while (count) {
for (int i=0; i < TEST_VALUES_COUNT; i++) {
y_us = values[i];
x = (uint32_t) (((double) y_us) * doublefactor);
if (CheckTest)
printConversion();
}
count--;
}
unsigned long runtime_us = micros() - start_us;
unsigned long runtime_ms = millis() - start_ms;
time_D_ms += runtime_ms;
time_D_us += runtime_us;
if (CheckTest)
Serial.printf("Test D took %u milliseconds, %u microseconds\n\n", runtime_ms, runtime_us);
}
void setup() {
#ifdef SERIAL_BAUD
Serial.begin(SERIAL_BAUD);
#else
Serial.begin(); // ESP32C3,
#endif
delay(5000);
Serial.println("\nConvert timeval.tv_usec to NTP timestamp 32 bit fraction of second\n");
runTest_A();
runTest_B();
runTest_C();
runTest_D();
CheckTest = false;
clearTimes();
Serial.println("Completed setup(), starting time test");
}
#define COUNTS 500
#define LOOPS 500
int loops = LOOPS;
void loop(void) {
if (loops>0) {
Serial.print(".");
runTest_A(COUNTS);
delay(10);
runTest_B(COUNTS);
delay(10);
runTest_C(COUNTS);
delay(10);
runTest_C(COUNTS);
delay(10);
runTest_D(COUNTS);
} else if (loops == 0) {
Serial.println("\n Timing Results\n");
Serial.printf("Test A (int mult) time: %f ms, %f µs\n", ((double) time_A_ms), ((double) time_A_us));
Serial.printf("Test B (int shift) time: %f ms, %f µs\n", ((double) time_B_ms), ((double) time_B_us));
Serial.printf("Test C (float mult) time: %f ms, %f µs\n", ((double) time_C_ms), ((double) time_C_us));
Serial.printf("Test D (double mult) time: %f ms, %f µs\n\n", ((double) time_D_ms), ((double) time_D_us));
double calcs = (double) LOOPS*COUNTS*TEST_VALUES_COUNT;
Serial.printf("Test A (int mult) average time: %f ms, %f µs\n", ((double) time_A_ms) / calcs, ((double) time_A_us) / calcs);
Serial.printf("Test B (int shift) average time: %f ms, %f µs\n", ((double) time_B_ms) / calcs, ((double) time_B_us) / calcs);
Serial.printf("Test C (float mult) average time: %f ms, %f µs\n", ((double) time_C_ms) / calcs, ((double) time_C_us) / calcs);
Serial.printf("Test D (double mult) average time: %f ms, %f µs\n", ((double) time_D_ms) / calcs, ((double) time_D_us) / calcs);
Serial.printf("\nCalcs: %f, tcount: %u\n", calcs, tcount);
}
loops--;
}
/*
; file: platformio.ini
;
; PlatformIO Project Configuration File
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = seeed_xiao_esp32c3
;default_envs = lolin_s2_mini
;default_envs = lolin32_lite
;default_envs = seeed_xiao_esp32s3
[extra]
monitor_baud = 115200 ; for non USB-CDC boards such as Lolin32 Lite
[env]
framework = arduino
platform = espressif32
[env:seeed_xiao_esp32c3]
board = seeed_xiao_esp32c3
monitor_speed = 460800
build_flags =
-DCORE_DEBUG_LEVEL=0
[env:seeed_xiao_esp32s3]
board = seeed_xiao_esp32s3
monitor_speed = 460800
build_flags =
-DCORE_DEBUG_LEVEL=0
[env:lolin32_lite]
board = lolin32_lite
monitor_speed = ${extra.monitor_baud}
monitor_port = /dev/ttyUSB0
upload_port = /dev/ttyUSB0
build_flags =
-DCORE_DEBUG_LEVEL=0
-DSERIAL_BAUD=${extra.monitor_baud}
[env:lolin_s2_mini]
board = lolin_s2_mini
build_flags =
-DCORE_DEBUG_LEVEL=0
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment