Формирование импульсов для IR-управления на аппаратном таймере-2 ATmega8

В этой статье приводится описание алгоритма формирования импульсов для IR-управления на аппаратном таймере-2 на примере ATmega8. К статье прилагается программа расчета констант и формирования фрагментов кода для AVR-С.

В процессе разработки поворотного стола для круговой фотосъемки возникла задача дистанционного управления фотокамерой. В поисках информации была найдена статья, автору которой я очень признателен за подробный разбор. Но у него стояла задача выдачи одной лишь команды на один тип камеры (ее выбор осуществляется на этапе компиляции), а мой девайс помимо этого занят другими важными делами, да и не люблю я транжирить память программ списком макросов Pulse/Pause. И к какой конфессии (Canon'истам или Nikon'истам) относится конечный пользователь — неизвестно. В результате было принято решение из статьи взять цифры, а остальное реализовать по-своему. Для этой задачи, как мне кажется, как нельзя лучше подходит аппаратный таймер-2. Потому что у нулевого нет прерывания по совпадению и, тем более, возможности «дрыгать» при этом ногой, а первый, 16-разрядный, тратить на это просто жалко.

Описание алгоритма для AVR

Как следует из вышеприведенной статьи, команды управления представляют последовательность серии импульсов определенной для каждой камеры частоты и пауз. Известна несущая частота, длительность серии импульсов и пауз. Например, для Canon несущая 32768Гц (данные из другого источника, который я не смог сейчас найти, в статье указана частота 33кГц), а серии импульсов и пауз (далее — фазы) таковы:
  1. импульсы длительностью 480 мкс;
  2. пауза 7324 мкс;
  3. импульсы 480 мкс.
Для формирования серии импульсов воспользуемся умением таймера T2 по прерыванию совпадения переключать состояние вывода OC2. Для этого нужно:
  • разрешить прерывание по сравнению для T2;
  • установить вывод OC2 в состояние выхода;
  • разрешить данный режим установкой бита FOC2 регистра TCCR2;
  • включить режим переключения состояния вывода OC2 на противоположное установкой комбинации битов COM20 и COM21 в регистре TCCR2: COM21=0, COM20=1;
  • разрешить режим CTC (сброс при совпадении) установкой бита WGM21 в TCCR2.
Для формирования паузы просто используем прерывание по сравнению без переключения вывода OC2 (но также с режимом CTC: — WGM21=1 ).

Для каждой фазы необходимо расчитать значения предделителя (CS2x) и регистра сравнения OCR2, а также программный декрементный счетчик (IR_TimerCounter), определяющий сколько импульсов в серии нужно выдать или интервалов в паузе отсчитать. Общее количество фаз, оставшихся до конца выдачи команды, будем хранить в декрементном счетчике (IR_PhaseCounter). Если для какой-то импульсной посылки не хватит 8-разрядного IR_TimerCounter, сделаем несколько импульсных «пачек» подряд — в этом случае количество фаз будет больше. Обозначим настройку регистра TCCR2 для формирования импульсов TCCR2_Pulse, а для пауз — TCCR2_Pause:
  • TCCR2_Pulse = 0x98 = 10011000
  • TCCR2_Pause = 0x08 = 00001000

Для загрузки нужного значения в TCCR2 к этим константам нужно добавить расчитанное значение предделителя CS2x:
  • Pulse: TCCR2 = TCCR2_Pulse + CS2x
  • Pause: TCCR2 = TCCR2_Pause + CS2x
Для хранения информации по каждой фазе объявим структуру (полностью код модуля содержится в аттаче):

typedef struct
	{
	u08 _OCR2;
	u08 _TCCR2;
	u08 _Counter;
	} TIR_T2Params;

И структуру для всей команды:

typedef struct
	{
	u08 PhaseCount;
	u08* pParams;
	} TIR_T2ParamsPointer;

и 3 глобальные переменные:

register volatile u08 IR_PhaseCounter asm("r8");	// Обратный счетчик фаз выполнения команды на съемку:
register volatile u08 IR_TimerCounter asm("r9");	// Обратный счетчик интервалов
volatile u08* pIR_CurrentData;				// Указатель на текущие данные

Желающие могут не использовать регистры (или использовать другие), но: а) в моем случае это было безопасно, б) это декрементные счетчики, будучи размещенными в регистрах, они дадут гораздо более компактный код. Если кто-то знает, как «научить» оптимизатор сделать это безопасно самому, я буду очень признателен.
Собственно, прототипы функций:

static inline void IR_Init(void) __attribute__((always_inline));	// Инициализация управления IR
static inline void IR_TimerReset(void) __attribute__((always_inline));	// Сброс предделителя и счетчика
static inline void IR_TimerStop(void) __attribute__((always_inline));	// Остановка таймера
extern void IR_Shot(u08 AIR_CameraType);				// Подача сингала для мгновенной съемки

Все функции кроме IR_Shot объявлены встроенными — они короткие и накладные расходы на их «полноценное» исполнение не рентабельны:

//-------------------------------------------------------------------------
//	Функции управления IR
//-------------------------------------------------------------------------
//	Инициализация порта и включение прерывания таймера2 по сравнению для управления IR
//
void IR_Init()
{
IR_PhaseCounter = 0;
IR_TimerCounter = 0;
LED_IR_DDR |= _BV(LED_IR);	// Вывод для IR - на выход 
TIMSK |= _BV(OCIE2);		// Разрешить прерывание по сравнению T2.
}

//-------------------------------------------------------------------------
//	Сброс предделителя и счетчика
//
void IR_TimerReset(void)
{
SFIOR = _BV(PSR2);
TCNT2 = 0;
}

//-------------------------------------------------------------------------
//	Остановка таймера
//
void IR_TimerStop(void)
{
TCCR2 = 0;
IR_PhaseCounter = 0;
IR_TimerCounter = 0;
LED_IR_PORT &= ~_BV(LED_IR);
}

А теперь собственно функции старта IR-команды и обработки прерывания:

//-------------------------------------------------------------------------
//	Обработка прерывания по сравнению таймера 2
//
ISR(TIMER2_COMP_vect)
{
sei();

//	
if(0 == IR_PhaseCounter)
	IR_TimerStop();
else
	{
	if(0 == IR_TimerCounter)
		{
		IR_PhaseCounter--;
		IR_TimerReset();

//	Загружаем данные из структуры TIR_T2Params 
		OCR2 = pgm_read_byte(pIR_CurrentData);
		pIR_CurrentData++; 
		TCCR2 = pgm_read_byte(pIR_CurrentData);
		pIR_CurrentData++; 
		IR_TimerCounter = pgm_read_byte(pIR_CurrentData);
		pIR_CurrentData++;		// Указатель показывает на структуру TIR_T2Params следующей фазы 
		}
	else
		IR_TimerCounter--;
	}

}

//-------------------------------------------------------------------------
//	Подача сингала для мгновенной съемки
//
void IR_Shot(u08 AIR_CameraType)
{
pIR_CurrentData = (u08*)&(IR_T2ParamsPointers[AIR_CameraType]);	// Получить указатель на стуктуру-указатель на массивы TIR_T2Params
IR_PhaseCounter = pgm_read_byte(pIR_CurrentData);		// Получить количество фаз
pIR_CurrentData++;						// Перейти на адрес собственно массива TIR_T2Params 
pIR_CurrentData = (u08*)pgm_read_word(pIR_CurrentData);		// Получить адрес собственно массива TIR_T2Params

//	Загружаем данные из структуры TIR_T2Params 
OCR2 = pgm_read_byte(pIR_CurrentData);
pIR_CurrentData++; 
TCCR2 = pgm_read_byte(pIR_CurrentData);
pIR_CurrentData++; 
IR_TimerCounter = pgm_read_byte(pIR_CurrentData);
pIR_CurrentData++;						// Указатель показывает на структуру TIR_T2Params следующей фазы 
}

Вот, собственно, и все. AIR_CameraType — приведенное к u08 значение перечислимого типа:

typedef enum
	{
	ctCanon = 0,		// Canon
	ctNikon = 1,		// Nikon
	ctPentax = 2		// Pentax
	} TIR_CameraType;

И заполненные константами структуры и массив указателей на них. О том, как они сформированы — в следующей части статьи.

Расчет констант для импульсов и пауз

Программа расчета реализована в среде разработки Delphi-2007. Полностью код я приводить не буду, предлагаю использовать программу «как есть», но основные «алгоритмические» процедуры приведены ниже:

procedure TdkCalcT2MainForm.ButtonCalcClick(Sender: TObject);
var
  i: Integer;
  j: Integer;
  Err: Integer;
  Cnt_int: Integer;
  Cnt_byte: byte;
  Pulse: boolean;
  Duration: integer;
  TCCR2: byte;
  OCR2: byte;
  CS2x: byte;
  Res: Integer;
begin
//  Очистка таблицы результатов

  for i := cxGridT2ResultTableView.DataController.RowCount - 1 downto 0 do
    cxGridT2ResultTableView.DataController.DeleteRecord(i);

//  Теперь цикл по всем строкам

  for i := 0 to cxGridT2SourceTableView.DataController.RowCount - 1 do
  begin
    Pulse := cxGridT2SourceTableView.DataController.Values[i, 0];
    Duration := cxGridT2SourceTableView.DataController.Values[i, 1];

    if(Pulse) then  //  Если импульс...
    begin
      Err := FactorizationPulse(Duration, OCR2, CS2x, Cnt_int);
      TCCR2 := TCCR2_Pulse + CS2x;

      Res := Cnt_int div 256;
      for j := 1 to Res do
        AddResultRow(Pulse,  Round(Duration * 256/Cnt_int),
          Dividers[CS2x], OCR2, TCCR2, 255, Round(Err * 256/Cnt_int));

      Cnt_byte := Cnt_int mod 256;
      if(Cnt_byte <> 0) then
        AddResultRow(Pulse, Duration - Res * Round(Duration * 256/Cnt_int),
          Dividers[CS2x], OCR2, TCCR2, Cnt_byte, Err - Res * Round(Err * 256/Cnt_int));
    end
    else  //  Если пауза...
    begin
      Err := FactorizationPause(Duration, OCR2, CS2x, Cnt_byte);
      TCCR2 := TCCR2_Pause + CS2x;
      AddResultRow(Pulse, Duration, Dividers[CS2x], OCR2, TCCR2, Cnt_byte, Err);
    end;
  end;
end;

function TdkCalcT2MainForm.FactorizationPause(ADuration: integer; var AOCR2, ADivIndex, ACnt: Byte): integer;
var
  i: Byte;
  j: Byte;
  k: Byte;
  F_CPU: double;
  Res: integer;
begin
  Result := 65535;
  F_CPU := FFreqCPU / 1000000;

  for i := 1 to 7 do
    for j := 1 to 255 do
      for k := 1 to 255 do
      begin
        Res := Ceil(Abs(ADuration - Dividers[i] * j * k / F_CPU));
        if(Res < Result) then
        begin
          Result := Res;
          ADivIndex := i;
          AOCR2 := j;
          ACnt := k;
        end;
      end;
  if(ACnt > AOCR2) then
  begin
    i := AOCR2;
    AOCR2 := ACnt;
    ACnt := i;
  end;
end;

function TdkCalcT2MainForm.FactorizationPulse(ADuration: integer; var AOCR2, ADivIndex: Byte; var ACnt: integer): integer;
var
  i: Byte;
  j: Byte;
  HalfT: double;
  F_CPU: double;
  Res: double;
  Cmp: double;
begin
  Res := 65535;
  HalfT := 1000000 / FFreqCarrier / 2;
  F_CPU := FFreqCPU / 1000000;

  for i := 1 to 7 do
    for j := 1 to 255 do
    begin
      Cmp := Abs(HalfT - Dividers[i] * j / F_CPU);
      if(Cmp < Res) then
      begin
        Res := Cmp;
        ADivIndex := i;
        AOCR2 := j;
      end;
    end;

  ACnt := Ceil(ADuration / HalfT);
  Result := Ceil(Res * ACnt);
end;


По сути алгоритм вычисления OCR2, CS2x и Cnt сводится к вычислению делителей в процедурах FactorizationPulse и FactorizationPause. Если будут вопросы, охотно отвечу в комментах. Вся остальная часть программы — по сути обеспечение ее интерфейса и интереса не представляет. О, константы:

const
  Dividers: array [1..7] of Integer = (1, 8, 32, 64, 128, 256, 1024);
  TCCR2_Pulse = $98;
  TCCR2_Pause = $08;

Справку для программы я не писал — там все ясно, особенно если сначала загрузить файлы примеров.
Вот и все, что я могу сказать об этом ((с) почти F.Gump) :)


Ссылки:
  • +5
  • 30 ноября 2014, 18:53
  • Kerim
  • 1
Файлы в топике: AVR_CalcT2.zip

Комментарии (7)

RSS свернуть / развернуть
Ну вот, наконец-то нормально оформленная статья. Из названия и первых двух предложений статьи ясен предмет обсуждения.
0
В приаттаченном архиве, похоже, не хватает некоего файла avrlibtypes.h, автором которого, если не ошибаюсь, является Pascal Stang. Добавьте, пожалуйста. Спасибо за статью.
0
Пожалуйста. Очень рад что первая публикация оказалась кому-то полезна. Но файл avrlibtypes.h — библиотечный, не думаю, что правильно его тут приводить. У меня студия 4.19 и avr-toolchain 3.3.0.710.
0
0
Да, спасибо, знаю. Просто пресловутый файл не входит в стандартный пакет avr-gcc(Linux Mint 17). А в состав avr-toolchain, видимо, входит.
0
Переменная pIR_CurrentData меняется в прерывании и в теле программы (если правильно понял, что IR_Shot это тело). А она неоднобайтная. Уверены, что не нужны критические секции?
0
В конкретно моем случае — да, поскольку вот весь код от старта счетчика до момента, когда можно уходить в прерывание:

TCCR2 = pgm_read_byte(pIR_CurrentData);
 d8c:	84 91       	lpm	r24, Z+
 d8e:	85 bd       	out	0x25, r24	; 37    //    Вот тут запустили счетчик
pIR_CurrentData++; 
 d90:	31 96       	adiw	r30, 0x01	; 1
IR_TimerCounter = pgm_read_byte(pIR_CurrentData);
 d92:	84 91       	lpm	r24, Z+
 d94:	98 2e       	mov	r9, r24
pIR_CurrentData++;	//	Указатель показывает на структуру TIR_T2Params следующей фазы 
 d96:	31 96       	adiw	r30, 0x01	; 1
 d98:	f0 93 7c 00 	sts	0x007C, r31
 d9c:	e0 93 7b 00 	sts	0x007B, r30
}
 da0:	08 95       	ret    //    Нам не страшен серый волк

У меня нет таких коротких интервалов, чтобы в эти 6 команд воткнулось прерывание. А логика программы не позволяет выполнить повторный вызов IR_Shot, пока не будет выполнена все посылка. Но в целом Вы правы: нужно из IR_Init перенести

TIMSK |= _BV(OCIE2);	//	Разрешить прерывание по сравнению T2.

в «хвост» IR_Shot, а в IR_TimerStop добавить

TIMSK &= ~_BV(OCIE2);	//	Запретить прерывание по сравнению T2.

Можно даже добавить в IR_Shot проверку, не включено ли прерывание TIMER2_COMP_vect, если да — на выход.

Да, это однозначно стоит сделать, потому что в эти 6 команд может воткнуться другое прерывание, вот тогда — да, можем словить нежданчик. Спасибо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.