Время от времени появляются люди, пытающиеся собрать индикатор уровня звука. В "аналоговую" эру, это можно было сделать с помощью делителя напряжения и ряда компараторв или использовать специальные микросхемы, например LM3914, LM3915. В наше время эти микросхемы найти все труднее и труднее, или вам нужно большее разрешение, или другой выходной профиль. Что же делать?
Современные микроконтроллеры способны с легкостью решить эту задачу.
- Их встроенного АЦП (10-бит или больше) более чем достаточно для таких задач;
- Их быстродействие позволяет легко использовать мультиплексирование, таким образом несколько выводов могут управлять большим количеством светодиодов;
- Их ШИМ-возможности также позволяют управлять катушкой стрелочных измерительных головок.
В этой статье мы используем первые два преимущества МК.
Выбор микроконтроллера:
Для нашей задачи мы должны выбрать микроконтроллер со встроенным АЦП и достаточным количеством выводов. Так как мы будем использовать мултиплексирование, мы будем использовать прерывание от таймера, чтобы точно соблюдать временные промежутки. Итак, нам нужен микроконтроллер со следующими функциями:
- Встроенный АЦП;
- Порт ввода-вывода;
- Таймер;
- Прерывания.
Этому соответствует большинство современных МК.
В программе описываемого измерителя уровня используется универсальный код для управления 6 – 16 сегментами, 2ух – 4ех разрядов, итого 12 – 64 светодиодами, от одного или двух каналов АЦП.
Код написан для PIC16F690.
Основные принципы работы:
Берется значение 10-битного АЦП и конвертируется в 12-битное логарифмическое значение, которое будет отображаться на светодиодах. Отображение работает в двух режимах: точка и столб.
Код написан таким образом, что каждый вывод конфигурируется индивидуально:
//hardware configuration #define NUM_OF_SEGS 6 //number of segments. #define SEG0_PORT PORTC #define SEG0_DDR TRISC #define SEG0 (1<<0) #define SEG1_PORT PORTC #define SEG1_DDR TRISC #define SEG1 (1<<1) #define SEG2_PORT PORTC #define SEG2_DDR TRISC #define SEG2 (1<<2) #define SEG3_PORT PORTB #define SEG3_DDR TRISB #define SEG3 (1<<4) #define SEG4_PORT PORTB #define SEG4_DDR TRISB #define SEG4 (1<<5) #define SEG5_PORT PORTB #define SEG5_DDR TRISB #define SEG5 (1<<6) #define SEG6_PORT PORTB #define SEG6_DDR TRISB //#define SEG6 (1<<6) #define SEG7_PORT PORTB #define SEG7_DDR TRISB //#define SEG7 (1<<7) //uncomment if not used #define SEG8_PORT PORTB #define SEG8_DDR TRISB //#define SEG8 (1<<0) //uncomment if not used #define SEG9_PORT PORTB #define SEG9_DDR TRISB //#define SEG9 (1<<0) //uncomment if not used #define SEG10_PORT PORTB #define SEG10_DDR TRISB //#define SEG10 (1<<0) //uncomment if not used #define SEG11_PORT PORTB #define SEG11_DDR TRISB //#define SEG11 (1<<0) //uncomment if not used #define SEG12_PORT PORTB #define SEG12_DDR TRISB //#define SEG12 (1<<0) //uncomment if not used #define SEG13_PORT PORTB #define SEG13_DDR TRISB //#define SEG13 (1<<0) //uncomment if not used #define SEG14_PORT PORTB #define SEG14_DDR TRISB //#define SEG14 (1<<0) //uncomment if not used #define SEG15_PORT PORTB #define SEG15_DDR TRISB //#define SEG15 (1<<0) //uncomment if not used #define CH0_PORT PORTC #define CH0_DDR TRISC #define CH0 (1<<7) #define CH1_PORT PORTB #define CH1_DDR TRISB #define CH1 (1<<7) #define CH2_PORT PORTA #define CH2_DDR TRISA //#define CH2 (1<<4) //uncomment if not used #define CH3_PORT PORTA #define CH3_DDR TRISA //#define CH3 (1<<5) //uncomment if not used //end hardware configuration
Вывод на светодиоды написан так, что он может быть с активной единицей или активным нулем:
//global defines #define LEDVU_DLY() NOP4() //guard against RMW //active high for vu pins #define SEG_ON(port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0) #define SEG_OFF( port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0) //active low for vu pins //#define SEG_ON(port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0) //#define SEG_OFF( port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0) //macros to control output channels //active low for dig pins - for small current applications #define CH_ON(port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0) #define CH_OFF(port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0) //active high for dig pins - better to control more leds through a npn/n-ch, high current applications //#define CH_ON(port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0) //#define CH_OFF(port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0)
Команда NOP()s добавлена для надежного корректного переключения между состояниями порта read-modify-write. В принципе, программа хорошо работает и без этого.
Мултиплексирование одинаково на разных видах МК: функция индикации, ledvu_display() обрабатывает статический индекс, который контролирует какой разряд или канал должен включитсья или выключиться. Основываясь на текущем отображаемом канале, он выбирает значение для вывода на светодиоды.
//display value //display upto 16 segments/ch * up to 4 chs void ledvu_display(void) { static uint8_t ch_sel=0; //00->ch0 lsb, 01->ch0 msb, 02->ch1 lsb, ch3->ch1 msb uint32_t val; //value to be displayed uint16_t seg; //pick the ch / seg to be displayed //tmp = (ch_sel & (1<<1))?ch1_vu:ch0_vu; //pick the ch_value to be displayed val = ch0_vu; //single channel output seg = (ch_sel & (1<<0))?(val >> (NUM_OF_SEGS)):(val); //pick the bits to be displayed. lsb 1-> msb, and lsb 0->lsb //seg = seg & ((1<(NUM_OF_SEGS))-1); //only display the last 10 segments //turn off all the channels CH_OFF(CH0_PORT, CH0); CH_OFF(CH1_PORT, CH1); #if defined(CH2) CH_OFF(CH2_PORT, CH2); #endif #if defined(CH3) CH_OFF(CH3_PORT, CH3); #endif //turn on the segs if (seg & (1<<0)) SEG_ON(SEG0_PORT, SEG0); else SEG_OFF(SEG0_PORT, SEG0); if (seg & (1<<1)) SEG_ON(SEG1_PORT, SEG1); else SEG_OFF(SEG1_PORT, SEG1); if (seg & (1<<2)) SEG_ON(SEG2_PORT, SEG2); else SEG_OFF(SEG2_PORT, SEG2); if (seg & (1<<3)) SEG_ON(SEG3_PORT, SEG3); else SEG_OFF(SEG3_PORT, SEG3); if (seg & (1<<4)) SEG_ON(SEG4_PORT, SEG4); else SEG_OFF(SEG4_PORT, SEG4); if (seg & (1<<5)) SEG_ON(SEG5_PORT, SEG5); else SEG_OFF(SEG5_PORT, SEG5); #if defined(SEG6) if (seg & (1<<6)) SEG_ON(SEG6_PORT, SEG6); else SEG_OFF(SEG6_PORT, SEG6); #endif #if defined(SEG7) if (seg & (1<<7)) SEG_ON(SEG7_PORT, SEG7); else SEG_OFF(SEG7_PORT, SEG7); #endif #if defined(SEG8) if (seg & (1<<8)) SEG_ON(SEG8_PORT, SEG8); else SEG_OFF(SEG8_PORT, SEG8); #endif #if defined(SEG9) if (seg & (1<<9)) SEG_ON(SEG9_PORT, SEG9); else SEG_OFF(SEG9_PORT, SEG9); #endif #if defined(SEG10) if (seg & (1<<10)) SEG_ON(SEG10_PORT, SEG10); else SEG_OFF(SEG10_PORT, SEG10); #endif #if defined(SEG11) if (seg & (1<<11)) SEG_ON(SEG11_PORT, SEG11); else SEG_OFF(SEG11_PORT, SEG11); #endif #if defined(SEG12) if (seg & (1<<12)) SEG_ON(SEG12_PORT, SEG12); else SEG_OFF(SEG12_PORT, SEG12); #endif #if defined(SEG13) if (seg & (1<<13)) SEG_ON(SEG13_PORT, SEG13); else SEG_OFF(SEG13_PORT, SEG13); #endif #if defined(SEG14) if (seg & (1<<14)) SEG_ON(SEG14_PORT, SEG14); else SEG_OFF(SEG14_PORT, SEG14); #endif #if defined(SEG15) if (seg & (1<<15)) SEG_ON(SEG15_PORT, SEG15); else SEG_OFF(SEG15_PORT, SEG15); #endif //turn on the ch switch (ch_sel & 0x01) { case 0: CH_ON(CH0_PORT, CH0); break; //turn on ch0 case 1: CH_ON(CH1_PORT, CH1); break; //turn on ch1 #if defined(CH2) case 2: CH_ON(CH2_PORT, CH2); break; //turn on ch2 #endif #if defined(CH3) case 3: CH_ON(CH3_PORT, CH3); break; //turn on ch3 #endif } ch_sel+=1; //advance to the next channel / seg }
Эта функция периодически вызывается из обработчика прерывания TMR0 isr, сконфигурированного следующим образом:
tmr0_init(TMR0_PS_32x, 250); //set tmr0 to run at 32x250us tmr0_act(ledvu_display); //install displayer routine for the isr ei(); //enable global isr
Все, что остается – это написать в основном теле программы обработчик АЦП и конвертацию в логарифмическую шкалу. Процедура индикации не зависит от основного тела программы.
Основной код:
int main(void) { uint16_t tmp=0x00; mcu_init(); //initialize the mcu ledvu_init(); //reset the led vu module tmr0_init(TMR0_PS_32x, 250); //set tmr0 to run at 32x250us tmr0_act(ledvu_display); //install displayer routine for the isr ei(); //enable global isr while (1) { delay_ms(50); //slow down the display - controls how fast the vu meter is updated tmp = rand_xor(); //simulate adc - random figure / 16-bit output //ch0_vu=dat10_dot16((tmp & 0x3ff) >> 4); //perform conversion to vu-scale. DOT pattern. should have used dat10_dot12() ch0_vu=dat10_bar16((tmp & 0x3ff) >> 4); //perform conversion to vu-scale. BAR pattern. should have used dat10_bar12() } }
Как видно, в данном случае вместо АЦП в демонстрационнх целях используется генератор случайных чисел. Вы можете самостоятельно написать чтение из АЦП.
Примечание:
- функции dat10_dot16() и dat10_bar16() выполняют логарифмическое преобразование из 10-битного значения АЦП и преобразуется в 16-битный паттерн для точки или столбца. Так как в даннмо случае спользуется только 6×2 = 12 светодиодов, написаны функции быстрого преобразования 10бит->12бит путем простого сдвига 4-х битов. Вы вожете написать самостоятельно функцию полного преобразования.
Изображение PIC16F690, управляющего 12-битным паттерном "точка":
Изображение PIC16F690, управляющего 12-битным паттерном "столб":
Примечания:
- Кроме PIC этот код лего портируется на AVR / Arduino, STM8 и другие МК.
- Код масштабируемый. PIC16F684 может управлять до 24-светодиодами от 1 аналогового канала. PIC16F690 может управлять до 40 диодами от двух аналоговых каналов. Большие МК, например PIC16F1936 могут управлять четырьма независимыми дисплеями.
- Если вы запараллелите несколько каналов, разделив их полосовыми фильтрами, у вас получится анализатор спектра.
Пример кода:
Кусок кода, используемый для отображения столбца:
//global defines #define BAR(dig) ((1<<(dig))-1) //return full digits for "dig", bar style #define DOT(dig) ((dig)?(1<<((dig)-1)):0) //return full digits for "dig", dot style //global variables //bar style routines //log-linear for 8-bit data into 8-bit led //log-linear uint8_t dat8_bar8(uint8_t w) { if (w<1) return BAR(0); if (w<3) return BAR(1); if (w<6) return BAR(2); if (w<12) return BAR(3); if (w<23) return BAR(4); if (w<46) return BAR(5); if (w<91) return BAR(6); if (w<182) return BAR(7); return 0xff; } //convert 8-digit dat to 16-led digits //log-linear uint16_t dat8_dot16(uint8_t w) { if (w<1) return DOT(0); if (w<2) return DOT(1); if (w<3) return DOT(2); if (w<4) return DOT(3); if (w<5) return DOT(4); if (w<7) return DOT(5); if (w<10) return DOT(6); if (w<14) return DOT(7); if (w<20) return DOT(8); if (w<27) return DOT(9); if (w<39) return DOT(10); if (w<54) return DOT(11); if (w<77) return DOT(12); if (w<108) return DOT(13); if (w<153) return DOT(14); if (w<216) return DOT(15); return DOT(16); //16; }
BAR(n) преобразует (bit) в паттерн-столбец, а DOT(n) преобразует (bit) в паттерн-точку. dat8_bar8() преобразует значение в логарифмическое, в номер (bit), а затем использует BAR() для преобразование этого бита в паттерн-столбце. DOT() преобразует то же число в в паттерн-точку.