Введение
Т.к. предыдущая статья вызвала интерес, то, как я и обещал, в этой статье будут рассмотрены примеры работы с семисегментными индикаторами, встроенным АЦП, а также произведена сборка программного проекта цифрового термометра на ATmega16 из нескольких рассмотренных в данной и предыдущей статье примеров работы с внутренними периферийными блоками микроконтроллера ATmega16.
Теоретические аспекты
Для лучшего понимания примеров с АЦП рекомендую прочитать эту статью. Для нашего случая во всех примерах частота дискретизации будет равна 10 Гц, что эквивалентно периоду дискретизации 100 мс. Т.к. мы используем 10-битный АЦП встроенный в микроконтроллер Atmega16, то количество уровней квантования будет 1024, что соответствует интервалу значений от 0 до 1023 для каждого опрошенного цифрового значения. Опорное напряжение во всех примерах используется внешнее и равно 5 В. Выше опорного напряжения на входе мы не увидим, ну и ниже земли, соответственно, тоже. Значит при напряжении на входе АЦП равном 5 В мы получим цифровое значение 1023, а при 0 В (земля) цифровое значение 0. При напряжении, скажем 3,5 В, мы получим цифровое значение равное (3,5/5) = 716. Поправки на нелинейность, нулевое смещение в примерах мы делать не будем.
Для измерения температуры используется аналоговый датчик TMP36. Это низковольтный, прецизионный датчик температуры, выходное напряжение которого прямо пропорционально температуре в шкале Цельсия. Данное напряжение мы и будем оцифровывать при помощи 10-битного АЦП встроенного в микроконтроллер ATmega16.
Программирование и отладка
Первый пример представляет собой вывод на трехсимвольный семисегментный индикатор чисел в диапазоне от 000 до 999. Индикаторы является статическими и управляются «0». Т.е. можно сказать, что каждый отдельный сегмент управляется, как отдельный светодиод. Каждый из трех семисегментных индикаторов подключен к отдельному порту (порты A, B, D). Подключения отдельных сегментов к номерам выводов независимо от порта одинаковы.
/*Подключим необходимые библиотеки*/ #include <ioavr.h> #include <intrinsics.h> #include <ina90.h> /*Необходимые определения*/ //Частота тактирования #define F_CPU 16000000 //Пользовательские типы #define UCHAR unsigned char #define UINT unsigned int //Числа для семисегментника //0 #define SEG_0 ~(0x3f) //1 #define SEG_1 ~(0x06) //2 #define SEG_2 ~(0x5b) //3 #define SEG_3 ~(0x4F) //4 #define SEG_4 ~(0x66) //5 #define SEG_5 ~(0x6d) //6 #define SEG_6 ~(0x7d) //7 #define SEG_7 ~(0x07) //8 #define SEG_8 ~(0x7F) //9 #define SEG_9 ~(0x6F) //A #define SEG_A ~(0x77) //b #define SEG_b ~(0x7c) //C #define SEG_C ~(0x39) //d #define SEG_d ~(0x5e) //E #define SEG_E ~(0x79) //F #define SEG_F ~(0x71) //Битовая маска числа #define SEG_MASK (0x7F) //Порты трехзначного семисегметного //индикатора //Порт первой декады #define SEG_1DEC_PORT PORTD //Порт второй декады #define SEG_2DEC_PORT PORTB //Порт третьей декады #define SEG_3DEC_PORT PORTA //Порт настройки первой декады #define SEG_1DEC_DDR DDRD //Порт настройки второй декады #define SEG_2DEC_DDR DDRB //Порт настройки третьей декады #define SEG_3DEC_DDR DDRA /*Необходимые определения макрофункций*/ //Задержка в микросекундах #define DELAY_US(us) __delay_cycles((F_CPU / 1000000) * (us)); //Задержка в милисекнудах #define DELAY_MS(ms) __delay_cycles((F_CPU / 1000) * (ms)); //Макрофункция настройки портов сегментного индикатора #define SEG_PORTS_INIT() ( SEG_3DEC_DDR |= SEG_MASK );\ ( SEG_2DEC_DDR |= SEG_MASK );\ ( SEG_1DEC_DDR |= SEG_MASK ); //Макрофункция очистки портов сегментного индикатора #define SEG_PORTS_CLEAR() ( SEG_3DEC_PORT &=~ SEG_MASK );\ ( SEG_2DEC_PORT &=~ SEG_MASK );\ ( SEG_1DEC_PORT &=~ SEG_MASK ); //Макрофункция вывода значений на порты сегментного индикатора #define SEG_PORTS_OUT(x,y,z) ( SEG_3DEC_PORT |= ( x & SEG_MASK ) );\ ( SEG_2DEC_PORT |= ( y & SEG_MASK ) );\ ( SEG_1DEC_PORT |= ( z & SEG_MASK ) ); /*Необходимые объявления глобальных переменных*/ //Числа для семисегментника const unsigned char numbers[16] = { SEG_0, //0 SEG_1, //1 SEG_2, //2 SEG_3, //3 SEG_4, //4 SEG_5, //5 SEG_6, //6 SEG_7, //7 SEG_8, //8 SEG_9, //9 SEG_A, //A SEG_b, //b SEG_C, //C SEG_d, //d SEG_E, //E SEG_F //F }; //Переменная для отображения UINT i = 0; /* ** Name: Seg_Write() ** Description: Функция трехзначного числа на трехцифровой ** семисегментный статический индикатор ** Parameters: UINT dec3number 0 - 999 ** Returns: none */ void Seg_Write(UINT dec3number) { //Переменные для хранения трех декад UCHAR dec3 = 0 , dec2 = 0 , dec1 = 0; //Отбрасываем 4 и последующие декады //если число четырехзначное и более dec3number = dec3number % 1000; //Выделяем значение третьей декады dec3 = dec3number / 100; //Отбрасываем третью декаду dec3number = dec3number % 100; //Выделяем значение второй декады dec2 = dec3number / 10; //Выделяем значение первой декады dec1 = dec3number % 10; //Очищаем биты портов вывода //кроме старших битов SEG_PORTS_CLEAR(); //Выводим числа 3-х декад //на индикаторы с сохранением //старших битов SEG_PORTS_OUT( numbers[dec3], //Третья декада numbers[dec2], //Вторая декада numbers[dec1] //Первая декада ); }//end func /* ** Name: main() ** Description: Главная функция программы, содержащая основной цикл ** Parameters: none ** Returns: none */ //Попадаем после сброса void main( void ) { //Настраиваем регистры периферии //Настройка портов сегментного индикатора SEG_PORTS_INIT(); //Вывести восьмерки Seg_Write(888); //Задержка в три секунды DELAY_MS(3000); //Обнуляем переменную для вывода i=0; //Основной бесконечный цикл for(;;) { //Выводим трехзначное число Seg_Write(i); //Инкрементируем переменную i++; //Вышли за предел трехзначного числа? //обнуляем переменную для вывода if (i == 1000) i=0; //Задержка в 100 мс DELAY_MS(100); }//end for }
Для вывода чисел используются hex-коды вида ~(0x3f), каждый из которых соответствует определенной десятичной цифре в диапазоне 0-9. Для каждого такого кода сделано определение вида SEG_X, где X — это выводимая на индикацию цифра. Данные hex-коды собраны в массив numbers[16] таким образом, что значение индекса массива соответствует выводимой цифре. Например запись в порт третьего элемента массива numbers[3] обеспечивает вывод на индикатор цифры 3.
Макрофункция SEG_PORTS_INIT() обеспечивает настройку используемых выводов портов A, B, D на выход. Используются все выводы портов кроме восьмого. Макрофункция SEG_PORTS_CLEAR() производит очистку портов семисегментных индикаторов, сбрасывая все выводы в «0». Эта макрофункция вместе с SEG_PORTS_OUT(x,y,z), которая осуществляет запись значений во все три порта через конъюнкцию с маской и дизъюнкцию, обеспечивает безопасную запись значения в порт без порчи старшего бита.
Для удобства вывода трехзначных чисел используется функция void Seg_Write(UINT dec3number), где dec3number — это выводимое трехзначное число. В функции осуществляется выделение каждой из трех декад числа при помощи операций деления на целое / и операции получения остатка от деления %. После чего значения каждой декады используются как индекс массива numbers[decX], где decX — это одна из трех декад, и записываются при помощи с SEG_PORTS_OUT(x,y,z) в порт согласно своей позиции в числе.
В основном цикле программы происходит последовательный вывод значений переменной i, которая инкрементируется и обнуляется при выходе за значение 999. Для анализа этого условия используется оператор if. Вывод значений переменной i происходит с интервалом 100 мс, который обеспечивает уже рассмотренная в предыдущей статье макрофункция задержки DELAY_MS(100).
Под отладкой можно увидеть последовательное инкрементирование переменной i, а также в какие hex-коды преобразуются значения данной переменной при помощи функции Seg_Write().
Второй пример это работа со встроенным АЦП. Аналоговый сигнал заведен на его седьмой канал, вход которого подключен соответственно к седьмому выводу порта A.
/*Подключим необходимые библиотеки*/ #include <ioavr.h> #include <intrinsics.h> #include <ina90.h> /*Необходимые определения*/ //Необходимые определения #define F_CPU 16000000 //Пользовательские типы #define UINT unsigned int /*Необходимые определения макрофункций*/ //Задержка в микросекундах #define DELAY_US(us) __delay_cycles((F_CPU / 1000000) * (us)); //Задержка в миллисекнудах #define DELAY_MS(ms) __delay_cycles((F_CPU / 1000) * (ms)); /*Необходимые объявления глобальных переменных*/ //Результат аналогового-цифрового преобразования UINT ADC_Result = 0; /*Основная функция программы*/ //Попадаем после сброса void main( void ) { //Настраиваем регистры периферии //Настройка светодиода //Настраиваем 7-й вывод порта D на выход DDRD_DDD7 = 1; //Устанавливаем 7-й вывод порта D в лог "0" PORTD_PORTD7 = 0; //Настройка АЦП //Настраиваем 7-й вывод порта A на вход DDRA_DDA7 = 0; //Выключение подтяжки на входе АЦП PORTA_PORTA7 = 0; ADMUX_MUX0 = 1; //Седьмой канал ADMUX_MUX1 = 1; ADMUX_MUX2 = 1; ADCSRA_ADEN = 1; //Включение АЦП ADCSRA_ADPS0 = 1; //Делитель частоты тактирования 16 МГц / 128 = 125кГц ADCSRA_ADPS1 = 1; //Из даташита оптимальный диапазон 50-200кГц ADCSRA_ADPS2 = 1; //Преобразование занимает 13 циклов частоты 125кГц 13 / 125000 = 104 мкс //Основной бесконечный цикл for(;;) { //Пустая операция для точки останова _NOP(); //Переключение 7-го вывода порта D из "0" в "1" и из "1" в "0" //При помощи исключающего или PORTD_PORTD7 ^= 1; //Запуск АЦ-преобразования ADCSRA_ADSC = 1; //Результат АЦ-преобразования готов? //Ожидание флага (104 мкс) while (ADCSRA_ADIF == 0); //Пустая операция для точки останова _NOP(); //Считывание результата АЦ-преобразования ADC_Result = ADC; //либо //ADC_Result = ADCL; //Младший байт результата //ADC_Result += (ADCH << 8); //Старший байт результата //Очищаем флаг ADCSRA_ADIF=1; //Задержка в 100 милисекунд (1/0,1 секунда = 10 Гц - частота дискретизации) DELAY_MS(100); }//end for }//end main
В рассматриваемом примере в функции main() после настройки светодиода, уже рассмотренного в предыдущей статье, осуществляется настройка седьмого вывода порта D на вход. Для этого бит DDD7 регистра управления DDRD сбрасывается в «0». После чего происходит принудительное отключение подтягивающего резистора на этом выводе при помощи сброса в «0» бита PORTA7 регистра данных PORTA.
После чего при помощи записи «1» в биты MUXx управляющего регистра ADMUX выбирается седьмой канал АЦП.
Далее включение АЦП осуществляется при помощи установки бита ADEN управляющего регистра ADCSRA.
Биты ADPSx порта управляющего регистра ADCSRA установлены в «1», чтобы обеспечить работу АЦП в оптимальном диапазоне частот 50-200кГц (как заявлено в даташите). Биты ADPSx обеспечивают значение предделителя 128. Конкретное значение частоты при тактовой частоте 16 МГц будет 16 МГц/128 = 128 кГц.
После всех необходимых настроек происходит циклический последовательный запуск и опрос АЦ-преобразований при помощи флагов. Для индикации события работы программы используется светодиод.
Запуск АЦ-преобразования происходит при помощи установки бита ADSC управляющего регистра ADСSRA. Готовность результата определяется установкой флага ADIF в данном управляющем регистре. При выбранной частоте работы АЦП одно преобразование будет длится 104 мкс. После установки флага ADIF результат АЦ-преобразования, диапазон которого 0 — 1023, может быть считан из регистров данных ADCL и ADCH. Результат АЦ-преобразования записывается в переменную ADC_Result. Далее необходимо очистить флаг ADIF записью «1». Интервал опроса равен 100 мс, что эквивалентно частоте дискретизации 10 Гц.
Под отладкой можно протестировать корректность настройки АЦП, а также оценить значение в квантах, получаемое в результате АЦ-преобразования.
Третий пример это тоже опрос АЦП, но уже с использованием дополнительных макрофункций.
/*Подключим необходимые библиотеки*/ #include <ioavr.h> #include <intrinsics.h> #include <ina90.h> /*Необходимые определения*/ //Необходимые определения #define F_CPU 16000000 //Пользовательские типы #define UINT unsigned int //Определения портов для макрофункции работы //со светодиодом #define LED_DDR DDRD #define LED_PORT PORTD #define LED_PIN DDD7 //Определения портов для макрофункции работы с АЦП #define ADC_IN_DDR DDRA #define ADC_IN_PORT PORTA #define ADC_IN_DDR_PIN DDA7 #define ADC_IN_PORT_PIN PORTA7 //Определения для выбора аналогового канала #define ADC0 (0<<MUX2)|(0<<MUX1)|(0<<MUX0) #define ADC1 (0<<MUX2)|(0<<MUX1)|(1<<MUX0) #define ADC2 (0<<MUX2)|(1<<MUX1)|(0<<MUX0) #define ADC3 (0<<MUX2)|(1<<MUX1)|(1<<MUX0) #define ADC4 (1<<MUX2)|(0<<MUX1)|(0<<MUX0) #define ADC5 (1<<MUX2)|(0<<MUX1)|(1<<MUX0) #define ADC6 (1<<MUX2)|(1<<MUX1)|(0<<MUX0) #define ADC7 (1<<MUX2)|(1<<MUX1)|(1<<MUX0) //Определения для выбора делителя частоты АЦП #define ADC_F_CPU_DIV_2 (0<<ADPS2)|(0<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_4 (0<<ADPS2)|(0<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_8 (0<<ADPS2)|(1<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_16 (0<<ADPS2)|(1<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_32 (1<<ADPS2)|(0<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_64 (1<<ADPS2)|(1<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_128 (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0) /*Необходимые определения макрофункций*/ //Макрофункция инициализации светодиода #define LED_INIT() ( LED_DDR |= (1<<LED_PIN) ); //Погасить светодиод #define LED_LOW() ( LED_PORT &=~ (1<<LED_PIN) ); //Зажечь светодиод #define LED_HIGH() ( LED_PORT |= (1<<LED_PIN) ); //Мигание светодиода #define LED_TOG() ( LED_PORT ^= (1<<LED_PIN) ); //Макрофункция инициализации входа АЦП #define ADC_IN_INIT() ( ADC_IN_DDR &= ~(0<<ADC_IN_DDR_PIN) );\ ( ADC_IN_PORT |= (0<<ADC_IN_PORT_PIN) ); //Макрофункция выбора канала #define ADC_SET_CHAN(x) ( ADMUX |= x ); //Макрофункция включения АЦП #define ADC_ON() ( ADCSRA |= (1<<ADEN) ); //Макрофункция выключения АЦП #define ADC_OFF() ( ADCSRA &=~ (1<<ADEN) ); //Выбор делителя частоты для АЦП #define ADC_SET_CLK_DIV(x) ( ADCSRA |= x ); //Запуск АЦ-преобразования #define ADC_START_CONV() ( ADCSRA |= (1<<ADSC) ); //Проверка готовности результата АЦ-преобразования #define ADC_RES_READY() ( ADCSRA & (1<<ADIF) ) //Очистка флага готовности результата АЦ-преобразования #define ADC_FLAG_CLEAR() ( ADCSRA |= (1<<ADIF) ); //Задержка в микросекундах #define DELAY_US(us) __delay_cycles((F_CPU / 1000000) * (us)); //Задержка в миллисекундах #define DELAY_MS(ms) __delay_cycles((F_CPU / 1000) * (ms)); /*Необходимые объявления глобальных переменных*/ //Результат аналогового-цифрового преобразования UINT ADC_Result = 0; /*Основная функция программы*/ //Попадаем после сброса void main( void ) { //Настраиваем регистры периферии //Настройка светодиода LED_INIT(); //Гасим светодиод LED_LOW(); //Настройка АЦП //Инициализация входа АЦП ADC_IN_INIT(); //Выбрали седьмой канал ADC_SET_CHAN(ADC7); ADC_ON(); //Включение АЦП //Делитель частоты тактирования 16 МГц / 128 = 125кГц ADC_SET_CLK_DIV(ADC_F_CPU_DIV_128); //Из даташита оптимальный диапазон 50-200кГц //Преобразование занимает 13 циклов частоты 125кГц 13 / 125000 = 104 мкс //Основной бесконечный цикл for(;;) { //Пустая операция для точки останова _NOP(); //Мигание светодиода LED_TOG(); //Запуск АЦ-преобразования ADC_START_CONV(); //Результат АЦ-преобразования готов? //Ожидание флага (104 мкс) while(!ADC_RES_READY()); //Пустая операция для точки останова _NOP(); //Считывание результата АЦ-преобразования ADC_Result = ADC; //либо //ADC_Result = ADCL; //Младший байт результата //ADC_Result += (ADCH << 8); //Старший байт результата //Очищаем флаг ADC_FLAG_CLEAR(); //Задержка в 100 миллисекунд (1/0,1 секунда = 10 Гц - частота дискретизации) DELAY_MS(100); }//end for }//end main
Инициализация входа седьмого канала АЦП осуществляется при помощи макрофункции ADC_IN_INIT(). Определения ADC_IN_DDR, ADC_IN_PORT, ADC_IN_DDR_PIN, ADC_IN_PORT_PIN обеспечивают легкое возможное изменение вывода и, соответственно, канала АЦП.
Выставки битов в управляющем регистре выбора канала ADMUX, при настройке, осуществляется при помощи макрофункции ADC_SET_CHAN(ADCx), аргумент которой ADCx определяет номер канала, где x выбирается в диапазоне от 0 до 7-ми. Каждое определение ADCx представляет собой совокупность значений управляющих битов MUXx, значение которых «0» и «1» соответствует выбранному номеру канала x.
Включение АЦП осуществляется макрофункцией ADC_ON(). Выбор делителя частоты осуществляется при помощи макрофункции ADC_SET_CLK_DIV(ADC_F_CPU_DIV_x), где аргумент выбирается из ряда определений ADC_F_CPU_DIV_x, в котором x берется из набора 2,4,8,16,32,64,128 и соответствует выбранному предделителю тактовой частоты микроконтроллера.
Запуск АЦ-преобразования осуществляется макрофункцией ADC_START_CONV(). Готовность АЦ-преобразования определяется макрофункцией ADC_RES_READY(). Очистка флага при помощи ADC_FLAG_CLEAR().
Четвертый пример представляет собой опрос АЦП с использованием механизма прерываний для фиксирования события готовности вместо флагов.
/*Подключим необходимые библиотеки*/ #include <ioavr.h> #include <intrinsics.h> #include <ina90.h> /*Необходимые определения*/ //Необходимые определения #define F_CPU 16000000 //Пользовательские типы #define UINT unsigned int /*Необходимые определения макрофункций*/ //Задержка в микросекундах #define DELAY_US(us) __delay_cycles((F_CPU / 1000000) * (us)); //Задержка в милисекнудах #define DELAY_MS(ms) __delay_cycles((F_CPU / 1000) * (ms)); /*Необходимые объявления глобальных переменных*/ //Результат аналогового-цифрового преобразования UINT ADC_Result = 0; /*Основная функция программы*/ //Попадаем после сброса void main( void ) { //Настраиваем регистры периферии //Настройка светодиода //Настраиваем 7-й вывод порта D на выход DDRD_DDD7 = 1; //Устанавливаем 7-й вывод порта D в лог "0" PORTD_PORTD7 = 0; //Настройка АЦП //Настраиваем 7-й вывод порта A на вход DDRA_DDA7 = 0; //Включение подтяжки на входе АЦП PORTA_PORTA7 = 1; ADMUX_MUX0 = 1; //Седьмой канал ADMUX_MUX1 = 1; ADMUX_MUX2 = 1; ADCSRA_ADEN = 1; //Включение АЦП ADCSRA_ADPS0 = 1; //Делитель частоты тактирования 16 МГц / 128 = 125кГц ADCSRA_ADPS1 = 1; //Из даташита оптимальный диапазон 50-200кГц ADCSRA_ADPS2 = 1; //Преобразование занимает 13 циклов частоты 125кГц 13 / 125000 = 104 мкс ADCSRA_ADIE = 1; //Разрешить прерывания от АЦП //Разрешить прерывания _SEI(); //Основной бесконечный цикл for(;;) { //Пустая операция для точки останова _NOP(); //Переключение 7-го вывода порта D из "0" в "1" и из "1" в "0" //При помощи исключающего или PORTD_PORTD7 ^= 1; //Запуск АЦ-преобразования ADCSRA_ADSC = 1; //Задержка в 100 миллисекунд (1/0,1 секунда = 10 Гц - частота дискретизации) DELAY_MS(100); }//end for }//end main /*Обработчик прерывания окончания АЦ-преобразования*/ #pragma vector=ADC_vect __interrupt void ISR_ADC(void) { //Пустая операция для точки останова _NOP(); //Считывание результата АЦ-преобразования ADC_Result = ADC; //либо //ADC_Result = ADCL; //Младший байт результата //ADC_Result += (ADCH << 8); //Старший байт результата }//end func
Для того чтобы включить прерывание по готовности результата АЦ-преобразования необходимо установить бит ADIE в регистре управления ADCSRA. После чего делаем глобальное разрешение всех маскируемых прерываний макрофункцией _SEI().
Логика работы программы представляет уже описанную в первой статье систему с суперциклом, где обеспечивается квазипараллельная работа основного цикла и обработчика прерывания. В основном цикле происходит запуск АЦ-преобразования с периодом 100 мс. Через интервал примерно равный 104 мкс основной цикл прерывается на обработку прерывания по событию готовности результата АЦ-преобразования. Обработка прерывания осуществляет при помощи функции-обработчика ISR_ADC(), которая вызывается по данному событию. Результат АЦ-преобразования копируется из регистров данных ADCL и ADCH в теле функции обработчика.
Под отладкой можно протестировать корректность работы подсистемы прерываний для периферийного модуля АЦП.
Четвертый пример представляет собой симбиоз второго и третьего примера. Это опрос АЦП при помощи прерываний с применением макрофункций.
/*Подключим необходимые библиотеки*/ #include <ioavr.h> #include <intrinsics.h> #include <ina90.h> /*Необходимые определения*/ //Необходимые определения #define F_CPU 16000000 //Пользовательские типы #define UINT unsigned int //Определения портов для макрофункции работы //со светодиодом #define LED_DDR DDRD #define LED_PORT PORTD #define LED_PIN DDD7 //Определения портов для макрофункции работы с АЦП #define ADC_IN_DDR DDRA #define ADC_IN_PORT PORTA #define ADC_IN_DDR_PIN DDA7 #define ADC_IN_PORT_PIN PORTA7 //Определения для выбора аналогового канала #define ADC0 (0<<MUX2)|(0<<MUX1)|(0<<MUX0) #define ADC1 (0<<MUX2)|(0<<MUX1)|(1<<MUX0) #define ADC2 (0<<MUX2)|(1<<MUX1)|(0<<MUX0) #define ADC3 (0<<MUX2)|(1<<MUX1)|(1<<MUX0) #define ADC4 (1<<MUX2)|(0<<MUX1)|(0<<MUX0) #define ADC5 (1<<MUX2)|(0<<MUX1)|(1<<MUX0) #define ADC6 (1<<MUX2)|(1<<MUX1)|(0<<MUX0) #define ADC7 (1<<MUX2)|(1<<MUX1)|(1<<MUX0) //Определения для выбора делителя частоты АЦП #define ADC_F_CPU_DIV_2 (0<<ADPS2)|(0<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_4 (0<<ADPS2)|(0<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_8 (0<<ADPS2)|(1<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_16 (0<<ADPS2)|(1<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_32 (1<<ADPS2)|(0<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_64 (1<<ADPS2)|(1<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_128 (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0) /*Необходимые определения макрофункций*/ //Макрофункция инициализации светодиода #define LED_INIT() ( LED_DDR |= (1<<LED_PIN) ); //Погасить светодиод #define LED_LOW() ( LED_PORT &=~ (1<<LED_PIN) ); //Зажечь светодиод #define LED_HIGH() ( LED_PORT |= (1<<LED_PIN) ); //Мигание светодиода #define LED_TOG() ( LED_PORT ^= (1<<LED_PIN) ); //Макрофункция инициализации входа АЦП #define ADC_IN_INIT() ( ADC_IN_DDR &= ~(0<<ADC_IN_DDR_PIN) );\ ( ADC_IN_PORT |= (0<<ADC_IN_PORT_PIN) ); //Макрофункция выбора канала #define ADC_SET_CHAN(x) ( ADMUX |= x ); //Макрофункция включения АЦП #define ADC_ON() ( ADCSRA |= (1<<ADEN) ); //Макрофункция выключения АЦП #define ADC_OFF() ( ADCSRA &=~ (1<<ADEN) ); //Выбор делителя частоты для АЦП #define ADC_SET_CLK_DIV(x) ( ADCSRA |= x ); //Запуск АЦ-преобразования #define ADC_START_CONV() ( ADCSRA |= (1<<ADSC) ); //Включить прерывания от АЦП #define ADC_INT_ON() ( ADCSRA |= (1<<ADIE) ); //Задержка в микросекундах #define DELAY_US(us) __delay_cycles((F_CPU / 1000000) * (us)); //Задержка в миллисекундах #define DELAY_MS(ms) __delay_cycles((F_CPU / 1000) * (ms)); /*Необходимые объявления глобальных переменных*/ //Результат аналогового-цифрового преобразования UINT ADC_Result = 0; /*Основная функция программы*/ //Попадаем после сброса void main( void ) { //Настраиваем регистры периферии //Настройка светодиода LED_INIT(); //Гасим светодиод LED_LOW(); //Настройка АЦП //Инициализация входа АЦП ADC_IN_INIT(); //Выбрали седьмой канал ADC_SET_CHAN(ADC7); ADC_ON(); //Включение АЦП //Делитель частоты тактирования 16 МГц / 128 = 125кГц ADC_SET_CLK_DIV(ADC_F_CPU_DIV_128); //Из даташита оптимальный диапазон 50-200кГц //Преобразование занимает 13 циклов частоты 125кГц 13 / 125000 = 104 мкс ADC_INT_ON(); //Разрешить прерывания от АЦП //Разрешить прерывания _SEI(); //Основной бесконечный цикл for(;;) { //Пустая операция для точки останова _NOP(); //Мигание светодиода LED_TOG(); //Запуск АЦ-преобразования ADC_START_CONV(); //Задержка в 100 миллисекунд (1/0,1 секунда = 10 Гц - частота дискретизации) DELAY_MS(100); }//end for }//end main /*Обработчик прерывания окончания АЦ-преобразования*/ #pragma vector=ADC_vect __interrupt void ISR_ADC(void) { //Пустая операция для точки останова _NOP(); //Считывание результата АЦ-преобразования ADC_Result = ADC; //либо //ADC_Result = ADCL; //Младший байт результата //ADC_Result += (ADCH << 8); //Старший байт результата }//end func
Для включения прерываний используется макрофункция ADC_INT_ON().
Рассмотрев все необходимые примеры перейдем к целому проекту цифрового термометра, сделанный на основе уже рассмотренных примеров работы с внутренними периферийными блоками микроконтроллера.
/*Подключим необходимые библиотеки*/ #include <ioavr.h> #include <intrinsics.h> #include <ina90.h> #include <stdbool.h> /*Необходимые определения*/ //Частота тактирования #define F_CPU 16000000 //Пользовательские типы #define UCHAR unsigned char #define UINT unsigned int #define FLOAT_TYPE float //Определения портов для макрофункции работы //с первым светодиодом #define LED_DDR DDRD #define LED_PORT PORTD #define LED_PIN DDD7 //Определения для выбора частоты нулевого таймера #define F_CPU_DIV_1 (0<<CS02)|(0<<CS01)|(1<<CS00) #define F_CPU_DIV_8 (0<<CS02)|(1<<CS01)|(0<<CS00) #define F_CPU_DIV_64 (0<<CS02)|(1<<CS01)|(1<<CS00) #define F_CPU_DIV_256 (1<<CS02)|(0<<CS01)|(0<<CS00) #define F_CPU_DIV_1024 (1<<CS02)|(0<<CS01)|(1<<CS00) //Значение счетного регистра нулевого таймера #define TCNT0_VALUE 99 //Предельное значение переменной счетчика тиков #define T0_TICK_CNT_LIMIT 10 //Определения портов для макрофункции работы с АЦП #define ADC_IN_DDR DDRA #define ADC_IN_PORT PORTA #define ADC_IN_DDR_PIN DDA7 #define ADC_IN_PORT_PIN PORTA7 //Определения для выбора аналогового канала #define ADC0 (0<<MUX2)|(0<<MUX1)|(0<<MUX0) #define ADC1 (0<<MUX2)|(0<<MUX1)|(1<<MUX0) #define ADC2 (0<<MUX2)|(1<<MUX1)|(0<<MUX0) #define ADC3 (0<<MUX2)|(1<<MUX1)|(1<<MUX0) #define ADC4 (1<<MUX2)|(0<<MUX1)|(0<<MUX0) #define ADC5 (1<<MUX2)|(0<<MUX1)|(1<<MUX0) #define ADC6 (1<<MUX2)|(1<<MUX1)|(0<<MUX0) #define ADC7 (1<<MUX2)|(1<<MUX1)|(1<<MUX0) //Определения для выбора делителя частоты АЦП #define ADC_F_CPU_DIV_2 (0<<ADPS2)|(0<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_4 (0<<ADPS2)|(0<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_8 (0<<ADPS2)|(1<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_16 (0<<ADPS2)|(1<<ADPS1)|(1<<ADPS0) #define ADC_F_CPU_DIV_32 (1<<ADPS2)|(0<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_64 (1<<ADPS2)|(1<<ADPS1)|(0<<ADPS0) #define ADC_F_CPU_DIV_128 (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0) //Числа для семисегментника //0 #define SEG_0 ~(0x3f) //1 #define SEG_1 ~(0x06) //2 #define SEG_2 ~(0x5b) //3 #define SEG_3 ~(0x4F) //4 #define SEG_4 ~(0x66) //5 #define SEG_5 ~(0x6d) //6 #define SEG_6 ~(0x7d) //7 #define SEG_7 ~(0x07) //8 #define SEG_8 ~(0x7F) //9 #define SEG_9 ~(0x6F) //A #define SEG_A ~(0x77) //b #define SEG_b ~(0x7c) //C #define SEG_C ~(0x39) //d #define SEG_d ~(0x5e) //E #define SEG_E ~(0x79) //F #define SEG_F ~(0x71) //Градус #define SEG_GRAD ~(0x63) //Битовая маска числа #define SEG_MASK (0x7F) //Порты трехзначного семисегметного //индикатора //Порт первой декады #define SEG_1DEC_PORT PORTD //Порт второй декады #define SEG_2DEC_PORT PORTB //Порт третьей декады #define SEG_3DEC_PORT PORTA //Порт настройки первой декады #define SEG_1DEC_DDR DDRD //Порт настройки второй декады #define SEG_2DEC_DDR DDRB //Порт настройки третьей декады #define SEG_3DEC_DDR DDRA /*Необходимые определения макрофункций*/ //Макрофункция инициализации первого светодиода #define LED_INIT() ( LED_DDR |= (1<<LED_PIN) ); //Погасить первый светодиод #define LED_LOW() ( LED_PORT &=~ (1<<LED_PIN) ); //Зажечь первый светодиод #define LED_HIGH() ( LED_PORT |= (1<<LED_PIN) ); //Мигание первого светодиода #define LED_TOG() ( LED_PORT ^= (1<<LED_PIN) ); //Выбор делителя частоты тактирования #define TIMER0_SET_CLK_DIV(x) ( TCCR0 |= x ); //Загрузка счетного регистра нулевого таймера #define TIMER0_SET_CNT(x) ( TCNT0 = x ); //Включение прерывания по переполнению нулевого таймера #define TIMER0_OVF_INT_ON() ( TIMSK|=(1<<TOIE0) ); //Макрофункция инициализации входа АЦП #define ADC_IN_INIT() ( ADC_IN_DDR &= ~(0<<ADC_IN_DDR_PIN) );\ ( ADC_IN_PORT &= ~(0<<ADC_IN_PORT_PIN) ); //Макрофункция выбора канала #define ADC_SET_CHAN(x) ( ADMUX |= x ); //Макрофункция включения АЦП #define ADC_ON() ( ADCSRA |= (1<<ADEN) ); //Макрофункция выключения АЦП #define ADC_OFF() ( ADCSRA &=~ (1<<ADEN) ); //Выбор делителя частоты для АЦП #define ADC_SET_CLK_DIV(x) ( ADCSRA |= x ); //Запуск АЦ-преобразования #define ADC_START_CONV() ( ADCSRA |= (1<<ADSC) ); //Включить прерывания от АЦП #define ADC_INT_ON() ( ADCSRA |= (1<<ADIE) ); //Макрофункция настройки портов сегментного индикатора #define SEG_PORTS_INIT() ( SEG_3DEC_DDR |= SEG_MASK );\ ( SEG_2DEC_DDR |= SEG_MASK );\ ( SEG_1DEC_DDR |= SEG_MASK ); //Макрофункция очитки портов сегментного индикатора #define SEG_PORTS_CLEAR() ( SEG_3DEC_PORT &=~ SEG_MASK );\ ( SEG_2DEC_PORT &=~ SEG_MASK );\ ( SEG_1DEC_PORT &=~ SEG_MASK ); //Макрофункция вывода значений на порты сегментного индикатора #define SEG_PORTS_OUT(x,y,z) ( SEG_3DEC_PORT |= ( x & SEG_MASK ) );\ ( SEG_2DEC_PORT |= ( y & SEG_MASK ) );\ ( SEG_1DEC_PORT |= ( z & SEG_MASK ) ); //Задержка в микросекундах #define DELAY_US(us) __delay_cycles((F_CPU / 1000000) * (us)); //Задержка в миллисекундах #define DELAY_MS(ms) __delay_cycles((F_CPU / 1000) * (ms)); /*Необходимые объявления глобальных переменных*/ //Объявление счетчика тиков для таймера T0 UINT T0_tick_cnt=0; //Числа для семисегментника const unsigned char numbers[16] = { SEG_0, //0 SEG_1, //1 SEG_2, //2 SEG_3, //3 SEG_4, //4 SEG_5, //5 SEG_6, //6 SEG_7, //7 SEG_8, //8 SEG_9, //9 SEG_A, //A SEG_b, //b SEG_C, //C SEG_d, //d SEG_E, //E SEG_F //F }; //Переменная для хранения температуры FLOAT_TYPE T = 0; //Результат аналогового-цифрового преобразования UINT ADC_Result = 0; //Счетчик подсчитывающий кол-во АЦ-преобразований UINT ADC_res_cnt = 0; //Сумма для фильтрации UINT Sum = 0; //Флаг готовности результат АЦ-преобразования bool ADCReadyFlag = false; /* ** Name: Seg_Write() ** Description: Функция вывода значения температуры на трехцифровой ** семисегментный статический индикатор ** Parameters: UINT T - выводимая температрура 0 - 99 C ** Returns: none */ void Seg_Write(UINT T) { //Переменные для хранения трех декад UINT dec2 = 0 , dec1 = 0; //Отбрасываем третью и последующие декады T = T % 100; //Вторая декада температуры dec2 = T / 10; //Первая декада температуры dec1 = T % 10; //Очищаем биты портов вывода //кроме старших битов SEG_PORTS_CLEAR(); //Выводим числа 3-х декад //на индикаторы с сохранением //старших битов SEG_PORTS_OUT( numbers[dec2], //Вторая декада numbers[dec1], //Первая декада SEG_GRAD //Градус ); }//end func /* ** Name: main() ** Description: Главная функция программы, содержащая основной цикл ** Parameters: none ** Returns: none */ //Попадаем после сброса void main( void ) { //Настраиваем регистры периферии //Настройка портов сегментного индикатора SEG_PORTS_INIT(); //Настройка светодиода LED_INIT(); //Гасим светодиод LED_LOW(); //Настройка таймера (режим Normal) TIMER0_SET_CLK_DIV(F_CPU_DIV_1024);// Частота тактирования 16 000 000 Гц // 16 000 000 Гц / 1024 = 15 625 Гц // 1 / 15 625 Гц = 0,000064 с =64 мкс TIMER0_SET_CNT(TCNT0_VALUE); // 156 * 0,000064 c = 0,009984 c (10 мс) // тогда начальное значение счетного регистра 255-156 = 99 TIMER0_OVF_INT_ON(); // Включить прерывание таймера по переполнению //Настройка АЦП //Инициализация входа АЦП ADC_IN_INIT(); //Выбрали седьмой канал ADC_SET_CHAN(ADC7); ADC_ON(); //Включение АЦП //Делитель частоты тактирования 16 МГц / 128 = 125кГц ADC_SET_CLK_DIV(ADC_F_CPU_DIV_128); //Из даташита оптимальный диапазон 50-200кГц //Преобразование занимает 13 циклов частоты 125кГц 13 / 125000 = 104 мкс ADC_INT_ON(); //Разрешить прерывания от АЦП //Обнуляем переменную для вывода T=0; //Обнуляем переменную суммы Sum = 0; //Разрешить прерывания _SEI(); //Основной бесконечный цикл for(;;) { //Пустая операция для точки останова _NOP(); //Если АЦ-преобразование окончено //Обработать результат if(ADCReadyFlag) { //Пустая операция для точки останова _NOP(); //Сбрасываем флаг ADCReadyFlag = false; //Мигание светодиода LED_TOG(); //Суммируем результат //АЦ-преобразования Sum += ADC_Result; //Инкрементируем счетчик //АЦ-преобразований ADC_res_cnt++; //Прошло восемь АЦ-преобразований? if ( ADC_res_cnt == 8 ) { //Пустая операция для точки останова _NOP(); //Обнуляем счетчик ADC_res_cnt = 0; //Получаем отфильтрованный результат ADC_Result = Sum / 8; //Обнуляем сумму Sum = 0; //Переводим кванты в температуру T = ADC_Result*(5.0/1023); // преобразовать в вольты T = ((T-0.75)*100)+25; // преобразовать в градусы //Выводим температуру Seg_Write((UINT)T); }//end if }//end if }//end for }//end main /*Обработчик прерывания таймера T0 по переполнению*/ #pragma vector=TIMER0_OVF_vect __interrupt void ISR_TickTimer(void) { //Пустая операция для точки останова _NOP(); //Нарастить счетчик тиков таймера T0 T0_tick_cnt++; //Если отсчитали 100 миллисекунд if (T0_tick_cnt >= T0_TICK_CNT_LIMIT) { //Пустая операция для точки останова _NOP(); //Обнулить счетчик тиков таймера T0 T0_tick_cnt=0; //Мигание второго светодиода //Запуск АЦ-преобразования ADC_START_CONV(); }//end for //Выставляем начальное значение //в счетном регистре TIMER0_SET_CNT(TCNT0_VALUE); }//end func /*Обработчик прерывания окончания АЦ-преобразования*/ #pragma vector=ADC_vect __interrupt void ISR_ADC(void) { //Пустая операция для точки останова _NOP(); //Считывание результата АЦ-преобразования ADC_Result = ADC; //либо //ADC_Result = ADCL; //Младший байт результата //ADC_Result += (ADCH << 8); //Старший байт результата //Установить флаг окончания АЦ-преобразования ADCReadyFlag = true; }//end func
В части настройки периферийных регистров используются уже рассмотренные ранее в примерах макрофункции настройки светодиода, аппаратного таймера и АЦП.
Логика работы программы сосредоточена в квазипараллельной работе основного цикла и двух обработчиков прерываний тикового аппаратного таймера и АЦП. В обработчике прерывания тикового таймера фиксируется событие прохождения интервала 100 мс запускается АЦ-преобразование при помощи макрофункции ADC_START_CONV(). Через интервал примерно равный 104 мкс срабатывает прерывание по готовности результата АЦ-преобразования. В теле обработчике ISR_ADC() происходит запись результата АЦ-преобразования в переменную ADC_Result и устанавливается флаг готовности ADCReadyFlag типа bool (для использования данного типа предварительно была подключена библиотека stdbool.h директивой #include).
Событие установки данного флага фиксируется в основном цикле оператором if. После чего флаг сбрасывается а результат АЦ-преобразования ADC_Result суммируется в переменную Sum, пока не будет суммирована восемь результатов АЦ-преобразований. Когда восемь результатов получено и просуммировано, сумма в переменной Sum делится на восемь. Таким образом реализуется простой вариант цифровой фильтрации и уменьшается шум младших битов из-за шумного питания по USB в дополнение к внешним схемам фильтрации.
Отфильтрованный результат записывается в переменную ADC_Result, а переменная Sum обнуляется. Далее полученный отфильтрованный результат в квантах переводится в вольты при помощи простых арифметических действий. Т.к. опорное напряжение равно 5 В, а максимальное значение в квантах соответствующее ему равно 1023, то чтобы осуществить перевод в вольты нашего значения в квантах достаточно поделить его на 1023 и умножить на 5. Результирующее дробное значение мы сохраним в вещественном типе. Далее, имея под рукой даташит на TMP36 и зная, что при 25 градусах на выходе должно быть 750 мВ и, что каждый градус вверх или вниз соответствует 10 мВ, легко перевести дробный результат в вольтах в градусы при помощи следующего выражения T = ((T-0.75)*100)+25. Далее значение температуры в градусах выводится на семисегментные индикаторы при помощи функции void Seg_Write(UINT T), которая в отличие от первого примера выводит двузначные числа с символом градуса. Логика выделения декад числа, выводимого на индикацию, такая же.
Под отладкой можно проследить все описанную логику работы программы, а также корректность расчета как значений промежуточных переменных, так и конечного результата.
Заключение
Если этот пост вызовет интерес, то в следующей части мы рассмотрим новые примеры на новой специально разработанной плате.
ссылка на оригинал статьи http://habrahabr.ru/post/226539/
Добавить комментарий