Учебный курс. Подключение энкодера. Типовая структура простой программы. Генератор на AVR.

19/11/2009 - 22:12
   Принцип действия, схема включения и исходник библиотеки для работы с инкрементным энкодером уже рассматривался мной в одной из статей. Сегодня мы поговорим о практическом применении энкодера. В качестве примера я выбрал программу генератора прямоугольного сигнала с диапазоном рабочих частот 1 – 100 Гц. Первоначальный замысел предполагал диапазон 1 - 1000 Гц, но на практике выяснилось, что перебирать тысячу значений утомительно даже с энкодером.

 

Подготовка

Создаем в пустом workspace`е новый проект
Project > Create New Project…
Тип шаблона C > main

Копируем в папку проекта файлы исходника библиотеки для работы с энкодером
encoder.h и encoder.c

Подключаем к нашему проекту файл encoder.c
Правая кнопка мышки в окне workspace и в открывшемся меню Add > Add Files…

Копируем файл bits_macros.h в папку проекта.


Подключаем заголовочные файлы

В начале файла main.c забиваем следующие строки
#include <ioavr.h>
#include <intrinsics.h>
#include "encoder.h"
#include "bits_macros.h"

Задаем настройки проекта

Project > Options

   Тип микроконтроллера
   General Options > Target > Processor Configuration > ATMega8535
   
   Разрешение использования имен битов определенных в заголовочных файлах
   General Options > System > Enable bit defenitions...

  Оптимизация кода по размеру
  C/C++ Compiler > Optimisations >Size High
 
  Тип выходного файла
  Linker > Output File  галочка Override default  и поменять расширение на hex
  Linker > Format > Other  выбрать Intel Standart

Жмем Ок. Сохраняем проект и workspace.
Теперь у нас есть пустой проект с подключенной либой и заданными настройками.

Задача

   Заставить микроконтроллер генерировать меандр с частотой от 1 до 100 Гц. Значение частоты должно задаваться с помощью энкодера. Поворот энкодера на одну позицию должен соответствовать изменению частоты генератора на 1 Гц.

Схема для нашего примера

схема подключения энкодера к микроконтроллеру
   К выводу, на котором будет генерироваться меандр, подключен светодиод, чтобы хоть как-то видеть результат работы программы. Вряд ли у многих под рукой осциллограф.

Алгоритм программы

   Прямоугольный сигнал генерируется с помощью 16 разрядного таймера Т1, который работает в режиме СТС – сброс при совпадении. Во флэш памяти микроконтроллера  храниться массив, содержащий для каждого значения требуемой частоты константу. Для доступа к элементам массива используется переменная pTimerValue. В прерываниях таймера Т1 значение константы считывается и записывается в регистр сравнения.
   Для генерации сигнала используется вывод PD5 (OC1A). У него есть альтернативные функции – он может менять свое состояние на противоположное при равенстве счетного регистра и регистра сравнения.
   Опрос энкодера выполняется в прерывании таймера Т0. Частота работы таймера ~ единицы килогерц. Режим работы – Normal (здесь не принципиально, какой режим).
   В основной программе в бесконечном цикле while микроконтроллер опрашивает буфер энкодера и в зависимости от его значения уменьшает или увеличивает переменную pTimerValue.
   В самом начале main`а располагается код инициализации периферии и необходимых переменных.

Структура программы

Для наглядности я изобразил структуру программы в виде диаграммы.

типовая структура простой микроконтроллерной программы

Это типовая структура построения простых программ. Прерывания естественно происходят в произвольном месте цикла.
  • Инициализация.
  • Бесконечный цикл (так называемый superloop), в котором происходит ожидание события, обычно в виде опроса флагов или какого-нибудь буфера.
  • Параллельная работа периферийных устройств, вызывающих прерывания. В них выполняется какой-то код (желательно короткий) и выставляются флаги.
Для простых задач такого подхода хватает за глаза. Для сложных существуют другие способы организации программ. Наберитесь терпения, скоро и до них дойдет дело.

Расчет констант для таймера Т1

   Рассчитаем значение константы для частоты 1 Гц. Подобный расчет я уже приводил, но будет не лишним его вспомнить
  Тактовая частота микроконтроллера 16 МГц (смотрите схему). Коэффициент предделителя таймера - 256. Он позволяет получить прерывания с любой частотой из нашего диапазона.
   Период одного тика таймера будет равен 1/(16 МГц/ 256) = 16 мкс
На выводе PD5 нам нужно получить сигнал частотой 1 Гц. Вывод меняет свое состояние на каждое прерывание таймера и значит, частота прерываний должна быть в 2 раза больше. Для нашего случая - 2 Гц.
   Сколько тиков таймера уложится в 2 Герца? (1/2 Гц)/16 мкс = 31250
   Это и есть искомая константа.
   Остальные значения рассчитываются аналогично. Я для этого обычно использую Exel.   

Полученные значения мы помещаем в массив

__flash unsigned int timerValue[] =
{

};

сохраняем его в отдельном файле – timer_value.h и подлючаем его к файлу main.c

  #include "timer_value.h"

Да, еще нужно добавить парочку констант в этот файл

#define MAX_TIM_VALUE 99
#define MIN_TIM_VALUE 0


Убедимся, что правильно рассчитали константы для таймера. Запустим его. Код программы будет такой.

//программирование AVR  на Си
//пример использования энкодера
//ChipEnable.ru     17.10.09

#include <ioavr.h>
#include <intrinsics.h>
#include "encoder.h"
#include "bits_macros.h"
#include "timer_value.h"

//индекс для доступа к элементам массива
volatile unsigned char pTimerValue = 0;

int main(void)
{
//инициализация таймера Т1
TCNT1 = 0;
TCCR1A = (0<<COM1A1)|(1<<COM1A0)|(0<<WGM11)|(0<<WGM10);
TCCR1B = (0<<WGM13)|(1<<WGM12)|(1<<CS12)|(0<<CS11)|(0<<CS10);
OCR1A = timerValue[pTimerValue];

//настройка вывода PD5 на выход
SetBit(PORTD, PD5);
SetBit(DDRD, PD5);

//ничего не делаем в бесконечном цикле
while(1);
return 0;
}

Думаю, пояснения требует только кусок инициализации таймера.

Обнуление счетного регистра
TCNT1 = 0;

Инициализация конфигурационных регистров таймера Т1.
TCCR1A = (0<<COM1A1)|(1<<COM1A0)|(0<<WGM11)|(0<<WGM10);
TCCR1B = (0<<WGM13)|(1<<WGM12)|(1<<CS12)|(0<<CS11)|(0<<CS10);

Где биты WGM13, WGM12, WGM11, WGM10 задают режим работы таймера – СТС,
CS12, CS11, CS10 – определяют коэффициент предделителя таймера –256,
COM1A1, COM1A0 – определяют поведение вывода PD5(OC1F) – в данном случае по сигналу таймера он будет менять свое состояние на противоположное

Инициализация регистра совпадения начальным значением.
OCR1A = timerValue[pTimerValue];

Компилируем программу и грузим в микроконтроллер. Светодиод должен моргать с частотой 1 Гц.
В программе нет никаких прерываний. Нет никаких манипуляций с выводом PD5. Однако светодиод моргает!

Программа

  Теперь нужно “прикрутить” к этой программе энкодер. Зададим настройки в хедер файле encoder.h – порт и выводы, к которым подключен энкодер, значения констант.

#define PORT_Enc   PORTA     
#define PIN_Enc      PINA
#define DDR_Enc     DDRA
#define Pin1_Enc     2
#define Pin2_Enc     1

#define RIGHT_SPIN 0x01
#define LEFT_SPIN 0xff

 Хедер содержит прототипы трех функций. Вспомним их назначение.

void ENC_InitEncoder(void) настраивает выводы микроконтроллера, к которым подключен энкодер на вход. Эту функцию нужно вызвать в начале main`а.

void ENC_PollEncoder(void) – однократно опрашивает энкодер, анализирует текущее и предыдущее состояния и записывает в буфер соответствующие константы (RIGHT_SPIN и LEFT_SPIN). Эта функция будет сидеть в прерывании таймера Т0.

unsigned char ENC_GetStateEncoder(void) – возвращает содержимое буфера энкодера. Если поворот на одну позицию не был зафиксирован – функция вернет 0, если поворот был зафиксирован функция вернет значение соответствующей константы. При этом значение буфера очистится. Эта функция будет вызываться в основном программе – в цикле while.

Дополняем нашу программу. Можете попробовать сделать это самостоятельно.

//программирование AVR  на Си
//пример использования энкодера
//ChipEnable.ru     17.10.09


#include <ioavr.h>
#include <intrinsics.h>
#include "encoder.h"
#include "bits_macros.h"
#include "timer_value.h"

#define TCNT0_const 253
#define TCCR0_const 5

volatile unsigned char pTimerValue = 0;

int main(void)
{
ENC_InitEncoder();

//инициализация таймера т0
TCNT0 = TCNT0_const;
TCCR0 = TCCR0_const;

//инициализация таймера т1
TCNT1 = 0;
TCCR1A = (0<<COM1A1)|(1<<COM1A0)|(0<<WGM11)|(0<<WGM10);
TCCR1B = (0<<WGM13)|(1<<WGM12)|(1<<CS12)|(0<<CS11)|(0<<CS10);
OCR1A = timerValue[pTimerValue];

//разрешение прерываний от таймеров
//т0 - по переполнению, т1 - по совпадению

TIMSK = (1<<TOIE0)|(1<<OCIE1A);

//настраиваем PD5 на выход
SetBit(PORTD, PD5);
SetBit(DDRD, PD5);

__enable_interrupt();
while(1){
  //считываем содержимое буфера энкодера
  //после считывания он очищается

  unsigned char stateEnc = ENC_GetStateEncoder();

  //если не пустой
  if (stateEnc != 0){
       //определяем направление вращения и изменяем переменную timerValue
       if (stateEnc == RIGHT_SPIN){
         if (pTimerValue == MAX_TIM_VALUE) pTimerValue = MIN_TIM_VALUE;
         else pTimerValue++;  
       }
      if (stateEnc == LEFT_SPIN) {
          if (pTimerValue == MIN_TIM_VALUE) pTimerValue = MAX_TIM_VALUE;
          else pTimerValue--;  
       }
  }
}  
return 0;
}


//опрос энкодера
#pragma vector=TIMER0_OVF_vect
__interrupt void timer0_ovf_my(void)
{
  TCNT0 = TCNT0_const;
  ENC_PollEncoder();
}


#pragma vector=TIMER1_COMPA_vect
__interrupt void timer1_compa_my(void)
{
  //обновляем значение регистра стравнения
  OCR1A = timerValue[pTimerValue];
}

Вроде все должно быть понятно.
Кусок кода, в котором изменяется значение pTimerValue, можно было бы написать еще так:

if (stateEnc != 0) {
    pTimerValue = pTimerValue + stateEnc;  
    if (pTimerValue == (MAX_TIM_VALUE + 1)) pTimerValue = MIN_TIM_VALUE;
    else if (pTimerValue == (MIN_TIM_VALUE - 1)) pTimerValue = MAX_TIM_VALUE;
}

При вращении энкодера вправо pTimerValue складывается с 1, то есть инкрементируется.
При вращении энкодера влево pTimerValue складывается с 0хff, что равносильно вычитанию 1. Одна и та же операция, а результат прямо противоположный.
(Двоичные числа можно вычитать складывая. Для этого одно из чисел нужно представить в дополнительном коде – вычесть из него 1 и проинвертировать все разряды. 0xff – это единица в дополнительном коде)

Файлы

Энкодер. Генератор на AVR – проект для IARa.
Энкодер. Генератор на AVR – проект для WINAVR.

Comments   

# Guest 2009-11-22 13:19
Здравствуйе
в проекте для IAR
в файле main.c забыли добавить
#include "encoder.c" //:)
без этого ошибку выдает.

P.S.
Спасибо за статьи по IAR, на просторах и-нета мало информации на руском и с обьяснениями !
# Pashgan 2009-11-22 17:19
encoder.c добавлен в проект.
в main`e включен хедер
#include "encoder.h"

у меня никаких ошибок нет.
# гость 2011-02-25 04:04
почему-то у меня не работает, нет выполнения условия в этих строках
if (tmp == b11100001) bufEnc = LEFT_SPIN;
if (tmp == b11010010) bufEnc = RIGHT_SPIN;
проверяю в протеусе
# Вад 2011-05-28 16:52
Quoting гость:
почему-то у меня не работает, нет выполнения условия в этих строках
if (tmp == b11100001) bufEnc = LEFT_SPIN;
if (tmp == b11010010) bufEnc = RIGHT_SPIN;
проверяю в протеусе


Возможно Вы неправильно подключили энкодер. "Общий" вывод не всегда посередине.
# Макс 2011-10-02 22:14
Почемуто некорректно работает энкодер... Работает только в одну сторону т.е. только прибавляет частоту или убавляет, если поменять местами его сигнальные выводы.
# Игорь89 2012-09-20 07:03
поясните пожалуста:
Quote:
На выводе PD5 нам нужно получить сигнал частотой 1 Гц. Вывод меняет свое состояние на каждое прерывание таймера и значит, частота прерываний должна быть в 2 раза больше.
почему в 2 раза больше, не пойму? )
# Pashgan 2012-09-20 14:57
Представь такую последовательность.

1****2****3****4****

цифры(1,2,3,4) - это моменты вызова обработчика прерывания.
звездочки(*)- тактовые импульсы таймера/счетчик а, период которых составляет, допустим, 1 секунду.

Согласно этой последовательно сти после каждой 4-ой секунды происходит прерывание. В одном прерывании мы устанавливаем на выводе микроконтроллер а лог. 1, в следующем прерывании - лог. 0 и т.д.

Частота прерываний будет равна 1/4с= 0,25 Гц. А частота генерируемого сигнала 1/8с=0,125 Гц. То есть в два раза меньше.
# Nick 2012-12-01 18:36
Почему при компиляции Iar ругается Warning[Pe111]: statement is unreachable C:\*\generator_ IAR\gen\main.c 55?
# Nick 2012-12-02 05:04
Наверно понял -из за цикла while
# Pashgan 2012-12-04 15:33
Да, из-за цикла while.
# hawk 2013-03-24 16:32
Code:#include <mega16.h>
#include <indicator.c>
#include <encoder.c>

interrupt [TIM0_OVF] void timer0_ovf_isr(void)
{

IND_Update();
ENC_PollEncoder();
}

unsigned int data=0;
void main(void)
{
IND_Init();
ENC_InitEncoder();

DDRB=(1<<DDB7) | (1<<DDB6) | (1<<DDB5) | (1<<DDB4) | (1<<DDB3) | (1<<DDB2) | (1<<DDB1) | (1<<DDB0);
PORTB=(1<<PORTB7) | (1<<PORTB6) | (1<<PORTB5) | (1<<PORTB4) | (1<<PORTB3) | (1<<PORTB2) | (1<<PORTB1) | (1<<PORTB0);

DDRC=(0<<DDC7) | (0<<DDC6) | (0<<DDC5) | (0<<DDC4) | (0<<DDC3) | (0<<DDC2) | (0<<DDC1) | (0<<DDC0);
PORTC=(1<<PORTC7) | (1<<PORTC6) | (1<<PORTC5) | (1<<PORTC4) | (1<<PORTC3) | (1<<PORTC2) | (1<<PORTC1) | (1<<PORTC0);

DDRD=(1<<DDD7) | (1<<DDD6) | (1<<DDD5) | (1<<DDD4) | (1<<DDD3) | (1<<DDD2) | (1<<DDD1) | (1<<DDD0);
PORTD=(1<<PORTD7) | (1<<PORTD6) | (1<<PORTD5) | (1<<PORTD4) | (1<<PORTD3) | (1<<PORTD2) | (1<<PORTD1) | (1<<PORTD0);

TCCR0=(0<<WGM00) | (1<<COM01) | (0<<COM00) | (0<<WGM01) | (0<<CS02) | (0<<CS01) | (1<<CS00);
TCNT0=0x00;
OCR0=0x00;

TCCR1A=(0<<COM1A1) | (0<<COM1A0) | (0<<COM1B1) | (0<<COM1B0) | (0<<WGM11) | (0<<WGM10);
TCCR1B=(0<<ICNC1) | (0<<ICES1) | (0<<WGM13) | (0<<WGM12) | (0<<CS12) | (0<<CS11) | (0<<CS10);
TCNT1H=0x00;
TCNT1L=0x00;
ICR1H=0x00;
ICR1L=0x00;
OCR1AH=0x00;
OCR1AL=0x00;
OCR1BH=0x00;
OCR1BL=0x00;

ASSR=0<<AS2;
TCCR2=(0<<PWM2) | (0<<COM21) | (0<<COM20) | (0<<CTC2) | (0<<CS22) | (0<<CS21) | (0<<CS20);
TCNT2=0x00;
OCR2=0x00;


TIMSK=(0<<OCIE2) | (0<<TOIE2) | (0<<TICIE1) | (0<<OCIE1A) | (0<<OCIE1B) | (0<<TOIE1) | (0<<OCIE0) | (1<<TOIE0);

// Global enable interrupts
#asm("sei")

while (1)
{

unsigned char stateEnc = ENC_GetStateEncoder();
if (stateEnc != 0)
{
if (stateEnc == RIGHT_SPIN) {
data++;
}
if (stateEnc == LEFT_SPIN) {
data--;
}
}

IND_Output(data, 0);

}
}
Подключил 7-сегм. индикатор и энкодер к мк. пытаюсь при вращении вывести на дисплей цифры, но ничего не меняется.
# hawk 2013-03-24 16:39
На дисплее горят нули, на энкодер никакой реакции. Подскажите в чем ошибка?
# Nick 2013-03-24 17:04
наверно стоит объявить volatile unsigned int data=0;
# hawk 2013-03-24 17:38
Не помогает. Энкодер такой: http://tixer.ru/catalog/buttons-switchers/encoders/enkoder_ec12e24204a8/. Не нашел на него распиновки, за землю взял средний вывод.
# hawk 2013-03-25 17:14
Вобщем намучался с этим энкодером :sad:
Изменил код Code:#include "mega16.h"
#include "indicator.h"
#include "encoder.c"

volatile unsigned int data = 500;

void main( void )
{
ENC_InitEncoder();
IND_Init();
//инициализация таймера Т0
TIMSK = (1<<TOIE0);
TCCR0 = (1<<CS02)|(0<<CS01)|(1<<CS00);
TCNT0 = 0xff;

#asm("sei");
while(1){
unsigned char stateEnc = ENC_GetStateEncoder();

if (stateEnc != 0)
{
//IND_Output(111, 0); // проверка
if (stateEnc == RIGHT_SPIN) {
data++;
}
if (stateEnc == LEFT_SPIN) {
data--;
}
}
IND_Output(data, 0);
//delay_ms(10);
}
}

interrupt [TIM0_OVF] void Timer(void)
{
TCNT0 = 0xff;
ENC_PollEncoder();
IND_Update();
}

результат также нулевой.
если сделать такую проверкуCode:if (stateEnc == 0)
{
IND_Output(111, 0); // проверка
}
то получается, что stateEnc у меня ноль. Не могу понять свою ошибку.
# Pashgan 2013-03-25 20:49
Сделай тему на форуме и выложи код, я посмотрю в чем дело.
# hawk 2013-03-26 18:25
Pashgan Весь код выше. Только мега у меня тактируется от внутреннего генератора 1 МГц
# Nick 2013-03-25 18:42
volatile unsigned int data = 500 изначально выводится на индикатор?В stateEnc хранится содержимое буфера энкодера, после считывания он очищается, то есть если он не поворачивается то будет 0. Думаю понятнее со схемой и проектом будет(неизвестн о что там в хидере и функциях, может ты к другим ногам цепляешь к примеру -схемы нет и второго файла си ). Совет: тут хороший пример - подключите диод, как в нем, и сначала сделай чтобы он менял частоту моргания. Если все заработает, то выводи на индикатор.
# hawk 2013-03-25 20:51
volatile unsigned int data = 500 выводится.
Пересоберу на макетке с диодом, посмотрим что выйдет.
# hawk 2013-03-28 18:28
Уважаемый автор, а какие фьюзы ставить?
# Pashgan 2013-03-28 18:35
Значения не помню. Фьюзы задающие в качестве источника тактирования внешний кварцевый резонатор. На схеме нарисован внешний кварц на 16 МГц.
# hawk 2013-03-28 20:12
Мде, воспользовался калькулятором avr.roboforum.ru/calc.html?part=ATmega16A выбрал Ext. Crystal/Resonat or Medium Freq.; Start-up time: 258 CK + 64 ms; [CKSEL=1100 SUT=01], в итоге залочил камень :-) Блин там (в калькуляторе) не указаны границы между low, Medium и High Freq.
Pashgan, как думаете, если оставить внутренний генератор 1 МГц и пересчитать массив timerValue, будет работать?

UPD. Со фьюзами разобрался, правда светодиод просто горит. Если не получится решить проблему, создам тему на форуме

UPD. Уряяя! Светик моргает, частота меняется :D . Теперь осталось индикатор присобачить
# Pashgan 2013-03-29 20:26
Работать будет, но лучше сконфигурируй внутренний генератор на 8 МГц.

Quote:
Уряяя! Светик моргает, частота меняется
Ну видишь, не зря мучался.
# Иван 2014-12-16 19:44
Добрый вечер товарищи купил энкодер абсолютный вот такой http://www.chipdip.ru/product/eaw0j-b24-ae0128l/был куплен для большой точности чем 24 импульса на оборот прикрутил к меге16, вроде заработал но он дает значения от 0,,,127 и по кругу понимаю на то он и абсолютный. Но хотелось бы чтоб как с инкрементальным пока не заполнится char или int. Просто не веду с чего функцию начинать писать опыта маловато. Направьте на путь истинный.

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