1

Consider this code:

void loop() {
    digitalWrite(pinTest, HIGH);
    digitalWrite(pinTest, LOW);
}

On an Arduino Mega 2560, running 16MHz (=0.06us), I would expect the width of the pulse to be somewhere around 0.1us.

However, when measuring with my oscilloscope, I get around 4us high and 5-6 us low. I understand it could take some cycles to run the digitalWrite code, but this seems quite a lot.

How is that difference explained?

jsotola
  • 1,249
  • 2
  • 10
  • 18
  • 1
    digitalWrite is not optimized to translate the value of `pinTest` to the proper port/bit combination. If you need that, do it once in advance. Your code will be hardware-specific for avr-gcc targets. – DataFiddler Jan 14 '23 at 12:25
  • @DataFiddler any resources on how to do that in advance? – Bart Friederichs Jan 14 '23 at 12:29
  • Watch this youtube [Why I’m switching over from the awesome Arduino IDE to Atmel Studio](https://www.youtube.com/watch?v=648Tx5N9Zoc) – hcheung Jan 14 '23 at 12:30
  • Thanks all. Too bad, I am on Linux, so Atmel Studio is not an option, but I found a wait to build assembly code using `arduino-cli` and Visual Studio Code. – Bart Friederichs Jan 14 '23 at 12:53

2 Answers2

3

You do not need to go all the way down to assembly in order to get that speed. You can do direct port access from your .ino file:

void setup() {
    DDRA |= _BV(PA0);  // pin 22 = PA0 as output
    for (;;) {
        PINA |= _BV(PA0);  // toggle PA0
    }
}

void loop(){}

This compiles to something that is almost equivalent to your assembly code. Actually, it is a bit faster, as it uses rjmp instead of the slower jmp instruction.

Edit: A few notes

  1. As pointed out by timemage in a comment, you can save another CPU cycle by writing PINA = instead of PINA |=.

  2. This code, as well as your two examples, will exhibit a glitch every 1,024 µs. This is caused by the periodic timer interrupt used by the Arduino core for timekeeping (millis(), micros() and delay()). You can avoid the glitch by disabling interrupts before going into the tight loop. Alternatively, if you do not use the Arduino core at all, you can define a function called main() instead of setup() and loop(): this will completely remove the Arduino core initialization for your compiled program.

  3. arduino-cli is useful for sparing you the complexity of the Arduino build system (automatic installation of the cores and libraries, libraries in multiple places that depend on the core you use...). If you do not use the Arduino core, arduino-cli is of little use: a very simple Makefile that calls avr-gcc and avrdude is all you need for basic AVR development.

Edgar Bonet
  • 39,449
  • 4
  • 36
  • 72
  • `PINA =` rather than `PINA |=`. I'm not 100% certain what will happen with the `SBI` that `PINA |= expr` appears to be generating; but it if actually behaves as `PINA = PINA | expr` it is trying to toggle port pins in part based on the current read value of the port. In any case, `PINA = ` is sufficient and winds up generating an `OUT` which is 1 cycle compared to `SBI`'s two cycles. It's peanuts. But then they are going for speed. – timemage Jan 14 '23 at 14:20
  • @timemage: You are right, `PINA =` is faster here. `sbi` is safe, as it is a real bitwise access, not a bytewise read-modify-write. `PINA |=`, on the other hand, is only safe when you know `PINA` lives in the bit-addressable part of the I/O space. – Edgar Bonet Jan 14 '23 at 15:42
  • I suspected that about `sbi` simply because it would be much less useful otherwise. Re. I/O space: Yeah I didn't spell it out, but `PINH`/`PINJ`/`PINK`/`PINL` (curiously no `PINI`; perhaps it looks too much like a 1) all result `lds`,`ori`,`sts` – timemage Jan 14 '23 at 16:16
0

I fixed it by using assembly code to run the program. Because I am using Linux, I couldn't use Atmel Studio, but you can easily use AVR assembly with arduino-cli.

Make your .ino like this:

extern "C" void myAssemblyMain();

void setup() {
    myAssemblyMain();
}
void loop() {
}

then add a pulse.S file with the assembly code:

#define __SFR_OFFSET 0

#include "avr/io.h"

.global myAssemblyMain

myAssemblyMain:
  sbi   DDRA, 0     ; Set PA0 as output

blink:
  sbi   PINA, 0     ; Toggle PINB
  jmp   blink

Using this code, I get a ~400ns pulse. Which is more than enough for my needs.