Нежелательные ореолы со светодиодами charlieplexed

У меня есть очень простая печатная плата, которая использует 3 GPIO на ATtiny85 для управления 6 светодиодами с помощью мультиплексирования.

Когда я пытаюсь запрограммировать определенные паттерны горящих светодиодов (псевдо ШИМ) У меня некоторые светодиоды горят тускло, когда они должны быть полностью выключены.

Пример, когда попеременно должны гореть только верхний или нижний три светодиода:

Video

Вы можете видеть, что светодиоды 2 и 5 тускло светятся, а не полностью выключены.

У меня нет проблем при последовательном освещении светодиодов по одному.

У меня нет проблем с попеременным освещением нечетных и четных светодиодов.

Схема такова:

schematic

Кнопки используются для переключения вверх и вниз через 6 режимов работы. Режим 0 последовательно загорает светодиоды по одному. Режим 1 чередует нечетные и четные светодиоды. Режим 2 освещает верхний или нижний три светодиода попеременно (и имеет проблему ореолов), другие режимы просто стабильно освещают соответствующий светодиод.

Код разбит на несколько файлов:

main.ino:

Button leftButton = Button(3);
Button rightButton = Button(4);
Sequencer sequencer = Sequencer(200); // мс на шаг
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;
  }
}

Генератор переменного тока.ино

class Alternator {

  private:
    unsigned long whenChanged = 0;
    int interval = 500; // мс
    byte pattern, pattern1, pattern2; // растровое изображение
    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);
    }

  }
  
};

Последовательность.ино

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 для светодиодов 0-5
const int lows[6]  = {1,0,1,2,0,2};

// Манипулирует 3 выходами GPIO для освещения одного из 6 светодиодов.
void lightLED(int i) {
  int h, l;     // pin для установки high, low
  if (i < 0 || i >= 6) { 
    h = 99; l = 99;  // все СВЕТОДИОДЫ выключены 
  } 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
    }
  }
}

Пуговицы.ино

class Button {
  
  private:
    int pin;
    const int DebounceDelay = 100;      // миллисекунды
    int lastState = HIGH;               // Предыдущее состояние кнопки
    unsigned long whenLastPressed = 0;  // Последнее нажатие кнопки
 
  public:
    Button(int pin) {
      this->pin = pin;
      pinMode(pin, INPUT_PULLUP);
    }

    // Был ли переход вверх (ВЫСОКИЙ) к вниз (НИЗКИЙ) с момента последнего вызова и все еще удерживался нажатым? 
    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;
    }
    
};

Я не уверен, где я ошибаюсь, есть какие-нибудь идеи?

, 👍2

Обсуждение

Я бы использовал прямой доступ к порту (но это не обязательно). И сначала установите все выходы нулевыми, затем все установите в качестве входов (если вы оставили какой-либо выход высоким и переключились в режим ввода, то будет включено внутреннее подтягивание). После этого вы можете установить режим вывода для правильных контактов и один из них как можно ВЫШЕ. (для трех контактов может быть достаточно просто СБРОСИТЬ все контакты, а затем установить правильный режим вывода и значения набора следующего цикла), @KIIV


2 ответа


Лучший ответ:

5

Код мультиплексирования не сбрасывает состояние неиспользуемого PINа, и если значение порта равно HIGH + mode INPUT, это означает, что подтягивание включено.

Решение состоит в том, чтобы сбросить все контакты до низкого уровня, прежде чем код начнет менять направление. И последним шагом будет установка правильного вывода на ВЫСОКИЙ уровень.

Если вы хотите сделать это в одном цикле, вы можете наблюдать действительно короткие импульсы (это может быть или не быть видно - на более высоких частотах это будет более очевидно).

Это также может быть намного быстрее, чем использование digitalWrite и pinMode, что-то вроде:

  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; // очистить выходные контакты
    DDRB  &= clrpins; // clear direction pins (0 == вход, 1 == выход)
    if (i < 6) {  // такое простое условие, если вы используете: uint8_t i
      DDRB  |= dirs[i]; // И установите правильные контакты в качестве выходов
      PORTB |= outs[i]; // И установите правильный контакт как высокий
    }
  }

Это не проверено, но это должно быть в значительной степени так

,

Этот код работает очень хорошо. Небольшое исправление, необходимое для моего конкретного заказа светодиода: constexpr uint8_t outs[] = { 0b001, 0b010, 0b100, 0b010, 0b100, 0b001};, @RedGrittyBrick

возможно, я делал это по нумерации светодиодов, а здесь и в исходном коде похоже, что 3-4 и 5-6 поменялись местами, @KIIV

Ах да, я поменял светодиоды при прокладке печатной платы, чтобы упростить маршрутизацию трека, и не обновил схему! Так что моя вина. :-), @RedGrittyBrick

Простое добавление digitalWrite(pin, 0) перед каждым pinMode(pin, INPUT) в моем коде исправило ореолы светодиодов в моем проекте charlieplexing Badgelife. Просто и спасибо за объяснение, пропустил проблему с подтягиванием в других документах., @haa


1

Вторичная проблема заключается в том, что использование millis() для обновления временных меток приведет к кумулятивному перетаскиванию интервала.

Чтобы сохранить интервал постоянным, обновите:

  • При изменении с интервалом
  • whenLastStep с stepTime.

Генератор переменного тока.ино

void check() {

  if ((millis() - whenChanged) > interval) {
    if (pattern == pattern1) { pattern = pattern2; }
    else                     { pattern = pattern1; }
    whenChanged += interval;  // Постоянный интервал.
  }

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

}

Последовательность.ино

void check() {
  if ((millis() - whenLastStep) > stepTime) {
    lightLED(seq++);
    if (seq > 5) { seq = 0; }
    whenLastStep += stepTime;  // Постоянный шаг по времени.
  }
}

Эта тонкая разница очень хорошо объясняется @edgar-bonet в его ответе об опросе датчика на частоте 100 Гц.

Кроме того, Button::wasPressed() подвержен отрицательному всплеску, возникающему во время выборки входного контакта, т. е. всплеск будет интерпретироваться как непрерывное нажатие DebounceDelay (100 мс). Сравните с этим простым дебоунсером, который я поставил на GitHub.

,