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

16/02/2010 - 04:11

Схема

Схема частотомера на 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 – текущее значение счетного регистра.
   Если честно, я так и сделал. Но это нисколько не обесценивает вышеизложенного объяснения!!!

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

Comments   

# gringo 2010-02-17 07:02
Не приходилось использовать таймер Т1 в подобных режимах, но теперь я буду в курсе нюансов. Спасибо.
# Guest 2010-02-17 11:13
Интересненько.. . только вот LCD... - это модно конечно, только не для провинции:(
Неплохо бы было бы закоментировать в коде процедурку для вывода на 7-сегментник)
# foxit 2010-02-17 14:26
Интересно.
А какие пределы измерения?
Как можно расширить?
# Pashgan 2010-02-17 15:56
Quote:
только вот LCD... - это модно конечно, только не для провинции:(
с LCD просто удобнее работать, поэтому и использую

Quote:
А какие пределы измерения? Как можно расширить?
~200..50000Гц. При меньшей частоте происходит переполнение счетного регистра. При большей - микроконтроллер не успевает обрабатывать прерывания. Чтобы расширить диапазон нужно:
- переключать предделитель таймера в зависимости от измеряемой частоты - так мы избежим переполнения
- использовать внешний счетчик для предварительног о деления частоты
# foxit 2010-02-17 16:07
раз уж зашел разговор про измерения, то напиши статьи, как измерять емкость, индуктивность, сопротивление.
# Pashgan 2010-02-18 21:17
поставлю в очередь..
# akl 2010-02-20 07:34
Здравствуйте.
Quote:
А частота = 1 /(ICR *1/8000000) = 8000000/ICR.

Это выражение справедливо для измерения частоты по одному периоду, что не всегда даст приемлемый результат. Более полное выражение измерения частоты таким методом
Fx=Fo*Nx/nx, где
Fx-измеряемая частота
Fo-частота тактирования таймера
Nx-целое число периодов Fx за время измерения
nx-целое число периодов Fo за время Nx

Quote:
Во-первых, между изменением состояния входа блока захвата и копированием значения счетного регистра
проходит время величиной в 2,5...3,5 такта микроконтроллера.
Это не так. Копирование происходит на аппаратном уровне и занимает всего один тик контроллера.
# akl 2010-02-20 07:43
Quote:
~200..50000Гц
Вот здесь добились гораздо более высоких параметров.
http://pro-radio.ru/controllers/5978/
Есть еще один момент, который (возможно компилятор это делает при инициализации)н е отражен в программе,
а именно, сброс флага ICF перед началом измерения.
# Pashgan 2010-02-20 17:27
Quote:
Это не так. Копирование происходит на аппаратном уровне и занимает всего один тик контроллера.
Задержку вносит синхронизатор и детектор фронтов.
"Both the Input Capture pin (ICP1) and the Analog Comparator output (ACO) inputs are sampled
using the same technique as for the T1 pin (Figure 16-1 on page 135). The edge detector is also
identical."
"The synchronization and edge detector logic introduces a delay of 2.5 to 3.5 system clock cycles
from an edge has been applied to the T1/T0 pin to the counter is updated."

Quote:
Вот здесь добились гораздо более высоких параметров
Видел я в интернете проекты частотомеров, но у меня изначально не стояло задачи добиваться высоких параметров. Я просто показал, как можно использовать блок захвата.

Quote:
Есть еще один момент, который (возможно компилятор это делает при инициализации)не отражен в программе, а именно, сброс флага ICF перед началом измерения.
Сброс флага происходит аппаратно, когда выполняется прерывание.
# Guest 2010-02-26 13:49
Ошибку можно исключить полностью, если не обнулять таймер. Достаточно просто сохранять предыдущее значение ICR и вычитать его из текущего. Получите точное количество тактов между фронтами независимо от размера пролога прерывания и наличия других прерываний. Вычисление фильтра лучше перенести в основную программу - время ее выполнения не критично в отличие от времени обработки прерываний.
# Guest 2010-09-24 19:51
Подскажите пожалуеста разбираю код на CodeVision и не могу въехать в смысл использования запрета прерывания #asm(cli), когда забираем значение tachBuf. Если не трудно, объясните поподробнее хотелось бы разобраться.
# Pashgan 2010-09-29 20:53
Смысл в том, чтобы обеспечить атомарность операции считывания 16-ти разрядной переменной. В то время как мы читаем один из байтов переменной tachBuf, может возникнуть прерывание и обработчик изменит ее значение. Тогда, по возвращению в основную программу, мы прочтем оставшуюся часть переменной уже от ее нового значения. Почитай вот эту статью chipenable.ru/index.php/programming-c/16-volatile-critical-section.html
# ditya 2010-10-27 15:13
можно ли сконфигурироват ь таймер так
чтоб по переднему фронту он очищался
а по заднему срабатывал захват
то есть можно было померить длительность
положительного импульса
# Pashgan 2010-10-31 20:54
Думаю да. Устанавливаем срабатывание схемы захвата на положительный фронт. Когда происходит прерывание - устанавливаем срабатывание на отрицательный фронт и обнуляем счетный регистр. Когда происходит захват по отрицательному фронту - сохраняем захваченное значение и снова меняем фронт срабатывания схемы захвата.
# Андрей 2012-11-13 17:36
Quoting Pashgan:
Думаю да. Устанавливаем срабатывание схемы захвата на положительный фронт. Когда происходит прерывание - устанавливаем срабатывание на отрицательный фронт и обнуляем счетный регистр. Когда происходит захват по отрицательному фронту - сохраняем захваченное значение и снова меняем фронт срабатывания схемы захвата.

Всем привет. Паша и друзья, очень нужна ваша помощь. Сразу хочу заметить, что я новичок и самоучка, поэтому, если можно, подробней объясните где и что я не так делаю.
Значит так; работаю с atmega128 и ultrasonic Sensor (HC-SR04). Решил написать код который будет измерять положительный импульс эхо сенсора, обрабатывать его выдавать расстояние.
Базу программы взял Пашину, выучил, почитал комментарии (особенно совета как можно при помощи прерывания блока .
# Андрей 2012-11-13 17:38
захвата посчитать положительный импульс) и сел писать программу... Но, ничего не работает. Даже тригер не дает на сенсор (проверил скопом).
Если есть добровольцы, которые готовы помочь, то я выложу программу.
Очень прошу помощи.
Зарание благодарен
# Axiles 2010-10-29 14:13
Подскажите, я сталкнулся с такой проблемой, что таймер Т1 не работает в режиме Fast PWM в AVR Studio. Все делаю как в даташите.
TCCR1A = (1
# Pashgan 2010-10-31 20:48
сделай тему на форуме, там разберемся
# San 2010-12-18 16:17
Многие, наверно, слышали про такие устройства, как "propeller clock", SpokePOV и т.п. Т.е. вывод рисунка при механическом вращении линейки светодиодов. Там все проекты на ASMe, а я только вот начал Си изучать, ассемблер пока не рассматриваю.
Так вот, как использовать таймер в случае подсчёта периода обращения колеса для формирования задержки вывода на индикацию?
Я с выводом информации-то разобрался. Но вот как засинхронизиров ать изображение, чтобы оно оставалось в статичном виде?
# Pashgan 2010-12-19 19:46
Завести строб сигнал на ножку внешнего прерывания и запускать формирование изображения по нему.
# ниндзя 2011-12-05 15:24
а можете полностью дать код на си? одним файлом/текстом. чтобы не раздроблено было
# Слава 2011-12-26 00:11
Можете пожалуйста фьюх биты, какие нужно в какие положения установить.
# DmAlex 2013-01-01 05:16
Отличная статья спасибо автору.
Но мне непонятно одна вещь, а именно:
когда вызывается прерывание по событию захвата? Если посмотреть по коду, то в перывании есть накопленное ICR1, непонятно за какой период оно накапливается.
# Андрей 2013-01-01 12:36
Всем привет.
Друзья нужна ваша помощь.
Работаю с: Atmega128 в CodeVision AVR (2.04.4a).
Запустил первый счетчик на измерение длительности положительного импульса (с прерыванием по захвату) - работает как часы.
Теперь необходимо запустить третий счетчик для точно такой же функции. Вроде все просто, но... Компайлер ругается; "undefined symbol ICR3" и "undefined symbol TCNT3". В чем проблема, не могу понять. Очень прошу вашей помощи.
Зарание благодарен.
# AlexAst 2013-04-08 09:28
Wozmozhno registry nazyvayutsya podrugomu?! Posmotri podrobnee Datasheet. U menya seichas 664P i tut woobsche est dve linii ragistrov na vyhode schetchika . est Registry A a est B.
# AlexAst 2013-04-08 09:36
Vozmi etu inicyalizaciyu


// Timer/Counter 3 initialization
// Clock source: T3 pin Falling Edge
// Mode: Normal top=0xFFFF
// OC3A output: Disconnected
// OC3B output: Disconnected
// OC3C output: Disconnected
// Noise Canceler: Off
// Input Capture on Falling Edge
// Timer3 Overflow Interrupt: Off
// Input Capture Interrupt: On
// Compare A Match Interrupt: Off
// Compare B Match Interrupt: Off
// Compare C Match Interrupt: Off
TCCR3A=(0
# AlexAst 2013-04-08 09:43
Code:
TCCR3A=(0<<COM3A1) | (0<<COM3A0) | (0<<COM3B1) | (0<<COM3B0) | (0<<COM3C1) | (0<<COM3C0) | (0<<WGM31) | (0<<WGM30);
TCCR3B=(0<<ICNC3) | (0<<ICES3) | (0<<WGM33) | (0<<WGM32) | (1<<CS32) | (1<<CS31) | (0<<CS30);
TCNT3H=0x00;
TCNT3L=0x00;
ICR3H=0x00;
ICR3L=0x00;
OCR3AH=0x00;
OCR3AL=0x00;
OCR3BH=0x00;
OCR3BL=0x00;
OCR3CH=0x00;
OCR3CL=0x00;

ETIMSK=(1<<TICIE3) | (0<<OCIE3A) | (0<<OCIE3B) | (0<<TOIE3) | (0<<OCIE3C) | (0<<OCIE1C);


Esche posmotri pravelniy vhodnoi pin
# DmitriyStud 2013-05-26 09:46
помогите переделать код под atmega16
# Роман 2013-07-23 20:42
При уменьшении частоты (например 50Гц) показания начинают прыгать. Я так понимаю происходит это из-за переполнения счётного регистра таймера Т1, максимальное значение которого 65536. Если мне нужно измерять частоту от 1 до 200Гц. могу ли я использовать предделитель на 256 - тогда при входной частоте 1Гц должно быть около 62500 тиков? Если я правильно думаю, то как при этом должна выглядеть формула расчёта частоты?
# Pashgan 2013-07-24 15:20
Да, скорее всего из-за переполнения. Формула при использовании предделителей отличных от 1 будет выглядеть так:
F = Fcpu/(ICR*Pre), где Fcpu - тактовая частота микроконтроллер а, ICR захваченное значение таймера, Pre - предделитель таймера.
# eess9 2013-11-17 11:54
А можно подробней об этом:
"Функция объявлена как мониторная, чтобы операции чтения и очистки буфера не могли быть прерваны прерыванием."
"мониторная функция" - сам придумал?
# Pashgan 2013-11-17 13:40
Назовем это сленгом. Функция объявлена с ключевым словом компилятора IAR AVR - __monitor. При входе в эту функцию содержимое регистра SREG копируется во временную переменную и запрещаются прерывания. При выходе из функции значение регистра SREG восстанавливает ся. Таким образом, пока выполняется эта функция, прерывания не могут вызываться.
# Peter 2014-05-27 20:20
А еще можно расширить нижнюю границу чувствительност и до бесконечности к 0Hz, добавив прерывание по переполнению и переменную в его обработчик в которую будет добавляться значение полного периода таймера и сбрасываться при получении долгожданного фронта ;)

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