Fast and object oriented digitalWrite/digitalRead replacement

In this posting: Faster Pin I/O on Zero? - #39 by OMac @OMac wrote two useful functions digitalWrite_fast and digitalRead_fast, in a response to a question from @wholder . I've measured it, and it improves the speed from 350 kHz for toggling a pin to 1.4 MHz.

The thread is closed, so I can't reply there, but my solution might be interesting for others as well. So here is my code with C++, which makes the code much more readable, and the toggle speed is now 3.4 MHz (see last code at the bottom) :

Left as an exercise to the reader: Integrate the pinMode call :slight_smile:

1 Like

You can flag the original topic and ask for it to be unlocked.

thanks for sharing

curious why those variable are uint32_t pointers ?

    volatile uint32_t* m_portSet;
    volatile uint32_t* m_portClear;
    volatile uint32_t* m_inReg;
    uint32_t m_mask;

otherwise the use of 0 and 1 does not make it easier to read, I would have given as example

// much easier to read "blinking" example
void loop() {
  led = HIGH;
  delay(1000);
  led = LOW;
  delay(1000);
}

It is an uint32_t pointer, because the reg element of the PORT_OUTSET_Type union type is an uint32_t, see here. This union is later used as elements of other structs, and then finally a pointer to such a struct (with the value of the hardware address of the microcontroller peripherals) is stored in the Arduino PORT array.

I've changed it to HIGH and LOW.

let me rephrase, what I did not get is that the registers are 8 bits and the memory address where they are mapped would do with 16 bits

I don't see any 8 bits in the code. The registers itself are 32 bit as well, see the datasheet on page 379, chapter 23.8.7, "Data Output Value Set": https://ww1.microchip.com/downloads/en/DeviceDoc/SAM-D21DA1-Family-Data-Sheet-DS40001882G.pdf

@frankbuss do you want me to unlock the original topic? Or provide a link to this one?

AVR port registers are 8 bits and mapped into the low part of the memory

Maybe I'm missing something but this is for a zero.

Woah! If simple tweaking allows to improve Arduino speed 10 fold, maybe the official Arduino library is a bit of a meh?

@kz: For fastest access to digital pins on AVR-based Arduinos, from the beginning of Arduino there has been the option of direct port manipulation.

I need my glasses !! :wink:

sorry about all the fuss

  1. This leaves out some of the things that digitalWrite() would normally do (like "turn on pullups if the pin is in input mode")

  2. It's a bit RAM-heavy - 16bytes of RAM per pin you define.

  3. Some of the speedup is just from being implemented "inline", which will increase flash usage "some", and also makes the toggle loop able to "cheat" a bit.

  4. The original Arduino code is, in fact, a bit of a "meh"... (the SAMD code a bit less "meh" than many, but still...)

I don't particularly like overloading used to make pins look like normal variables (cause part of "embedded" is about noticing how different they are), but Frankbuss's code should work equally well with a "write" method instead of an operator overload. And it looks like a good example of how to trade off the RAM/Flash consumption for performance.

(I think it will also work fine and pretty optimally for dynamic usage without changing APIs. That's pretty neat,)

void softSpiSend(int clockpin, int datapin, int output) {
  FastPin clock(clockpin);
  FastPin data(datapin);
  for (int i=0; i<8 i++) {
      clock = 0;
      data = output & 1;
      output  >>= 1;
      clock = 1;
  }
}
1 Like

@frankbuss
Thank You for your contribution.
Did You ever check the pointers?

I use direct port access with the Arduino DUE since years. Not only for speeding up but for synchronizing the bit output. So I do not only address a single bit but a group of bits with an accordingly mask in one write access.
When I developed those routines for the SAM3X, I simply studied the data sheet of the processor and made a simple address list with the values from the data sheet. That was easy.

When I did the same with the SAMD21 my Arduino Zero crashed. I did not understand it.
Then I extended the routine pinMode in wiring_digital.c with writing the content of the register pointer into memory and after calling the routine I fetched the memory content and showed it via serial output.
And was surprised, that it was not the register address of 0x41004400 as defined in the data sheet, but the address 0x08000000.
Do you (or any other expert) know, why it is this way?

In the next days I will test my direct register access by using the original "Arduino-Pointers", but I would feel better, if I understand the different addresses.

Not without seeing your modified code.
There shouldn’t be anything at 0x80000000

This needs an additional "output >> = 1", and would be nice to show the input as well.

I've done the same to access multiple pins at once, just writing directly to the registers. For example to set PA4 to PA11 with one byte with my Arduino Zero compatible circuit, I use this function:

inline void writeMOSI(uint8_t data) {
  PORT->Group[0].OUTCLR.reg = 0xff << 4;
  PORT->Group[0].OUTSET.reg = data << 4;
}

Depending on your application, you might need something like this, to avoid spikes when it first clears the register, by using the OUT register instead of OUTCLR and OUTSET. Untested:

  PORT->Group[0].OUT.reg = PORT->Group[0].OUT.reg & ~(0xff << 4) | (data << 4);

Doesn't matter in my case, because I use it to feed 8 SPI chips in parallel and the data is only latched with changing clock, and 2 register writes are probably faster than a register read and the additional logical operations.

@frankbuss and @westfw
Thank you for the answers.
For my register access I use the following data type:

typedef struct
{
  uint32_t  *pioPtr;
  uint32_t  mask;
} LocPioDescr, *PtrLocPioDescr;

And with a global data definition
LocPioDescr locPioDescr;
I added these lines at the end of pinMode :

locPioDescr.pioPtr  = PORT->Group[g_APinDescription[ulPin].ulPort].DIR.reg;
locPioDescr.mask    = (uint32_t)(1<<g_APinDescription[ulPin].ulPin);

Returning the pointer to the data structure and printing the content showed the surprising result. The mask was okay, but the pointer has the unexpected content of 0x0800000.
To repeat the main problem: If I use the pointer value 0x41004400 from the SAMD21 data sheet, the Arduino Zero crashes (I have to make the double-click reset to enable the usage).

I think you're missing an ampersand:

locPioDescr.pioPtr  = &PORT->Group[g_APinDescription[ulPin].ulPort].DIR.reg;

Yep (fixed!)
It was only meant as an example - the point was that instantiating clock and data would essentially cache the gAPinDescription lookups; something that "fast" bit-bang functions frequently do manually and less clearly.