2

I have a very simple PCB that uses 3 GPIO on a ATtiny85 to control 6 LEDs using charlieplexing.

When I try to program certain patterns of lit LEDs (pseudo PWM) I get some LEDs lighting dimly when they should be fully off.

Example where only top or bottom three LEDs should be on alternately:

Video

You can see that LEDs 2 and 5 are dimly glowing rather than being fully off.

I do not have problems when lighting the LEDs one at a time in sequence.

I do not have problems lighting odd and even LEDs alternately.

The schematic is:

schematic

The buttons are used to change up and down through 6 modes of operation. Mode 0 lights the LEDs one at a time in sequence. Mode 1 alternates the odd and even LEDs. Mode 2 lights upper or lower three LEDs alternately (and has ghosting problem), the other modes just steadily illuminate the corresponding LED.

The code is split into several files:

main.ino:

Button leftButton = Button(3);
Button rightButton = Button(4);
Sequencer sequencer = Sequencer(200); // ms per step
Alternator alternator = Alternator(0b00101010, 0b00010101);

void setup() {
  pinMode(0, OUTPUT); 
  pinMode(1, OUTPUT); 
  pinMode(2, OUTPUT); 
}

// ------------------------------------------------------------------

const int maxMode = 5;
int mode = 0;

void loop() {
  if (leftButton.wasPressed())  {   
    if (++mode > maxMode) { mode = 0; }
    initMode(mode);
  }
  if (rightButton.wasPressed()) { 
    if (--mode < 0) { mode = maxMode; }
    initMode(mode);
  }
  
  switch(mode) {
    case (0): sequencer.check(); break;
    case (1): 
    case (2): alternator.check(); break;
    default: lightLED(mode); break;
  }
}

// ------------------------------------------------------------------

void initMode(int mode) {
  switch (mode) {
    case (0): sequencer.reset(); break;  
    case (1): alternator = Alternator(0b00101010, 0b00010101); break;
    case (2): alternator = Alternator(0b00111000, 0b00000111); break;
  }
}

Alternator.ino

class Alternator {

  private:
    unsigned long whenChanged = 0;
    int interval = 500; // ms
    byte pattern, pattern1, pattern2; // bitmap
    int count = 0;
        
  public:
  Alternator(byte pattern1, byte pattern2) {
    this->pattern1 = pattern1;
    this->pattern2 = pattern2;
  }

  void check() {
    
    if ((millis() - whenChanged) > interval) {
      if (pattern == pattern1) { pattern = pattern2; } 
      else                     { pattern = pattern1; }
      whenChanged = millis();
    }

    if (++count > 5) { count = 0; }
    if (((pattern >> count) & 1) == 1) {
      lightLED(count);
    }

  }
  
};

Sequence.ino

class Sequencer {

  private:
    int seq;                            // LED# 0-5
    unsigned long whenLastStep;         // ms
    int stepTime;                       // ms
    const int aDay = 24 * 3600 * 1000;  // ms

  public:
    Sequencer(int stepTime) {
      if (stepTime < 1) { stepTime = 1; }
      if (stepTime > aDay) { stepTime = aDay; }
      this->stepTime = stepTime;
      this->seq = 0;
      this->whenLastStep = 0;  
    }

    void check() {
      if ((millis() - whenLastStep) > stepTime) {
        lightLED(seq++);
        if (seq > 5) { seq = 0; }
        whenLastStep = millis();
      }      
    }

    void reset() {
      this->seq = 0;
    }
};

Charlieplex.ino

const int highs[6] = {0,1,2,1,2,0};  // GPIOs for LEDs 0-5
const int lows[6]  = {1,0,1,2,0,2};

// Manipulates 3 GPIO outputs to light one of 6 LEDs.
void lightLED(int i) {
  int h, l;     // pin to set high, low
  if (i < 0 || i >= 6) { 
    h = 99; l = 99;  // all LEDS off 
  } else {
    h = highs[i];
    l = lows[i];
  }
  
  for (int j=0; j<=2; j++) {
    if (j==h || j==l) {
      pinMode(j, OUTPUT);
      if (j==h) {
        digitalWrite(h, HIGH);
      } else {
        digitalWrite(l, LOW);
      }
    } else {
      pinMode(j, INPUT); // tri-state
    }
  }
}

Buttons.ino

class Button {
  
  private:
    int pin;
    const int DebounceDelay = 100;      // milliseconds
    int lastState = HIGH;               // Previous status of button
    unsigned long whenLastPressed = 0;  // Last time button was pressed
 
  public:
    Button(int pin) {
      this->pin = pin;
      pinMode(pin, INPUT_PULLUP);
    }

    // Was there an up (HIGH) to down (LOW) transition since last call, and still held down? 
    bool wasPressed() {
      bool result = false;
      if ((millis() - whenLastPressed) > DebounceDelay) { 
        int state = digitalRead(pin);
        if ((state == LOW) && (lastState == HIGH)) {
          result = true; 
          whenLastPressed = millis();
        }
        lastState = state;
      }
      return result;
    }
    
};

I'm not sure where I am going wrong, any ideas?

1
  • 1
    I'd use direct access to the port (but it's not neccesary). And first set all outputs zero, then all set as inputs (if you left any output high and switch to input mode, there will be internal pull up enabled). After that you can set output mode for correct pins and one of them as HIGH. (for three pins it might be enough just RESET all pins and then set correct output mode and next loop set values)
    – KIIV
    Jun 22 at 19:44
4

Charlieplexing code doesn't reset the state of unused pin and if the port value is HIGH + mode INPUT, it means the pull-up is enabled.

Solution is reset all pins to LOW before code starts changing directions. And the last step would be setting correct pin to HIGH.

If you want to do it in single loop, you might observe really short pulses (it might or might not be visible - at the higher frequencies it'll be more obvious)

It could be also much faster than using digitalWrite and pinMode, something like:

  void charlieplex(uint8_t i) {  
    constexpr uint8_t clrpins = ~(_BV(PB0) | _BV(PB1) | _BV(PB2));
    constexpr uint8_t dirs[]  = { 0b011, 0b011, 0b110, 0b110, 0b101, 0b101};
    constexpr uint8_t outs[]  = { 0b001, 0b010, 0b010, 0b100, 0b001, 0b100};

    PORTB &= clrpins; // clear output pins
    DDRB  &= clrpins; // clear direction pins (0 == input, 1 == output)
    if (i < 6) {  // such easy condition if you use:  uint8_t i
      DDRB  |= dirs[i]; // And set the correct pins as outputs
      PORTB |= outs[i]; // And set the correct pin as high
    }
  }

It's not tested, but it should be pretty much it

3
  • This code works very well. Small fix needed for my particular LED ordering: constexpr uint8_t outs[] = { 0b001, 0b010, 0b100, 0b010, 0b100, 0b001}; Jun 22 at 21:21
  • it's possible, I was doing it according to the led numbering, and here and in the original code it looks like 3-4 and 5-6 are swapped
    – KIIV
    Jun 22 at 21:27
  • Ah yes, I changed the LEDs around when laying out the PCB to simplify track routing and didn't update the schematic! So my fault. :-) Jun 22 at 21:33
1

A secondary issue is that using millis() to update the timestamps will cause the interval to cumulatively drag.

To keep the interval constant, update:

  • whenChanged with interval
  • whenLastStep with stepTime.

Alternator.ino

void check() {

  if ((millis() - whenChanged) > interval) {
    if (pattern == pattern1) { pattern = pattern2; }
    else                     { pattern = pattern1; }
    whenChanged += interval;  // Constant interval.
  }

  if (++count > 5) { count = 0; }
  if (((pattern >> count) & 1) == 1) {
    lightLED(count);
  }

}

Sequence.ino

void check() {
  if ((millis() - whenLastStep) > stepTime) {
    lightLED(seq++);
    if (seq > 5) { seq = 0; }
    whenLastStep += stepTime;  // Constant time step.
  }
}

This subtle difference is explained very well by @edgar-bonet in his answer about polling a sensor at 100 Hz.

Also, Button::wasPressed() is prone to a negative blip occurring at the time of sampling the input pin, i.e. the blip will be interpreted as a continuous press of DebounceDelay (100 ms). Compare with this simple debouncer I put on GitHub.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.