Схема
Проверяя работу частотомера, будьте аккуратны – не подайте на вход микроконтроллера повышенного напряжения! Логика программы
В начале программы выполняется конфигурирование портов ввода/вывода (вывод ICP настраивается на вход и включается подтягивающий резистор), настройка таймера Т1 и инициализация символьного LCD.
Затем микроконтроллер попадает в бесконечный цикл, где каждые N мс считывает содержимое буфера, вычисляет частоту и выводит ее значение на дисплей. Также на дисплей выводится содержимое регистра захвата.
Затем микроконтроллер попадает в бесконечный цикл, где каждые 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 – текущее значение счетного регистра.
Если честно, я так и сделал. Но это нисколько не обесценивает вышеизложенного объяснения!!! Продолжение - Частотомер на микроконтроллере. Улучшенная версия