BAM, или управляем кучей светодиодов

В статье пойдет речь о примере реализации BAM для плавного изменения яркости 22-х светодиодов. BAM — Bit Angle Modulation, подробности очень хорошо описаны тут bsvi.ru/bam-alternativa-shimu/

Начнем с того, что меня посетила весьма интересная идея подарка любимой девушке на 8 марта. Описывать не буду, скажу только, что это был светодиодный цветок :)
Решено было сделать его динамичным, применив многоцветные светодиоды. Жертвой выбора пала атмега8 (тем более, в крупном дип-корпусе), плясал от неё. Итак, в моем распоряжении находились 22 линии ввода-вывода. Немного порассуждав, решил, что для красивого внешнего вида и визуальных эффектов неплохо было бы использовать не менее десятка светодиодов, причем сколько-то из них должны быть обязательно RGB… в голову закрались сомнения о целесообразности такого решения, были идеи использвать сдвиговые регистры или подключать все в матрицу… Конец сомнениям положила покупка светодиодов — В наличии оказалось всего 3 RGB, 5 RG светодиодов в матовых корпусах, ну и докупил еще три синих :)
Этим делом нужно было как-то управлять, причем хотелось, чтобы все было плавно. Программный 8-ми битный ШИМ на 22 канала был отметен сразу, т.к. его частота получилась бы совсем маленькой, менее 100Гц. (Мега тактировалась от встроенной RC цепочки на 8Мгц, внешний кварц было лепить просто некуда). Итак, значит остается BAM.
Готовой реализации в Сети не нашел, пришлось изобретать велосипед.

На коде особо останавливаться не буду, все ясно из комментариев.
Конфигурируем таймер-счетчик 1 в режим CTC, включаем прерывание по совпадению с регистром OCR1A, будем с его помощью отсчитывать интервалы времени. При возникновении прерывания будем выводить заготовленную маску в порты, переконфигурировать таймер, сбрасывать счетчик и готовить маску для следующего вызова прерывания.


#include <avr/io.h>
#include <stdlib.h>
#include <avr/interrupt.h>
#include <util/delay.h>

//Global variables here
volatile unsigned char leds[22], direct[22]; //leds - непосредственно значения BAM для светодиодов, direct - направление изменения яркостей для эффектов
unsigned char mask_b, mask_c, mask_d;
unsigned char stage;

char compAArray[8][2] = { {0x4B, 0}, {0x96, 0}, {0x2C, 0x01}, {0x58, 0x02}, {0xB0, 0x04}, 
														{0x60, 0x09}, {0xC0, 0x12}, {0x80, 0x25} };
#define COMPA_HI 1
#define COMPA_LO 0

ISR(TIMER1_COMPA_vect)
{	
	//Паузу делаем после текущего бита. Т.е. если текущий - 7, пауза максимальная
	OCR1AH = compAArray[stage][COMPA_HI];
	OCR1AL = compAArray[stage][COMPA_LO];
	
	PORTB = mask_b;
	PORTC = mask_c;
	PORTD = mask_d;

	TCNT1 = 0;	//Обнуляем таймер-счетчик, иначе он будет продолжать считать до 0xFFFF
	
	//Следующее значение stage
	if (stage > 0)
	{
		stage --;
	} else
	{
		stage = 7;
	}
    mask_b = 0;
    mask_c = 0;
    mask_d = 0;
		
	//Вычисляем маски для следующего вызова прерывания
/************************************************/
	if( (leds[0] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 0);
	} 
/**************************************/		
	if( (leds[1] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 1);
	} 

/************************************************/
	if( (leds[2] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 2);
	} 
/**************************************/		
	if( (leds[3] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 7);
	} 		
/************************************************/		
	if( (leds[4] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 5);
	} 
/**************************************/		
	if( (leds[5] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 6);
	} 
/************************************************/		
	if( (leds[6] & (1 << stage)) != 0)
	{
		mask_c |= (1 << 5);
	} 
/**************************************/		
	if( (leds[7] & (1 << stage)) != 0)
	{
		mask_c |= (1 << 4);
	} 
/************************************************/		
	if( (leds[8] & (1 << stage)) != 0)
	{
		mask_c |= (1 << 3);
	} 
/**************************************/		
	if( (leds[9] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 3);
	} 
/************************************************/		
	if( (leds[10] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 4);
	} 
/**************************************/		
	if( (leds[11] & (1 << stage)) != 0)
	{
		mask_d |= (1 << 7);
	} 
/************************************************/		
	if( (leds[12] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 0);
	} 
/**************************************/		
	if( (leds[13] & (1 << stage)) != 0)
	{
		mask_c |= (1 << 2);
	} 
/************************************************/		
	if( (leds[14] & (1 << stage)) != 0)
	{
		mask_c |= (1 << 1);
	}
/**************************************/		
	if( (leds[15] & (1 << stage)) != 0)
	{
		mask_c |= (1 << 0);
	} 
/************************************************/		
	if( (leds[16] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 5);
	} 	
/**************************************/		
	if( (leds[17] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 4);
	} 
/************************************************/		
	if( (leds[18] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 3);
	} 
/**************************************/		
	if( (leds[19] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 6);
	} 
/************************************************/		
	if( (leds[20] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 2);
	} 
/**************************************/		
	if( (leds[21] & (1 << stage)) != 0)
	{
		mask_b |= (1 << 1);
	} 




}





void TimerInit(void)
{

	// Timer/Counter 1 initialization
	// Clock source: System Clock
	// Clock value: 8000,000 kHz
	// Mode: CTC top=OCR1A
	// OC1A output: Discon.
	// OC1B output: Discon.
	// Noise Canceler: Off
	// Input Capture on Falling Edge
	// Timer 1 Overflow Interrupt: On
	// Input Capture Interrupt: Off
	// Compare A Match Interrupt: Off
	// Compare B Match Interrupt: Off
	TCCR1A = 0x00;
	TCCR1B = 0x09;
	TCNT1H = 0x00;
	TCNT1L = 0x00;
	ICR1H  = 0x00;
	ICR1L  = 0x00;
	OCR1AH = 0x00;
	OCR1AL = 0x10;
	OCR1BH = 0x00;
	OCR1BL = 0x00;
	TIMSK  = 0x10;
}




int main(void)
{
	DDRB = 0xFF;
	DDRC = 0xFF;
	DDRD = 0xFF;
	TimerInit();
	stage = 7;
	mask_b = 0x00;
	mask_c = 0x00;
	mask_d = 0x00;
	
	unsigned char i, j, f = 0, mode = 0;
	for(i = 0; i < 22; i++)
		{
			leds[i] = 0;
			direct[i] = 0;
		}
	asm("sei");

	while(1)
	{

		switch(mode)
		{
			case 0:
				for(i = 0; i < 22; i++)
				{
					leds[i] = rand();
				}
		
				_delay_ms(500);

				break;
			case 1:  //плавно гасим все
				for(j = 0; j< 255; j++)
				{
					for(i = 0; i< 22; i++)
					{
						if (leds[i]>0) { f = 1; leds[i]--; }
					}
					if (f == 1)
					{
						_delay_ms(10);
						f = 0;
					}
				}
				break;
			
			case 2:  //плавно зажигаем все
				for(j = 0; j< 255; j++)
				{
					for(i = 0; i< 22; i++)
					{
						if (leds[i]<255) { leds[i]++; f = 1;}
					}
					if (f == 1)
					{
						_delay_ms(8);
						f = 0;
					}
				}
				break;

			case 3:  //плавно зажигаем случайно выбранные
				for(i = 0; i< 22; i++)
				{
					direct[i] = rand() % 2; //остаток от деления на 2 - четное/нечетное (1/0)
				}
				for(j = 0; j< 255; j++)
				{	
					for(i = 0; i< 22; i++)
					{
						if ( (leds[i]<255) && (direct[i]==1) ) { f = 1; leds[i]++; }
					}
					if (f == 1)
					{
						_delay_ms(10);
						f = 0;
					}
				}
				break;
			case 4:  //плавно гасим случайно выбранные
				for(i = 0; i< 22; i++)
				{
					direct[i] = rand() % 2; //остаток от деления на 2 - четное/нечетное (1/0)
				}
				for(j = 0; j< 255; j++)
				{	
					for(i = 0; i< 22; i++)
					{
						if ( (leds[i]>0) && (direct[i]==1) ) { f = 1; leds[i]--; }
					}
					if (f == 1)
					{
						_delay_ms(10);
						f = 0;
					}
				}
				break;
			case 5:  //Плавно зажигаем, потом тушим по очереди, зажигаем по очереди 
				
				for(j = 0; j< 255; j++) //Плавно зажигаем все
				{
					for(i = 0; i< 22; i++)
					{
						if (leds[i]<255) { leds[i]++; f = 1; }
					}
					if (f == 1)
					{
						_delay_ms(12);
						f = 0;
					}
				}


				for(i = 0; i< 22; i++)
				{
					direct[i] = rand() % 2; //остаток от деления на 2 - четное/нечетное (1/0)
				}
				for(j = 0; j< 255; j++)
				{	
					for(i = 0; i< 22; i++)
					{
						if ( (leds[i]>0) && (direct[i]==1) ) { f = 1; leds[i]--; }
					}
					if (f == 1)
					{
						_delay_ms(10);
						f = 0;
					}
				}
				break;
		}
		mode = rand() % 6;
	}

}


Код выкладываю приведенный в приличный вид и переделанный, оговорюсь — текущую версию в железе не проверял, впопыхах мог не усмотреть ошибки, критика приветствуется. Код написан в WinAVR. Надеюсь, что статья окажется полезной хоть кому-то :)

Статья обновлена, спасибо всем откликнувшимся за критику и советы. Отдельное спасибо Reobne и Fki.

UPD: Печатка и описание конструкции в аттаче.
  • +4
  • 09 апреля 2012, 09:52
  • chem_kot
  • 2
Файлы в топике: BAM.zip, led_tree.zip

Комментарии (37)

RSS свернуть / развернуть
и считай ни слова о том, как же оно работает
вот тут есть хорошее описание метода
bsvi.ru/bam-alternativa-shimu/
+2
В статье есть ссылка на ту статью :) Хм, она не подсветилась, спасибо, уже исправляю.
+1
теперь вижу, ага
0
а как функция rand() реализована? в стандартных библиотеках CodeVision? и как у нее с рандомностью?
0
Надо полагать стандартно. Не помню формулу, но она очень простая и при правильно подобранных коэффициентах дает достаточно равномерную последовательность. Стопроцентно предсказуемую, правда, но это заботит только криптографов.
0
И реализована она в данном случае в avr-libc, т.к. компилятор — GCC, а не CV.
0
Как вы посчитали, что мк на 8 МГц не сможет «реализовать» ШИМ 100 Гц для 22 каналов?
0
  • avatar
  • ZiB
  • 09 апреля 2012, 11:29
А чет упустил что хотели восемь бит получить (
На с другой стороны за чем так много?
Вот пример трех бит
www.getchip.net/posts/083-svetilnik-transformer-flexilight-na-ehffektore/
по моему нормально )
+1
Всё-таки у BAM больше преимуществ перед ШИМом, при одной тактовой частоте выше разрядность, требуется гораздо меньше прерываний для обработки, мерцание заметно меньше кроме случая с 50% скважностью) Лучше него, наверное, только сигма-дельта модуляция :)
0
делал ШИМ программный, на частоте атмеги 8мгц, как раз частота была 122гц. преимущество ВАМ только при большом объеме полезного кода который надо обрабатывать.
0
Мне кажется вот это:
mask_b = 0x00;
        mask_c = 0x00;
        mask_d = 0x00;

нужно перенести рядом с
TCNT1 = 0;      //Обнуляем таймер-счетчик, иначе он будет продолжать считать до 0xFFFF

А саму её перенести в самое начало обработчика таймера.
+1
То есть таймер сбрасывать сразу по срабатыванию, а маски сбрасывать перед их вычислениями.
0
Да, пожалуй, лучше так. Хотя, задержка там незначительная в отношении периода срабатывания. Сейчас исправлю.
0
В "//плавно зажигаем все"
не хватает «f = 1;»
0
вы поняли зачем нужна f? я что-то не въехал
имхо, задержку нужно делать на каждой итерации цикла (от 1 до 255), что бы было плавно, как в комментариях к коду написано.
Если выставить f один раз перед циклом, как вы предложили, то у светодиодов будет только две яркости: рандомная на старте (это где «case 0:») и через 500 мс финальная на каждом конкретном цикле (т.к. циклы, фактически, без задержки выставляют конечную яркость)
0
+ конечная яркость после циклов сразу же заменится на рандомную. По сути, диодики всегда будут гореть с рандомной яркостью, которая будет меняться раз в 500 мс.
0
f — признак того, что процесс не идёт в холостую. Т.е. если хотябы один светодиод меняет яркость. Например если два раза подряд выпадет режим «зажигания всех», то второй цикл прокрутится в холостую (светодиоды и так горят на полную), но без задержек, по быстрому.
0
спасибо за разъяснение, все понятно
я поторопился, смотрел именно тот кейс, где нет f = 1, поэтому не разобрался
0
+ конечная яркость после циклов сразу же заменится на рандомную. По сути, диодики всегда будут гореть с рандомной яркостью, которая будет меняться раз в 500 мс.
Это если режим 0 всё время выпадать будет, то да. :)
А если, например, в режиме 3, //плавно зажигаем случайно выбранные:
Заранее случайно выбранные будут «leds[i]++;», прибавляться по единичке, до предела. И если хоть один будет меняться, то "_delay_ms(10);" десять милисикунд будет светиться новое значение, и так далее. То есть плавно зажигаем.
0
небольшой импрувмент:
stage может быть индексом в массиве, значения которого буду инициализировать OCR1AH/OCR1AL:
char сompAInitializer[8][2] = { {0x4B, 0}, {0x96, 0}, {0x01, 0x2C} и т.д. };
#define COMPA_HI 1
#define COMPA_LO 0
Вместо свича:
OCR1AH = сompAInitializer[state][COMPA_HI];
OCR1AL = сompAInitializer[state][COMPA_LO];
+1
  • avatar
  • fki
  • 11 апреля 2012, 15:34
Да, всё правильно, так даже лучше. На досуге исправлю :)
По поводу эффектов — исправлю, не углядел! Спасибо за критику.
0
Я бы еще упомянул, что ВАМ лучше ШИМ-а только при количестве светодиодов больше 8-ми (при восьмибитном ВАМ и ШИМ), иначе в общем случае прерываний будет только больше. Суть в том, что мы выбрали такие интервалы, чтобы прерываний у нас всегда было не больше 8-ми за цикл таймера, а в ШИМ-е их количество может вырасти до 256.

Просто я когда первый раз столкнулся с ВАМ, сразу не понял, в чем смысл этой затеи, потому что быстренько прикинул в голове ситуацию для 3–4 диодов.
0
Попробовал реализовать указанный алгоритм для чего портировал его на attiny2313 и сократил количество каналов до 12 (мне нужно управлять 4 RGB LED лентами). И что-то не получается нормально…
При попытке плавно зажечь или погасить ленты белым цветом яркость меняется не плавно, а как то «толчками», т.е. сначала идет плавно примерно до 1/2 яркости, потом яркость резко падает (визуально примерно на 1/3) но не до конца и снова нарастает уже до полной. При плавном гашении — тоже самое.
Если сделать нарастание яркости или ее гашение очень медленным, то видно, что такие скачки яркости происходят всегда, когда идет переход к большему весу яркости ВАМ от меньших весов, т.е. например, было 0b00000111, стало 0b00001000 — скачек.
Яркость скачет тем сильнее чем больший вес включается, т.е. для более старших разрядов, особенно для последнего 7-го, вообще визуально происходит просто гашение ленты и снова ее плавное зажигание.
Я не могу понять в чем дело? То ли для коэффициенты неверные в программе, то ли это особенности тиньки.
Вобщем — ХЕЛП!!!
0
MIBAM вам поможет. я тут как раз статейку пишу, там упоминается.
но я нескоро допишу, посмотрите пока вот сюда www.picbasic.co.uk/forum/showthread.php?t=10564
0
да, времена лучше всего проверить анализатором. но MIBAM+двойная буферизвция точно помогут
0
решили проблему? у меня сначала тоже так же получилось на 2313, потом после пары часов мучений вспомнил про фуз CLKDIV8 — по умолчанию он активен, т.е. вместо 8мгц камень работает на 1. Сбросил — все стало плавно-шоколадно
0
Этот фьюз я сразу сбросил. Проблему решить не удалось, поэтому переписал код полностью и сделал просто ШИМ.
теперь все работает замечательно
0
Исправил статью, обновил код и прикрепленный файл :)
0
а фотку сего чуда? )) а схему )) а платку ) интересно же полюбоваться )_
0
перечитал, надеюсь нагло не звучит, а то хотел шутливо что бы выглядело, но как то корябает слух… так что если что не так, извиняюсь )
0
Плата осталась на домашнем ПК, он за сотни километров… как только попаду домой с учебы — выложу) фото не делал, идея слепить статью возникла уже потом, посчитал, что мой код должен кому-то пригодитья :) Тем более, с правками, коллективно еще более оптимизированный)
0
пригодится… а то меня жена сильно за увлечение электроникой пилит… надо что то придумать… а своих идей нету… придется чужие заимстовать )
0
Набрел, вспомнил, выложил печатку и описание конструкции. Несвоевременно, но вдруг пригодится
0
Спасибо!
0
Почему именно такие значения для таймера взяты? На 8МГц вроде 8мс получается один цикл… или я неправильно считаю?
0
0
производительность можно вдвое повысить, если использовать таймер с двумя сравнениями А и B.
Тогда CTC режим не нужен, и вычислять нужно значения сравнения. Половина диодов сидит на одном прерывании, половина на другом. Поскольку в ВАМ есть узкое место — единицы бит, то если разнести эти времена, то получится выигрыш ровно в 2 в эти моменты. Ну а когда старшие биты — понятное дело выигрыш несоизмеримо больше. По всем показателям перед ШИМом.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.