Программный ШИМ
Своя версия реализация 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.
Комментариев нет:
Отправить комментарий