Микросхемы шкальных индикаторов типа К1003ПП1…ПП3 и их импортные аналоги известны всем, и, казалось бы, полностью должны удовлетворить любые потребности любителей мастерить индикаторы в виде линейки светодиодов. Ан нет, иной раз хочется чего-то более изысканного, например, побольше светодиодов в шкале или не одну,а двеа тои три шкалы, или индикацию не простов виде столбика или точки, а болееэкзотическую –в виде 2-х или 3-х точек… В общем, всегда что-то найдется такое. Для чего эти микросхемы не очень подходят. Какой же выход?
Буду не оригинален – тут поможет микроконтроллер, как обычно, семейства AVR.
Опубликовать схему и прошивку – дело плевое, однако это удовлетворит лишь небольшую группу любителей, а остальные будут недовольны – то одно в прошивке или схеме не так, то другое… Это мы проходили,и знаем лекарство: не нравитсяготовое – сделай сам! Но знаем мы и другое: знаешь сам – научи другого! И потому эта статья будет посвящена теории и практике написания программы для шкального индикатора. Программа будет писаться на Си (советую запасаться компиляторами) WinAVR, с минимальными переделками пойдет и на других диалектах (CodeVisionи др.). Основная цель – обучение характерным приемам, освоив которые каждый желающий должен быть в состоянии разработать индикатор под свои конкретные нужды, во всяком случае, я в этоискренне верю.
Очень рекомендую сначала прочитать все до самого конца, и только потом начинать что-то делать в AVR Studio.
Итак, первый этап – постановка задачи.
Начинать будем с простого – сделаем шкалу из двух линеек по 8 светодиодов, которые «столбиком» будут показывать уровень входного напряжения на двух входах МК, т. е. сделаем «стерео» индикатор. Обязательно применим динамическую индикацию, т. к. иначе выводов МК может и не хватить (пока не задумываемсяо конкретном типе МК). Сначала добьемся линейного соответствия количества светящихся светодиодов и входного напряжения, т. е. наша шкала будет иметь динамический диапазон примерно 18 dB. Напомню, что динамический диапазон определяется как 20log (dU), где dU – отношение максимального уровня индикации к минимальному.Так каку нас всего 8 светодиодовв шкале, то dU=8, это очевидно. Кстати, если шкала будет индицировать мощность, то динамический диапазон автоматически уменьшится вдвое…
Этап второй – схема.
Пока что схему нарисуем безотносительно к конкретным выводам конкретного МК – будем пробовать сделать универсальную программу. Надеюсь, когда потребуется, ни у когоиз читателейне возникнет проблемы «распределить» виртуальные названия выводов по «реальным» ножкам выбранного контроллера. И с учетом этого наша схема получается такой, как на рисунке 1.
Конденсатор на сигналеAREF может и отсутствовать – это уже будет определяться конкретными требованиями к конкретному МК. Так же могут появиться некоторые дополнительные соединения (например, для atmega8 требуется всегда соединять вместе выводы GND и AGND, а так жеVCC и AVCC). Эти нюансыв статьене рассматриваются ввиду их очевидности.
На аналоговые входы AIN0 и AIN1 будут подаваться наши измеряемые сигналы: это должно быть постоянное напряжение не более2,5В. Подать такие сигналы лучше всего с эмиттерного повторителя, т. к.он имеет высокое входное и низкое выходное сопротивление, что очень удачно вписывается в концепцию как со стороны «входа», так и «выхода». Однако, варианты возможны и в этом случае, потому снова отбросим нюансы источника сигнала и не будемо них задумываться, главное для нас – программа.
Этап третий – алгоритм работы.
Не будем рисовать диаграммы и структурные схемы, а обдумаем алгоритм программы «на словах».Как обычно, вначале нужно проинициализировать все необходимые периферийные устройства МК. Динамическая индикация проще всего и наиболее красиво реализуется в виде фонового процесса – по прерыванию от таймера. Процедура обработки этого прерывания должна постоянно обновлять уровни на портах (согласно принятым на схеме обозначениям) LEDS, СОМ0 и СОМ1, обеспечивая поочередное включение восьмерок светодиодов.
В основном цикле наша программа должна постоянно и непрерывно осуществлять измерение при помощи АЦП уровни на входахAIN0 и AIN1, обрабатывать их по определенному алгоритму (например. проводить цифровую фильтрацию или масштабирование и т. п.),и подготавливать данные для вывода на шкалы – те самые, которые будут использованы в функции динамической индикации.
Вот, собственно, и все…
Этап четвертый – написание программы.
Будем писать программу по блочно-модульному принципу, т. е. она будет состоять из несколькихфайлов – так легче продвигаться от «крупного» к «мелкому», ведь обучаться легче постепенно вникая в мелочи, чем сразу погрузиться в их пучину,не так ли? После того, как все будет готово, никто не запретит вам объединить все файлы в один, если по каким-то причинам вам покажется это более удобным. Хотя блочно-модульный принцип позволяет лече контролировать правильность программописания – можно компилировать каждый модуль в отдельности(не делая сборку проекта) и сразу корректировать ошибки.
Создаем проект WinAVR GCC в AVR Studio, указываем папку проекта и название главного файла нашего проекта (например, SCALE.C), затем выбираем из списка желаемый тип МК. Затем в опциях проекта указываем желаемую тактовую частоту.
В файл SCALE.C вводим следующее:
#include <avr/io.h> // обязательно подключаем описание портов и периферии #include <util/delay.h> // это МОЖЕТ понадобиться #include "scale.h" // а в этом файле соберем все свои собственные описания
// опишем прототипы наших функций void initialize(void); // функция инициализации всей периферии unsigned int get_adc(unsigned char chanel); // функция получения отсчетаАЦП unsigned int prepare(unsigned int value); // функция обработки сигнала void output(unsigned int val, unsigned int ch); // функция «вывода» уровня на указанную шкалу
adc = get_adc(i); // измеряем первыйвход adc = prepare(adc); // обрабатываем результаты output(adc,i); // выводим результаты на шкалу
}
// возможно, тут потребуется добавить задержку // _delay_ms(1); }
}
Этот текст программы на 100% отражает ранее описанный алгоритм «в общих чертах». Если создать пока пустой файл SCALE.H – программу можно откомпилировать и убедиться, что ошибок в ней нет. Но и функционала тоже нет никакого. Надо его создавать. Итак,
Этап четвертый, шаг второй – работа с АЦП.
Возможно, для столь простого проекта это и не очень рационально, но все же создадим отдельный файл ADC.C, подключим его к нашему проекту и введемв него следующее:
#include <avr/io.h> // без этого - никуда! #include "scale.h" // теперь и без этого - никуда
// опишем функцию работы сАЦП unsigned int get_adc(unsigned char chanel){
ADCSR |= 1<< ADSC; while(ADCSR & (1<<ADSC)); // ждем завершения преобразования result += ADC; // вычисляем сумму замеров
} return result / ADC_AVERAGE; // возвращаем среднее значение нескольких замеров
}
Мы сделали единственную функцию, которая на входе получает номер канала АЦП chanel (беззнаковый байт), а возвращает результат нескольких замеров сигнала по этому самому каналу. Количество замеров определяется константой ADC_AVERAGE, которая должна быть описана в файле SCALE.H, например так:
#define ADC_AVERAGE (10) /* среднее по 10-и замерам */
Тут есть небольшой нюанс: файл SCALE.H подключается у насв различных файлах, потому, чтобы не было конфликтов и ошибок, этот файл должен иметь особую структуру:
#ifndef _MY_ENTRY_SCALE #define _MY_ENTRY_SCALE 1 // здесь начинаются описания наших макросов, констант и т.п.
// здесь все описания должны быть закончены #endif // ниже этой строки никаких описаний быть не должно!
Вначале директивой условной компиляции мы проверяем:описана ли константа _MY_ENTRY_SCALE? Если онаописана – весь условный блок компилятором игнорируется, что равносильно пустому содержимому файла. А вот если константа не описана (это может быть только в том случае, если файл SCALE.H встретился компилятору впервые), то первым делом эта константа описывается (значение 1, но может быть абсолютно любое, даже отсутствовать), а потом уже обрабатываются все прочие строки файла. Это обычная практика в Си – хотите научиться хорошо писать на Си – привыкайте! Я не буду больше упоминать это «обрамление», но помните, что все описания в нашем проекте должны находиться внутри него.
Еще в функцииget_adc используется другая константа REFERENCE. Она должна содержать байтовую константу, содержащую биты, которые необходимо установить в регистре ADMUX для выбора источника опорного напряжения АЦП. Рекомендую использовать встроенный источник 2,56В с подключением внешнего конденсатора. Обычно для этого следует задать такое значение константы:
#define REFERENCE ((1< REFS0)|(1<<REFS1)) /* обязательно уточните по даташиту на ваш МК!!! */
Попутно хочу обратить внимание всех начинающих Си-программистов на 3 правила для директивы #define:
скобки лишними не бывают
никакой точки с запятойв конце
комментарии только в «классическом стиле» (две косые не катят!!!)
Несоблюдение этих правил часто приводит к труднопонимаемым сообщениям об ошибках компиляции или необъяснимому поведению программы. Даже если вам надо описать одно-единственное число (см. ранееADC_AVERAGE) – заключайте его в скобки.Это нетрудно, заодно вырабатывает привычку, а в будущем эта привычка обережет вас от проблем.
Почему замеры АЦП усредняются? В принципе, для нашего случая это делать вовсе необязательно, просто я хотел показать пример универсальной функции, которая выручит вас в любом проекте. В конце концов никто не запретит сделать ADC_AVERAGE равной 1, и умный компилятор «соптимизирует» функцию, выкинув цикл и деление на 1. А вот когда вы соберетесь делать вольтметр с цифровойиндикацией –тут-то вам и пригодится усреднение.
Этап четвертый, шаг третий – динамическая индикация.
Динамическая индикация потребует от нас немного больше размышлений, чем работа с АЦП.Это фоновый процесс, который можно рассматривать как отдельную «программу в программе».А раз так, сформулируем задачу, продумаем алгоритм и т. п.–т. е. повторим все этапы, как для основной задачи, только кратко. Шкал у нас 2, выбор шкалы осуществляется сигналами (см. рисунок 1) COM0 и COM1. Для простоты условимся, что эти сигналы соответствуют линиям (битам) одного порта. Чтобы выбрать одну шкалу, надо подать 0 наСОМ0 и 1 наСОМ1, чтобы включилась другая шкала – наоборот. Светящиеся светодиоды в шкале соответствуют установленным в 1 битам порта LEDS.
Динамическая индикация в нашей шкале будет заключаться в том, что при каждом вызове наша функция должна сначала погасить светящуюся в данный момент шкалу (записью в соответствующийСОМх единички), затем вывести в портLEDS заранее вычисленный байт (собственно «уровень») для другой шкалы, после чего включить эту самую другую шкалу.
Разобравшись с алгоритмом, обдумаем необходимые нам переменные. Во-первых, раз шкал две, то должно быть как минимум 2 глобальных(т. е. доступных из любой функции всей программы) байтовых переменных, хранящих текущий уровень на шкале.В основном цикле в эти переменные будут записываться значения, а в фоновом процессе индикации извлекаться и выводиться наружу. Наиболее удобно использовать массив, т. к. это позволит при желании достаточно просто увеличить число шкал. Еще нам надо где-то «помнить» номер текущей шкалы, заодно знать, какими битами в каких портах все эти наши шкалы управляются. Все константы опишем, как обычно, в файле SCALE.H, а остальное –в файле IND.C, который создадим и подключимк нашему проекту.
Итак, опишем следующие константы и макросыв файле SCALE.H:
// распределимпорты #define COM PORTB /* можно указать любой порт вашего МК */ #define DCOM DDRB /* порт управления режимом порта СОМ - тоже приведите в соответствие с выбранным портом*/ #define LEDS PORTD /* можно указать любой порт вашего МК */ #define DLEDS DDRD /* снова привести в соответствие с портом для LEDS */
// назначим номера битов управления #define COM0 (0) /* пусть первая шкала управляется младшим битом выбранного порта */ #define COM1 (1) /* а вторая шкала - следующим битом */
// определим вспомогательные константы #define OFF_ALL ((1<COM0)|(1<<COM1)) /* этим можно погасить обе шкалы сразу */ #define SCALE0 ((unsigned char) ~(1<<COM0)) /* этим включается первая шкала */ #define SCALE1 ((unsigned char) ~(1<<COM1)) /* этим включается вторая шкала*/
#define SCALE_NUM (2) /* количество шкал */
Несколько комментариев к описаниям.
Во-первых, обратите внимание, что мы определяем новые имена реальных портов МК, чтобы все функции нашей программы были неизменны (или максимально неизменны) при использовании любых МК, меняться будет только содержимое файла SCALE.H (т. е. этот файл у нас получается платформо-зависимым, а все остальные платформо-независимыми).
Во-вторых, аналогично поступаем с номерами битов этих портов – вообще, использование символьных имен вместо конкретных числовых констант есть признак хорошего стиля программирования. Т.е чем меньше внутри функций у вас будет обращений к аппаратным(т. е. платформо-зависимым) средствам МК, тем лучше.
Ну, а теперь обратимся к собственно файлу IND.C:
#include <avr/io.h> // куда ж безэтого? #include "scale.h" // и безэтого? #include <avr/interrupt.h> // это нам потребуется для работы по прерываниям
// функция-обработчик прерывания от таймера для динамической индикации ISR(USER_VECTOR){
static unsigned char current_scale = 0; // номер светящейся шкалы
COM |= OFF_ALL; // гасим обе шкалысразу LEDS = scale[current_scale]; // выводим значение следующейшкалы COM &= commons[current_scale]; // включаем следующуюшкалу if(++current_scale == SCALE_NUM) current_scale = 0; // переключаемшкалу // здесь может потребоваться переустановка счетчика таймера
}
Что занимательного в этом коде?
Во-первых, использована пока нигде не описанная константа USER_VECTOR для задания номера вектора прерывания –о нейречь позже.
Во-вторых, стиль описания обработчика прерывания – специфичен для каждого компилятора. Как яи предупреждал, этот код для WinAVR (макрос ISR характерен именно для него).
В-третьих,не смотряна ранее описанный алгоритм, массивов у насдва – один, как ранее сказано, для хранения уровней шкал, а второй хранит константы для управления включением шкал. Для 2-х шкал это не принципиально,но если захочется больше – будет очень удобно.
В-четвертых, обратите внимание, что вывод в портCOM ведется не напрямую,а при помощи операторов |= и &= – это важно: так обеспечивается корректная работа с линиями портов, если часть из них задействована под другие нужды устройства.
Этап четвертый, шаг четвертый – настройка периферии.
Теперь мы готовык тому, чтобы произвести конфигурацию задействованной аппаратуры. Для этого по традиции создадим отдельный файл GLOBAL.C следующего содержания:
// вот она, функция инициализациивсего void initialize(void){
// начнем с портов DLEDS = 0xFF; // порт шкалы работает навывод // с портом управления не все так просто - его линии могут быть заняты // не только под индикацию (теоретически). В данном примере это не учитывается, // а в реальности нужно добавить конфигурацию прочих линий этогопорта
DCOM = OFF_ALL; // на вывод работают только линии управления шкалами // если надо, добавьте строчку DCOM |= (другие биты) для включения на вывод еще каких-то линий
// настроимАЦП ADCSRA = (1<<ADEN) | ADC_SPEED; // включаем АЦП и задаем его скорость
// настраиваем таймер // пример ориентирован на использование для индикации нулевого таймера, // имеющегося во всех МК, но ничто не мешает использовать любойиной // учтите. Что если для других целей задействованы другие прерывания таймеров // надо скорректировать инициализациюTIMSK! TIMSK = (1<<TOV0); // разрешим прерывания по переполнению TCCR0 = TMR0_SPEED; // включаем таймер
// разрешаем глобально прерывания sei();
}
В общем, код подробно прокомментирован и в дополнительных пояснениях не нуждается.А вот про новые описания в файле SCALE.H следует сказать:
ADC_SPEED – это значение битов предделителя тактовой частоты вашего АЦП. Зависит от того, что вы измеряете,с какой точностью желаете получать результат и от тактовой частоты вашего МК. Я предлагаю использовать самую «медленную» скорость работы АЦП, а уж вы, исходя из собственных предпочтений, установите свое значение.
TMR0_SPEED – это значение битов предделителя таймера. Точно так же зависит от тактовой частоты вашего МК и от того, как часто надо «мигать» светодиодами шкал. Если МК больше ничего. Кроме индикации не делает –в большинстве случаев подойдет это значение, хотя его можно и уменьшатьи увеличивать.
USER_VECTOR – это алиас константы, определяющей номер вектора прерывания от выбранного таймера. Если вы будете использовать таймер другой, или в другом режиме (например, CTC) – измените эту константу на соответствующую. Обязательно приведите в соответствие значение этой константы и значенияTIMSK! Иначе будете удивляться, что прерывания не возникают.
Этап четвертый, шаг заключительный – обработка и вывод значений.
Нам осталось совсем немного: написать функции обработки и вывода измеренных АЦП значений сигнала. Обработку пока не будем делать, облегчим себе жизнь. А вотс индикацией разберемся.
АЦП возвращает нам число от 0 до 1023, пропорциональное уровню на входе от 0 до 2,56В. А шкалау нас дискретная из восьми светодиодов. Вспомним исходные требования: наша шкала должна быть линейной, т. е. каждый светодиод должен соответствовать 1/8 всего измеряемого диапазона. Разобьем весь диапазон от 0 до1023 на8 равных частей, и пусть наши светодиоды начинают светиться, если уровень сигнала перешагнет «середину» соответствующего участка.
Разместим эти функции в файле PREP.C, который так же подключим к нашему проекту.
#include <avr/io.h> #include "scale.h"
extern unsigned char scale[]; // этот массив определен в файле IND.C
// функция предварительной обработки значений unsigned int prepare(unsigned int val){
return val; // пока что никакой обработки
}
// функция вывода значения на указаннуюшкалу void output(unsigned int val, unsigned int ch){
unsigned char i, tmp=0;
for(i=0; i<8; i++){
if (val < (1024/16*(i*2+1))) break; tmp = (tmp<<1)|1;
} scale[ch] = tmp;
}
Функция outputв цикле по переменной i осуществляет сравнение входного значения valс пороговыми.Если входное значение больше порогового –в вспомогательной переменнй tmp устанавливается в единицу очередной бит, начиная с младшего.Как только окажется, что входное напряжение меньше порогового, цикл досрочно прекращается, и полученноек этому моменту значение tmp заносится в соответствующую ячейку массива «уровней» шкал.
Обратите внимание, что функция обращается к «внешнему» массиву, который определен совсем в другом модуле проекта.
Этап пятый – компиляция и сборка проекта.
Если вы выполняли промежуточные компиляции отдельных файлов проекта, или уверены, что в них нет ошибок, можно выполнить сборку проекта. Надеюсь, вы знаете, как это делается. Для экспериментови отладки лучше отключить оптимизацию компилятора (указать в опциях проекта параметр -O0). Если сборка пройдет успешно, в окне сообщений вы увидите сведения о проценте использования памяти МК кодом. Если будутошибки – придется их исправлять.
Надеюсь, у вас все получится с первого раза, тем более что готовые исходники уже имеются в архиве – вам даже вводить вручную ничего не надо.
Ну, а для отладки хорошо подойдет протеус (и для него проектик имеется в архиве). Вы сможете двигать переменные резисторы и наблюдать, как показывают шкалы. В обоих проектах я использовал микроконтроллер atmega8, но уверяю вас, что программный код с минимальными усилиями переносим на любой другой МК, имеющий АЦП и достаточное количество портов!
Перед тем, как прошивать полученный hex-файлв реальный микроконтроллер, рекомендую пересобрать проект с установленной опцией оптимизации -Os – вы сразу заметите, как уменьшится размер кода!
Этап шестой – мечты и планы.
Итак, основа шкального индикатора для «стереоварианта» готова. Надеюсь, есть и необходимые знания для продвижения вперед. И остается только выбрать направление для следующего шага.
Могу для начала посоветовать кое-что.
Во-первых,в коде заложена возможность предварительной обработки сигнала. Она может быть довольно сложной. Например, разве не интересно вам получить индикатор, который на пиковые уровни будет реагировать мгновенно, а потом медленно и плавно спадать, если больше пиков не повторяется?
Во-вторых, все готово для многошкального индикатора. Надо лишь изменить константу SCALE_NUM, соответственно определить необходимое количество констант СОМх, заполнить массив commons этими значениями. В основном цикле вместо константы 2 надо будет поставить SCALE_NUM (это, кстати, полезно сделать сразу, даже для 2-х шкал). Как и для чего применять многоканальный индикатор – это уж дело ваше.
В-третьих, достаточно просто переделать индикатор под 16-уровневую шкалу: всего лишь надо переделать главный цикл (не надо измерять 2 канала)и функцию output (надо рассчитать соответствующие уровни и «распределить» из по обоим элементам массива scale[]), а имеющиеся2 шкалы разместить «паровозиком».
В-четвертых, можно сделать шкалу нелинейной, логарифмической. Теоретически АЦП AVR позволяет получить динамический диапазон измерений не менее54 dB, а в идеале 60. Этого более чем достаточно для любого даже высококачественного звукоусилительного устройства. А делов-то: переделать функцию output, т. е. пересчитать пороги срабатывания…
В-пятых…ноне хватит ли?У вас, уверен, и своя голова на плечах! Удачи!
P.S. Для кто читал невнимательно: все упомянутые в тексте статьи исходные тексты модулей, а так же проект Протеуса доступны для скачивания из файлового архива.