РадиоКот :: Шкальный индикатор на основе МК.
Например TDA7294

РадиоКот >Статьи >

Теги статьи: Добавить тег

Шкальный индикатор на основе МК.

Автор: ARV
Опубликовано 22.04.2008

Микросхемы шкальных индикаторов типа К1003ПП1...ПП3 и их импортные аналоги известны всем, и, казалось бы, полностью должны удовлетворить любые потребности любителей мастерить индикаторы в виде линейки светодиодов. Ан нет, иной раз хочется чего-то более изысканного, например, побольше светодиодов в шкале или не одну, а две а то и три шкалы, или индикацию не просто в виде столбика или точки, а более экзотическую - в виде 2-х или 3-х точек: В общем, всегда что-то найдется такое. Для чего эти микросхемы не очень подходят. Какой же выход? Буду не оригинален - тут поможет микроконтроллер, как обычно, семейства AVR.
Опубликовать схему и прошивку - дело плевое, однако это удовлетворит лишь небольшую группу любителей, а остальные будут недовольны - то одно в прошивке или схеме не так, то другое: Это мы проходили, и знаем лекарство: не нравится готовое - сделай сам! Но знаем мы и другое: знаешь сам - научи другого! И потому эта статья будет посвящена теории и практике написания программы для шкального индикатора. Программа будет писаться на Си (советую запасаться компиляторами) WinAVR, с минимальными переделками пойдет и на других диалектах (CodeVision и др.). Основная цель - обучение характерным приемам, освоив которые каждый желающий должен быть в состоянии разработать индикатор под свои конкретные нужды, во всяком случае, я в это искренне верю.
Очень рекомендую сначала прочитать все до самого конца, и только потом начинать что-то делать в AVR Studio.

Итак, первый этап - постановка задачи.

Начинать будем с простого - сделаем шкалу из двух линеек по 8 светодиодов, которые "столбиком" будут показывать уровень входного напряжения на двух входах МК, т.е. сделаем "стерео" индикатор. Обязательно применим динамическую индикацию, т.к. иначе выводов МК может и не хватить (пока не задумываемся о конкретном типе МК). Сначала добьемся линейного соответствия количества светящихся светодиодов и входного напряжения, т.е. наша шкала будет иметь динамический диапазон примерно 18 dB. Напомню, что динамический диапазон определяется как 20log(dU), где dU - отношение максимального уровня индикации к минимальному. Так как у нас всего 8 светодиодов в шкале, то dU=8, это очевидно. Кстати, если шкала будет индицировать мощность, то динамический диапазон автоматически уменьшится вдвое.

Этап второй - схема.

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

Схема

Конденсатор на сигнале 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); // функция "вывода" уровня на указанную шкалу

int main(void){
    unsigned int adc; // значение "замера" сигнала
    unsigned char i; // вспомогательная переменная

    initialize(); // инициализируем периферию
    // главный бесконечный цикл
    while(1){
        for(i=0; i<2; i++){
        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){
    unsigned char i;
    unsigned int result = 0;
    ADMUX = REFERENCE | chanel; // включаем нужный канал АЦП
for(i=0; i<ADC_AVERAGE;i++){
    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 (т.е. этот файл у нас получается платформо-зависимым, а все остальные платформо-независимыми ( На 100% не всегда получается сделать независимыми все фалы, но я старался.)).
Во-вторых, аналогично поступаем с номерами битов этих портов - вообще, использование символьных имен вместо конкретных числовых констант есть признак хорошего стиля программирования. Т.е чем меньше внутри функций у вас будет обращений к аппаратным (т.е. платформо-зависимым) средствам МК, тем лучше.
Ну, а теперь обратимся к собственно файлу IND.C:

#include <avr/io.h> // куда ж без этого?
#include "scale.h" // и без этого?
#include <avr/interrupt.h> // это нам потребуется для работы по прерываниям

unsigned char scale[SCALE_NUM]; // глобальный массив "уровней" шкал
const unsigned char commons[SCALE_NUM] = {SCALE0, SCALE1}; // массив выбора шкал

// функция-обработчик прерывания от таймера для динамической индикации
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 следующего содержания:

#include <avr/io.h>
#include "scale.h"
#include <avr/interrupt.h>

// вот она, функция инициализации всего
void initialize(void){
    // начнем с портов
    DLEDS = 0xFF; // порт шкалы работает на вывод
    // с портом управления не все так просто - его линии могут быть заняты
    // не только под индикацию (теоретически). В данном примере это не учитывается,
    // а в реальности нужно добавить конфигурацию прочих линий этого порта
    DCOM = OFF_ALL; // на вывод работают только линии управления шкалами
    // если надо, добавьте строчку DCOM |= (другие биты) для включения на вывод еще каких-то линий

    // настроим АЦП
    ADCSRA = (1<<ADEN)|ADC_SPEED; // включаем и устанавливаем скорость

    // настраиваем таймер
    // пример ориентирован на использование для индикации нулевого таймера,
    // имеющегося во всех МК, но ничто не мешает использовать любой иной
    // учтите. Что если для других целей задействованы другие прерывания таймеров
    // надо скорректировать инициализацию TIMSK!
    TIMSK = (1<<TOV0); // разрешим прерывания по переполнению
    TCCR0 = TMR0_SPEED; // включаем таймер

    // разрешаем глобально прерывания
    sei();
}

В общем, код подробно прокомментирован и в дополнительных пояснениях не нуждается. А вот про новые описания в файле SCALE.H следует сказать:

#define ADC_SPEED ((1<<ADPS0)|(1<<ADPS1)|(1<<ADPS2))
#define TMR0_SPEED ((1<<CS00)|(1<<CS01))
#define USER_VECTOR TIMER0_OVF_vect

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, т.е. пересчитать пороги срабатывания...
В-пятых... но не хватит ли? У вас, уверен, и своя голова на плечах! Удачи!

Файлы:
Исходники и файл проекта для Proteus

Все вопросы - сюда.
Удачи.




Как вам эта статья?

Заработало ли это устройство у вас?

11 2 3
1 0 0