Время от времени появляются люди, пытающиеся собрать индикатор уровня звука. В "аналоговую" эру, это можно было сделать с помощью делителя напряжения и ряда компараторв или использовать специальные микросхемы, например 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() преобразует то же число в в паттерн-точку.