Naguissa / uTimerLib

Arduino tiny and cross-device compatible timer library
https://www.foroelectro.net/electronica-digital-microcontroladores-f8/utimerlib-libreria-arduino-para-eventos-temporizad-t191.html
GNU Lesser General Public License v3.0
20 stars 9 forks source link

unprecisse timing on SAMD21 (Arduino Zero) #18

Closed daniel-mohr closed 10 months ago

daniel-mohr commented 1 year ago

On an Arduino Zero (SAMD21) using this simple sketch shows 12 us or 13 us instead of 10 us:

#include "uTimerLib.h"

/* serial interface */
#define SERIAL_BAUD_RATE 115200 // baud = bits per second
#define SERIAL_TIMEOUT 1000 // timeout in 1e-6 seconds

#define LED_PIN 0

unsigned long microstime_old = 0;
unsigned long microstime_delta = 0;

void handler (void) {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  const unsigned long mymicros = micros();
  microstime_delta = mymicros - microstime_old;
  microstime_old = mymicros;
}

void setup() {
  // set serial communication:
  Serial.begin(SERIAL_BAUD_RATE);
  Serial.setTimeout(SERIAL_TIMEOUT);
  delay(1000);
  pinMode(LED_PIN, OUTPUT);
  TimerLib.setInterval_us(handler, 10);
}

void loop() {
  const unsigned long mtime = millis();
  Serial.print("mtime: ");
  Serial.print(mtime);
  Serial.print(" SystemCoreClock: ");
  Serial.print(SystemCoreClock);
  Serial.print(" microstime_delta: ");
  Serial.println(microstime_delta);
  delay(1000);
}

A more precise measurement with an oscilloscope shows there is an additional time of about 2.5 us (= 2.5e-6 s). So the handler function is called every 12.5 us instead of every 10 us.

The same is true for longer periods. Also for TimerLib.setInterval_us(handler, 100); I get about 102.5 us instead of 100 us.

Unfortunately I have to say that this is not a hardware problem. With my own trigger implementation I get the setting as measurement.

And further it is not clear for my why uTimerLib shows this behavior. Maybe you have an idea?

Naguissa commented 1 year ago

Maybe it's a miscalculation proble.

My 1st thought was "overhead", but overhead should be usually the same, so it should be cancelled by previous one.

Maybe rounding or similar, I'll take a look.

As I have one of this boards I can test it by my own....

Naguissa commented 10 months ago

Checked and it works correctly. When using 500 us it deviates 1~5 us, but it's understandable because Serial takes its time, and at 500 us rate it really floods Serial.

Few thoughts about your sketch:

I rewrote the test this way:


#include "Arduino.h"
#include "uTimerLib.h"

volatile unsigned long int prevMillis = 0;
volatile unsigned long int actMillis = 0;

void timed_function() {
    SerialUSB.println(actMillis - prevMillis);
    prevMillis = actMillis;
}

void setup() {
    SerialUSB.begin(57600);
    TimerLib.setInterval_us(timed_function, 500);
    prevMillis = micros();
}

void loop() {
    actMillis = micros();
}
Naguissa commented 10 months ago

Also, take in mind it's 48MHz CPU, so it's not rounded to us:

        Name            Prescaler   Freq        Base Delay      Overflow delay
        GCLK_TC            1         48MHz      0,020833333us      1365,333333333us;    1,365333333333ms
        GCLK_TC/2          2         24MHz      0,041666667us      2730,666666667us;    2,730666666667ms
        GCLK_TC/4          4         12MHz      0,083333333us      5461,333333333us;    5,461333333333ms
        GCLK_TC/8          8          6MHz      0,166666667us     10922,666666667us;   10,922666666667ms
        GCLK_TC/16        16          3MHz      0,333333333us     21845,333333333us;   21,845333333333ms
        GCLK_TC/64        64        750KHz      1,333333333us     87381,333311488us;   87,381333311488ms
        GCLK_TC/256      256        187,5KHz    5,333333333us    349525,333311488us;  349,525333311488ms
        GCLK_TC/1024    1024        46.875Hz    21,333333333us  1398101,333333333us; 1398,101333333333ms; 1,398101333333333s
daniel-mohr commented 10 months ago

Few thoughts about your sketch:

* Never use milis / micros inside an interrupt

I know, it is not perfect. But it shows an easy possibility, which can everyone do without any special equipment (e. g. without an oscilloscope).

* Do interrupt as small as possible, specially when using so small timing.

Yes, you are right. But for 10 us it is still small enough.

void timed_function() { SerialUSB.println(actMillis - prevMillis); prevMillis = actMillis; }

I do not think SerialUSB.println is better than milis / micros. Especially if we go down to 10 us.

But anyway the result of your test script is something like:

502
503
504
501
504

This does not look like the expected 500, did it?

Also, take in mind it's 48MHz CPU, so it's not rounded to us:

If we think at 10 us and use the prescaler 16 then the resulting frequency is 3 MHz (as you already stated). This means we have 10e-6 / (1/3e6) = 10e-6 * 3e6 = 30 (precisely!) periods for 10 us. There is no need to round here, is it?

The output from my script above for your library uTimerLib would be something like:

mtime: 5154 SystemCoreClock: 48000000 microstime_delta: 13
mtime: 6154 SystemCoreClock: 48000000 microstime_delta: 13
mtime: 7154 SystemCoreClock: 48000000 microstime_delta: 12
mtime: 8155 SystemCoreClock: 48000000 microstime_delta: 13
mtime: 9155 SystemCoreClock: 48000000 microstime_delta: 13

As you can see this is not precise.

Therefore I did a measurement with an oscilloscope. As I a already wrote:

A more precise measurement with an oscilloscope shows there is an additional time of about 2.5 us (= 2.5e-6 s). So the handler function is called every 12.5 us instead of every 10 us.

The same is true for longer periods. Also for TimerLib.setInterval_us(handler, 100); I get about 102.5 us instead of 100 us.

As you can see in fast_samd21_tc tested using fast_blink_led_tc3.ino with fast_samd21_tc it is possible to do much more precise, e. g.:

set interval expected T mean T min. T max. T std T
2 4 4.68 4.65 4.85 0.060
4 8 8.67 8.65 9.02 0.097
8 16 16.66 16.66 16.57 0.003
16 32 32.63 32.01 32.91 0.165

I did the same with your library and get that there is an additional 2.5 us.

The example measure_timing.ino from fast_samd21_tc shows that using micros works in an interrupt.

I adapted this for your library:

#include "uTimerLib.h"

/* serial interface */
#define SERIAL_BAUD_RATE 115200 // baud = bits per second
#define SERIAL_TIMEOUT 1000 // timeout in 1e-6 seconds

unsigned long start_microstime = 0;
unsigned long stop_microstime = 0;
volatile uint8_t it = 0;
volatile unsigned long duration = 0;

void handler (void) {
  if (it++ == 0) {
    stop_microstime = micros();
    duration = stop_microstime - start_microstime;
    start_microstime = stop_microstime;
  }
}

void setup() {
  // set serial communication:
  Serial.begin(SERIAL_BAUD_RATE);
  Serial.setTimeout(SERIAL_TIMEOUT);
  while (!Serial);
  TimerLib.setInterval_us(handler, 500);
}

void loop() {
    delay(3);
    static unsigned long mtime = millis();
    Serial.print(" mtime: ");
    Serial.print(mtime);
    Serial.print(" microstime_delta: ");
    Serial.print(((double) duration) / 255);
    Serial.println(" us");
}

This leads with your library uTimerLib to something like mtime: 1823 microstime_delta: 12.71 us

Whereas fast_samd21_tc leads to a much better result: mtime: 104481 microstime_delta: 10.04 us.

You can also run this with an interval of 500. You will get with your library uTimerLib something like mtime: 1783 microstime_delta: 504.65 us, whereas fast_samd21_tc leads to mtime: 10573 microstime_delta: 501.98 us.

So, it is not the hardware. It is the software. And therefore I think your library has a bug!

If you do not believe the simple tests with micros in the interrupt, please believe the measurements with an oscilloscope.

Naguissa commented 10 months ago

Wow, very in-deep analysys, thanks!

I'll recheck time-divissions; maybe use smaller periods. That whould add very little overhead but improve accuracy.