Arduino Wifi Rev2, ошибка при считывании значения рабочего цикла ШИМ на выводе 6

Я пытаюсь прочитать рабочий цикл ШИМ, установленный для контакта 6 на Arduino Wifi Rev2.

По умолчанию используется таймер TCB0, используемый в 8-битном режиме ШИМ. 16-битный канал сравнения CCMP используется в качестве двух 8-битных регистров, младший байт CCMPL предназначен для периода, а старший байт CCMPH - для рабочего цикла.

Однако когда я пытаюсь прочитать регистр канала сравнения, происходит что-то странное: значение CCMPH неожиданно копируется в CCMPL.


// ШИМ на выводе 6, таймер TCB0
//TCB_t* timer_B0 = (TCB_t *)&TCB0; // указатель на структуру таймера

void setup() {
  pinMode(6, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  setPWMValueOnPin6(20);
  setPWMValueOnPin6(40);
  setPWMValueOnPin6(60);
  setPWMValueOnPin6(80);
}

void printCompareChannelValues()
{
  Serial.print("Compare channel = ");
  unsigned int r = TCB0_CCMP;
  //байт r = timer_B0->CCMP;
  Serial.println(r);
 
  Serial.print("Compare channel L = ");
  byte l = TCB0_CCMPL;
  //byte l = timer_B0->CCMPL;
  Serial.println(l);
 
  Serial.print("Compare channel H = ");
  byte h = TCB0_CCMPH;
  //byte h = timer_B0->CCMPH;
  Serial.println(h);
}

void setPWMValueOnPin6(byte value)
{
  analogWrite(6, value);
  printCompareChannelValues();
  delay(2000);
}

Вот выход последовательного монитора:

Compare channel = 5375
Compare channel L = 255
Compare channel H = 20
Compare channel = 10260
Compare channel L = 20
Compare channel H = 40
Compare channel = 15400
Compare channel L = 40
Compare channel H = 60
Compare channel = 20540
Compare channel L = 60
Compare channel H = 80
Compare channel = 5200
Compare channel L = 80
Compare channel H = 20
Compare channel = 10260
Compare channel L = 20
Compare channel H = 40
Compare channel = 15400
Compare channel L = 40
Compare channel H = 60
Compare channel = 20540
Compare channel L = 60
Compare channel H = 80
Compare channel = 5200
Compare channel L = 80
Compare channel H = 20

Проблема возникает даже при удалении последовательных функций (при наблюдении за интенсивностью светодиода на выводе 6). Такое же поведение происходит и на выводе 3 (таймер TCB1).

Кто-нибудь знает, что происходит? Спасибо за вашу помощь.

, 👍2


1 ответ


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

1

Вероятное местоположение ошибки

Проблема, по-видимому, заключается в коде analogWrite():

    timer_B->CCMPH = val;

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

16-битный аппаратный доступ к регистрам и внутренние устройства analogWrite()

Согласно "Доступу к 16-битным регистрам" спецификации ATmega4809 (среди прочего, без сомнения):

Шина данных AVR имеет ширину восемь бит, поэтому доступ к 16-битным регистрам требует атомарных операций. Доступ к этим регистрам должен осуществляться байтами с помощью двух операций чтения или записи. 16-битные регистры подключаются к 8-битной шине , а временный регистр - к 16-битной шине.

Для операции записи младший байт 16-битного регистра должен быть записан перед старшим байтом. Затем младший байт записывается во временный регистр. Когда старший байт 16-битного регистра записывается, временный регистр копируется в младший байт 16-битного регистра в том же такте.

Для операции чтения младший байт 16-битного регистра должен быть прочитан перед старшим байтом. Когда младший байтовый регистр считывается процессором, старший байт 16-битного регистра копируется во временный регистр в том же такте , что и младший байт. Теперь старший байт будет считан из временного регистра.

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

Таким образом, timer_B->CCMPH = val; - это не просто запись реального аппаратного регистра CCMPH, это также запись реального регистра CCMPL с тем, что находится в этом анонимном временном регистре, который был заполнен последней операцией чтения или записи.

Итак, вот что происходит в вашем коде:

  • Вы выполняете byte l = TCB0_CCMPL; в printCompareChannelValues(), который одновременно считывает TCB0_CCMPL и загружает временный регистр со значением реального регистраCCMPH, что, как вы увидите, важно.

  • Вы выполняете байт h = TCB0_CCMPH; который считывает временный регистр, а не реальный аппаратный регистр CCMPH, из которого временный регистр был загружен в вашем чтении из TCB0_CCMPL выше. Примечание: технически это утверждение не играет никакой роли в проблеме. Я упоминаю об этом только для того, чтобы прояснить поведение временного регистра.

  • Вы выходите из printCompareChannelValues() и exit-and-reenter setPWMValueOnPin6(), который вызывает analogWrite(), который внутри запускает timer_B->CCMPH = val;, который сохраняет val в реальном CCMPH и сохраняет временный регистр (содержащий предыдущее значение CCMPH) в реальном CCMPL.

Так что да, каждый раз, когда предыдущий CCMPH становится новым значением CCMPL.

Латание ядра

Возможно, что ядро должно содержать вместо timer_B->CCMPH = val;что-то вроде:

{
    const uint8_t saved_SREG = SREG;
    cli();

    uint16_t temp  = timer_B->CCMP;
    temp          &= 0xFF;
    temp          |= (uint16_t)val << 8;
    timer_B->CCMP  = temp;

    SREG = saved_SREG;
}

Здесь мы выполняем полное чтение 16-битного регистра. Загрузка из CCMP сначала считывает CCMPL, который заполняет временный регистр реальным CCMPH. Затем вторая половина нагрузки из CCMP пытается прочитать CCMPH, но на самом деле правильно считывает временный регистр, содержащий значение CCMPH. И теперь нет никаких неприятных сюрпризов, когда все происходит наоборот.

Оператор, назначающий CCMP, записывает CCMPL (на самом деле временный регистр), затем записывает в реальный CCMPH и одновременно фиксирует временный регистр в реальный CCMPL.

Вы можете исправить вышеизложенное, обрезанное над timer_B->CCMPH = val; в вашем PATH-TO-ARDUINO15-DIRECTORY/packages/arduino/hardware/megaavr/CORE-VERSION/cores/arduino/wiring_analog.c

Я не потратил кучу времени на размышления об этом патче, так что будьте немного осторожны. Возможно, я забыл что-то важное.

Вместо этого заполняем временный регистр

Если вы не хотите возиться с ядром или не можете по той или иной причине, уродливым взломом было бы превратить ваш простой analogWrite(6, value); call во что-то вроде следующего:

{
  const uint8_t saved_SREG = SREG; cli();
  TCB0_CCMPL = TCB0_CCMPL;
  analogWrite(6, value);
  SREG = saved_SREG;
}

Помимо отключения прерываний и последующего их восстановления, единственное, что здесь происходит, - это несколько нелепо выглядящее утверждение:
TCB0_CCMPL = TCB0_CCMPL;

Все, что он делает, - это заполняет временный реестр для вас. В правой части задания мы читаем реальный регистр CCMPL. Это также загрузка временного регистра с реальным CCMPH, но нас это не волнует. Затем в левой части задания мы пытаемся записать данные в CCMPL, но на самом деле мы записываем их во временный регистр. И теперь ваш временный регистр имеет правильное значение CCMPL для запуска timer_B->CCMPH = val; в исходном коде analogWrite().

Итак, я полагаю, что если вы хотите сделать это и не можете / не хотите изменять свое ядро, вы можете сделать это, потенциально обернув #if #else на основе версии ядра Arduino megaavr, если они обновят ядро в будущем с помощью патча, приведенного выше, или чего-то эквивалентного.

Патч ядра с использованием той же техники грунтовки

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

{
    const uint8_t saved_SREG = SREG; cli();
    timer_B->CCMPL = timer_B->CCMPL; // Первичный временный регистр
    timer_B->CCMPH = val;
    SREG = saved_SREG;
}
,

Большое спасибо @timemage за этот очень подробный и отличный ответ! Мне очень нравится вариант заполнения временного регистра. Я хотел бы отправить эту ошибку команде Arduino на github, чтобы они позаботились об исправлении ядра, и дать им ссылку на эту страницу для ссылки на исправление. Тебя это устраивает?, @Wind

Конечно, вы можете попробовать. Я почти уверен, что не смог бы остановить тебя, даже если бы захотел. =) Как бы то ни было, это влияет и на плату "Nano Every". По сути, обе из единственных двух плат, которые в настоящее время входят в пакет arduino: megaavr., @timemage

Я отправил это как ошибку, которую нужно исправить на Arduino github (github.com/arduino/ArduinoCore-megaavr/issues/91 ). Меня попросили сделать запрос на извлечение. @timemage Было бы вам интересно сделать этот ПИАР для вашей правильной атрибуции?, @Wind

Это то, что займет у меня много времени, чтобы исследовать, решить и сделать, если это то, что я решу. Короче говоря, я не знаю. Если вы хотите поговорить об этом, вы можете найти меня на freenode под этим именем., @timemage

Я не нахожусь на freenode, извините @timemage. Поскольку мне действительно нужно было бы это исправить, я думаю, что основной патч, использующий технику заполнения, мог бы стать первым предложением для PR, а затем обсуждение того, что нужно сделать или изменить, можно было бы провести на github с командой Arduino., @Wind

Ладно. Удачи вам в этом., @timemage