Нежелательное переключение на OC1A

Приведенный ниже код для ATmega328P (Arduino Pro Mini 5 В @ 16 МГц) создает периодическую последовательность на выводах таймера 1 OC1A и OC1B:

#include "Arduino.h"

// Для Arduino Pro Mini 5 В @ 16 МГц
// Предделитель 1 для наивысшего разрешения (1 тактовый импульс = 62,5 нс)
// Для достижения задержек до 4,096 мс требуется таймер 1 (16 бит)

const uint8_t OC1A_PIN = 9;
const uint8_t OC1B_PIN = 10;

const uint16_t TIME_B_US = 1000; // 1 миллисекунда для импульса сброса на OC1B
const uint16_t TIME_A_US = 1000; // длительность импульса 1 миллисекунда на OC1A
const uint16_t REPETITION_PERIOD_US = 10000;  // период повторения последовательности в микросекундах

bool inverse_polarity_A = false; // При значении true OC1A включает принудительное включение OC1B при запуске!!???

uint32_t _period_start_timestamp_us;
uint8_t _flip_A_cnt;
uint8_t FLIP_A_CNT_MAX = 3;

void setup() {
  pinMode(OC1A_PIN, OUTPUT);
  digitalWrite(OC1A_PIN, inverse_polarity_A);  //устанавливаем сигнальный вывод OC1A в исходное состояние
  pinMode(OC1B_PIN, OUTPUT);
  digitalWrite(OC1B_PIN, false);  //устанавливаем вывод сигнала SYNC в исходное состояние (выкл)

  cli();
  TIFR1 = (1 << OCF1A);   // очистить ожидающее прерывание канала A
  TIFR1 |= (1 << OCF1B);  // очистить ожидающее прерывание канала B
  TIMSK1 = (1 << OCIE1A); // Включить прерывание сравнения выходов для сигнала на OC1A
  TCCR1A = 0;
  TCCR1B = (1 << WGM12);  // включить режим CTC и остановить таймер 1
  TCNT1 = 0;
  sei();
}

void loop() {
  uint32_t now_us = micros();
  
  if (now_us - _period_start_timestamp_us >= REPETITION_PERIOD_US) {
    _period_start_timestamp_us = now_us;
    send_sequence(); // Использует таймер 1
  }
}

void send_sequence() {
  _flip_A_cnt = 0;

  TCCR1A = (1 << COM1B0);  // Включить переключение OC1B при сравнении совпадений
  TCCR1A |= (1 << COM1A0); // Включить переключение OC1A при сравнении совпадений
  OCR1B = TIME_B_US << 4;  // * 16, чтобы получить время в микросекундах (16 * 62,5 нс = 1 мкс)
  OCR1A = TIME_B_US << 4;
  TCNT1 = 0;
  TCCR1C = (1 << FOC1B);  // Устанавливаем OC1B на высокий уровень.
  // TCCR1C |= (1 << FOC1A); // Добавление этого предотвращает неправильное переключение OC1A в самой первой последовательности
                              // когда inverse_polarity_A == true, но тогда ошибка возникает во всех последующих последовательностях!!
  TCCR1B |= (1 << CS10);   // запустить таймер с предделителем 1, чтобы получить самое высокое разрешение (1 тактовый цикл = 62,5 нс)
}

// Для сигнала на OC1A
ISR(TIMER1_COMPA_vect) {
  TCCR1A &= ~(1 << COM1B0);  // Отключить переключатель Compare Match для канала B

  if (_flip_A_cnt < FLIP_A_CNT_MAX) {
    OCR1A = TIME_A_US << 4;
    _flip_A_cnt++;
  }
  else {
    _flip_A_cnt = 0;  // сброс
    TCCR1B &= ~(1 << CS10); // Очищаем бит CS10, чтобы остановить таймер 1
  }
}

Это отлично работает для случая «inverse_polarity_A = false», как показано на рисунке ниже: верхний синий сигнал — это выход OC1B, нижний желтый сигнал — это выход OC1A.

Инвертированная_полярность_A = false

При установке «inverse_polarity_A = true» желтый сигнал на OC1A должен стать инвертированным, ожидаемый сигнал должен выглядеть следующим образом:

Ожидаемый сигнал для 'inverse_polarity_A = true'

Однако сигнал на осциллографе показывает дополнительное переключение на OC1A, когда OC1B переходит в высокий уровень, но только для самой первой последовательности, что портит сигнал:

Нежелательный первый переключатель на OC1A в первой последовательности

Я снова и снова читаю множество других вопросов и ответов о таймерах Arduino, а также раздел технических описаний таймеров, но так и не могу понять, в чем причина.

Как это исправить?

, 👍1

Обсуждение

Было бы полезно кратко объяснить, что делает ваш код. При первом прочтении я не до конца понял, да и порты навскидку не помню, но возможно ли, что проблема в строке *TCCR1C = (1 << FOC1B);*? Мне кажется, это перезапишет бит FOC1A и установит OC1A в низкий уровень, хотя вы хотите изменить только OC1B. Использование |= для установки бита FOC1B может помочь., @InBedded16

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

Когда обработчик прерываний (ISR) и основная программа используют общую переменную _flip_A_cnt, крайне важно объявить _flip_A_cnt _volatile_. Возможно, это не имеет отношения к вашей проблеме, но это очевидные правила и запреты при реализации ISR., @hcheung


2 ответа


Лучший ответ:

2

Есть два способа управления выводом OC1A: либо как обычный GPIO контакт, или через генератор сигналов таймера 1. Эти два способа независимы, и только один из них активен в любой момент времени. Когда вы установите inverse_polarity_A в значение true и setup() выдаст ошибку

digitalWrite(OC1A_PIN, inverse_polarity_A);

Это устанавливает вывод в состояние HIGH, не влияя на таймер каким-либо образом. Генератор сигналов по-прежнему считает, что на контакте должен быть НИЗКИЙ уровень. Затем, начало последовательности импульсов, как выдает send_sequence()

TCCR1A |= (1 << COM1A0);

генератор сигналов берет на себя управление выводом, и поэтому вывод НИЗКИЙ. Это паразитический переход, который вы наблюдаете. Схема выходного блока сравнения поясняет, как управление выводами осуществляется из обычного регистра PORT в Генератор сигналов: в этой схеме это реализовано через мультиплексор.

Чтобы это исправить, я предлагаю вам настроить таймер. Полностью в setup(), включая биты COM1x0. Теперь пин Сначала под контролем генератора сигналов. Затем, всё ещё в setup(), вы устанавливаете начальный уровень с помощью стробоскопа FOC1A.

,

Огромное спасибо! Теперь всё работает. Полный код ниже., @Jan Poppeliers


1

@Эдгар, ваш ответ проясняет причину странного поведения! Теперь всё работает отлично, большое спасибо!

Похоже, я не совсем понял: «Функция порта общего ввода-вывода переопределяется выходным сравнением (OC1x) генератора сигналов, если любой из «Установлены биты COM1x1:0» в техническом описании.

Рабочий код:

#include "Arduino.h"

// Для Arduino Pro Mini 5 В @ 16 МГц
// Предделитель 1 для наивысшего разрешения (1 тактовый импульс = 62,5 нс)
// Для достижения задержек до 4,096 мс требуется таймер 1 (16 бит)

const uint8_t OC1A_PIN = 9;
const uint8_t OC1B_PIN = 10;

const uint16_t TIME_B_US = 1000; // 1 миллисекунда для импульса сброса на OC1B
const uint16_t TIME_A_US = 1000; // длительность импульса 1 миллисекунда на OC1A
const uint16_t REPETITION_PERIOD_US = 10000;  // период повторения последовательности в микросекундах

bool inverse_polarity_A = true;

uint32_t _period_start_timestamp_us;
volatile uint8_t _flip_A_cnt;
const uint8_t FLIP_A_CNT_MAX = 3;

void setup() {
  pinMode(OC1A_PIN, OUTPUT);
  pinMode(OC1B_PIN, OUTPUT);
  cli();
  TIFR1 = (1 << OCF1A);   // очистить ожидающее прерывание канала A
  TIFR1 |= (1 << OCF1B);  // очистить ожидающее прерывание канала B
  TIMSK1 = (1 << OCIE1A); // Включить прерывание сравнения выходов для сигнала PPM на OC1A
  TCCR1A = (1 << COM1B0);  // Включить переключение OC1B при сравнении совпадений
  TCCR1A |= (1 << COM1A0); // Включить переключение OC1A при сравнении совпадений
  if (inverse_polarity_A) {
    TCCR1C |= (1 << FOC1A);  // Принудительно переводим OC1A в высокий уровень
  }
  TCCR1B = (1 << WGM12);  // включить режим CTC и остановить таймер 1
  TCNT1 = 0;
  sei();
}

void loop() {
  uint32_t now_us = micros();
  
  if (now_us - _period_start_timestamp_us >= REPETITION_PERIOD_US) {
    _period_start_timestamp_us = now_us;
    send_sequence(); // Использует таймер 1
  }
}

void send_sequence() {
  _flip_A_cnt = 0;   // сбросить счетчик
  TCCR1A |= (1 << COM1B0);  // Повторно включить переключатель OC1B при совпадении сравнения после того, как этот бит был очищен ISR(TIMER1_COMPA)
  OCR1B = TIME_B_US << 4;  // * 16, чтобы получить время в микросекундах (16 * 62,5 нс = 1 мкс)
  OCR1A = TIME_B_US << 4;
  TCNT1 = 0;
  TCCR1C |= (1 << FOC1B);  // Принудительно переводим OC1B в высокое положение.
  TCCR1B |= (1 << CS10);   // запустить таймер с предделителем 1, чтобы получить самое высокое разрешение (1 тактовый цикл = 62,5 нс)
}

// Для сигнала PPM
ISR(TIMER1_COMPA_vect) {
  TCCR1A &= ~(1 << COM1B0);  // Отключить переключатель при сравнении совпадений для канала B; для этой последовательности нужен только 1 переключатель

  if (_flip_A_cnt < FLIP_A_CNT_MAX) {
    OCR1A = TIME_A_US << 4;
    _flip_A_cnt++;
  }
  else {
    TCCR1B &= ~(1 << CS10); // Очищаем бит CS10, чтобы остановить таймер 1
  }
}

Вот правильный вывод:

Ожидаемый сигнал для обратной полярности

,