Введение
Т.к. предыдущая статья вызвала интерес, то, как я и обещал, в этой статье будут рассмотрены примеры работы с семисегментными индикаторами, встроенным АЦП, а также произведена сборка программного проекта цифрового термометра на 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), которая в отличие от первого примера выводит двузначные числа с символом градуса. Логика выделения декад числа, выводимого на индикацию, такая же.
Под отладкой можно проследить все описанную логику работы программы, а также корректность расчета как значений промежуточных переменных, так и конечного результата.
Заключение
Если этот пост вызовет интерес, то в следующей части мы рассмотрим новые примеры на новой специально разработанной плате.
Автор: love_energy