Arduino Uno Serial.write() сколько бит фактически передается одновременно через UART и влияние скорости передачи на другие прерывания

С помощью Arduino Uno Rev3 я пытаюсь поддерживать точную синхронизацию при передаче данных. Я хочу отправлять по 6 байт за раз, что занимает около 44 мкс с учётом времени выполнения функции Serial.write(). Время, необходимое для передачи данных от последовательного порта Arduino к компьютеру, в моём случае не имеет значения. Меня интересует только время выполнения функции.

Моя идея заключается в отправке не всех 6 байтов, а нескольких за раз, чтобы общее время отправки было распределено между задачами, чувствительными ко времени. Обратите внимание, что я никогда не превышу размер буфера, поэтому метод Serial.write() никогда не будет дожидаться завершения передачи.

const uint8_t MSG_LEN = 6;
uint8_t snd_buffer[MSG_LEN];
uint32_t us_now = 0L;
uint8_t snd_byte_cnt = MSG_LEN;
uint8_t dummy_var = 0;

void setup() {
  Serial.begin(115200);
  delay(1000);
}

void loop() {
  us_now = micros();

  operation_dummy();
  
  com_dummy();

  //цель — не использовать функции задержки
}

void com_dummy(){ //ex communication
  if (snd_byte_cnt < MSG_LEN){ //байты необходимо отправить
    Serial.write(snd_buffer[snd_byte_cnt]); //отправляем целевой байт
    snd_byte_cnt++; //перейти к следующему байту ind
  } //иначе ничего не отправлять
}

void operation_dummy() { //ex operation
  dummy_var++; //сделать что-то (на самом деле это чувствительно ко времени)
  if (snd_byte_cnt == MSG_LEN){ //если нет данных, ожидающих отправки
    * (uint32_t *) &snd_buffer[1] = us_now; //изменить буфер между 1-м и 5-м байтами (оба включительно)
    snd_buffer[MSG_LEN - 1] = dummy_var; 
    snd_byte_cnt = 0; //триггер начала отправки
  } //иначе ждем следующего цикла для отправки
}

Мой вопрос: сколько байтов следует использовать с Serial.write() за раз?

Я слышал, что библиотека Serial обрабатывает 16 бит (2 байта) за раз. Что произойдёт, если я попытаюсь отправить один байт? Ожидает ли она определённое время второго байта, и если он не приходит, отправляет один байт? (Исправление: это оказалось недоразумением, и 16 бит имеет только указатель FIFO, а не сами данные для отправки. * Источник: https://forum.arduino.cc/t/serial-write-time/233360/23 )*

Разве вызов функции сна и использование micros() ИЛИ скорости передачи данных не повлияют на мой отсчёт времени? Думаю, micros() и передача данных на самом деле используют прерывания. Поэтому разные скорости передачи данных могут по-разному задерживать вызовы micros(), поскольку в обоих случаях прерывания должны быть отключены.

, 👍2

Обсуждение

Какую плату Arduino вы используете?, @fabiuz7

UART передает по одному биту за раз... так работает последовательная связь... заголовок вашего сообщения не соответствует остальной части сообщения, @jsotola

Чистый USART будет отправлять 10 бит на байт (8N1) с желаемой скоростью передачи данных, однако всякий раз, когда вы используете USB-To-Serial или CDC (на платах с собственным USB), он будет полностью зависеть от USB, @KIIV

Поскольку вы не указали, какую версию Arduino вы используете, с какой скоростью передачи данных и с какой частотой передаёте 6 байт, мы можем ответить только в общем виде. Пожалуйста, [отредактируйте] свой вопрос, добавив эту информацию. Также, пожалуйста, укажите на утверждение об обработке двух байтов — это бессмыслица., @the busybee

Почему вы использовали тег «softwareserial», используете ли вы его?, @the busybee

Спасибо за отзыв. Я отредактировал вопрос соответствующим образом., @theGD


2 ответа


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

3

Последовательный порт Arduino записывает по одному байту за раз. Существует метод для записывает буфер произвольной длины, но все, что делает этот метод, это несколько раз вызывать write(uint8_t) для каждого байта в пределах буфер:

size_t Print::write(const uint8_t *buffer, size_t size)
{
  size_t n = 0;
  while (size--) {
    if (write(*buffer++)) n++;
    else break;
  }
  return n;
}

Обратите внимание, что между байтами нет ожидания.

Если вы не заполняете буфер передачи, это должно быть очень быстро: все write(uint8_t) — это помещает байт в кольцевой буфер. Если ваш Тайминги находятся на уровне микросекунд, вы можете просто отправить шесть байтов сразу.

Я слышал, что библиотека Serial обрабатывает 16 бит (2 байта) за раз?

Очень сомнительно. Есть ли у вас ссылка на это утверждение?


Редактировать: Если у вас возникают проблемы с таймингами в диапазоне микросекунд, вы можно было бы отправлять байты, напрямую записывая их в регистр данных UART, Вместо того, чтобы полагаться на библиотеку ядра Arduino, чтобы сделать это. Чтобы сделать это, это, вы бы:

  1. Отключите прерывание «USART, Data Register Empty», которое является прерывание, используемое ядром Arduino для отправки байтов в порт.

  2. Перед отправкой байта проверьте флаг «USART, Data Register Empty» в чтобы узнать, готов ли UART принять байт для отправки.

  3. Запишите байт, который вы хотите отправить, непосредственно в регистр данных UART, таким образом полностью обходится буфер TX, используемый ядром Arduino.

Обходя кольцевой буфер и связанное с ним прерывание, вы должны таким образом можно выиграть несколько микросекунд.

Вот версия вашего примера кода, использующего этот подход:

const uint8_t MSG_LEN = 6;
uint8_t snd_buffer[MSG_LEN];
uint32_t us_now = 0L;
uint8_t snd_byte_cnt = MSG_LEN;
uint8_t dummy_var = 0;

void setup() {
  Serial.begin(115200);
  UCSR0B &= ~_BV(UDRIE0); //отключаем «USART, регистр данных пуст»; прерывать
  delay(1000);
}

void loop() {
  us_now = micros();

  operation_dummy();
  
  com_dummy();

  //цель — не использовать функции задержки
}

void com_dummy(){ //ex communication
  if (snd_byte_cnt < MSG_LEN && bit_is_set(UCSR0A, UDRE0)){ //байты должны быть отправлены и могут быть отправлены
    UDR0 = snd_buffer[snd_byte_cnt]; //отправить целевой байт
    snd_byte_cnt++; //перейти к следующему байту ind
  } //иначе ничего не отправлять
}

void operation_dummy() { //ex operation
  dummy_var++; //сделать что-то (на самом деле это чувствительно ко времени)
  if (snd_byte_cnt == MSG_LEN){ //если нет данных, ожидающих отправки
    * (uint32_t *) &snd_buffer[1] = us_now; //изменить буфер между 1-м и 5-м байтами (оба включительно)
    snd_buffer[MSG_LEN - 1] = dummy_var; 
    snd_byte_cnt = 0; //триггер начала отправки
  } //иначе ждем следующего цикла для отправки
}
,

За исключением UNO-R4. Он блокирует всю длину последовательной передачи., @Delta_G

@Delta_G: Ого! Звучит глупо. Есть идеи, зачем они это делают?, @Edgar Bonet

Возможно, это просто базовая реализация без оптимизаций., @fabiuz7

@EdgarBonet, не понимаю, почему они так сделали. Вижу, они пытались использовать реализацию FSP UART, но, похоже, просто запутались. Я отправил запрос на исправление, но его долгое время игнорировали. https://github.com/arduino/ArduinoCore-renesas/pull/304, @Delta_G

@Delta_G Мне было интересно, почему ваш код в некоторых местах в diff-файлах так не выровнен, и в нем смешаны табуляции и пробелы... Очень раздражает, так как всегда есть кто-то с другой остановкой табуляции :D, @KIIV

Потому что у меня в vs code не настроено правильное форматирование, и я не смог найти руководство по стилю или согласованную форму, которой нужно следовать., @Delta_G

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

@Delta_G, это можно сделать, выбрав «Вставить пробелы» в настройках. PR будут объединены как есть, никто не сможет это исправить., @KIIV

Вы правы насчет 16 бит, видимо я неправильно понял этот пост: https://forum.arduino.cc/t/serial-write-time/233360/23, @theGD

Ладно, тогда я просто оставлю последовательный порт блокирующим, так как не могу заставить vsc правильно отформатировать. Я не знал, что это важнее. Пожалуй, оставлю себе версию без блокировки., @Delta_G

Я отредактировал вопрос, чтобы сделать его более понятным., @theGD

какой классный хак с обходом буфера! спасибо., @theGD


7

Я хочу отправлять 6 байтов за раз, что занимает около ~44 мкс с учетом времени выполнения Serial.write().

Вы измеряете время, необходимое Serial.write для помещения байтов во внутренний буфер (который содержит 64 байта), а не время их передачи. Вы не указали используемую скорость передачи данных, но я сомневаюсь, что она составляет 1363652 бит/с.

6 bytes in 44 µs is one byte in 7.3333 µs.
Each byte consists of 10 bits (start bit, 8 data bits, stop bit)
Therefore you are talking about 0.7333 µs per bit.
1/7.333e-6 = 1363652 bits per second.

Быстрый тест показывает, что для помещения 6 байт в последовательный буфер действительно требуется 48 мкс (на моем Uno и моей IDE).

Изменение записи на запись только одного байта сокращает время с 48 мкс до 12 мкс, поэтому меньше времени тратится на копирование данных в этот буфер.

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

При извлечении каждого байта из буфера и отправке его на оборудование возникнут некоторые накладные расходы (я не уверен в их точном размере).

Я слышал, что библиотека Serial обрабатывает 16 бит (2 байта) за раз.

Это чушь. Хотя было бы неплохо, если бы вы указали, какая у вас Arduino.

Что произойдет, если я попытаюсь отправить один байт?

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

Если время очень критично, я бы посоветовал отключить прерывания, но тогда, конечно, вы вообще не сможете передавать ни одного байта.

Мне это очень напоминает проблему XY. Вы увязли в рассуждениях о времени передачи одного байта, но так и не объяснили:

  • Какой у вас Arduino?
  • Какую скорость передачи данных вы используете?
  • Какой у вас код?
  • Чего именно вы пытаетесь достичь в высшей степени своевременно?

Что касается заголовка вашего вопроса:

сколько битов фактически передается одновременно через UART

Один бит будет передан «за один раз» — так работает последовательная связь.


Повлияет ли вызов функции сна и использование micros() ИЛИ скорости передачи данных на мой отсчет времени?

Функция сна? Каково её предназначение?

Если вы не отключите прерывания или не предпримете других действий (например, не отмените таймер 0), то micros будет работать, поскольку он, по сути, возвращает содержимое аппаратного таймера (0) и счётчик переполнений. Кроме того, это будет достаточно быстро. Работа самого таймера будет периодически вызывать прерывания (примерно каждую миллисекунду).

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

Думаю, micros() и передача данных используют прерывания. Значит, разные скорости передачи данных могут по-разному задерживать вызов micros(), поскольку в обоих случаях прерывания должны быть отключены?

micros() не использует прерывания напрямую (за исключением случаев, указанных ниже), однако накопление переполнений таймера происходит в прерывании. Обратите внимание, что micros() на короткое время отключает прерывания, чтобы получить счётчик переполнений без его изменения в течение нескольких тактов, пока он копируется.

См. hardware/arduino/avr/cores/arduino/wiring.c для фактического кода в micros() и millis().

Обратите внимание, что из-за настройки таймера 0 функция micros() имеет разрешение 4 мкс. Другими словами, вы не сможете заметить разницу между интервалами 2 мкс и 3 мкс.

В ответ на комментарий я просто уточню, что отключение прерываний просто откладывает обработку этого прерывания до тех пор, пока они не будут включены снова.

,

Спасибо! У меня Uno (в названии) rev3. Скорость передачи данных — 115200, чтобы обеспечить совместимость со всеми ОС. Что касается моих опасений по поводу отправки 16 бит, вы правы, я перепутал, что указатель Serial FIFO равен 16 битам, как упоминалось здесь https://forum.arduino.cc/t/serial-write-time/233360/23. Наконец, это именно то, что я хотел узнать. Я хотел минимизировать время записи в последовательный буфер, чтобы можно было «обработать» его потоками, не превышая при этом ни одной операции более 50 мкс. Обмен данными и так очень слабый и нечастый, поэтому я могу предположить, что последовательная запись никогда не будет заблокирована из-за полного заполнения., @theGD

Я отредактировал вопрос, чтобы лучше сформулировать свою мысль., @theGD

Что касается Uno, я посмотрел на теги, где обычно указывается, какая у вас Arduino. Я добавил тег Uno специально для вас., @Nick Gammon

Я ответил на ваши отредактированные пункты., @Nick Gammon

Я упомянул, что *работа таймера сама по себе будет вызывать периодические прерывания*. Я не говорил и не намеревался подразумевать, что отключение прерываний приведёт к прекращению обслуживания прерываний, очевидно, что это не так. Однако, пока прерывания отключены, они не будут обслуживаться. Подробнее см. [мою страницу о прерываниях](https://www.gammon.com.au/interrupts)., @Nick Gammon

Также: https://arduinoprosto.ru/q/30968/how-do-interrupts-work-on-the-arduino-uno-and-similar-boards/30969#30969, @Nick Gammon

Я добавил ещё одно предложение, чтобы это осветить. Спасибо за предложение. :), @Nick Gammon