понедельник, 22 октября 2012 г.

Программный ШИМ

Программный ШИМ

Своя версия реализация appnote AVR136 для управления RGBW светодиодами.

Собрался я как-то сделать себе "утреннюю" подсветку дабы не столь лениво было просыпаться по утрам. Для начала была закуплена RGB лента с ИК контроллером в одном из китайских магазинов. Её ИК приёмник был прикручен к компьютерному передатчику и, посредством lirc начала будить меня по утрам, имитируя утро. Но уж очень мне не нравилось, что нельзя нормально выставить яркость или хотя бы зажечь ленту с "запомненного" состояния — только встроенные яркости, которые слишком яркие. И, как на беду, валялась у меня на столе Arduino Nano и несколько полевых транзисторов из условно-бесплатных образцов. "А почему-бы не попробовать?", — подумалось мне…

Была сделана печатная плата с ИК приёмником, силовыми транзисторами, RS485 портом (а так, побаловаться и попробовать) и панелькой под Nano… Не буду расписывать эпопею с 485, библиотекой IRremote и прочим, а остановлюсь только на светодиодах.

Само собой, экспериментируя с Arduino, невозможно не воспользоваться её ШИМ функциями (analogWrite)… Вот тут меня поджидал первый сюрприз — отведённая мною для светодиода "нога" оказалась принудительно-необходима для IRremote. Сам виноват — не изучил вопрос перед изготовлением платы… Ну да ладно, поменял "ногу", домучил RS232/485 обмен и, наконец, подключил к плате светодиодную ленту вместо тестовых одиночных диодов.

(трагическая мелодия)

А они, заразы, моргают! Противненько так, подмаргивают… Таки пришлось разбираться, как же работает "железо". Выяснилось, что, мало того, что таймеры, отвечающие за ШИМ работают в разных режимах, так ещё и на разных частотах…

Далее начались поиски и эксперименты — благо, под рукой оказался одинокий микроконтроллер с макетной платой, avr-gcc и программатор… Были испробованы несколько версий BAM — вроде выгодно, не так часто надо дёргать прерывания, но так и не удалось добиться правильного и плавного перехода цветов на границах степени двойки (2, 4, 8, 16, 32…). И таки пришлось вернуться к традиционному программному ШИМу.

В итоге был получен нижеприведённый код. Как оказалось, он почти повторяет пример из AVR136. Исключение — способ вычисления управляющей маски. Если сбрасывать маску "в ноль" по окончании PWM цикла, как предложено в примере, — моргает! Ну и экспериментально опробованы частоты ШИМа, когда мерцание не заметно. Как и предполагалось, они оказались ниже "стандартной" частоты ШИМ arduino. Оптимальной выбрана частота примерно 163Гц, но рабочими оказались и другие частоты ниже 200Гц.

Итак, общие комментарии к коду:
  • Используется штатное прерывание ШИМ, отвязанное от аппаратных выводов
  • Фактически через настройки ШИМ запускается таймер по переполнению, а внутри обработчика прерывания считается "искусственный" ШИМ
  • У меня уже разведена плата и выходы оказались в разных портах контроллера, что привело к удвоению промежуточных переменных
  • Я использую много файлов в проекте Wiring, это один из таких файлов, а не кусок скетча. В скетч оно включается обычным #include 
  • У меня управление ведётся через внешние MOSFET-ы, потому сигнал получается инвертированным. Подключённые напрямую к "+" диоды будут гореть при яркости 0 и гаснуть при 255


// file: pwm.h
#ifndef __PWM_H__
#define __PWM_H__

#include <"avr/interrupt.h">

// User-changeable variables
volatile byte R_color;
volatile byte G_color;
volatile byte B_color;
volatile byte W_color;

// PORTB
#define WPIN   _BV(1)
#define RPIN    _BV(2)

// PORTD
#define GPIN    _BV(5)
#define BPIN    _BV(6)

// RGBW buffer
volatile byte rgbw_buf[4];

ISR(TIMER1_COMPA_vect) {
  static byte _pwm_cntr=0;
  byte mask1 = 0;
  byte mask2 = 0;

 // reset every 256 cycles  
  if (++_pwm_cntr == 0) {
    rgbw_buf[0] = R_color;
    rgbw_buf[1] = G_color;
    rgbw_buf[2] = B_color;
    rgbw_buf[3] = W_color;
    // from AppNote avr136. Flickers
    //    mask1 = 0;
    //    mask2 = 0;
  }
  
  // calc on/off state
  if (rgbw_buf[0] <= _pwm_cntr) mask1 |= RPIN;
  if (rgbw_buf[3] <= _pwm_cntr) mask1 |= WPIN;
  if (rgbw_buf[1] <= _pwm_cntr) mask2 |= GPIN;
  if (rgbw_buf[2] <= _pwm_cntr) mask2 |= BPIN;

  // Set ports
  PORTB |= (RPIN|WPIN) & ~mask1;
  PORTB &= ~mask1;
 
  PORTD |= (GPIN|BPIN) & ~mask2;
  PORTD &= ~mask2;
  
 }

// call once from setup()
void pwm1_init() {
    TCCR1A = 0;
    TCCR1B = _BV(WGM12)|_BV(CS11); // STC (OCR1A overflow) mode, 1/8x clock
    OCR1A = 0x27; // Tested minimal flicker divider
    TIMSK1 = _BV(OCIE1A); // Enable interrupt
}

// call to set all RGB[W] values
void pwm(byte r, byte g, byte b, byte w=0) {
  R_color = r;
  G_color = g;
  B_color = b;
  W_color = w;
}
#endif __PWM_H__

PS: Через некоторое время приедут радиомодули NRF24L01+ и есть немалый шанс начать всё сначала, но уже с добавлением радиоканала ;) (или заменой RS485 на него)
PPS: Таки оказалось всё непросто. Эксперимент показал, что на минимальных значениях таки есть моргание. Пришлось уменьшить делитель таймера и экспериментально подобрать OCR1A.

Комментариев нет:

Отправить комментарий