Программирование и JTAG-отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

в 9:00, , рубрики: avr, diy или сделай сам, IAR, jtag, Программинг микроконтроллеров, Электроника для начинающих, метки: , ,

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Введение

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

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Второй пример это работа со встроенным АЦП. Аналоговый сигнал заведен на его седьмой канал, вход которого подключен соответственно к седьмому выводу порта 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 выбирается седьмой канал АЦП.

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Далее включение АЦП осуществляется при помощи установки бита ADEN управляющего регистра ADCSRA.

Биты ADPSx порта управляющего регистра ADCSRA установлены в «1», чтобы обеспечить работу АЦП в оптимальном диапазоне частот 50-200кГц (как заявлено в даташите). Биты ADPSx обеспечивают значение предделителя 128. Конкретное значение частоты при тактовой частоте 16 МГц будет 16 МГц/128 = 128 кГц.

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

После всех необходимых настроек происходит циклический последовательный запуск и опрос АЦ-преобразований при помощи флагов. Для индикации события работы программы используется светодиод.

Запуск АЦ-преобразования происходит при помощи установки бита ADSC управляющего регистра ADСSRA. Готовность результата определяется установкой флага ADIF в данном управляющем регистре. При выбранной частоте работы АЦП одно преобразование будет длится 104 мкс. После установки флага ADIF результат АЦ-преобразования, диапазон которого 0 — 1023, может быть считан из регистров данных ADCL и ADCH. Результат АЦ-преобразования записывается в переменную ADC_Result. Далее необходимо очистить флаг ADIF записью «1». Интервал опроса равен 100 мс, что эквивалентно частоте дискретизации 10 Гц.

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

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Третий пример это тоже опрос АЦП, но уже с использованием дополнительных макрофункций.


/*Подключим необходимые библиотеки*/
#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().

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Логика работы программы представляет уже описанную в первой статье систему с суперциклом, где обеспечивается квазипараллельная работа основного цикла и обработчика прерывания. В основном цикле происходит запуск АЦ-преобразования с периодом 100 мс. Через интервал примерно равный 104 мкс основной цикл прерывается на обработку прерывания по событию готовности результата АЦ-преобразования. Обработка прерывания осуществляет при помощи функции-обработчика ISR_ADC(), которая вызывается по данному событию. Результат АЦ-преобразования копируется из регистров данных ADCL и ADCH в теле функции обработчика.

Под отладкой можно протестировать корректность работы подсистемы прерываний для периферийного модуля АЦП.

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Четвертый пример представляет собой симбиоз второго и третьего примера. Это опрос АЦП при помощи прерываний с применением макрофункций.


/*Подключим необходимые библиотеки*/
#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), которая в отличие от первого примера выводит двузначные числа с символом градуса. Логика выделения декад числа, выводимого на индикацию, такая же.

Под отладкой можно проследить все описанную логику работы программы, а также корректность расчета как значений промежуточных переменных, так и конечного результата.

Программирование и JTAG отладка микроконтроллера Atmega16 на языке C в среде IAR, часть 2

Заключение

Если этот пост вызовет интерес, то в следующей части мы рассмотрим новые примеры на новой специально разработанной плате.

Автор: love_energy

Источник

Поделиться

* - обязательные к заполнению поля