Серийное прерывание

Я хочу создать простую программу, которая будет реагировать на отправку символа через консоль Arduino IDE. Я использую ардуино УНО. Вот моя программа:

volatile bool MeasReceived=false;

ISR(PCINT2_vect){  
  PCICR = 0b00000000;    // отключаем порт d
  MeasReceived=true;
}

void setup() {    
  Serial.begin(9600);
  Serial.flush();

  Serial.print("Hello PC");
  delay(1000);
  cli();      
  PCICR |= 0b00000100;    // включаем порт d
  PCMSK2 |= 0b0000001;    // включаем пины PD0, PCINT16
  sei();


}

void loop() {

  if(MeasReceived==true){
    MeasReceived=false;
    inByte = Serial.read();
     
    if(inByte=='A'){   //A: ПК запрашивает данные измерений у Arduino
       Serial.print('B');
    }
    else{
      Serial.print(inByte);
    }
  }
  PCICR = 0b00000001; // повторно включить порт d
}

Что я делаю, так это устанавливаю контакт RX в качестве прерывания смены контакта. После этого я отправляю символ через консоль, например 'A'. Срабатывает ISR, сначала отключая прерывание, а после этого флаг устанавливается в true. Таким образом, я ввожу часть кода, где я выполняю команду Serial.read(), чтобы узнать, какой символ был отправлен мне через консоль.

Проблема в том, что независимо от того, какой символ я отправляю, результатом всегда будет 255. Это означает, что значение inByte после Serial.read() всегда равно 255. Что пойдет не так?

EDIT: исходный вопрос, на который я ответил ниже. Однако в новом коде, опубликованном ниже, произошла одна интересная вещь. Если я переведу Arduino в спящий режим (режим powerDown) и попытаюсь разбудить его с помощью последовательного прерывания, если я отправлю только один символ, то arduino просыпается, но не входит в ISR! Если я отправляю два символа, то происходит доступ к ISR и устанавливаются флаги, при этом программа работает нормально. Вот код, чтобы воспроизвести проблему:

volatile bool MeasReceived=false;
uint8_t inByte;



ISR(PCINT2_vect){
  PCMSK2 = 0b00000000;    // отключаем порт d
  MeasReceived=true;
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);

 
  Serial.begin(9600);
  Serial.flush();
  Serial.print("Hello PC");
  delay(1000);
      
  cli();
  PCICR = 0b00000100;    // включаем порт d
  PCMSK2 = 0b00000001;    // включаем пины PD0, PCINT16
  sei();
}

void loop() {

  if(MeasReceived==true){
    MeasReceived=false;
    delay(2000);
    inByte = Serial.read();        
       
    if(inByte=='A'){   //A: ПК запрашивает данные измерений у Arduino
       Serial.print('B');
    }
    else{
      Serial.print(inByte);
    }
    PCMSK2 = 0b00000001;
    
  }
  
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);    //Установить режим отключения питания
  sleep_bod_disable();  //отключаем обнаружение затухания
  digitalWrite(LED_BUILTIN, LOW); 
  sleep_enable();
  sleep_cpu();
  sleep_disable();
  digitalWrite(LED_BUILTIN, HIGH); 
  delay(1000);
  // int i=Serial.available();
  //Серийный.принт(я);
  delay(1000);     
  
}

Итак, отправив один символ, Arduino просыпается, светодиод светится, и через две секунды Arduino снова уходит в сон, так и не войдя в ISR и, как следствие, не установив флаг. Кроме того, отправив один символ, если я раскомментирую печать Serial. available(), она напечатает «0»!

Если я отправляю два символа, Arduino просыпается, и программа работает нормально.

, 👍0

Обсуждение

Вы видели это? ... https://www.arduino.cc/reference/en/language/functions/communication/serial/serialevent/, @jsotola

и это., @jsotola

Почему вы хотите использовать здесь прерывание смены контакта? Я не понимаю, зачем это нужно делать, так как последовательный (UART) интерфейс имеет свои собственные прерывания., @chrisl

@chrisl Мне нужно максимально возможное энергосбережение, которое обеспечивает режим PWR_DWN. Чтобы выйти из этого режима, мне нужно использовать прерывание смены контакта для моего UART RX., @NickG


2 ответа


0

После дополнительного тестирования выяснилось, что проблема заключалась в том, что когда я отправлял символ через консоль, интервал между временем t1, когда изменение было замечено, и запуском ISR, и временем t2, когда Serial.read( ) произошло, было очень коротким, почти мгновенным. В результате байт (символ), который я отправил через последовательную консоль, еще не был готов для чтения, а команда Serial.read() вернула простые единицы.

Кроме того, чтобы отключить/повторно включить прерывание смены контакта, кажется правильным использовать регистр PCMSK, а не PCICR. Первоначально опубликованный код имел дополнительную проблему: когда PCICR был повторно включен (установлен на 0x01), ISR сразу же снова запускался, считывая дополнительный символ (очевидно, все «1»). Это было решено путем отключения / повторного включения прерывания смены контакта с помощью регистра PCMSK. Вот исправленный код, который работает нормально:

volatile bool MeasReceived=false;
uint8_t inByte;    

ISR(PCINT2_vect){   //ISR для прерываний смены контакта, происходящих на PD0-PD7
  
  PCMSK2 = 0b00000000;    // отключаем прерывание PD0
  MeasReceived=true;      //Установить полученный флаг char
  
}

void setup() {

  Serial.begin(9600);
  Serial.flush();
  Serial.print("Hello PC");
  delay(1000);
  
  cli();      
  PCICR = 0b00000100;    // включаем прерывания смены вывода порта d
  PCMSK2 = 0b0000001;    // включаем пин PD0, PCINT16
  sei();
}

void loop() {

  if(MeasReceived==true){  //Если запущен ISR
    MeasReceived=false;    //Сброс флага
    delay(1000);           // Подождите некоторое время, пока символ будет установлен. 1000 мс может быть слишком много
    inByte = Serial.read();  // Читаем отправленный символ
                        
    if(inByte=='A'){   //Если символ был A, выводим B на консоль
       Serial.print('B');
    }
    else{   //иначе, записываем ASCII-код полученного символа
      Serial.print(inByte);
    }
    PCMSK2 = 0b00000001;   // повторно разрешаем прерывание PD0
  }
}
,

2

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

чтобы отключить/повторно включить прерывание смены контакта, правильный путь кажется использовать регистр PCMSK, а не PCICR.

Каноническая процедура включения любого прерывания по фронту такова: сначала очистить флаг прерывания, а затем установить бит разрешения прерывания. Обратите внимание, что флаг прерывания очищается записью в него логической единицы. Для прерывания PCINT2 это будет:

PCIFR =  _BV(PCIF2);  // очистить флаг прерывания
PCICR |= _BV(PCIE2);  // разрешить прерывание

При этом нет смысла использовать прерывание смены контакта в первое место. Вместо этого вы можете использовать каноническую идиому

if (Serial.available() > 0) {
    char inByte = Serial.read();
    // ...
}

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


Правка 1. Самый простой способ перевести ЦП в спящий режим до следующего получено прерывание, заключается в вызове sleep_mode() в конце loop(), например:

// Приостановить работу, пока не будет получен следующий символ.
set_sleep_mode(SLEEP_MODE_IDLE);
if (!Serial.available()) {
    sleep_bod_disable();
    sleep_mode();
}

Однако здесь есть условие гонки: если прерывание срабатывает после вы тестируете Serial.available(), но перед сном вы будете спать вместо обработки полученного символа. Решение этой проблемы это сделать тест с отключенными прерываниями, а затем включить их правильно перед выдачей инструкции sleep, как описано в документация avr-libc:

set_sleep_mode(SLEEP_MODE_IDLE);
cli();
if (!Serial.available()) {
    sleep_enable();
    sleep_bod_disable();
    sei();
    sleep_cpu();
    sleep_disable();
}
sei();

Правка 2: ответы на вопросы в комментариях.

  1. Прерывание смены контакта позволяет вам спать глубже, чем IDLE, но если вы сделать это, UART будет остановлен и пропустит стартовый бит, который может сбить его сроки. Я предпочел бы придерживаться IDLE и использовать power_*_disable() отключает ненужные периферийные устройства.

  2. У меня есть теория, но я не уверен, что она верна. Посмотрите в схема обнаружения PCINT. Если часы остановились, изменение уровня вывода заставит вентиль XOR переключиться на 1, что предположительно пробуждает MCU, но это не будет записано нисходящие триггеры, которые полагаются на часы. Если вывод возвращается в свой первоначальный уровень до перезапуска часов (что занимает некоторое время, когда выход из PWR_DOWN), тогда триггеры никогда не видят смены контакта состояние, и флаг прерывания не поднимается.

Обновление 3: я сделал тест, который подтверждает, что при пробуждении от PWR_DOWN импульсным событием PCINT, ISR не запускается, если импульс слишком короткий. В установке участвуют два Arduino:

  • «тестер» Arduino:
    • отправляет импульсы различной длины через цифровой 8 = PB0
    • пересылает на TX данные, полученные на RX
  • «DUT» Arduino:
    • засыпает в режиме PWR_DOWN
    • пробуждение по прерыванию смены контакта на цифре 8 = PB0 = PCINT0
    • сообщает, работала ли ISR на последовательном порту

Соединения:

DUT   Tester    PC
───────────────────
        USB --- USB
GND --- GND
+5V --- +5V
  8 --- 8
 TX --- RX

Программа «тестер»:

const uint32_t PULSE_PERIOD = 500;  // импульсы каждые 500 мс

void setup() {
    DDRB |= _BV(PB0);  // цифровой 8 = PB0 в качестве выхода
    Serial.begin(9600);
    Serial.println("Tester: ready");
}

void loop() {
    // Направить последовательную связь.
    while (Serial.available())
        Serial.write(Serial.read());

    // Периодически отправлять импульс.
    static uint32_t last_pulse_time;
    if (millis() - last_pulse_time >= PULSE_PERIOD) {
        last_pulse_time += PULSE_PERIOD;

        // Отправляем импульс длительностью 2^n микросекунд.
        static uint8_t n = 0;
        uint16_t length = 1U << n;
        Serial.print("Tester: sending pulse of ");
        Serial.print(length);
        Serial.println(" us");
        Serial.flush();
        uint32_t now = millis();
        while (millis() == now) ;  // ждем тика
        PORTB |= _BV(PB0);
        delayMicroseconds(length);
        PORTB &= ~_BV(PB0);

        // Размер следующего импульса.
        if (++n >= 16) n = 0;
    }
}

Программа «ДУТ»:

#include <avr/sleep.h>

// PCINT ISR только устанавливает флаг.
volatile bool isr_ran;
ISR(PCINT0_vect) { isr_ran = true; }

void setup() {
    // Настройка прерывания.
    PCMSK0 = _BV(PCINT0);  // смысл PCINT0 = PB0
    PCIFR |= _BV(PCIF0);   // очистить флаг прерывания
    PCICR  = _BV(PCIE0);   // разрешить прерывание PCINT0

    // Избегайте других прерываний.
    TIMSK0 = 0;         // избежать прерывания таймера
    PORTD |= _BV(PD0);  // поднимем RX на высокий уровень, чтобы предотвратить прерывание USART_RX
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);

    Serial.begin(9600);
    Serial.println("DUT: ready");
}

void loop() {
    loop_until_bit_is_clear(PINB, PB0);  // ждем ввода LOW
    Serial.println("DUT: going to sleep");
    Serial.flush();
    isr_ran = false;
    sleep_mode();
    Serial.print("DUT: woke up, ISR ran: ");
    Serial.println(isr_ran ? "YES" : "NO");
}

И результаты:

DUT: going to sleep
Tester: sending pulse of 1 us
DUT: woke up, ISR ran: NO
DUT: going to sleep
Tester: sending pulse of 2 us
...
Tester: sending pulse of 1024 us
DUT: woke up, ISR ran: NO
DUT: going to sleep
Tester: sending pulse of 2048 us
DUT: woke up, ISR ran: YES
DUT: going to sleep
Tester: sending pulse of 4096 us
DUT: woke up, ISR ran: YES
...

Вывод: импульс длительностью 1024 мкс или короче пробуждает Arduino, но ISR не работает. Если импульс 2048 мкс или больше, ISR работает.

,

Спасибо за ваш ответ! Два вопроса: 1) Я использовал serial.available, но прерывание смены контакта позволяет мне спать глубже. Разве это не так? 2) Я включил правильный способ включения прерываний смены контакта, но та же проблема сохраняется. Отправка одного символа пробуждает UC, но доступ к ISR не осуществляется. Почему это?, @NickG

Чтобы было понятно, я не понимаю, как можно выйти из спящего режима и при этом не запускать код ISR. Вот что происходит, когда я отправляю один символ из консоли Arduino IDE., @NickG

@NickG: см. обновленный ответ., @Edgar Bonet

Ваше объяснение звучит хорошо. Я протестировал код, используя «SLEEP_MODE_STANDBY», который оставляет работающим только осциллятор, и он работал, как и ожидалось. Я оставлю вопрос открытым на несколько дней, если кто-то еще захочет добавить или проверить то, что мы обсуждали., @NickG