Учебный курс. 16 разрядный таймер/счетчик Т1. Прерывание по событию захват. Простой частотомер на AVR

Схема

Схема частотомера на AVR
Проверяя работу частотомера, будьте аккуратны – не подайте на вход микроконтроллера повышенного напряжения!

Логика программы

   В начале программы выполняется конфигурирование портов ввода/вывода (вывод ICP настраивается на вход и включается подтягивающий резистор), настройка таймера Т1 и инициализация символьного LCD.
   Затем микроконтроллер попадает в бесконечный цикл, где каждые N мс считывает содержимое буфера, вычисляет частоту и выводит ее значение на дисплей. Также на дисплей выводится содержимое регистра захвата.
   По положительному фронту сигнала на выводе ICP (PD6 для mega8535) блок захвата таймера Т1 сохраняет значение счетного регистра и генерирует запрос на прерывание. В обработчике прерывания счетный регистр обнуляется, а значение регистра захвата записывается в буфер.

Структура проекта

Проект состоит из 4-ех программных модулей.
bcd.c – содержит функцию  для перевода 16-ти разрядных двоичных чисел в двоично-десятичные. На ней мы останавливаться не будем.
timer.c – содержит функцию инициализации таймера Т1, обработчик прерывания по событию захват и функцию возвращающую содержимое буфера.
lcd_lib.c – библиотека для работы с символьным дисплеем.
main.c -  основная программа.

Рассмотрим содержимое модуля timer.c

Инициализация таймера/счетчика Т1

void TIM_Init(void)
{
TIMSK = (1<<TICIE1);        
TCCR1A=(0<<WGM11)|(0<<WGM10);
TCCR1B=(0<<ICNC1)|(1<<ICES1)|(0<<WGM13)|(0<<WGM12)|(0<<CS12)|(0<<CS11)|(1<<CS10);
TCNT1 = 0;
}

   В проекте используется прерывание по событию захват, это прерывание должно быть разрешено установкой бита TICIE1 в регистре TIMSK.

TIMSK = (1<<TICIE1);  

   Для конфигурирования таймера Т1 служат регистры TCCR1A, TCCR1B, TCCR1C. С помощью них устанавливаются режим работы таймера, коэффициент деления тактовой частоты, настройки блока захвата и еще много того, что не понадобится для реализации частотомера.  
   Режим работы таймера определятся значениями битов WGM13…WGM10.  В нашем случае режим работы таймера – Normal, поэтому все биты WGM равны нулю.
   Коэффициент деления тактовой частоты определяется битами CS12…CS10. Чтобы получить максимально возможную точность измерений, частота работы таймера должна быть как можно выше. Делаем предделитель равным 1 - (0<<CS12)|(0<<CS11)|(1<<CS10)
   Для управления блоком захвата в регистре TCCR1B есть два бита – ICNC1 и ICES1. ICNC1 включает/выключает схему подавления помех. Если он сброшен в 0, то схема подавления помех выключена и захват производится по первому же активному фронту на выводе ICP. Если бит установлен в 1, схема подавления помех включена и захват производится только в случае 4-ех одинаковых выборок, соответствующих  активному фронту сигнала.
   ICES1 определяет активный фронт сигнала, то есть фронт по которому блок захвата будет выполнять сохранение счетного регистра. Если бит сброшен в 0, активным является спадающий фронт. Если установлен в 1, то активным будет нарастающий фронт.
   В нашем случае схема подавления помех выключена, активный фронт нарастающий -  (0<<ICNC1)|(1<<ICES1)

TCCR1A=(0<<WGM11)|(0<<WGM10);
TCCR1B=(0<<ICNC1)|(1<<ICES1)|(0<<WGM13)|(0<<WGM12)|(0<<CS12)|(0<<CS11)|(1<<CS10);

   Обнуляем на всякий случай счетный регистр.

TCNT1 = 0;

Прерывание по событию захват

В простейшем случае обработчик прерывания выглядел бы следующим образом:

volatile unsigned int
tachBuf = 0; //буфер

#pragma vector=TIMER1_CAPT_vect
__interrupt void Timer1Capt(void)
{
    TCNT1 = 0;         //обнуляем счетный регистр
    tachBuf  = ICR1;  //сохраняем значение регистра захвата в буфере
}

Вполне рабочий вариант, но лучше вычислять значение частоты по нескольким измерениям периода. Это сделает показания частотомера более стабильными, они не будут дергаться.  

volatile unsigned int tachBuf = 0;
unsigned long tachFltr = 0;
unsigned char count = 0;

//прерывание по событию захват
#pragma vector=TIMER1_CAPT_vect
__interrupt void Timer1Capt(void)
{
    TCNT1 = 0;
    
   //накапливаем 8 измерений и вычисляем среднее арифметическое
    tachFltr += ICR1;
    count++;
    if (count == 8)  
    {
      tachBuf = (unsigned int)(tachFltr >> 3);
      tachFltr = 0;
      count = 0;
    }
}

Функция, возвращающая содержимое буфера

__monitor unsigned int TIM_GetTachBuf(void)
{
  unsigned int tmp = tachBuf;
  tachBuf = 0;
  return tmp;
}

    Копируем содержимое буфера во временную переменную, очищаем буфер, а значение временной переменной возвращаем. Функция объявлена как мониторная, чтобы операции чтения и очистки буфера не могли быть прерваны прерыванием.

Основная программа

//****************************************************
//  Author(s)...: Pashgan    http://ChipEnable.Ru   
//  Target(s)...: ATMega8535
//  Compiler....: IAR 5.11A
//  Description.: Таймер Т1. Прерывание по событию захват.
//                         Простой частотомер на AVR
//  Data........: 13.02.10
//*****************************************************

#include <ioavr.h>
#include <intrinsics.h>
#include "lcd_lib.h"
#include "bcd.h"
#include "timer.h"

int main(void)
{
  unsigned int tachValue;

  PORTD = 0xff;
  DDRD = 0x00;
 
  LCD_Init();
  TIM_Init();
 
  __enable_interrupt();
  while(1){
    __delay_cycles(1000000);
    
    //берем “захваченное” значение и выводим его на  lcd
    tachValue = TIM_GetTachBuf();
    LCD_Goto(0,1);
    BCD_5IntLcd(tachValue);
    
    //вычисляем значение частоты и выводим его на  lcd
    tachValue = (unsigned int) (8000000UL/tachValue);
    LCD_Goto(0,0);
    BCD_5IntLcd(tachValue);    
  }
  return 0;
}

Алгоритм работы программы был разобран выше, единственное на чем стоит остановиться это то,  как вычисляется частота сигнала.

  Значение, которое сохраняет блок захвата, соответствует числу тиков таймера укладывающихся в период измеряемого сигнала. Предделитель таймера Т1 равен 1, соответственно тактовая частота таймера равна тактовой частоте микроконтроллера – 8 МГц. Отсюда период измеряемого сигнала  = ICR * 1/80000000. А частота = 1 /(ICR *1/8000000) = 8000000/ICR.

    tachValue = (unsigned int) (8000000UL/tachValue);

UL – указание компилятору, что тип констаныт unsigned long. Конечно, он и сам может догадаться, но это не будет лишним.
(unsigned int) – указание компилятору преобразовать результат вычисления к типу unsigned int.

Погрешность

   Компилируем проект и прошиваем микроконтроллер. Подаем на вывод ICP прямоугольный сигнал с генератора и смотрим на LCD. Показания похожи на правду, но с повышением частоты измеряемого сигнала, наш частотомер начинает врать. В чем дело? Похоже мы не учли какой-то нюанс. А нюанс действительно есть.

   Во-первых, между изменением состояния входа блока захвата и копированием значения счетного регистра проходит время величиной в 2,5...3,5 такта микроконтроллера. При включении схемы подавления помех задержка увеличивается еще на 4 такта.

   Во-вторых, наименьшее время отклика для любого прерывания составляет 4 такта, в течении которых происходит сохранение счетчика команд в стеке. В течении следующих 2-3 тактов выполняется команда перехода к подпрограмме обработки прерывания. (Если прерывание произойдет во время выполнения команды длящейся несколько тактов, то генерация прерывания произойдет только после выполнения этой команды)

   И в третьих. В обработчике прерывания по событию захват мы обнуляем счетный регистр. Если заглянуть в ассемблерный листинг модуля timer.c, то мы увидим, что это происходит не сразу, так как в начале прерывания несколько регистров сохраняются в стеке.

     20          __interrupt void Timer1Capt(void)
   \                     Timer1Capt:
     21          {
   \   00000000   93AA               ST      -Y, R26
   \   00000002   939A               ST      -Y, R25
   \   00000004   938A               ST      -Y, R24
   \   00000006   93FA               ST      -Y, R31
   \   00000008   93EA               ST      -Y, R30
   \   0000000A   934A               ST      -Y, R20
   \   0000000C   933A               ST      -Y, R19
   \   0000000E   932A               ST      -Y, R18
   \   00000010   931A               ST      -Y, R17
   \   00000012   930A               ST      -Y, R16
   \   00000014   B74F               IN      R20, 0x3F
     22              TCNT1 = 0;
   \   00000016   E000               LDI     R16, 0
   \   00000018   BD0D               OUT     0x2D, R16
   \   0000001A   BD0C               OUT     0x2C, R16
  .....

Каждая инструкция ST – это два такта микроконтроллера . IN, LDI, OUT – один такт. Итого 24 такта. (Кстати эта задержка может меняться в зависимости от компилятора)

   В итоге получается, что между захватом и обнулением счетного регистра проходит время  равное (2,5..3,5) + 4 + (2..3) + 24 ~ 34 тактам и это время мы не учитываем при расчете частоты.  
   Исправим этот момент.

volatile unsigned int tachBuf = 0;
unsigned long tachFltr = 0;
unsigned char count = 0;
#define ERROR 34

//прерывание по событию захват
#pragma vector=TIMER1_CAPT_vect
__interrupt void Timer1Capt(void)
{
    TCNT1 = 0;
    
    tachFltr += (ICR1 + ERROR);
    count++;
    if (count == 8)
    {
      tachBuf = (unsigned int)(tachFltr >> 3);
      tachFltr = 0;
      count = 0;
    }
}

Прошиваем микроконтроллер и снова проверяем частотомер. Ого, намного лучше!!!

Файлы

Частотомер на AVR. IAR
Частотомер на AVR. WINAVR
Частотомер на AVR. CodeVision
Проект для Proteus`a

PS

   Значение ERROR можно определить и экспериментально. Подаем на вывод ICP сигнал частотой 1 КГц и смотрим на значение счетного регистра (мы ведь не случайно выводим его на дисплей!). При тактовой частоте микроконтроллера в 8МГц значение счетного регистра должно быть равно 8000. Поправка ERROR будет равна 8000 – текущее значение счетного регистра.
   Если честно, я так и сделал. Но это нисколько не обесценивает вышеизложенного объяснения!!!

Продолжение -  Частотомер на микроконтроллере. Улучшенная версия

У вас недостаточно прав для комментирования.