How to reduce jitter with Arduino Nano Every (Assembly)

Hello,

I am trying to implement a timing-critical application with an Arduino Nano Every.

I want to detect an external pulse and output several pulses at fixed intervals after this input pulse. I have connected my input signal (5V, 1.2 µs pulse at ~ 100 kHz) to pin D11 (PE0) (with pull-down resistor 4.7 kOhm to GND) and output my programmed signal via PIN D17 (PD0).

I have relatively little experience, I mainly used the youtube-tutorials by Anas Kuzechie and the AVR Instruction Set Manual to get started with assembly programming.

For the most part the code does what it's supposed to, but I can't get the time delay between my input pulse and the output under control (the absolute value doesn't matter, but I need it to be reasonably constant). Here are a few oscilloscope images to clarify.


This is my code:

ino-File:

//-------------------------
// C Code for trigger pulse manipulation
//-------------------------
extern "C"
{
  void start();
  void trigger();
}
//----------------------------------------------------
void setup()
{
  start();
}
//----------------------------------------------------
void loop()
{
  trigger();
  digitalWrite(LED_BUILTIN, HIGH);  //turn on LED if this section is reached
  delay(10000);
}

S-File:

;---------------
; Assembly Code
;---------------
#include "avr/io.h"
;------------------------
.global start
.global trigger
;------------------------
start:
    LDI   R20, 0x01           ;load "00000001" into register 20
    OUT   VPORTD_DIR, R20     ;set PORTD0 to output
    LDI   R20, 0xFE           ;load "11111110" into register 20
    OUT   VPORTE_DIR, R20     ;set PORTE0 to input, 1-7 to output
    LDI   R21, 0x00           ;load "00000000" into register 21
    LDI   R22, 0x00           ;load "00000000" into register 22
    RET                       ;return to setup() function
;---------------------------------------------------------------------------
trigger:                                                                                      ;5 cycles if true 10 cycles if false
    LDI   R20, 0x01;                                                                          1 cycle
    LDI   R22, 0x00;                                                                          1 cycle
    IN    R21, VPORTE_IN      ;Read Vport E and store to R21                                  1 cycle
    CPI   R21, 0x01           ;Compare R21 to "00000001"                                      1 cycle
    BREQ  pulse               ;if input is high -> go to pulse                                1 cycle if true, 2 cycles if false
    OUT   VPORTD_OUT, R22     ;output of bit pattern "00000000" to all d-pins                 1 cycle
    NOP;                                                                                      1 cycle
    RJMP  trigger             ; else -> trigger loop                                          2 cycles
    RET                       ;return to loop() function ; Should not be reached                   
;---------------------------------------------------------------------------
pulse:                                                                                        ;15 cycles
    OUT   VPORTD_OUT, R20     ;output of bit pattern "00000001" to all d-pins    OUTPUT ON    1 cycle
    NOP;                                                                                      1 cycle  
    OUT   VPORTD_OUT, R22     ;output of bit pattern "00000000" to all d-pins   OUTPUT OFF    1 cycle
    NOP;                                                                                      1 cycle
    OUT   VPORTD_OUT, R20     ;OUTPUT ON                                                      1 cycle
    NOP;                                                                                      1 cycle 
    OUT   VPORTD_OUT, R22     ;OUTPUT OFF                                                     1 cycle
    NOP;                                                                                      1 cycle 
    OUT   VPORTD_OUT, R20     ;OUTPUT ON                                                      1 cycle
    NOP;                                                                                      1 cycle
    OUT   VPORTD_OUT, R22     ;OUTPUT OFF                                                     1 cycle
    NOP;                                                                                      1 cycle
    NOP;                                                                                      1 cycle
    RJMP  trigger             ;                                                               2 cycles
    RET                       ;return to loop() function ; Should not be reached 
;--------------------------------------------------------------------------------------------------------------------------------------------

Do you notice anything that could explain this jitter? Is there something i'm missing that I need to do to compile the code correctly? I simply upload it via the Arduino IDE.
Or is it simply not possible to achieve single-clock accuracy with the Nano Every?

I would like to use NOP-Statements to adjust my timings (pulse widths and intervals) for the application but with this amount of jitter it is kind of pointless. The pulse durations of 125 ns match to clock cycles at 16 MHz so I don't think i'm too far off...

I am grateful for any advice!

Many thanks in advance!

Andy

No idea except , possibly a power supply issue.

Check your scope prob is setup correctly on a square wave .

Thank you for the hint; The probes are adjusted reasonably well (but I will double check) Currently I'm powering it via USB from my PC or the scope; I wil try an external power supply.

The external power supply and adjusting/changing the probes didn't change the result.

The Arduino Sketch creates a timer IRQ for the millis() function. This can interrupt your code at any time and create the jitter.

code below gives a stable output on an ESP32
on 100kHz pulse (30% duty cycle) rising edge generates interrupt
ISR then outputs two pulses

// ESP32 - on pulse rising edge output two pulses

#define RISE1 18  // pin to input pulse
#define BLINK_GPIO 19  // output pulses

volatile int counter;
// on rising edge output two pulses
void IRAM_ATTR rising1() {
  digitalWrite(19, 1);
  __asm__ __volatile__("nop");
  __asm__ __volatile__("nop");
  __asm__ __volatile__("nop");
  digitalWrite(19, 0);
  __asm__ __volatile__("nop");
  __asm__ __volatile__("nop");
  __asm__ __volatile__("nop");
  digitalWrite(19, 1);
  __asm__ __volatile__("nop");
  __asm__ __volatile__("nop");
  __asm__ __volatile__("nop");
  digitalWrite(19, 0);
  counter++;
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(19, OUTPUT);
  Serial.printf("\n\nESP32 on pulse rising edge output two pulses\n");
  attachInterrupt(digitalPinToGPIONumber(RISE1), rising1, RISING);  // detect rising edge on pin 18
}

// display counts/second
void loop() {
  static long timer1 = millis();
  if (millis() - timer1 > 1000) {
    timer1 = millis();
    Serial.printf("counter %ld\n", counter);
    counter = 0;
  }
}

serial monitor displays

counter 100093
counter 100093
counter 100093
counter 100093
counter 100093
counter 100092
counter 100093

scope output

try it on the Nano Every?
may give you some ideas?

Dear @horace,

thank you very much, i will try to adapt this code to the Nano Every;

@MicroBahner

I've been trying to avoid any delay(), millis() or micro() functions to get around such issues; is this automatically generated without me explicitly calling it? If so, how can this be avoided?

Thanks!

Andy

did some more experiments
1 using the loop() code of post 8 (printing counter every second) the two output pulses have a jitter of approximately 250nSec

2 if loop() returns immediately

void loop() {
  return;

give a jitter of 50nSec (calling SerialEvent code?)

3 a while loop(1);

void loop() {
  while(1);

no visible jitter on scope

Thanks again, I tried to convert your sketch to my Nano Every. This is what I ended up with:

// on pulse rising edge output two pulses

int inPin = 11;
int outPin = 17;

void trigger() {
  digitalWrite(outPin, 1);
  __asm__ __volatile__("nop");
  digitalWrite(outPin, 0);
  __asm__ __volatile__("nop");
  digitalWrite(outPin, 1);
  __asm__ __volatile__("nop");
  digitalWrite(outPin, 0);
}

void setup() {
  pinMode(inPin, INPUT_PULLUP);     // sets the digital pin 11 as input
  pinMode(outPin, OUTPUT);          // sets the digital pin 17 as output
  attachInterrupt(digitalPinToInterrupt(inPin), trigger, RISING);  // detect rising edge on pin 11 and execute trigger
}

//
void loop() {
  while(1);
}

It is much more stable than before, with jitter in the single-digit ns range. Howoever this sketch is much slower than my previous attempt, with pulse widths of ~ 3.2 µs for a single NOP-delay; So unfortunately it can't keep up with my 100 kHz external trigger; And sometimes it appears as if the output is delayed by one cycle, so the whole output waveform is shifted by ~3.2 µs;


I think this approach will only work on faster devices (If I understand it correctly your ESP32 runs at 240 MHz, compared to the 16 MHz of my Nano);

But the interrupt approach seems to be the way to go, i will look into it if i can combine this with my assembly "pulse generator";

the ESP32 is certainly a much faster device than the Nano Every
on the ESP32 the time between the input pulse rising edge and the first output pulse is approximately 2uSec (the time it takes to save registers etc, call the ISR and execute the digitalWrite())
looking at your Nano Every plot the time appears to be approximately 10uSec which means you miss the next pulse rising edge as it is still executing the ISR code of the first pulse

perhaps move project to an ESP32 or even faster processor

EDIT: what generates the input pulse?

At the moment the input pulse is generated by a function generator, when the trigger circuit works the input will be replaced by a laser controller operating between 50 and 500 kHz; The input pulse will remain 5V with adjustable pulse widths (10ns - 1 µs).

think you will have problems at higher frequencies
the code of post 8 works OK up to 200kHz after that alternate pulse rising edges are missed because the previous ISR is still executing (same problem you had with the Nano Every in post 12)
scope display at 500KHz

you can see there are 12 input pulses (yellow) but only 6 or 7 pairs of output pulses (blue)

possibly try a 600MHz teensy 4.0 or a Raspberry Pi 4

if your input pulse is 5volts used a potential divider if inputting it into a 3.3V logic microcontroller such as the ESP32 or RPi 4 or you may damage the host

alternatively consider an FPGA?

Thank you for your efforts and the hint about the 3.3V devices; I just ordered a ESP32 to get the application running (and propably would have fried it with the 5V input).

However my primary goal was to understand what caused the jitter in my initial code; I would prefer to run computational efficient code on a slow device rather than throwing a lot of computational power at the problem... since the output signal of my Nano Every was both fast (2 compute cycle pulses) and repeatable i feel like it should be possible to use this device...

I think you are pretty much lost with every CPU based system and want to enter the FPGA land.

Buts lets see what we got here:
Your trigger loop has 8 commands. I don't know how many CPU cycles this is but it defines your base jitter. Your signal can hit the loop at any point.

Additionally interrupts are enabled. The background handling for millis() - counting timer overflows - will occasionally throw off your timing by another magnitude.

Now for some ideas:

  • Tweak the code in #12
    • digitalWrite() is probably the slowest variant to set a pin
    • the switch c - asm - c might add some overhead (rescuing and restoring register values)
    • attachInterrupt() -> the handling for calling trigger() might add some overhead
      You probably want to implement a ISR directly and use port manipulation for setting the pin.
  • Avoid the trigger loop and let the CPU sleep
      disableInterrupts();
      setup wake up on pin change
      while(1){
        sleep();
        pulsePin();
      }
  • Try to offload everything to the hardware. There is some interesting stuff in your Nano Every:
    • Eventsystem
    • Timer
    • SPI
    • CLC
2 Likes

the problem with the Arduino environment is you don't know exactly what is going on in the background, e.g. at a minimum the millis() clock and checking Serial IO in SerialEvent every time loop() exits.

when working events and control at microsecond timing you are even pushing microcontrollers such as the ESP32, RP2040, etc never mind slower devices

when programming PIC24, PIC32, etc microcontrollers using MPLABX in C I know exactly what timers, IO devices, etc I have enabled what clock speeds, what interrupt priority each device is, etc etc. It is also simple to look at the code generated by MPLABX to support peripheral devices. Similar when programming STM32 microcontrollers with STM32CubeIDE.

with the ESP32 I generally use gpio_set_level() and gpio_get_level() from the the Espressif GPIO & RTC GPIO library

You didn't disable interrupts. Add a cli instruction at the start of trigger and you should eliminate jitter. Add a sei before the return to re-enable interrupts. (Whether breaking all the things that rely on interrupts is acceptable is another question. Although since currently trigger never returns, it's a bit accademic :slight_smile: )

BTW, you don't need a semicolon after each opcode. The semicolon starts a comment, like // in C++, but if there is no comment, you don't need it.

1 Like

Thank you all for your input;

This is the point i didn't get; Thank you, now I understand why it wasn't possible to eliminate the jitter with my initial approach;

The key for improvements was to shorten the "trigger" cycle, i managed to optimize it a bit, so i now have three clock cycles when there is a pulse and 6 cycles when there is no pulse;

;---------------
; Assembly Code
;---------------
#include "avr/io.h"
;------------------------
.global start
.global trigger
;------------------------
start:
    LDI   R20, 0x01           ;load "00000001" into register 20
    OUT   VPORTD_DIR, R20     ;set PORTD0 to output
    LDI   R20, 0xFE           ;load "11111110" into register 20
    OUT   VPORTE_DIR, R20     ;set PORTE0 to input, 1-7 to output
    LDI   R21, 0x00           ;load "00000000" into register 21
    LDI   R22, 0x00           ;load "00000000" into register 22
    LDI   R20, 0x01           ;load "00000001" into register 20
    RET                       ;return to setup() function
;---------------------------------------------------------------------------
trigger:              ;3 cycles if true 6 cycles if false
    IN    R21, VPORTE_IN      ;Read Vport E and store to R21        1 cycle
    CPI   R21, 0x01           ;Compare R21 to "00000001"            1 cycle
    BREQ  pulse               ;if input is high -> go to pulse      1 cycle if true, 2 cycles if false
    RJMP  trigger             ; else -> trigger loop                2 cycles
    RET                       ;return to loop() function ; Should not be reached                   
;---------------------------------------------------------------------------
pulse:                 ;13 cycles
    OUT   VPORTD_OUT, R20     ;output of bit pattern "00000001" to all d-pins       1 cycle
    NOP;                                                                            1 cycle  
    OUT   VPORTD_OUT, R22     ;output of bit pattern "00000000" to all d-pins       1 cycle
    NOP;                                                                            1 cycle
    OUT   VPORTD_OUT, R20     ;OUTPUT ON                                            1 cycle
    NOP;                                                                            1 cycle 
    OUT   VPORTD_OUT, R22     ;OUTPUT OFF                                           1 cycle
    NOP;                                                                            1 cycle 
    OUT   VPORTD_OUT, R20     ;OUTPUT ON                                            1 cycle
    NOP;                                                                            1 cycle
    OUT   VPORTD_OUT, R22     ;OUTPUT OFF                                           1 cycle
    RJMP  trigger             ;                                                     2 cycles
    RET                       ;return to loop() function ; Should not be reached 
;-----------------------------------------------------------------------------

With that, the jitter is exactly down to three clock cycles and by increasing the clock frequency from 16 to 20 MHz the maximum delay between the "fastest" and the "slowest" output pulse train is now down to 150ns; (I also reduced the widths of the input pulse to 330 ns to avoid multiple triggers during the loop; so if I can't find a single cycle "read and compare" instruction or a single cycle "jump" instruction this should be the limit.

I will look into your other suggestions, especially the set the CPU to sleep sounds promising
.

Thank you all for your input!

You can check at the end of pulse if the trigger is still active.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.