Почему Serial.Write работает медленнее при записи x+1 символов, чем при записи x символов?

serial performance

Использование Serial.Write() в приложении, чувствительном ко времени. Я понимаю, что количество записываемых символов не только повлияло на время, затрачиваемое на запись в буфер, но на самом деле оказало большее влияние, чем ожидалось.

Рассмотрим следующий скетч, на котором измеряется время, необходимое для записи символов NB_CHARS, с использованием Serial.Write(). Я сделал выборку 1000 раз, чтобы получить более надежные данные.

#define NB_CHARS 100

void setup()
{
  Serial.begin(115200);

  uint8_t chars[NB_CHARS];
  for (int i=0; i<NB_CHARS; i++)
  {
    chars[i] = 66;
  }

  unsigned long time1, time2;
  unsigned long totalTime = 0;

  delay(100);

  for (int i=0; i<1000; i++)
  {
    time1 = micros();
    Serial.write(chars, NB_CHARS);
    time2 = micros();
    totalTime += time2 - time1;

    delay(10);
    Serial.println("");
    delay(10);
  }

  float meanTime = (float)totalTime / 1000.0f;
  Serial.print("meanTime = ");
  Serial.println(meanTime);
}

void loop()
{

}

Вот несколько результатов для разных значений NB_CHARS:

NB_CHARS: Time (microseconds)
1: 12
2: 18
3: 24
[...]
99: 2912
100: 2997
101: 3082
[...]
199: 11412
200: 11497
201: 11582

Кажется, что сначала каждый новый символ добавляет около 6 микросекунд задержки, но потом она становится 85 микросекунд. Я проверил все значения от 1 до 100, чтобы определить, где они меняются, и, похоже, это сразу после 70:

Задержка Serial.Write() как функция количества символов

Мне это кажется странным.

Быстрый поиск в Google привел меня к этой публикации Тома Карпентера, 2013 год. Я не уверен, насколько это правда и актуально ли это до сих пор.

Отчасти проблема в том, что библиотека аппаратного последовательного интерфейса настолько невероятно неэффективно (не думаю, что они могли бы сделать его менее эффективным) эффективно, если бы они попытались!). После отправки каждого байта происходит вызывается прерывание, которое тратит всю жизнь на загрузку следующего байта в буфер для отправки. Функция записи также тратит много времени на выполнение расчеты относительно того, куда поместить символ в кольцевой буфер.

[...]

Почему такая «длинная» задержка для каждого символа и есть ли способ сделать ее быстрее?

Что происходит после 70 символов, что замедляет его еще больше?

, 👍5

Обсуждение

установите более высокую скорость передачи данных. При хорошем соединении должно быть не менее 460800 бод., @Juraj

цитата больше не верна. при скорости передачи данных более 500 кбит/с прерывание не используется, если буфер пуст. (скорость передачи данных, а не скорость передачи данных в бодах), @Juraj


3 ответа


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

14

Ядро Arduino имеет буфер передачи размером 64 байта. Символы отправляются из этого буфера аппаратно.

Когда вы удаляете много символов из серийного номера, первые 64 просто помещаются в этот буфер — почти мгновенно.

Как только этот буфер заполнится, ваш скетч заблокируется до тех пор, пока оборудование не отправит символ, чтобы освободить место.

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

>

С этого момента вы полностью зависите от выбранной вами скорости передачи данных.

Чтобы получить более предсказуемую пропускную способность (и максимизировать скорость), вам следует обойти ядро Ardiuno и напрямую манипулировать регистрами UART.

,

"Вы должны обойти ядро Ardiuno и манипулировать регистрами UART напрямую". Есть ли хорошая библиотека для этого?, @Alex Millette

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

Я думаю, было бы полезно, @Majenko, если бы ты мог указать Алексу на какой-нибудь пример кода..., @boatcoder

Существует большая вероятность того, что после обхода ядра ваш код будет работать так же медленно, как правая часть, а не так быстро, как левая., @IMil

Да. В лучшем случае более эффективный код позволит вам делать больше вещей между отправкой символов. Но вы не можете ускорить отправку символов. Но, возможно, «делать больше вещей между» — это то, что вам нужно., @user253751

Для бесперебойной последовательной связи вам действительно понадобится микроконтроллер с прямым доступом к памяти (DMA), который будет выполнять передачу данных за вас..., @Majenko


3

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

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

Я хочу отметить, что вы можете использовать последовательный порт USB, который есть внутри некоторых микроконтроллеров и процессоров. Это не настоящий последовательный UART со скоростью передачи данных и часами, а USB-устройство CDC.
У atmega32u4 и Arm m0+ есть такой последовательный порт USB CDC.
В Arduino Leonardo, Micro и Pro Micro используется atmega32u4.
В сериях Arduino Zero, m0 и mkr используется процессор Arm m0+.

А как насчет этих цифр:

NB_CHARS: Time (microsec)
1: 6.93
2: 7.60
3: 8.06
[...]
99: 153
100: 154
101: 152
[...]
199: 1685
200: 1624
201: 1705

Это с процессором Arm m0+. Я использовал тот же скетч, что и вы, но изменил Serial на SerialUSB.

,

3

Ваша «долгая» задержка вовсе не слишком велика.

Если вы измерите пропускную способность правой части, у вас будет около 30 символов, отправленных за 2500 микросекунд. Это означает, что скорость составляет примерно (30 * 8 * 1 000 000) / 2500, что составляет 96 000 бит/сек.

Вы указали немного меньше 115 200, но последовательные протоколы используют служебные биты, поэтому на самом деле вам придется заменить 8 на 10. Это дает 120 000 бод, что находится в пределах погрешности измерения.

Левая часть на самом деле работает намного быстрее, чем вам нужно, но, как правильно заметил @Majенко, это благодаря буферизации. Судя по всему, вы можете увеличить размер буфера до 256 байт. Если вам нужна согласованность, а не максимальная скорость, вы можете вместо этого уменьшить размер буфера.

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

,