Индикатор уровня звука на PIC

UVmeter barВремя от времени появляются люди, пытающиеся собрать индикатор уровня звука. В "аналоговую" эру, это можно было сделать с помощью делителя напряжения и ряда компараторв или использовать специальные микросхемы, например LM3914, LM3915. В наше время эти микросхемы найти все труднее и труднее, или вам нужно большее разрешение, или другой выходной профиль. Что же делать?

Современные микроконтроллеры способны с легкостью решить эту задачу.

  1. Их встроенного АЦП (10-бит или больше) более чем достаточно для таких задач;
  2. Их быстродействие позволяет легко использовать мультиплексирование, таким образом несколько выводов могут управлять большим количеством светодиодов;
  3. Их ШИМ-возможности также позволяют управлять катушкой стрелочных измерительных головок.

В этой статье мы используем первые два преимущества МК.

Выбор микроконтроллера:

Для нашей задачи мы должны выбрать микроконтроллер со встроенным АЦП и достаточным количеством выводов.  Так как мы будем использовать мултиплексирование, мы будем использовать прерывание от таймера, чтобы точно соблюдать временные промежутки. Итак, нам нужен микроконтроллер со следующими функциями:

  1. Встроенный АЦП;
  2. Порт ввода-вывода;
  3. Таймер;
  4. Прерывания.

Этому соответствует большинство современных МК.

В программе описываемого измерителя уровня используется универсальный код для управления 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()
 }
}

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

Примечание:

  1. функции dat10_dot16() и dat10_bar16() выполняют логарифмическое преобразование из 10-битного значения АЦП и преобразуется в 16-битный паттерн для точки или столбца. Так как в даннмо случае спользуется только 6×2 = 12 светодиодов, написаны функции быстрого преобразования 10бит->12бит путем простого сдвига 4-х битов. Вы вожете написать самостоятельно функцию полного преобразования.

Изображение PIC16F690, управляющего  12-битным паттерном "точка":

UVmeter dot

Изображение PIC16F690, управляющего  12-битным паттерном "столб":

UVmeter bar

Примечания:

  1. Кроме PIC этот код лего портируется на AVR / Arduino,  STM8 и другие МК.
  2. Код масштабируемый. PIC16F684 может управлять до 24-светодиодами от 1 аналогового канала. PIC16F690 может управлять до 40 диодами  от двух аналоговых каналов. Большие МК, например PIC16F1936 могут управлять четырьма независимыми дисплеями.
  3. Если вы запараллелите несколько каналов, разделив их полосовыми фильтрами, у вас получится анализатор спектра.

Пример кода:

Кусок кода, используемый для отображения столбца:

//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() преобразует то же число в в паттерн-точку.

Загрузка...