Optimizing multiple digital writes

Hello everybody.

Sometimes we need to use multiple digital writes. One classical example is reproducing a LED pattern, like KITT (for older people KITT's Lights from Knight Rider Lights - YouTube) or a modern side-turn LED lamp (using discrete LEDs...not a beautiful LED strip :slightly_smiling_face:).

So, imagine 4 LEDs connected to Arduino pins 2 to 5.

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(2, HIGH);      // O X X X - 100 ms
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  digitalWrite(5, LOW);  
  delay(100);

  digitalWrite(2, LOW);       // X O X X - 100 ms
  digitalWrite(3, HIGH);
  digitalWrite(4, LOW);
  digitalWrite(5, LOW);  
  delay(100);

  digitalWrite(2, LOW);       // X X O X - 100 ms
  digitalWrite(3, LOW);
  digitalWrite(4, HIGH);
  digitalWrite(5, LOW);  
  delay(100);

  digitalWrite(2, LOW);       // X X X O  - 100 ms
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  digitalWrite(5, HIGH);  
  delay(100);

}

Now imagine, you have 8 LEDs, the code is long and difficult to change.

So another approach (classical in embedded controllers world) consists in taking an integer and associating each bit to a LED and using bit values (1 or 0) to turn on/off each LED.

For the previous pattern using 4 bits, we could use an array and a function that translate the array value into digitalWrites :smirk:

void loop() {
  // put your main code here, to run repeatedly:
  int LEDS[4] = {1,2,4,8};
  
  for(int i=0; i<4;i++){
       emitPins(LEDS[i], 2,5);
       delay(100);
  }
}

Now, changing the array value, you change the LED lighting pattern.

The complete code for 8 bits is shown below:

// 8 LEDs are connected to pins 2 - 9
void setup() {
  // put your setup code here, to run once:
  for(int i=2; i<10;i++){
    pinMode(i, OUTPUT);
  }
}

// LEDS contains an 8-sequence pattern similar to https://www.youtube.com/watch?v=8lS-VvMhDus or to another side turn sequential LEDs

void loop() {
  // put your main code here, to run repeatedly:
  int LEDS[8] = {1,2,4,8,16,32,64, 128};
  
  for(int i=0; i<8;i++){
       emitPins(LEDS[i], 2,9);
       delay(100);
  }
}

// Basic variant of the function
void emitPins(int val2emit, int pinIni, int pinFinal)
{
  int nPins = pinFinal - pinIni + 1;
  int pin2emit = pinIni;

  for(int i=0; i <nPins; i++){
  if(bitRead(val2emit, i) == 1)
    digitalWrite(pin2emit, HIGH);
  else
    digitalWrite(pin2emit, LOW);
  pin2emit ++;
  }

}

In a previous working thread about this, TheMemberFormerlyKnownAsAWOL pointed out that sometimes pins are not connected sequentially.

In that case, you can use this code:

// 8 LEDs are connected to a non-ordered set of pins 
int pins[8] = {2,7,8,9,3,4,5,6};

void setup() {
  // put your setup code here, to run once:
  for(int i=0; i<8;i++){
    pinMode(pins[i], OUTPUT);
  }
}

// LEDS contains an 8-sequence pattern similar to https://www.youtube.com/watch?v=8lS-VvMhDus or to another side turn sequential LEDs

void loop() {
  // put your main code here, to run repeatedly:
  int LEDS[8] = {1,2,4,8,16,32,64, 128};

  for(int i=0; i<8;i++){
       emitPinsRandom(LEDS[i], pins, 0, 7);
       delay(1000);
  }
}

// Emitting for non-ordered pins
void emitPinsRandom(int val2emit, int pins[], int pinIni, int pinFinal)
{
  int nPins = pinFinal - pinIni + 1;
  int pin2emit = pinIni;

  for(int i=0; i <nPins; i++){
    if(bitRead(val2emit, i) == 1)
      digitalWrite(pins[pin2emit], HIGH);
    else
      digitalWrite(pins[pin2emit], LOW);
    pin2emit ++;
  }

}

TheMemberFormerlyKnownAsAWOL and also aarg suggested optimizing emitPins functions taking into account HIGH and LOW as 1 and 0 or indexing pin directly instead of counting them.

// A second variant of the function - indexing on the number of pins
void emitPins(int val2emit, int pinIni, int pinFinal)
{
  int nPins = pinFinal - pinIni + 1;
  int pin2emit = pinIni;

  for(int i=0; i <nPins; i++){
    digitalWrite(pin2emit, bitRead(val2emit, i));
     pin2emit ++;
  }

}

Their final optimization is:

void emitPins(int val2emit, int pinIni, int pinFinal)
{
  for(int i = pinIni; i <= pinFinal; i++) {
     digitalWrite(i, bitRead(val2emit, i-pinIni));
  }
}

Thank you guys and the rest of the people participating in the working thread.

I don't get the point of these "multiple digital writes" threads.

a) you are not doing a multiple digital write, you are just using a for loop and do single digital writes.

b) If you really want to write a nice wrapper for a "multiple" write - read about direct port access. Use the Arduino functions to get from pins to the bits and ports. See the wiring_digital.c of the Arduino core how to implement such functions abstracted from the microcontroller pin assignements.

uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);

Use this to combine all bits on one port to do one real "multiple digital write". Evaluate which ports are needed for all your pins you want to modify and then combine the bitmask for one single write for each needed register. In best case it is only one out register.

c) furthermore for the usecase of a "running light" you don't need at all a multiple write. You just need to schwitch of one pin (the "old" one) and switch on one other pin (the new pin). You don't have to touch all pins.

d) my version of a non blocking Larson scanner with variable width (variable number of LEDs)
https://werner.rothschopf.net/microcontroller/202109_millis_larson_scanner_en.htm

Hi, thank you for the answer.

I don't get the point of these "multiple digital writes" threads.

a) you are not doing a multiple digital write, you are just using a for loop and do single digital writes.

From the high-level function, it looks like just one function. So it is Ok using the plural. The function does multiple writes.

b) If you really want to write a nice wrapper for a "multiple" write - read about direct port access. Use the Arduino functions to get from pins to the bits and ports. See the wiring_digital.c of the Arduino core how to implement such functions abstracted from the microcontroller pin assignements.

uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);

Use this to combine all bits on one port to do one real "multiple digital write". Evaluate which ports are needed for all your pins you want to modify and then combine the bitmask for one single write for each needed register. In best case it is only one out register.

Yes, direct port access is another solution. No claims here to be the only one.

c) furthermore for the usecase of a "running light" you don't need at all a multiple write. You just need to schwitch of one pin (the "old" one) and switch on one other pin (the new pin). You don't have to touch all pins.

For this specific case you need actually two, right? Turn on one and turn off another.

This was just a simple case. Imaging for example another pattern:
O X O X
X O X X
O X O X
O O O O

int LEDS[4] = {10,4,10,15}; // will do it, just changing data or description

There could be many patterns.

d) my version of a non blocking Larson scanner with variable width (variable number of LEDs)
https://werner.rothschopf.net/microcontroller/202109_millis_larson_scanner_en.htm

Thank you, I did not know it. It is a very interesting example with a variable number of LEDs.

I'm happy you are asking for a sketch with LED patterns:
https://werner.rothschopf.net/microcontroller/202109_millis_led_sequence_en.htm
obviously it uses a for loop to control each pin

Can I be picky for a moment?

digitalWrite(i, bitRead(val2emit, i-pinIni));

Strictly speaking, this isn't a good idea.

If you check the page about digitalWrite() it says

Syntax
digitalWrite(pin, value)

Parameters
pin: the Arduino pin number.
value: HIGH or LOW.

bitRead() returns 0 or 1. Not HIGH or LOW. So you are using digitalWrite() in an undocumented way.

Before you say it... I am aware that, as far as I know, every Arduino compatible board made to date, and every version of the Arduino IDE to date, has, "behind the scenes", defined HIGH to be 1 and LOW to be 0. So the above works. But it's not documented anywhere that it will work, so there's no guarantee that it always will work in the future on all Arduino compatible boards and all future versions of the IDE. Maybe that will never happen, and I hope it doesn't.

Point is, if you are giving examples to others, you shouldn't really set a bad example by telling them it's ok to rely on undocumented features.

Here's a couple of ways to avoid the problem in this case:

digitalWrite(i, bitRead(val2emit, i-pinIni) == 1 ? HIGH : BBCLOW);

or

const int pinLevel[2] = { LOW, HIGH };
...
digitalWrite(i, pinLevel[bitRead(val2emit, i-pinIni)]);
1 Like

Thanks, very nice post!

By the way, I was thinking that it would be possible to optimize the writes if array content is XORed, detecting that way the differences between a pattern value and the next. Then toggling only the positions at 1.

It will combine in a certain way, your first approach to variable length.

No time now, to check it, unfortunately.

And perhaps it is not so general if several fragments of the array are used at different times breaking the sequence.

Yes, thanks! You are right in that sense, LOW and HIGH are not officially documented as 0 and 1.

digitalWrite(i, bitRead(val2emit, i-pinIni) == 1 ? HIGH : LOW);

It is very elegant, but not absolutely sure if it translates at assembly level in the same way as if. In that case, there would be no higher execution throughput, but the construction is very nice.

So here goes another variant:

void emitPins(int val2emit, int pinIni, int pinFinal)
{
  for(int i = pinIni; i <= pinFinal; i++) {
     digitalWrite(i, bitRead(val2emit, i-pinIni) == 1 ? HIGH : LOW);
  }
}

Thanks!

Just to keep it alive

What makes you think it should be? :thinking:

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