среда, 19 апреля 2017 г.

Простой индикатор уровня звука на WS2812B. Программа для Arduino Pro Mini.

Простой индикатор уровня звука на WS2812B. Программа для Arduino Pro Mini.

Всем привет :)

В предыдущей статье я показывал как подключить источник аудиосигнала к индикатору уровня звука. Но чтобы индикатор «заиграл», этого недостаточно. Поэтому в этот раз я расскажу о программной части устройства. И чтобы любой мог повторить/модернизировать VU-метр - приложу программный код для Arduino Pro Mini к статье и объясню как он работает. Поехали.

Для более лёгкого представления работы индикатора уровня звука предлагаю ознакомиться с блок-схемой программы:

Блок-схема работы простого индикатора уровня звука на WS2812B и Arduino Pro Mini.

Заметка: Блог не поддерживает подсветку программного кода, что, конечно же, меня расстроило. Как жить дальше?) На просторах интернета удалось найти решение проблемы. Попытка применения этого решения стоила мне заблокированного аккаунта, двух заблокированных блогов, созданных для экспериментов, и потраченного вечера. Мда... возможно, я что-то делал не так)) В итоге всё без проблем получилось с сервисом Gist от GitHub.
P.S. Первое решение заработало только на третьем блоге, но мне больше понравился сервис Gist))

Теперь, глядя на блок-схему, можно разбираться с самой программой. Первым делом, до зацикливания программы, контроллер нужно настроить. Т.к. я использую среду Arduino не совсем стандартным способом, то необходимо сбросить настройки контроллера, которые среда делает скрытно от пользователя скетча:

void reset_arduino_settings()
{
// Таймер 0
TCCR0B = 0x00; // сброс
TCCR0A = 0x00; // настроек
TIMSK0 = 0x00; // запрет прерываний
TCNT0 = 0x00; // сброс счётного регистра
TIFR0 = 0xFF; // очистка флагов прерываний
// Таймер 1
TCCR1B = 0x00; // сброс
TCCR1A = 0x00; // настроек
TIMSK1 = 0x00; // запрет прерываний
TCNT1 = 0x00; // сброс счётного регистра
TIFR1 = 0xFF; // очистка флагов прерываний
// Таймер 2
TCCR2B = 0x00; // сброс
TCCR2A = 0x00; // настроек
TIMSK2 = 0x00; // запрет прерываний
TCNT2 = 0x00; // сброс счётного регистра
TIFR2 = 0xFF; // очистка флагов прерываний
// АЦП
ADCSRA = 0; // сброс настроек и выключение АЦП
// USART
UCSR0B = 0x00;
#if !defined(DEBUG_UART)
UCSR0A = 0x00;
UCSR0C = 0x00;
UBRR0H = 0x00;
UBRR0L = 0x00;
#endif
}

Как я уже говорил, нам необходимо 3 канала АЦП: опорный – для измерения постоянной составляющей и два канала для измерения аудиосигналов. Поэтому далее инициализируем АЦП микроконтроллера:

void ADC_init()
{
// настройка ножек-каналов АЦП:
// перевод ножек в состояние Hi-z
io_hiz(ADC_REF); // Arduino A0
io_hiz(ADC_CH1); // Arduino A1
io_hiz(ADC_CH2); // Arduino A2
// настройка АЦП:
// источник опорного напряжения: AVCC;
// ADLAR = 0 => измеренное значение берём из ADCL и ADCH<<8
ADMUX = (1<<REFS0);
// вкл. АЦП; запускаем преобразования; частота АЦП: F/128
ADCSRA = (1<<ADEN)|(1<<ADSC)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);
}
Для улучшения точности измерений частоту работы АЦП уменьшаем до минимально возможной: 125 кГц. Будем использовать все 10 бит, выдаваемых преобразователем. Обратите внимание, что в качестве максимально возможного измеренного значения берётся напряжение с ножки AVCC (аналоговое питание контроллера), а не AREF, как это указано на схеме из предыдущей статьи:

Схема подключения аудиоканалов к индикатору уровня звука на WS2812B и Arduino Pro Mini.

Это сделано для упрощения подключения сигналов к Arduino. Ножка AVCC объединена с VCC, поэтому и подключать её уже не нужно. А ножка AREF в Arduino Pro Mini никуда не выведена, а лишь подключена на общий провод через конденсатор для уменьшения помех во время измерений АЦП.

Для передачи кода цветов на умные светодиоды я захотел использовать аппаратный интерфейс SPI. Как выяснилось, из-за жёстких таймингов прерывать передачу нельзя, а хочется)... хочется в это время получать измеренные значения с АЦП. Поэтому сделано наоборот: передача очередного значения на WS2812B прерывает выполнение основной программы.

void SPI_init()
{
// Настройка ножек SPI:
io_out(SPI_MOSI); // Arduino 11
io_out(SPI_CS); // Arduino 10
// включаем SPI, работаем как MASTER
// 1 или 3 режим SPI (чтобы сигнал MOSI был в 0, когда нет сигнала)
SPCR = (1<<SPE)|(1<<MSTR)|(1<<CPHA);
// работаем на 8 MHz (1 байт SPI = 1 бит кодировки цвета)
SPSR = (1<<SPI2X);
}
Прерывание от SPI разрешается непосредственно перед передачей первого байта и запрещается - после последнего. Очень важно, чтобы после передачи на шине MOSI был логический ноль, иначе светодиоды "запутаются" в принятых данных.

Вопрос: Если не настроить ножку 10 Arduino (аппаратный сигнал CS => PORTB 2) на выход или вход с подтяжкой, то передача по SPI не работает. При программировании контроллеров Atmel в IAR с таким не сталкивался. Возможно у кого-то есть ответ на такое поведение Arduino :)

SPI настраивается на максимальную частоту 8 МГц, равную половине тактовой частоты контроллера. В этом случае 1 бит цвета можно закодировать 1 байтом. И получим скорость передачи 1 Мбит информации о цвете в секунду. Таким образом можно рассчитать время, которое требуется для разовой передачи данных на все светодиоды. Один умный светодиод требует 24 бит - у меня их 64 штуки. Тогда на одну передачу уйдёт времени: 24 бит * 64 / 1 Мбит/с = 1.536 мс.

Эксперименты показали, что времени уходит в 5 с половиной раз больше, а именно 8.5 мс. Всему виной оказались прерывания - очень много времени теряется на вход в подпрограмму обработки прерывания и выход из неё. Это создавало сложности в виде ограничения времени нахождении в прерывании, но их удалось разрешить. Также я исследовал передачу по SPI без прерываний, на неё потребовалось 2.2 мс. Это уже ближе к теории :). "Почему обязательно нужны прерывания?" - спросите Вы. Не нужны, также как и не обязательно использовать SPI. С таким же успехом можно применить UART или просто программно дёргать ножкой. В программе простого VU-метра я даже заложил возможность выбора режима передачи по SPI с прерываниями и без. Это можно сделать с помощью макроса TRANSMISSION_INTERRUPT_DATA. Так что можно поэкспериментировать.

АЦП и SPI настроили - можно измерять сигналы, обрабатывать данные и передавать закодированные цвета на умные светодиоды WS2812B - да, всё верно. Вот только не хватает периодичности передачи - без таймера не обойтись :). Нам хватит 8-битного таймера, например, возьмём второй и настроим:

#define TIME_UPDATE_LEDS (float)10.0 // мс
void timer2_init()
{
TCCR2B = 0x00; // остановка таймера
TCNT2 = 0x00; // сброс счётного регистра
OCR2A = (uint32_t)((float)(QUARZ/1024)*TIME_UPDATE_LEDS)/1000-1; // коэф. периода прерываний
TCCR2A = (1<<WGM21); // режим: CTC (сброс при совпадении)
TIFR2 = 0xFF; // очистка флагов прерываний
TIMSK2 = (1<<OCIE2A); // прерывание по совпадению с OCR2A
TCCR2B = (1<<CS22)|(1<<CS21)|(1<<CS20); // делитель 1024 и старт таймера
}
Снова прерывания. И опять можно обойтись без них, т. к. временная точность не нужна. Но так захотелось, и, возможно, кому-то это пригодится. Таймер после прерывания перезапускается автоматически, поэтому дополнительно что-то настраивать не придётся. Выбранный делитель 1024 даёт возможность настроить периодичность прерывания от 1 до 16 мс. Это задаётся макросом TIME_UPDATE_LEDS. Кстати, период прерывания будет точным, если значение кратно числу 1.6. Это значение нужно выбрать так, чтобы прерывание не вклинилось в передачу по SPI. То есть время должно быть больше времени, затрачиваемого на полную посылку. У нас она занимает 8.5 мс, поэтому выбрано значение 10.0. Необходимо ещё учесть, что перед отправкой данные нужно подготовить, поэтому сделал запас.

Ну что ж, всё настроено, теперь начинается самое интересное - двигаемся дальше :). После подачи питания в контроллеры умных светодиодов может попасть какой-то сигнал - "мусор". И чтобы мы этого не заметили, после инициализации нужно затушить все светодиоды:

#define LOGIC_1 0xFC // 1 = ¯¯¯|_ = 0.75 мкс + 0.25 мкс
#define LOGIC_0 0xC0 // 0 = ¯|___ = 0.25 мкс + 0.75 мкс
#define BASE_COLOR_NUMBER 3 // кол-во базовых цветов (R, G и B)
#define CHANNEL_NUMBER 2 // кол-во каналов
#define LEDS_CH_NUMBER 32 // кол-во светодиодов на канал
#define LEDS_NUMBER (LEDS_CH_NUMBER*CHANNEL_NUMBER) // кол-во светодиодов
#define ELEMENTS_NUMBER (BASE_COLOR_NUMBER*LEDS_NUMBER) // кол-во байт цветов
void Leds_Off()
{
for(uint16_t bit_i=0; bit_i<8*ELEMENTS_NUMBER; bit_i++)
{
SPDR = LOGIC_0;
while(!(SPSR & (1<<SPIF)));
}
}
view raw WS2812B_off.cpp hosted with ❤ by GitHub
В цикле для каждого из 64-х светодиодов передаётся по 8*3 бита. А так как 1 бит информации о цвете кодируется одним байтом (макросы LOGIC_1 и LOGIC_0), то необходимо передать 8*3 байт для одного светодиода. В качестве полного количества элементов, которое нужно передать на светодиоды использую количество байт цветов. То есть в качестве базиса взят байт цвета, что очень удобно.

Инициализация завершена, можно приступить к основной части программы. Во-первых, это периодическое считывание данных с АЦП для каждого из трёх каналов:

enum ADC_channels
{
ADC_REF_CASE = 0x00,
ADC_CH1_CASE = 0x01,
ADC_CH2_CASE = 0x02,
};
uint16_t adc_ch, ref_ch;
uint8_t adc_read_value()
{
static uint8_t channel_adc;
if(ADCSRA & (1<<ADIF))
{
switch(channel_adc)
{
case ADC_REF_CASE:
ref_ch = ADCL;
ref_ch |= (ADCH<<8);
ADMUX |= ADC_CH1_CASE;
ADCSRA |= (1<<ADSC);
channel_adc = ADC_CH1_CASE;
return ADC_REF_CASE;
case ADC_CH1_CASE:
adc_ch = ADCL;
adc_ch |= (ADCH<<8);
ADMUX &= ~ADC_CH1_CASE;
ADMUX |= ADC_CH2_CASE;
ADCSRA |= (1<<ADSC);
channel_adc = ADC_CH2_CASE;
return ADC_CH1_CASE;
case ADC_CH2_CASE:
adc_ch = ADCL;
adc_ch |= (ADCH<<8);
ADMUX &= ~ADC_CH2_CASE;
ADCSRA |= (1<<ADSC);
channel_adc = ADC_REF_CASE;
return ADC_CH2_CASE;
default:
ADMUX &= ~(ADC_CH1_CASE|ADC_CH2_CASE);
ADCSRA |= (1<<ADSC);
channel_adc = ADC_REF_CASE;
}
}
return ADC_REF_CASE;
}

Последовательно по готовности узнаём отсчёты канала опорного напряжения, первого и второго каналов аудиосигнала. Функция возвращает номер канала, значение которого было получено от АЦП.

Во-вторых, если есть измеренные значения, то их можно куда-то сложить, обработать. Этим занимается следующая функция:

#define FILTER_MEAS_NUMBER 32 // кол-во значений в фильтре (кратное степени 2 для быстрых вычислений)
uint32_t filter_ch1[FILTER_MEAS_NUMBER], // массив квадратов измеренных значений для канала 1
filter_ch2[FILTER_MEAS_NUMBER]; // массив квадратов измеренных значений для канала 2
bool filter_full_ch1 = false, // флаг заполнения массива для канала 1
filter_full_ch2 = false; // флаг заполнения массива для канала 2
uint8_t index_ch1 = 0, // индекс заполнения массива для канала 1
index_ch2 = 0; // индекс заполнения массива для канала 2
uint32_t sum_ch1 = 0, // сумма всех значений массива для канала 1
sum_ch2 = 0; // сумма всех значений массива для канала 2
void calculate_value(uint8_t channel_No)
{
uint32_t x;
// избавление от постоянной составляющей:
if(adc_ch > ref_ch) adc_ch = adc_ch-ref_ch;
else adc_ch = ref_ch-adc_ch;
// вычисление квадрата измеренного значения:
x = adc_ch*adc_ch;
// для канала 1
// добавление квадрата измеренного значения в массив фильтрации и
// вычисление суммы всех значений массива с учётом нового значения:
if(channel_No == ADC_CH1_CASE)
{ // если массив заполнен, то замещаем самое старое значение в сумме
// иначе просто добавляем новое значение в сумму
if(filter_full_ch1 == true) sum_ch1 += x - filter_ch1[index_ch1];
else sum_ch1 += x;
filter_ch1[index_ch1] = x;
index_ch1++;
if(index_ch1 >= FILTER_MEAS_NUMBER){ index_ch1=0, filter_full_ch1=true; }
}
// аналогично для канала 2
else if(channel_No == ADC_CH2_CASE)
{
if(filter_full_ch2 == true) sum_ch2 += x - filter_ch2[index_ch2];
else sum_ch2 += x;
filter_ch2[index_ch2] = x;
index_ch2++;
if(index_ch2 >= FILTER_MEAS_NUMBER){ index_ch2=0, filter_full_ch2=true; }
}
}
view raw calculate1.cpp hosted with ❤ by GitHub

В коде есть комментарии, но на всякий случай поясню, что конкретно тут выполняется. Наш индикатор будет отображать среднеквадратичный уровень сигнала. Поэтому здесь мы вычисляем квадрат измеренного значения. Этот квадрат кладём в ячейку массива и попутно вычисляем сумму всех значений массива. Сумма квадратов нам нужна, чтобы потом (когда это будет необходимо) быстро вычислить среднеквадратичное значение, а массив нужен для того, чтобы потом вычитать из суммы неактуальные более значения (что ускорит процесс вычисления) и заполнять его новыми квадратами.

Сразу же, раз об этом упомянул, опишу вычисление среднеквадратичного значения. Тут всё просто, вычисляем среднее от суммы квадратов для обоих каналов, а затем вычисляем квадратный корень также для обоих каналов:

uint16_t sqrt_u16(uint16_t x)
uint16_t sqrt_u32(uint32_t arg)
/***************************************************************************/
/* Обработка накопленных значений массива (вычисление среднеквадратичного
значения). Получаем количество светодиодов, которое нужно зажечь
Передача значений происходит через входные параметры (указатели) */
/***************************************************************************/
void processing_values(uint16_t* calc_value1, uint16_t* calc_value2)
{
uint32_t filter_value_ch1,
filter_value_ch2;
if(filter_full_ch1 == true) filter_value_ch1 = sum_ch1/FILTER_MEAS_NUMBER;
else filter_value_ch1 = sum_ch1/(index_ch1+1);
if(filter_full_ch2 == true) filter_value_ch2 = sum_ch2/FILTER_MEAS_NUMBER;
else filter_value_ch2 = sum_ch2/(index_ch2+1);
if(filter_value_ch1 > 0xFFFF) filter_value_ch1 = sqrt_u32(filter_value_ch1);
else filter_value_ch1 = sqrt_u16(filter_value_ch1);
if(filter_value_ch2 > 0xFFFF) filter_value_ch2 = sqrt_u32(filter_value_ch2);
else filter_value_ch2 = sqrt_u16(filter_value_ch2);
*calc_value1 = filter_value_ch1;
*calc_value2 = filter_value_ch2;
}
/***************************************************************************/
/* Вычисление квадратного корня для значений до 2 байт включительно */
/***************************************************************************/
uint16_t sqrt_u16(uint16_t x)
{
uint16_t m, y, b;
m = 0x4000;
y = 0;
if(x!=0)
{
while (m!=0)
{
b = y|m;
y = y>>1;
if(x>=b)
{
x = x-b;
y = y|m;
}
m = m>>2;
}
}
return y;
}
/***************************************************************************/
/* Вычисление квадратного корня для значений до 4 байт включительно */
/***************************************************************************/
uint16_t sqrt_u32(uint32_t arg)
{
uint8_t count;
uint32_t res, tmp;
count=16;
res=0;
tmp=0;
if(arg!=0)
{
if(!(arg&0xFF000000)) { arg<<=8;count-=4; }
res=1;
while((tmp<1)&&(count))
{
count--;
if(arg&0x80000000UL)tmp|=2;
if(arg&0x40000000UL)tmp|=1;
arg<<=2;
};//поиск первой 1-ы
tmp--;
for(;count;count--)
{
tmp<<=2;
res<<=1;
if(arg&0x80000000UL)tmp|=2;
if(arg&0x40000000UL)tmp|=1;
arg<<=2;
if( tmp>=((res<<1)|1))
{
tmp-=((res<<1)|1);
res|=1;
}
}
}
return (uint16_t)res;
}
view raw calculate2.cpp hosted with ❤ by GitHub

Отличие вычислений только в заполненности массива фильтрации и величине значения суммы квадратов. С заполненностью всё ясно - тут просто разный делитель. А что касается функции вычисления квадратного корня, то тут 2 нюанса. Стандартная функция, которая работает с типом float, требует для себя длительного времени вычисления, поэтому решено было использовать приближённые методы вычисления квадратного корня. Для более быстрого вычисления решил применить 2 функции, оптимизированные под размер значения. Одна работает только с числами размером до 2-х байт, включительно, а вторая до 4-х байт, включительно. 

В-третьих, необходимо подготовить данные для отправки на светодиоды. Это самая "творческая" функция, потому как от неё зависит в каком виде будет отображаться уровень звука на умных светодиодах. У нас простой VU-метр, поэтому способ отображения только один:

/******************************************************************************/
/* Выбор очерёдности отображения каналов: 1->2 или 2->1.
Чем меньше значение, тем выше приоритет отображения канала */
/******************************************************************************/
#define DISPLAY_PRIORITY_CH1 0 // канал 1
#define DISPLAY_PRIORITY_CH2 1 // канал 2
/******************************************************************************/
/* Прямой или обратный порядок отображения светодиодов в каждом из каналов
(0 - обратный, любое значение кроме 0 - прямой) */
/******************************************************************************/
#define DIRECTION_LEDS_CH1 1 // канал 1
#define DIRECTION_LEDS_CH2 1 // канал 2
/******************************************************************************/
/* Настройка порядка цветов в таблице. В светодиодах WS2812B по умолчанию GRB.
GRB == 012 -> RGB == 102. Для изменения порядка цветов в таблицах нужно
менять макросы COLOR1, COLOR2, COLOR3 */
/******************************************************************************/
#define RED 1
#define GREEN 0
#define BLUE 2
#define COLOR1 RED
#define COLOR2 GREEN
#define COLOR3 BLUE
uint8_t leds_buf[ELEMENTS_NUMBER]; // основной буфер для передачи значений на светодиоды
uint8_t base_color[BASE_COLOR_NUMBER] = {COLOR1, COLOR2, COLOR3}; // порядок базовых цветов
// таблица цветовых значений для столбиков
const uint8_t user_multicolored_table[BASE_COLOR_NUMBER][LEDS_CH_NUMBER] PROGMEM =
{
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
};
/******************************************************************************/
/* Сформировать буфер для передачи значений светодиодам */
/******************************************************************************/
void set_leds_buf()
{
// вспомогательные буферы для формирования столбиков
static uint8_t elements_ch1[ELEMENTS_CH_NUMBER],
elements_ch2[ELEMENTS_CH_NUMBER];
// количество светящихся светодиодов без обработки
uint16_t calc_value_ch1,
calc_value_ch2;
// количество светящихся светодиодов после обработки
static uint8_t led_on_ch1,
led_on_ch2;
uint8_t led_index = 0, // индекс светодиода
color_index = 0, // индекс цвета светодиода
element_index = 0; // индекс байта для передачи по SPI на светодиоды
/******************************************************************************/
/* Вычисление количества светящихся светодиодов.
значения calc_value_ch1 и calc_value_ch2 */
/******************************************************************************/
processing_values(&calc_value_ch1, &calc_value_ch2);
// количество светящихся светодиодов для каналов 1 и 2
if(calc_value_ch1 > LEDS_CH_NUMBER) led_on_ch1 = LEDS_CH_NUMBER;
else led_on_ch1 = calc_value_ch1;
if(calc_value_ch2 > LEDS_CH_NUMBER) led_on_ch2 = LEDS_CH_NUMBER;
else led_on_ch2 = calc_value_ch2;
/******************************************************************************/
/* Формирование разноцветных столбиков */
/******************************************************************************/
// для канала 1
element_index = 0;
for(led_index=0; led_index<LEDS_CH_NUMBER; led_index++)
{
if(led_index<led_on_ch1)
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
elements_ch1[element_index+color_index] = pgm_read_byte(&user_multicolored_table[base_color[color_index]][led_index]);
}
else
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
elements_ch1[element_index+color_index] = 0;
}
element_index += BASE_COLOR_NUMBER;
}
// для канала 2
element_index = 0;
for(led_index=0; led_index<LEDS_CH_NUMBER; led_index++)
{
if(led_index<led_on_ch2)
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
elements_ch2[element_index+color_index] = pgm_read_byte(&user_multicolored_table[base_color[color_index]][led_index]);
}
else
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
elements_ch2[element_index+color_index] = 0;
}
element_index += BASE_COLOR_NUMBER;
}
/******************************************************************************/
/* Выбор очерёдности отображения каналов: 1->2 или 2->1.
Прямое или обратное отображение светодиодов в каждом из каналов */
/******************************************************************************/
#if DISPLAY_PRIORITY_CH1 <= DISPLAY_PRIORITY_CH2
// Сначала канал 1
element_index = 0;
for(led_index=0; led_index<LEDS_CH_NUMBER; led_index++)
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
#if DIRECTION_LEDS_CH1 == 0
leds_buf[element_index+color_index] = elements_ch1[ELEMENTS_CH_NUMBER-element_index+color_index-BASE_COLOR_NUMBER];
#else
leds_buf[element_index+color_index] = elements_ch1[element_index+color_index];
#endif
element_index += BASE_COLOR_NUMBER;
}
// затем канал 2
element_index = 0;
for(led_index=0; led_index<LEDS_CH_NUMBER; led_index++)
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
#if DIRECTION_LEDS_CH2 == 0
leds_buf[ELEMENTS_CH_NUMBER+element_index+color_index] = elements_ch2[ELEMENTS_CH_NUMBER-element_index+color_index-BASE_COLOR_NUMBER];
#else
leds_buf[ELEMENTS_CH_NUMBER+element_index+color_index] = elements_ch2[element_index+color_index];
#endif
element_index += BASE_COLOR_NUMBER;
}
#else
// Сначала канал 2
element_index = 0;
for(led_index=0; led_index<LEDS_CH_NUMBER; led_index++)
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
#if DIRECTION_LEDS_CH2 == 0
leds_buf[element_index+color_index] = elements_ch2[ELEMENTS_CH_NUMBER-element_index+color_index-BASE_COLOR_NUMBER];
#else
leds_buf[element_index+color_index] = elements_ch2[element_index+color_index];
#endif
element_index += BASE_COLOR_NUMBER;
}
// затем канал 1
element_index = 0;
for(led_index=0; led_index<LEDS_CH_NUMBER; led_index++)
{
for(color_index=0; color_index<BASE_COLOR_NUMBER; color_index++)
#if DIRECTION_LEDS_CH1 == 0
leds_buf[ELEMENTS_CH_NUMBER+element_index+color_index] = elements_ch1[ELEMENTS_CH_NUMBER-element_index+color_index-BASE_COLOR_NUMBER];
#else
leds_buf[ELEMENTS_CH_NUMBER+element_index+color_index] = elements_ch1[element_index+color_index];
#endif
element_index += BASE_COLOR_NUMBER;
}
#endif
}

Разберём функцию void set_leds_buf(). Для начала, об этом я писал чуть выше, вычисляется для обоих каналов среднеквадратичное значение, которое говорит о том, сколько светодиодов должно зажечься. Далее, во вспомогательные буферы записываются яркости светодиодов, которые будут светиться. Яркости берутся из таблицы: user_multicolored_table (по сути двумерный массив). Столбец отвечает за номер светодиода канала, а строка за цвет. Порядок цветов можно задать с помощью макросов COLOR1COLOR2COLOR3. Сейчас установлен общепринятый порядок: RGB. 

Заметка: Стандартный порядок цветов (то есть последовательность байт, которую нужно передать) у светодиодов WS2812B: GRB. Чтобы не путаться тем, кто использует GRB и тем, кто привык к RGB, решил сделать возможность быстрой смены порядка отправки цветов.

Во время выполнения программы таблица эта хранится в энергонезависимой flash памяти, что экономит ОЗУ и не вызовет никаких проблем, если мы захотим создать много таких цветовых таблиц.

После того как цветовые буферы сформированы, остаётся их объединить в один массив и передать на светодиоды. И тут я предусмотрел ещё небольшое удобство, и связано оно вот с чем: от того как будут объединяться буферы зависит в каком порядке будут следовать каналы (первый -> второй или второй -> первый) и в каком порядке будут следовать светодиоды в каждом из каналов (в прямом или обратном). Всё это можно задать с помощью макросов: DISPLAY_PRIORITY_CH1 и DISPLAY_PRIORITY_CH2 (порядок отображения каналов), DIRECTION_LEDS_CH1 и DIRECTION_LEDS_CH2 (порядок отображения светодиодов в канале).

Приближаемся к финишу. В-четвёртых, осталось передать сформированные данные. Покажу только вариант с прерыванием по SPI (вариант без использования прерывания отличается незначительно). Он состоит из функции, которая запускает передачу и разрешает прерывание по SPI и непосредственно самого прерывания:

/******************************************************************************/
/* Старт передачи значений светодиодам */
/******************************************************************************/
void send_data()
{
SPCR |= (1<<SPIE); // передаём данные в прерывании
SPDR = 0x00;
}
/******************************************************************************/
/* Непосредственно передача значений светодиодам в прерывании */
/******************************************************************************/
ISR(SPI_STC_vect)
{
static uint8_t bit_i = 0x80,
element_index = 0;
if(leds_buf[element_index] & bit_i) SPDR = LOGIC_1;
else SPDR = LOGIC_0;
if( (bit_i >>= 1) == 0) // передача всех битов одного цвета
{
bit_i = 0x80;
if(++element_index >= ELEMENTS_NUMBER) // передача значений для всех элементов светодиодов
{
SPCR &= ~(1<<SPIE); // запрещаем прерывания
element_index = 0; // все данные переданы
}
}
}

Вот, по большому счёту, вся программа. Ссылку на проект простого индикатора уровня звука на умных светодиодах WS2812B для Arduino Pro Mini со скетчем прилагаю: https://github.com/onikita/Simple-VU-meter.git Для скачивания проекта нужно нажать зелёную кнопку Clone or download и выбрать Download ZIP. Чтобы запустить проект, необходимо скачанную папку поместить в папку libraries среды Arduino. Проект проверен и работает в Arduino-1.0.6. Также напоминаю: узнать о схеме подключения VU-метра можно в этой статье.

P.S. Мы разобрали программу простого индикатора уровня - без "излишеств". Здесь, например, нет регулировки плавности, падающей точки, авторегулировки уровня, каких-то изысканных цветовых схем. При необходимости и знании можно самостоятельно добавить различные настройки, эффекты и цветовые схемы. Например, у нас получился такой индикатор уровня:




С уважением, Никита О.

5 комментариев:

  1. Ответы
    1. Отвечу за Никиту (он автор статьи и мой коллега) : Спасибо, мы старались))))

      Удалить
  2. Работает!Молодцы!Испытать бы ваш доработанный вариант.

    ОтветитьУдалить
    Ответы
    1. Отвечу "спасибо" за Никиту (он автор статьи и мой коллега) :). А доработанный вариант уже есть. Тут пока о нем рассказывать не планировалось (на время ведение блога притормозилось) но если интересно пишите на почту PhSound@mail.ru или найдите меня в инстаграмме, с радостью расскажу о проекте. Мой профиль: https://www.instagram.com/kmworkline/

      Удалить
  3. А для Ардуиночайников можно рассказать как то подоходчивее? Как скомпилировать все из этих кусков?

    ОтветитьУдалить