GIGA R1 Wifi : Fast digital output handling

Hi,
I have discovered recently this forum and because I came here to solve a stupid bug and lost the time of nice people, I decided to show something that you could put in a 'tips & tricks' folder.

Question : how to manage on a Giga R1 the digital output ports in cases when time is critical ?

I have seen some complicated libraries, but let's try to keep simple things simple. Here we do NOT do multi threading.

Rem : the code below has been improved thanks to comments made by various users. It's the last version, those before having no interest for someone searching a solution. **The test results are in post #41.

Idea : create a c++ class, and use it !

class DigitalOutput
{
  public:
  GPIO_TypeDef *gpio;
  uint32_t      setMask;
  uint32_t      unsetMask;
  bool          bInverted;  // true -> High=0, Low=1
                            // false -> High=1, Low=0

  public:
                DigitalOutput()
                {
                  gpio      = NULL;
                  setMask   =
                  unsetMask = 0;
                  bInverted = false;
                }

//              Initialisation with defined PIN name (as D2, LED_BUILTIN,...)
                void init(int _arduinoPin, bool _bInverted = false)
                {
                  uint32_t       bit, portNum,pinName,portAddr;

                  // low 4 bits of "pinName" is the bit number
                  // upper bits of pinName are the port number (GPIOA = 0, GPIOB = 1, ...)
                  // These are evenly spaced in the memory map, so we can just offset from PORTA_BASE
                  pinName  = digitalPinToPinName(_arduinoPin);
                  
                  bit      = pinName        & 0x0F; 
                  portNum  = (pinName >> 4) & 0x0F;
                  portAddr = GPIOA_BASE + portNum * (GPIOB_BASE - GPIOA_BASE);

                  init((GPIO_TypeDef *)portAddr, bit, _bInverted);
                }

//              Initialisation with GPIO / bit rank
  void          init(GPIO_TypeDef *_gpio, uint32_t _bit, bool _bInverted = false)
                {
                  gpio      = _gpio;

                  setMask   = 1<<_bit;       // BSRR 0..15
                  unsetMask = setMask<<16;   // BSRR 16..31

                  bInverted = _bInverted;

                  // Set bit as digital output
                  // Rem : in MODER 2 bits configuration per bit
                  //          00 : input
                  //          01 : output
                  //          11 : analog
                  //          10 : alternate function mode (not used as i/o port)
                  gpio->MODER = (gpio->MODER & ~(0b11<<(_bit<<1))) | (0b01<<(_bit<<1));
                }

//              Handles logical/electrical levels association 
  inline void   setInverted(bool _bInverted) { bInverted = _bInverted; }

  inline bool   isInverted() __attribute__((always_inline))
                { return bInverted; }

//              Changes to logical Low
  inline void   setLow() __attribute__((always_inline))
                { gpio->BSRR = (bInverted)? setMask : unsetMask; }

//              Changes to logical High
  inline void   setHigh() __attribute__((always_inline))
                { gpio->BSRR = (bInverted)? unsetMask : setMask; }

//              Changes to logical level (false = logical Low, true = logical High)
  inline void   setLevel(bool _bLevel) __attribute__((always_inline))
                { gpio->BSRR = (_bLevel ^ bInverted)? setMask : unsetMask; }

//              Flips the output pin
  inline  void  flip() __attribute__((always_inline))
                { gpio->BSRR = (gpio->ODR & setMask)? unsetMask : setMask; }

//              Gets the real value of output pin
  inline int    getRealValue() __attribute__((always_inline))
                { return (gpio->ODR & setMask) ? HIGH : LOW; }
};

If you want to invert the behavior, you call setInverted(bool) to ask for it. If true, high and low states are 0/1 and not 1/0.

Example of use : the nice RGB LED.

If you check the documentation, the RGB leds are associated to PI12, PE3, PJ13.

It means GPIO I bit 12, GPIO E bit 3 and GPIO J bit 13.
For them, the logic is inverted : 0 is HIGH in the sense that light is produced, 1 : LOW.

Now, the code :

// Variables declaration

DigitalOutput led[3];


//
// Fonction qui fait clignoter une led
//

void Blink(DigitalOutput &_out, int _ms, int _nb=10)
{
  for (int i=0; i < _nb; i++)
  {
    _out.setHigh();
    delay(_ms);
    _out.setLow();
    delay(_ms);
  }
}

void setup()
{
// Each LED is declared and initialized as digital output (inverted)

  led[0].init(GPIOI ,12, true);  // R
  led[1].init(GPIOJ ,13, true);  // G
  led[2].init(GPIOE , 3, true);  // B

// Each LED blinks 5 times 200ms

  for(int i = 0; i < 3;i++)
    Blink(led[i], 200,5);
}

Why is it interesting to do that way ?

Because during the execution everything is resolved in the Blink() function, nothing is called with stack handling and other complicated stuff.

Other example : stepper motor control.

At high speed, 3 motors with 1600 pulses (200 full steps at 1/8) for one complete rotation each, at 10 rotations/s you have 1600x3x10x2 (high/low) changes to do each second, 96000 outputs !

The M7 is the core dedicated to wifi and USB with the computer : high level control.

The M4 does the low level job, ie generates the pulses and the direction changes.
At 240MHz, if you round 96000 to 100K, you have about 2400Hz/pulse, time for 5000 instructions.

If pulsing a motor takes a little bit time, the fact that at a moment you have to move several together can change the delay (d) between pulses :

[(motor 3)] - d - [(motor 3)] - d - [(motor 1)(motor 2)(motor 3)] - d - [(motor 3)] - ...

For motor 3, the delays are d, d + (motor 1&2 handling), d, ...

The execution has to be fast and smooth, the shorter the time used by each motor check, the less chance there is for a stall.

All optimisation of this code is welcome but, please, it should be kept human compatible !

really tested?? :slight_smile:

PS: arduino recommends camelCase

As your topic relates to an individual Arduino project and not the Arduino Project itself, which is the forum category that you posted in, it has been moved to a more relevant forum category

Not tested on this card. This function will be removed. Outputs are not inputs.

If you think you have a better way to write code, that extraordinary claim requires extraordinary proof.
Things like timing comparisons and assembly listings.

When I write 'not tested on this card', it's a response to J-M-L about a removed method called GetValue().

For the rest, I have to admit I didn't go to the school of those who prefer calls to Java from Python launched in a shellscript loop. When I started computing, computers ran at 1MHz. The big IBMs ran at 16MHz (monsters!).

I'm counting on your extraordinary mix of logic and feminine intuition to understand that a simple assignment is faster than a heavy function call.

My point was about the bug in the use of | instead of & in the function when you were applying the mask.

When I started in 1959, we only had analog computers.
I don't need a function to flip a bit, it's one line of code.
My first language was machine code, then I got assembler, wow, what an improvement.
Again, prove your work.

You are right.

On DsPIC you cannot read an output digit, but on some specific cards, the device keeps trace of the last output (often used for functional tests).

I'll test the function with & mask and if it works I add it again.

PS: if you prefer 1, the '|' wasn't bad...

You would get 1 all the time indeed because the mask was not null. So you were getting a value but it had nothing to do with the pin status :slight_smile:

Your request is as strange as if you asked me to prove that the LEDs are actually blinking.

I have nothing to prove to you. I owe you nothing.
Take example from J-M-L : nice and usefull comments.

Your statement implies you can invert a bit faster. But even better, you imply the rest of us are using some sort of library to do that.

I checked the result (whith '&') and the register returns the last output really done.

The code is now complet in the first comment.

Thanks.

Yes sir !

The usual way is :

  1. pinMode(...,INPUT/OUTPUT/...)
  2. digitalWrite(...,HIGH/LOW).

These functions are in libraries. It's a good and simple start to use things. But these functions are not efficient.

For testing, I decided to do 1000000 on/off, with digitalWrite() calls and then the same with setHigh()/setLow() :

int  ledRGB[]={D86,D87,D88};
DigitalOutput led[3];

void test()
{
  pinMode(ledRGB[0], OUTPUT);
  pinMode(ledRGB[1], OUTPUT);
  pinMode(ledRGB[2], OUTPUT);

  led[0].init(GPIOI,12,true);
  led[1].init(GPIOJ,13,true);
  led[2].init(GPIOE, 3,true);

  int t1,t2;
  int i;

  _println("test :");

  t1 = millis();
  for(i=0;i<1000000;i++)
  {
    digitalWrite(ledRGB[0], LOW);
    digitalWrite(ledRGB[0], HIGH);
  }
  t2 = millis();

  _println("delta1 = " + String(t2-t1));

  t1 = millis();
  for(i=0;i<1000000;i++)
  {
    led[0].setHigh();
    led[0].setLow();
  }
  t2 = millis();

  _println("delta2 = " + String(t2-t1));
}

The result is :

test : 
delta1 = 636
delta2 = 171

3.4 times faster in non optimised code.

another thing to consider if you have multiple instances

in init you sets the pin mode correctly

gpio->MODER = (gpio->MODER & ~(0b11<<(_bit<<1))) | (0b01<<(_bit<<1));

as this modifies only the two bits corresponding to the selected pin.

However, the setHigh and setLowmethods modify the ODR register directly using |= or &= ~mask

āžœ This is safe for a single pin, but if multiple DigitalOutput instances share the same GPIO port, and one instance modifies ODR without proper atomic operations or consideration of concurrency (e.g., interrupts also changing ODR), there is a risk that simultaneous writes could unintentionally change other pins' states. So it works fine in single-threaded, simple usage, but in more complex setups, you need to ensure that multiple instances on the same GPIO port do not interfere.

āžœ Explore using the bit set/reset register (BSRR) instead of modifying ODR directly, which avoids accidentally changing other pins as on STM32, writing to BSRR atomically sets or resets a pin

something like

inline void setLow() {
  if (gpio == nullptr) return;
  if (bInverted)
    gpio->BSRR = (mask << 16);
  else
    gpio->BSRR = mask;
}

inline void setHigh() {
  if (gpio == nullptr) return;
  if (bInverted)
    gpio->BSRR = mask;
  else
    gpio->BSRR = (mask << 16);
}

inline int getValue() {
  if (gpio == nullptr) return -1; // OR WHATEVER
  return (gpio->ODR & mask) ? 1 : 0;
}

Sorry, I misunderstood your OP. I thought you were talking about some new way to invert a bit.
As to your post 14 do you think a real system will notice the few microseconds it saved?
You should know, I was twice awarded a commendation for saving $5 5Million to buy new computers because I made a small change in the code that was more efficient, but it was invoked 10s of millions of times a day.
I made a similar small change in a customers code that their expert had been trying to find for 2 years.
My point is these are examples of major costs and performance hits due to small coding errors. Your example of twiddling a pin mode doesn't get me excited. Have I misunderstood yet again?

Does this work on an Uno?
What is GPIO_TypeDef?

Question 1 : I don't know. I use an Arduino GIGA R1.

Question 2 :

GPIO_TypeDef is a structure mapped in the card memory at several places to control the GPIOs. It's a structure that respects exactly the specs described in the µ-proc documentation.

In the file stm32h747xx.h, for example, you find :

/**
  * @brief General Purpose I/O
  */

typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;     /*!< GPIO port bit set/reset,               Address offset: 0x18      */
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
} GPIO_TypeDef;

then memory addresses :

...
#define PERIPH_BASE               (0x40000000UL) /*!< Base address of : AHB/APB Peripherals */
...
#define D3_AHB1PERIPH_BASE       (PERIPH_BASE + 0x18020000UL)
...
#define GPIOA_BASE            (D3_AHB1PERIPH_BASE + 0x0000UL)
#define GPIOB_BASE            (D3_AHB1PERIPH_BASE + 0x0400UL)
#define GPIOC_BASE            (D3_AHB1PERIPH_BASE + 0x0800UL)
#define GPIOD_BASE            (D3_AHB1PERIPH_BASE + 0x0C00UL)
#define GPIOE_BASE            (D3_AHB1PERIPH_BASE + 0x1000UL)
#define GPIOF_BASE            (D3_AHB1PERIPH_BASE + 0x1400UL)
#define GPIOG_BASE            (D3_AHB1PERIPH_BASE + 0x1800UL)
#define GPIOH_BASE            (D3_AHB1PERIPH_BASE + 0x1C00UL)
#define GPIOI_BASE            (D3_AHB1PERIPH_BASE + 0x2000UL)
#define GPIOJ_BASE            (D3_AHB1PERIPH_BASE + 0x2400UL)
#define GPIOK_BASE            (D3_AHB1PERIPH_BASE + 0x2800UL)

On which the usable structures are mapped :

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH               ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI               ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ               ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK               ((GPIO_TypeDef *) GPIOK_BASE)

Check if you find the same stuff for your platform.

No such thing for an Uno.

So your class, as is, is only usable with a GIGA?

It directly manipulates the STM32 GPIO registers to configure pins and set their output states, bypassing the Arduino digitalWrite abstraction. It is not applicable to AVR-based Arduinos, nor to ESP32