Diving into the PWM

sadman41
Offline
Зарегистрирован: 19.10.2016

Коллеги, предлагаю собрать в кучку каверзные задачки по PWM на ATMega328P/PB.

Может оно никому и не будет нужно, но таки начну.

sadman41
Offline
Зарегистрирован: 19.10.2016

Эксперимент #1: включение и выключение таймера.

Цель: включить и выключить генерацию по событию.

Результат: частично удовлетворительный.

Решение задачи, казалось бы, очень простое - манипулируй Clock Select Bit и бед не знай. Надо остановить - обнуляй, запустить - устанавливай. Однако, тут нас поджидает подводный камень - на связанном с каналом таймера выходе однозначное состояние само по-себе не устанавливается при остановке таймера. В каком состоянии застало вход обнуление прескалера, в таком он и висит.

Помочь нам пытается Datasheet: "The extreme values for the OCR1x register represents special cases when generating a PWM waveform output in the phase correct PWM mode. If the OCR1x is set equal to BOTTOM the output will be continuously low and if set equal to TOP the output will be set to high for non-inverted PWM mode."

Т.е. хотите зафиксировать выход как LOW в неинвертированном режиме - делайте OCRnx = BOTTOM, нужен HIGH - применяйте OCRnx = TOP. Для инвертированного всё наоборот.

С таким приёмом всё становится более лучше - вход можно "заморозить" в требуемом состоянии. Однако, это не может произойти мгновенно в силу того, что регистры OCRnx реализованы с применением технологии "double buffered output compare registers", которая предписывает предварительно помещать новое значение регистра во временный буфер с целью корректировки "настоящего" в определённых точках. Для режима "Phase and frequency correct" (#8, #9) - при совпадении TCNTn с BOTTOM (нулём);  для "Phase correct" (#10, #11) - при совпадении TCNTn с TOP (ICRn или OCRnx). 

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

В ходе экспериментов у меня родилась следующая идея: для скорейшей остановки генерации требуется задать OCRnx, затем вызвать его обновление установкой TCNTn = BOTTOM или TCNTn = TOP, а затем быстро скинуть TCNTn в положение BOTTOM для фактической установки LOW на выходе. 

Я её проверил. В коде: 

#include <util/atomic.h>

// defined => Mode 11: Phase correct, update of OCR1x at = TOP
// undefined =>  Mode 9: Phase and frequency correct, update of OCR1x at = BOTTOM
#define PHASE_CORRECT


#define D10_Low PORTB &=B11111011
#define D10_High PORTB|=B00000100

#define D13_High PORTB |= B00100000
#define D13_Low  PORTB &= B11011111

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

uint16_t timerTop = 600, timerDuty = timerTop/4;

inline void timerStart() {
  sbi(TCCR1A, COM1B1);
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    OCR1B = timerDuty;
    // Jump to OCR1B update point
#if defined(PHASE_CORRECT)
    TCNT1 = OCR1A;
#else
    TCNT1 = 0x00;
#endif
  }
  D13_High;
}

inline void timerStop() {
  cbi(TCCR1A, COM1B1);
  D10_Low;

  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    OCR1B = 0x00;
    // Jump to OCR1B update point
#if defined(PHASE_CORRECT)
    TCNT1 = OCR1A;
#else
    // Update works as unexpected - we have full HIGH period after jump on update point
    // TCNT1 = 0x00;
#endif
  }
  D13_Low;
}

void setup() {
  Serial.begin(115200);
  pinMode(13, OUTPUT);
  pinMode(10, OUTPUT);

  TCCR1A = TCCR1B = TCNT1 = 0x00;
  OCR1A = timerTop;
  OCR1B = timerDuty;
  // Channel A, pin 9, off
  // Channel B, pin 10, clear OC1B on compare match, set OC1B at BOTTOM (non-inverting mode)
  TCCR1A |= (0 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0);
#if defined(PHASE_CORRECT)
  // Mode 11: Phase correct, update of OCR1B at on TOP
  TCCR1A |= (1 << WGM11) | (1 << WGM10);
#else
  // Mode 9: Phase and frequency correct, update of OCR1B on BOTTOM
  TCCR1A |= (0 << WGM11) | (1 << WGM10);
#endif
  TCCR1B |= (1 << WGM13) | (1 << CS11) | (1 << CS10);
}

void loop() {
  static uint32_t prevTimerToggle;
  static uint8_t stateTimerToggle;

  uint32_t nowTime = millis();

  if (nowTime - prevTimerToggle > 30UL) {
    prevTimerToggle = nowTime;
    stateTimerToggle ? timerStart() : timerStop();
    stateTimerToggle = !stateTimerToggle;
  }
}

На картинках с осциллографа можно видеть, что в режиме "Phase & frequency correct" при попытке принудительной синхронизации при остановке генерации происходит выброс на целый период. Однако, начало генерации можно привязать к моменту событию.

Режим "Phase correct" более благосклонен к просьбам остановить генерацию. Впрочем, в некоторых случаях (при изменении частоты), я видел, что выход "замораживается" с небольшим отставанием. Максимально приблизить начало работы генератора к моменту события можно "прыгнув" в точку синхронизации сразу после установки OCRnx.

К сожалению никакие фокусы с последовательными прыжками сначала в точку синхронизации и, затем, в точку "заморозки" выхода не принесли стабильного результата. Хотя и в некоторых диапазонах частот это срабатывало, но, при более масштабной проверке, картинка портилась. Полагаю, что это связано с направлением входа (счёт сверху или снизу) TCNTx в точку переключения, а управлять им нет возможности.

Тем не менее, нашёлся небольшой грязный хак, позволяющий отрубать лишнее - принудительное отключение выхода МК от Waveform Generator и перевод в заданное состояние (см. применение cbi()/sbi() в функциях запуска и остановки генерации). Это наврядли можно назвать хорошим решением для высоких скоростей, но...

Впрочем, отрицательный результат тоже полезен.

Дополнения, объяснения спецэффектов и критика приветствуются.

 

UPD: На ATMega328PB с использованием OCM результат более достойный - сигналы прекращаются одновременно, без "перелётов". Запуск, правда, синхронизировать не удаётся - как наложатся частоты, так и будет.

sadman41
Offline
Зарегистрирован: 19.10.2016

Эксперимент #2

Цель: манипуляция выходом таймера, когда он подключен к Waveform Generator.

Результат: отрицательный

Подобно сибирским мужикам из анекдота про лом и бензопилу я давно собирался проверить одну глупую мысль - а что будет, если портить форму сигнала, который вылезает изнутри таймера, простой установкой состояния пина. В даташите, конечно, написано "The general I/O port function is overridden by the output compare (OC0x) from the waveform generator if either of the COM0x1:0 bits are set.", но... как оно выглядит на самом деле?

Запускаем генератор в режиме #11 на 1Hz и наваливаем в pin D10 уровень HIGH:

#define D13_High PORTB |= B00100000
#define D10_High PORTB |= B00000100

#define D13_Low  PORTB &= B11011111
#define D10_Low  PORTB &= B11111011

void setup() {
  pinMode(13, OUTPUT);
  pinMode(10, OUTPUT);

  TCCR1A = TCCR1B = 0x00;
  // Channel A, pin 9, off
  // Channel B, pin 10, clear OC1B on compare match, set OC1B at BOTTOM (non-inverting mode)
  TCCR1A |= (0 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0);
  // Mode 11: Phase correct, update of OCR1B at on TOP
  TCCR1A |= (1 << WGM11) | (1 << WGM10);
  TCCR1B |= (1 << WGM13);
  OCR1A = 7810;
  OCR1B = OCR1A / 4;
  TCNT1 = OCR1A;
  TCCR1B |= (1 << CS12) | (1 << CS10);
}

void loop() {
  D10_High;
  D13_High;
}

ATMega328 не задымилась, позиции не сдала, продолжала генерировать как ни в чем не бывало.

sadman41
Offline
Зарегистрирован: 19.10.2016

Эксперимент #3: "Fast PWM mode" vs "Phase and frequency correct mode"

Цель: увидеть разницу между двумя режимами.

Результат: успех.

В Datasheet читаем: "Fast PWM Mode. ... This high frequency makes the fast PWM mode well suited for power regulation, rectification, and DAC applications. High frequency allows physically small sized external components (coils, capacitors), hence reduces total system cost." и "Phase and Frequency Correct PWM Mode ...  However, due to the symmetric feature of the dual-slope PWM modes, these modes are preferred for motor control applications."

Что же такого плохого в дешёвом Fast PWM? Компилируем, заливаем...

#include <util/atomic.h>

const uint16_t timerTop = 127;
const uint16_t stepDuration = 50;
const uint16_t stopDuration = 5000;

// defined => Mode 14: Fast PWM, update of ICR1 at on BOTTOM
// undefined => Mode 8: Phase and frequency correct, update of OCR1x on BOTTOM
#define FAST_PWM

void setup() {
  Serial.begin(115200);
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);

  TCCR1A = TCCR1B = 0x00;
  // Channel A, pin 9,  clear OC1B on compare match, set OC1B at BOTTOM (non-inverting mode)
  // Channel B, pin 10, clear OC1B on compare match, set OC1B at BOTTOM (non-inverting mode)
  TCCR1A |= (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0);
#if defined(FAST_PWM)
  // Mode 14: Fast PWM, update of ICR1 at on BOTTOM
  TCCR1A |= (1 << WGM11);
  TCCR1B |= (1 << WGM13) | (1 << WGM12);
#else
  // Mode 8: Phase and frequency correct, update of OCR1x on BOTTOM
  TCCR1B |= (1 << WGM13);
#endif 
  ICR1 = timerTop;
  OCR1A = timerTop / 4;
  OCR1B = 0x00;
  TCNT1 = 0x00;
  TCCR1B |= (1 << CS11) | (1 << CS10);
}

void loop() {

  for (int32_t duty = 1; timerTop >= duty ; duty++) {
    Serial.println(duty);
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      OCR1B = (uint16_t) duty;
    }
    delay(stepDuration);
  }
  delay(stopDuration);

  for (int32_t duty = timerTop; 0x00 <= duty; duty--) {
    Serial.println(duty);
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      OCR1B = (uint16_t) duty;
    }
    delay(stepDuration);
  }
  delay(stopDuration);
}

Смотрим на осциллографе картинки:

Вон оно чего - на "fast PWM" всплески при скважности 0%. Двигатель будет подёргивать на стоянке. Ибо "The extreme values for the OCR1x registers represents special cases when generating a PWM waveform output in the Fast PWM mode. If the OCR1x is set equal to BOTTOM (0x0000) the output will be a narrow spike for each TOP+1 timer clock cycle. Setting the OCR1x equal to TOP will result in a constant high or low output (depending on the polarity of the output which is controlled by COM1x[1:0])."

Дополнительное отличие - импульсы "толще" вдвое. Потому что это "The dual-slope operation" - смена состояния выхода происходит как на подъёме TCNTn к TOP, так и на спуске к BOTTOM.

К слову - в Wiring всё красиво. Таймеры инициализируются в "Phase correct pwm mode (8-bit)"

sadman41
Offline
Зарегистрирован: 19.10.2016

Эксперимент #4: Синхронизация таймеров

Цель: увидеть разницу между сигналами синхронизированных и несинхронизированных генераторов PWM.

Результат: успех. 

Немного переработав честно украденный у dimax-а код, рассмотрим эффект синхронизации под микроскопом:

#include <util/atomic.h>

// Sync all timers
//#define SYNC_TIMERS
// Use big prescaler - to see no different with syncing
//#define BIG_PRESCALER


void setup() {

#if defined(SYNC_TIMERS)
  GTCCR = (1 << TSM) | (1 << PSRASY) | (1 << PSRSYNC);
#endif

  // Timer #1
  const uint16_t timerOneTop = 255;
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  TCCR1A = TCCR1B = 0x00;
  // Channel A, pin 9,  clear on compare match, set at BOTTOM (non-inverting mode).
  // Channel B, pin 10, clear on compare match, set at BOTTOM (non-inverting mode).
  TCCR1A |= (1 << COM1A1) | (0 << COM1A0) | (1 << COM1B1) | (0 << COM1B0);
  // Mode 14: Fast PWM, TOP = ICR1
  TCCR1A |= (1 << WGM11);
  TCCR1B |= (1 << WGM13) | (1 << WGM12);
  ICR1 = timerOneTop;
  // Channel A duty 50%
  OCR1B = timerOneTop / 2;
  // Channel A duty 25%
  OCR1A = timerOneTop / 4;
  TCNT1 = 0x00;
#if defined(BIG_PRESCALER)
  // Prescaler /256
  TCCR1B |= (1 << CS12);
#else
  // Prescaler /8
  TCCR1B |= (1 << CS11);
#endif

  // Timer #2
  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = TCCR2B = 0x00;
  // Channel A, pin 3,  clear on compare match, set at BOTTOM (non-inverting mode).
  // Channel B, pin 11, clear on compare match, set at BOTTOM (non-inverting mode).
  TCCR2A |= (1 << COM2A1) | (0 << COM2A0) | (1 << COM2B1) | (0 << COM2B0);
  // Mode 3: Fast PWM, TOP = 0xFF
  TCCR2A |= (1 << WGM21) | (1 << WGM20);
  // Channel A duty 40%
  OCR2A = 0xFF / 5 * 2;
  // Channel B duty 75%
  OCR2B = 0xFF / 4 * 3;
  TCNT2 = 0x00;
#if defined(BIG_PRESCALER)
  // Prescaler /256
  TCCR2B |= (1 << CS22) | (1 << CS21);
#else
  // Prescaler /8
  TCCR2B |= (1 << CS21);
#endif

#if defined(SYNC_TIMERS)
  GTCCR = 0;
#endif
}

void loop() {}

Разница между несинхронно стартовавшими таймерами видна на относительно больших частотах

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

На низких частотах, по-моему мнению, синхронизация - перфекционизм чистой воды.

 

sadman41
Offline
Зарегистрирован: 19.10.2016

Эксперимент #5: Hard&Soft PWM

Цель: "перенести" PWM на выходы, не связанные с таймерами.

Результат: успех.

На Arduino Pro Micro загрузчик плавно моргает набортным светодиодом. Получится ли сделать и на Mini/Nano/Uno сделать красиво? В принципе - да.

Просто будем паразитировать на каком-нибудь таймере и устроим дискотеку на D13 или любом другом пине, не поддерживающем PWM на аппаратном уровне. 

#include <util/atomic.h>

const uint16_t timerTop = 2000;                 // Freq = 1KHz on /8 prescaler
const uint32_t flipFlopDelay = 2000;
const uint32_t stepDelay = 1;

#define D13_Low  PORTB &= B11011111
#define D13_High PORTB |= B00100000

// LED pin toggles to inverted state of Timer #1 OC1B pin 
//#define D13_MODE_INVERTED
// Just show LED effect on low frequency (prescaler /1024)
//#define LOW_TIMER_SPEED

ISR(TIMER1_OVF_vect) {
// Use special stuff to avoid spikes on duty 0%
#if defined(D13_MODE_INVERTED)
  (0x00 == OCR1B) ? D13_High : D13_Low;
#else 
  (0x00 == OCR1B) ? D13_Low : D13_High;
#endif 
}

ISR(TIMER1_COMPB_vect) {
// Use special stuff to avoid spikes on duty 100%
#if defined(D13_MODE_INVERTED)
  (timerTop == OCR1B) ? D13_Low : D13_High;
#else 
  (timerTop == OCR1B) ? D13_High : D13_Low;
#endif 
}

void setup() {
  uint16_t timerDuty = 0x00;
  int8_t dutyStep = 1;

  pinMode(13, OUTPUT);

  // Timer #1
  pinMode(10, OUTPUT);
  TCCR1A = TCCR1B = 0x00;
  // Channel A, pin 9, off
  TCCR1A |= (0 << COM1A1) | (0 << COM1A0);
  // Channel B, pin 10
  // Clear OC1A/OC1B on Compare Match, set OC1A/OC1B at BOTTOM (non-inverting mode)
  TCCR1A |= (1 << COM1B1) | (0 << COM1B0);
  // Mode #15: Fast PWM, TOP = OCR1A
  TCCR1A |= (1 << WGM11) | (1 << WGM10);
  TCCR1B |= (1 << WGM13) | (1 << WGM12);
  OCR1A = timerTop;
  OCR1B = 0x00;

#if defined(LOW_TIMER_SPEED)
  TCCR1B |= (1 << CS12) | (1 << CS10);
#else
  TCCR1B |= (1 << CS11) | (0 << CS10);
#endif

  TIMSK1 = (1 << TOIE1) | (1 << OCIE1B);

  // Change duty bothways: 0% to 100% and 100% to 0%
  while (true) {
    timerDuty += dutyStep;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      OCR1B = timerDuty;
    }
    delay(stepDelay);
    if (timerTop == timerDuty || 0x00 == timerDuty) {
      delay(flipFlopDelay);
      dutyStep = -dutyStep;
    }
  }
}

void loop() {}

Сдвиг относительно основного сигнала, естественно, имеется. Стабильность такого ШИМ-а, разумеется, зависит от того, насколько долго будут блокироваться прерывания МК.

sadman41
Offline
Зарегистрирован: 19.10.2016

Эксперимент #5, продолжение: Hard&Soft PWM

Цель: найти применение возможностям, выявленными в ходе эксперимента #5

Результат: вроде успешно

Так, как мигание одним светодиодом на псевдо-PWM-е не сильно показательно и полезно, приделаем 6 синих сервомеханизмов к трем 16-битным таймерам ATMega328PB и будем их крутить ногами A0...A5.  Напомню, что таймеры #3 и #4 штатно выводят на пины D0, D1, D2, что не всегда удобно. 

Заливаем полуиндусский код, подключаем сервы, радуемся (возможно). У меня шести серв под рукой нет, но две штуки я поперетыкал в разные выходы.

#include <util/atomic.h>

// Servo Set #1, Timer #1 on A0, A1
// Servo Set #2, Timer #3 on A2, A3
// Servo Set #3, Timer #4 on A4, A5

/** user area ***/
// Can be used with both 16-bit & 8-bit timers
const uint32_t prescaler = 256;

const uint16_t timerTop = F_CPU / prescaler * 2 / 100; // Fast PWM: timerTop = 1250 for freq = 50Hz on /256 prescaler
const uint16_t workAngleMin = 0;
const uint16_t workAngleMax = 180;

const uint32_t flipFlopDelay = 2000;
const uint32_t stepDelay = 10;

/** system area ***/
const uint16_t workAngleMinPulse = 544;
const uint16_t workAngleMaxPulse = 2400;

const uint16_t dutyForAngleMin = (F_CPU / prescaler * workAngleMinPulse) / 1000000UL;
const uint16_t dutyForAngleMax = (F_CPU / prescaler * workAngleMaxPulse) / 1000000UL;

#define D13_Low  PORTB &= B11011111
#define D13_High PORTB |= B00100000

#define A0_High PORTC |=B00000001
#define A1_High PORTC |=B00000010
#define A2_High PORTC |=B00000100
#define A3_High PORTC |=B00001000
#define A4_High PORTC |=B00010000
#define A5_High PORTC |=B00100000

#define A0_Low PORTC &= B11111110
#define A1_Low PORTC &= B11111101
#define A2_Low PORTC &= B11111011
#define A3_Low PORTC &= B11110111
#define A4_Low PORTC &= B11101111
#define A5_Low PORTC &= B11011111

// Timer #1 ISRs.
ISR(TIMER1_OVF_vect) {
  (0x00 == OCR1A) ? A0_Low : A0_High ;
  (0x00 == OCR1B) ? A1_Low : A1_High;
}

ISR(TIMER1_COMPA_vect) {
  (timerTop == OCR1A) ? A0_High : A0_Low;
}

ISR(TIMER1_COMPB_vect) {
  (timerTop == OCR1B) ? A1_High : A1_Low;
}

// Timer #3 ISRs
ISR(TIMER3_OVF_vect) {
  (0x00 == OCR3A) ? A2_Low : A2_High ;
  (0x00 == OCR3B) ? A3_Low : A3_High;
}

ISR(TIMER3_COMPA_vect) {
  (timerTop == OCR3A) ? A2_High : A2_Low;
}

ISR(TIMER3_COMPB_vect) {
  (timerTop == OCR3B) ? A3_High : A3_Low;
}

// Timer #4 ISRs
ISR(TIMER4_OVF_vect) {
  (0x00 == OCR4A) ? A4_Low : A4_High ;
  (0x00 == OCR4B) ? A5_Low : A5_High;
}

ISR(TIMER4_COMPA_vect) {
  (timerTop == OCR4A) ? A4_High : A4_Low;
}

ISR(TIMER4_COMPB_vect) {
  (timerTop == OCR4B) ? A5_High : A5_Low;
}

void setup() {
  int16_t workAngle  = 0x00;
  int8_t angleStep   = 7;

  pinMode(13, OUTPUT);
  for (uint8_t i = A0; A5 >= i; i++) {
    pinMode(i, OUTPUT);
  }

  TCCR1A = TCCR1B = TCCR3A = TCCR3B = TCCR4A = TCCR4B = 0x00;
  ICR1 = ICR3 = ICR4 = timerTop;
  OCR1A = OCR3A = OCR4A = workAngleMax;
  OCR1B = OCR3B = OCR4B = workAngleMin;
  TIMSK1 = TIMSK3 = TIMSK4 = (1 << TOIE1) | (1 << OCIE1A) | (1 << OCIE1B); // TOIE1 eq TOIE3 eq TOIE4 and so on

  // Timer #1
  // Channel A, B turned off
  // Mode #14: Fast PWM, TOP = ICR1
  TCCR1A |= (1 << WGM11) | (0 << WGM10);
  TCCR1B |= (1 << WGM13) | (1 << WGM12);
  // /256
  TCCR1B |= (1 << CS12);

  // Timer #3
  // Channel A, B turned off
  // Mode #14: Fast PWM, TOP = ICR3
  TCCR3A |= (1 << WGM31) | (0 << WGM30);
  TCCR3B |= (1 << WGM33) | (1 << WGM32);
  // /256
  TCCR3B |= (1 << CS32);

  // Timer #4
  // Channel A, B turned off
  // Mode #14: Fast PWM, TOP = ICR3
  TCCR4A |= (1 << WGM41) | (0 << WGM40);
  TCCR3B |= (1 << WGM43) | (1 << WGM42);
  // /256
  TCCR4B |= (1 << CS42);

  // Change servo's angle bothways: min..max degree and max..min degree
  while (true) {
    workAngle += angleStep;

    workAngle = constrain(workAngle, (int16_t)workAngleMin, (int16_t)workAngleMax);

    // Servo set #1 on Timer #1
    uint8_t  workAngleSetN1 = workAngle;
    // 0..180
    uint16_t timerDutyServoOne = map(workAngleSetN1, workAngleMin, workAngleMax, dutyForAngleMin, dutyForAngleMax);
    // 180..0
    uint16_t timerDutyServoTwo = map(workAngleMax - workAngleSetN1, workAngleMin, workAngleMax, dutyForAngleMin, dutyForAngleMax);
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      OCR1A = timerDutyServoOne;
      OCR1B = timerDutyServoTwo;
    }

    // Servo set #2 on Timer #3
    uint8_t  workAngleSetN2 = workAngle / 2;
    // 0..90
    uint16_t timerDutyServoThree = map(workAngleSetN2, workAngleMin, workAngleMax, dutyForAngleMin, dutyForAngleMax);
    // 180..90
    uint16_t timerDutyServoFour  = map(workAngleMax - workAngleSetN2, workAngleMin, workAngleMax, dutyForAngleMin, dutyForAngleMax);
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      OCR3A = timerDutyServoThree;
      OCR3B = timerDutyServoFour;
    }

    // Servo set #3 on Timer #4
    uint8_t  workAngleSetN3 = workAngle / 4 * 3;
    // 0..135
    uint16_t timerDutyServoFive = map(workAngleSetN3, workAngleMin, workAngleMax, dutyForAngleMin, dutyForAngleMax);
    // 45..180
    uint16_t timerDutyServoSix  = map(workAngleMax - workAngleSetN3, workAngleMin, workAngleMax, dutyForAngleMin, dutyForAngleMax);
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      OCR4A = timerDutyServoFive;
      OCR4B = timerDutyServoSix;
    }

    delay(stepDelay);
    if (workAngleMin == workAngle || workAngleMax == workAngle) {
      delay(flipFlopDelay);
      angleStep *= -1;
    }
  }
}

void loop() {}

 

sadman41
Offline
Зарегистрирован: 19.10.2016

Пытаясь объяснить себе возникающие в эксперименте #1 спецэффекты, я экспериментально искал ответы на глупые и странные вопросы относительно режимов PWM #9 "Phase & Frequency Correct" и #11 "Phase Correct", которые, вроде бы и не должны были возникать у человека разумного.

Забегая вперёд, хочу констатировать, что на данный момент режим "Phase Correct" мне кажется более капризным и требующим большей аккуратности при использовании.

Примечание: все нижеприведённые "аномалии" будут заметны только при крайне низкой частоте PWM, либо частом принудительном изменении значений счётчика и/или точек экстремума. В "обычной жизни" все эти спецэффекты практически незаметны.
 
Итак, сначала кратко, потом более развернуто:

Q: До какого значения увеличивается TCNT1 при запуске таймера без предварительного сброса (TCNT1 = BOTTOM)?
A: До величины MAX

Q: Сбрасывает ли счёт установка TCNT1 = BOTTOM после запуска таймера без предварительного сброса?
A: Нет, счёт идёт вверх, до величины MAX.

Q: Каково направление счёта таймера после установки TCNT1 = BOTTOM?
A: Всегда вверх.

Q: Каково направление счёта таймера после установки TCNT1 = TOP?
A: Всегда вниз.

Q: Изменяется ли направление счёта после принудительной установки TCNT1 = N (N != BOTTOM, N != TOP) ?
A: Нет, не изменяется. Счёт идёт в прежнем направлении, начиная с N.

Q: Когда вызывается прерывание TOVF?
A: На следующем такте прескалера. Например, если TOVF = BOTTOM, то в ISR(TIMER1_OVF_vect) TCNT1 будет равен 1.

Q: Когда вызывается прерывание COMPB при движении вниз?
A: На следующем такте прескалера. Например, при OCR1B = 5, ISR(TIMER1_COMPB_vect) TCNT1 будет равен 4.

Q: Когда вызывается прерывание COMPB при движении вверх?
A: На следующем такте прескалера. Например, при OCR1B = 5, ISR(TIMER1_COMPB_vect) TCNT1 будет равен 6.
 
Q: Удерживает ли генератор формы сигнала состояние выхода при счёте?
A: Да. Заданное состояние выхода восстанавливается при обратном подключении выхода к  генератору после его (выхода) отключения и принудительного изменения состояния.
 
Q: Что срабатывает сначала – переключение выхода генератором формы сигнала или ISR?
A: Предварительные данные эксперимента – ISR выполняется после переключения выхода генератором.
 
Q: Существуют ли "особенные" комбинации значений/точек экстремума счётчика.
A: Да. В режиме #9, движение из точки BOTTOM при счёте вверх ведёт к переключению выхода в активное состояние. В режиме #11 движение из точки TOP при счёте вниз приводит к принудительной деактивации выхода.
 
Q: Возможно ли сразу зафиксировать выход генератора в заданном состоянии, установив TCNT в значение, соответствующее точке обновления регистров OCRnx?
A: Нет. Переключение выхода в заданное состояние происходит при возникновении события "Compare match". Но, в силу того, что присвоение регистру TCNTn какого-либо значения блокирует операцию его сравнения с величиной, заданной в OCRnx, на один такт прескалера, вышеуказанное событие не произойдёт. Изменения скажутся в течении следующего периода счёта.
 
Для лучшего усвоения я нарисовал себе иллюстрации, коими делюсь и с вами. 
 
Ситуация #1, запуск генератора в режиме #9 с инициализацией счётчика нулём.
 
 
При инициализации TCNT1 нулём перед запуском генератора в режиме #9, на следующем такте прескалера произойдет обновление регистра OCR1B (Update on BOTTOM), непосредственно перед этим (но в том же такте прескалера) связанный с таймером выход OC1B перейдёт в состояние HIGH и будет находится в нём до достижения счётчиком значения (OCR1B + 1) - наступления события COMPB на счёте вверх (up-counting). Затем выход будет переведён в состояние LOW до достижения счётчиком значения (OCR1B - 1) - события COMPB на счёте вниз (down-counting), в котором будет находится до следующего события COMPB (OCR1B + 1). Далее по кругу.
 
Ситуация #2, запуск генератора в режиме #9 с инициализацией счётчика "не нулём".
 
 
При инициализации TCNT1 числом, отличным от нуля, перед запуском генератора в режиме #9, связанный с таймером выход останется в прежнем состоянии. Счётчик будет увеличиваться, пока не дойдёт до значения MAX и не будет сброшен в BOTTOM. На этой позиции произойдёт обновление регистра OCR1B, будет активирован выход OC1B и т.д. Так же, уже после старта таймера, существует возможность сбросить TCNT1 в 0, перейдя в точку обновления регистра OCR1B, что вызовет переход выхода OC1B в активное состояние. 
 
Обратите внимание на то, что событие COMPB не наступит до тех пор, пока TCNT1 не побывает в точке BOTTOM (регистр OCR1B не обновится).
 
Ситуация #3, запуск генератора в режиме #11 с инициализацией счётчика нулём.
 
 
При инициализации TCNT1 нулём перед запуском генератора в режиме #11, несмотря на то, что обновление регистров OCR1A и OCR1B должно происходить в точке TOP, счётчик будет увеличиваться до TOP, а не до MAX. Так же будет пройдено событие COMPB, которое, впрочем, ничего не изменит, так как выход OCR1B в этот момент уже находится в неактивном состоянии. Он будет переведён в состояние HIGH только при счёте вниз TCNT1. Далее счётчик схожим с режимом #9 образом (за исключением момента обновления регистров OCR1A и OCR1B).
 
Ситуация #4, запуск генератора в режиме #11 с инициализацией счётчика "не нулём".
 
 
При инициализации TCNT1 числом, отличным от нуля, перед запуском генератора в режиме #11, выход OC1B будет находится в прежнем состоянии. Счётчик продолжит увеличиваться, пока не дойдёт до значения MAX и не будет сброшен в BOTTOM. На этой позиции тоже, как не странно, произойдёт обновление регистра OCR1B и OCR1A. Далее счётчик будет нарастать, пройдёт точку события COMPB, которая ничего не изменит, и, при следующем возникновении события COMPB, выход OC1B изменит своё состояние.
 
Так же, как и в режиме #9, уже после старта таймера, существует возможность сбросить TCNT1 в 0, что "перезапустит" таймер.
 
Обратите внимание на то, что прерывание COMPB не будет вызвано до тех пор, пока TCNT1 не побывает в точке BOTTOM.
 
Ситуация #5, сброс значения TCNT до значения BOTTOM при работе генератора в режиме #9
 
 
 
Процедура присвоения TCNT1 значения BOTTOM в режиме #9, проведённая в любой точке нахождения счётчика, приводит к установке (на следующем такте прескалера) выхода соответствующего канала таймера в активное состояние. В зависимости от момента "сброса" счётчика порождаются разнообразные переходные эффекты, характеризующиеся преимущественно увеличением времени нахождения выхода МК в активном состоянии.
 
Ситуация #6, сброс значения TCNT до значения BOTTOM при работе генератора в режиме #11
 
 
 
В режиме #11 присвоение TCNT1 значения BOTTOM, не изменяет состояния выхода связанного с таймером выхода, что может привести к резкому увеличению пауз между импульсами (пропуску импульса).
 
Ситуация #7, повышение значения TCNT до значения TOP при работе генератора в режиме #9
 
 
 
Повышение значения TCNT1 до TOP в режиме #9 до значения может сильно затянуть как импульс, так и паузу.

Ситуация #8, повышение значения TCNT до значения TOP при работе генератора в режиме #11

Повышение значения TCNT1 до TOP в режиме #11 имеет одну особенность - выход, связанный с каналом таймера, на следующем такте прескалера будет принудительно переведён в неактивное состояние.  В точке BOTTOM состояние выхода не меняется.
 
ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

зачистят, но всё же скажу, помнишь, я как-то говорил тебе об аналогичной задаче методом паразитирования и, что ты не можешь не написать )))

sadman41
Offline
Зарегистрирован: 19.10.2016

Итак, руководствуясь справочными материалами и результатами эксперимента, можно понять, что за "выброс" следовал после гиперпрыжка TCNT в точку BOTTOM в эксперименте #1

После последовательного помещения в регистр сравнения OCR1B (а фактически - во временный буфер, "виртуальный регистр") и в регистр счётчика TCNT1 значения BOTTOM, произошёл переход в точку "обновления", где из буферного (виртуального) регистра OCR1B содержимое было перенесено в физический. При этом, специальном значении (OCR1B = BOTTOM), выход OC1B должен "удерживаться" (на практике - постоянно устанавливаться при совпадении TCNT1 и OCR1B) микроконтроллером в состоянии LOW (режим #9, неинвертированный). 

При изменении направления счёта TCNT1 вверх из точки BOTTOM, выход OC1B был принудительно переведён в состояние HIGH, а "прыжок"  привёл к пропуску (блокировке) операции сравнения TCNT1 и OCR1B на один такт, как и описано в документации. Вследствие этого выход OC1B не был переключен и оставался в прежнем состоянии на протяжении целого периода. 

При следующем совпадении (compare match) TCNT1 и OCR1B в точке BOTTOM, выход был штатно переведён и начал "удерживаться" в состоянии LOW.

Всё это кратко описано в разделе "Using the Output Compare Unit" даташита: "If the value written to TCNT1 equals the OCR1x value, the compare match will be missed, resulting in incorrect waveform generation. Do not write the TCNT1 equal to TOP in PWM modes with variable TOP values. The compare match for the TOP will be ignored and the counter will continue to 0xFFFF. Similarly, do not write the TCNT1 value equal to BOTTOM when the counter is downcounting."

Теперь же сухие строчки документации можно рассмотреть и на цветных картинках.

Подводя итог, приведу наиболее, на мой взгляд, беспроблемный и быстрый код старта/остановки генерации PMW на выходе OC1B в режине #9. Но, следует помнить, что, несмотря на отключение физического выхода от Waveform Generator, последний продолжает работать и следует выдержать не менее одного периода генерации перед тем, как вновь "подключить генератор" к физическому выходу.

#include <util/atomic.h>

#define D10_Low PORTB &=B11111011
#define D10_High PORTB|=B00000100

#define D13_High PORTB |= B00100000
#define D13_Low  PORTB &= B11011111

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

const uint16_t timerTop = 600, timerDuty = timerTop / 4;

inline void timerStart() {
  sbi(TCCR1A, COM1B1);
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    OCR1B = timerDuty;
    TCNT1 = 0x00;
  }
}

inline void timerStop() {
  cbi(TCCR1A, COM1B1);
  D10_Low;
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    OCR1B = 0x00;
  }
}

void setup() {
  pinMode(13, OUTPUT);
  pinMode(10, OUTPUT);

  TCCR1A = TCCR1B = TCNT1 = 0x00;
  OCR1A = timerTop;
  OCR1B = timerDuty;
  // Channel B, pin 10, clear OC1B on compare match, set OC1B at BOTTOM (non-inverting mode)
  TCCR1A |= (1 << COM1B1);
  // Mode 9: Phase and frequency correct, update of OCR1B on BOTTOM
  TCCR1A |= (1 << WGM10);
  TCCR1B |= (1 << WGM13) | (1 << CS11) | (1 << CS10);
}

void loop() {
  delay(110);
  D13_Low;
  timerStop();
  delay(1);
  D13_High;
  timerStart();
}

 

-NMi-
Offline
Зарегистрирован: 20.08.2018

МОЛОДЕЦ!!!  Прекрасно изложил!!!  Плюсики поставил!!! 

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016
trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011