В чем логика Arduino, встраивающая `HardwareSerial::_rx_complete_irq()` для получения последовательных данных (но НЕ `_tx_udr_empty_irq()`)?

c++ header inline

В: Какова логика вставки Arduino HardwareSerial::_rx_complete_irq() для получения последовательных данных (но НЕ _tx_udr_empty_irq()), и когда это целесообразно?

Из нижней части класса HardwareSerial в HardwareSerial.h:

// Обработчики прерываний - не предназначены для внешнего вызова
inline void _rx_complete_irq(void);  // <======= встроенный!
void _tx_udr_empty_irq(void);        // <======= НЕ встроенный! Почему?

Кроме того, какова логика разработки, стоящая за размещением одного из определений последовательной функции ISR в заголовочном файле, а не в исходном файле? Кроме того, когда это хороший дизайн и каковы компромиссы, а когда это незаконно или не разрешено языком, компилятором или чем-то еще?

Вот точный сценарий, который заставил меня задуматься над этим вопросом:


См. здесь файлы реализации HardwareSerial: https://github.com/arduino/ArduinoCore. -avr/дерево/мастер/ядра/ардуино

Вот основной заголовочный файл. https://github.com/arduino/ArduinoCore-avr/blob/master/ ядра/arduino/HardwareSerial.h

Встроенный ISR _rx_complete_irq():

  • Строка 138 файла "HardwareSerial.h" объявляет встроенный ISR для получения последовательных данных:

     inline void _rx_complete_irq(void);
    
    • Этот ISR вызывается всякий раз, когда "в приемном буфере присутствуют непрочитанные данные". (Техническое описание ATmega328 20.7.3 стр.190)

    • Строки 40 и 48–50 файла "HardwareSerial0.cpp" ; здесь настраивается ISR:

         ISR(USART_RX_vect)  // строка 40
         {                   // строка 48
           Serial._rx_complete_irq();  // строка 49
         }                   // строка 50
      
    • Строка 101–121 файла "HardwareSerial_private.h"< /a> реализует встроенную функцию _rx_complete_irq():

        void HardwareSerial::_rx_complete_irq(void)
        {
          if (bit_is_clear(*_ucsra, UPE0)) {
            // Нет ошибки четности, прочитать байт и сохранить его в буфере, если есть
            // номер
            unsigned char c = *_udr;
            rx_buffer_index_t i = (unsigned int)(_rx_buffer_head + 1) % SERIAL_RX_BUFFER_SIZE;
      
            // если мы должны сохранить полученный символ в место
            // как раз перед хвостом (это означает, что голова будет продвигаться к
            // текущее местоположение хвоста), мы вот-вот переполним буфер
            // и поэтому мы не пишем символ и не продвигаем голову.
            if (i != _rx_buffer_tail) {
              _rx_buffer[_rx_buffer_head] = c;
              _rx_buffer_head = i;
            }
          } else {
            // Ошибка четности, прочитать байт, но отбросить его
            *_udr;
          };
        }
      

НЕ встроенный ISR _tx_udr_empty_irq():

  • Строка 139 файла "HardwareSerial.h" объявляет ISR для передачи последовательных данных: void _tx_udr_empty_irq(void);
    • Этот ISR запускается сообщением "Регистр данных USART пуст" флаг и вызывается всякий раз, когда буфер передачи передал свое значение регистру сдвига и теперь "готов к приему новых данных"; (Техническое описание ATmega328 20.11.2 стр.200)

    • Его реализация находится в строках 89–113 файла "HardwareSerial. .cpp"

         void HardwareSerial::_tx_udr_empty_irq(void)
         {
           // Если прерывания разрешены, на выходе должно быть больше данных
           // буфер. Отправить следующий байт
           unsigned char c = _tx_buffer[_tx_buffer_tail];
           _tx_buffer_tail = (_tx_buffer_tail + 1) % SERIAL_TX_BUFFER_SIZE;
      
           *_udr = c;
      
           // очищаем бит TXC -- "можно очистить, записав в его бит единицу
           // местоположение". Это гарантирует, что flush() не вернется до тех пор, пока байты
           // действительно было написано. Другие биты r/w сохраняются, а нули
           // записывается в остальные.
      
         #ifdef MPCM0
           *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << MPCM0))) | (1 << TXC0);
         #else
           *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << TXC0)));
         #endif
      
           if (_tx_buffer_head == _tx_buffer_tail) {
             // Буфер пуст, поэтому отключите прерывания
             cbi(*_ucsrb, UDRIE0);
           }
         }
      

В чем разница? Почему встроен один ISR, а не другой?

Опять же, из нижней части класса HardwareSerial в HardwareSerial.h:

// Обработчики прерываний - не предназначены для внешнего вызова
inline void _rx_complete_irq(void);  // <======= встроенный!
void _tx_udr_empty_irq(void);        // <======= НЕ встроенный! Почему?

Почему несколько сложный набор из 3+ файлов? В основном:

  • HardwareSerial.h
  • HardwareSerial_private.h
  • HardwareSerial.cpp

Впервые я задокументировал это для себя и задумался об этом 31 января 2018 года, изучая исходный код Arduino. Я хотел бы услышать больше информации по этой теме от других разработчиков.

, 👍4


1 ответ


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

7

Какова логика вставки HardwareSerial::_rx_complete_irq() в Arduino для получения последовательных данных (но НЕ _tx_udr_empty_irq()), и когда это целесообразно?

Есть несколько причин, которые приводят к такому расположению:

  1. Прерывание RX должно быть быстрым, поскольку мы реагируем на внешнее воздействие. Прерывание TX не обязательно должно быть быстрым, поскольку оно используется только для перемещения данных из буфера в UART.

Мы должны иметь возможность быстро реагировать на входящие данные, чтобы мы могли сохранить их в кольцевом буфере RX вовремя, чтобы обработать следующий прибывший байт. На самом деле это не проблема при низких скоростях передачи, но когда дело доходит до более высоких, каждый цикл, который можно сократить, это время прерывания выгодно. Однако прерывание TX на самом деле не имеет значения. Все, что он делает, это загружает следующий байт в исходящий буфер UART, и не имеет значения, если это займет немного больше времени, поэтому нет необходимости сокращать дополнительные циклы команд, потребляемые вызовом функции.

  1. Прерывание TX вызывается из нескольких мест, а RX — нет.

Код прерывания RX вызывается только при срабатывании прерывания. Это означает, что он будет вызываться только через один путь выполнения. Однако код прерывания TX вызывается не только самим прерыванием, но и другими областями кода. Например, если прерывания отключены, функция write будет вручную опрашивать UART для определения состояния буфера и вручную вызывать функцию прерывания для отправки данных (см. com/arduino/ArduinoCore-avr/blob/master/cores/arduino/HardwareSerial.cpp#L262">строка 262 файла HardwareSerial.cpp).

Если бы код прерывания TX был встроенным, то в коде было бы много мест, где этот блок дублировался бы, что приводило к раздуванию кода. Это беспричинное раздувание кода (см. пункт 1), поэтому этого следует избегать.

Кроме того, какова логика дизайна для размещения одного из определений последовательной функции ISR в заголовочном файле, а не в исходном файле?

Встраивание может выполняться только в пределах одной единицы перевода. Если вы хотите, чтобы функция была встроена в несколько единиц перевода (HardwareSerial0.cpp, HardwareSerial1.cpp и т. д.), вам нужно будет иметь ее в этих единицах перевода. А это значит, что он нужен в шапке1. Поскольку прерывание TX нежелательно встраивать, вместо этого его можно поместить в файл CPP.

Почему несколько сложный набор из 3+ файлов?

Просто: управление. Кодом (довольно сложным UART) легче управлять, если он разбит на разные области. У вас есть общий базовый объект в HardwareSerial.cpp, который затем используется кодом для каждого отдельного UART в файлах HardwareSerial[0-3].cpp. У вас есть один заголовочный файл, который определяет класс, все его методы и данные (HardwareSerial.h). И тогда у вас есть "частное" заголовок с любыми встроенными функциями, которые включены там, где это необходимо.

Верно, вы можете объединить HardwareSerial.h и HardwareSerial_private.h в один, но таким образом встроенные функции будут исключены из единиц трансляции, где они не нужны. Таким образом, вы не получите эти фрагменты кода, включенные в ваш скетч. Правда компоновщик отбросит их, так как они не используются, но так будет аккуратнее - функции включаются только в те единицы перевода, где эти функции действительно используются.


Примечания:

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

и эта часть была написана до использования LTO, @Juraj

Верно. Я думал об упоминании LTO, но думал, что это, вероятно, выходит за рамки, поскольку это не повлияло на стиль кодирования, поскольку в то время его не было в компиляторе., @Majenko

файлы HardwareSerial[0-3].cpp были оптимизацией без LTO., @Juraj