Скорость передачи данных по USB (PC / Arduino) с использованием SerialPort в C #, похоже, «обрезается»

#c# #arduino #virtual-serial-port

#c# #arduino #виртуальный последовательный порт

Вопрос:

Приготовьтесь к пространному объяснению — извините.

Я создаю приложение на C # с использованием Visual Studio 2019, которое будет взаимодействовать с некоторыми устройствами Arduino MEGA 2560 R3. Соединение осуществляется через последовательный USB (если я не ошибаюсь, это USB CDC, верно?).

Связь работает нормально при всех скоростях передачи данных, даже до 2000000.

Чтобы убедиться, что мой Arduino может «следовать» скорости команд, отправляемых моим компьютером, я использую ACK-протокол, который так же прост, как и мой Arduino, подтверждающий каждую команду последовательностью «A n».

В попытке провести стресс-тестирование связи я позволяю приложению отправлять команды «0123456789 n» как можно быстрее, ожидая «A n» после каждой отправленной команды. Это означает отправку 11 символов и получение 2 символов, в общей сложности 13 символов.

И вот тут начинается загадка.

Начиная с 38400 бод, цикл передачи / передачи данных, похоже, обрезается со скоростью 244 в секунду. Если я увеличу скорость передачи данных до 2000000, она останется на уровне 244 циклов передачи / передачи данных в секунду. Что здесь происходит!

Ниже приведен код ПК, который представляет собой простое консольное приложение, написанное в Visual Studio 2019:

 using System;
using System.Diagnostics;
using System.IO.Ports;


namespace BasicComTest
{
    class Program
    {
        public static SerialPort _serialPort;


        static void Main(string[] args)
        {
            _serialPort = new SerialPort();
            _serialPort.BaudRate = 115200;
            _serialPort.PortName = "COM4";
            _serialPort.Handshake = Handshake.None;
            _serialPort.NewLine = "n";


            _serialPort.ReadTimeout = 2000;
            _serialPort.WriteTimeout = 2000;


            _serialPort.Open();


            _serialPort.WriteLine("");


            Stopwatch sw = new Stopwatch();
            sw.Start();


            ulong count = 0;


            while (!Console.KeyAvailable)
            {
                _serialPort.WriteLine($"0123456789");
                string ack = _serialPort.ReadLine();


                count  ;
                if (count == 1000)
                {
                    sw.Stop();
                    Debug.WriteLine($"1000 commands in {sw.ElapsedMilliseconds} - {1000000/sw.ElapsedMilliseconds} commands/sec");
                    count = 0;
                    sw.Restart();
                }
            }


            _serialPort.Close();
        }
    }
}
 

И ниже приведен простой эскиз Arduino (я использую процедуру ISR в попытке увеличить скорость):

 volatile uint8_t rcvd = 0;

String sCommandRx;
String sCommand;
bool bCommandRx;

ISR(USART0_RX_vect)
{
  // received data
  rcvd = UDR0;

  if (rcvd == 'n')
  {
    sCommandRx = sCommand;
    bCommandRx = true;
    sCommand = "";
  }
  else
    sCommand  = (char)rcvd;
}

void setup()
{
  UBRR0H = 0;
  UBRR0L = 0;

  // USART initialization with 16MHz system clock, 8-none-1, RX interrupt
  UCSR0A = 1<<U2X0 | 0<<MPCM0;
  UCSR0B = 1<<RXCIE0 | 0<<TXCIE0 | 0<<UDRIE0 | 1<<RXEN0 | 1<<TXEN0 | 0<<UCSZ02;
  UCSR0C = 0<<UMSEL01 | 0<<UMSEL00 | 0<<UPM01 | 0<<UPM00 | 0<<USBS0 | 1<<UCSZ01 | 1<<UCSZ00 | 0<<UCPOL0;

  sCommand = "";
}

void loop()
{
  if (bCommandRx)
  {
    if (sCommandRx == "0123456789")
    {
      UDR0 = 'A';
      while((UCSR0A amp; 1<<TXC0)==0);
      UCSR0A |= 1<<TXC0;
     
      UDR0 = 'n';
      while((UCSR0A amp; 1<<TXC0)==0);
      UCSR0A |= 1<<TXC0;
    }

    bCommandRx = false;
  }
}
 

I can already guarantee that the Arduino code works perfect, because I tested it with 2 Arduino’s back to back to rule out that they are causing the clipping. If you want to read the full story, you will find it on the Arduino forum here:

https://forum.arduino.cc/index.php?topic=719361.0

If I measure the communication speed with the 2 back-to-back Arduino’s, then it goes as fast as 6060 Tx/Rx sets per second at 2000000 baud (you have to count some overhead, which is measured at about 100 msec for 1000 commands — you can read the full report in the Arduino forum).

But using my console application, I get maximum 244 Tx/Rx sets per second. Except (and this makes it even weirder), if I build the «An» in the ISR routing in my Arduino, then I’m getting exactly the double of 488 Tx/Rx sets per second:

 volatile uint8_t rcvd = 0;


ISR(USART0_RX_vect)
{
    // received data
    rcvd = UDR0;
    
    // reply with "An" after receiving "n"
    if(rcvd == 'n')
    {
        UDR0 = 'A';
        while((UCSR0A amp; 1<<TXC0)==0);
        UCSR0A |= 1<<TXC0;
        
        UDR0 = 'n';
        while((UCSR0A amp; 1<<TXC0)==0);
        UCSR0A |= 1<<TXC0;
    }
}

void setup()
{
    // set baud rate (500 Kbps)
    UBRR0H = 0;
    UBRR0L = 3;

    // USART initialization with 16MHz system clock, 8-none-1, RX interrupt
    UCSR0A = 1<<U2X0 | 0<<MPCM0;
    UCSR0B = 1<<RXCIE0 | 0<<TXCIE0 | 0<<UDRIE0 | 1<<RXEN0 | 1<<TXEN0 | 0<<UCSZ02;
    UCSR0C = 0<<UMSEL01 | 0<<UMSEL00 | 0<<UPM01 | 0<<UPM00 | 0<<USBS0 | 1<<UCSZ01 | 1<<UCSZ00 | 0<<UCPOL0;
}

void loop(){
}
 

Но независимо от того, помещаю ли я передачу «A n» в цикл () или в ISR, результаты последовательного Arduino остаются неизменными.

Извините за это очень длинное и подробное описание. Но мой вопрос, очевидно, очень прост. В чем причина этого отсечения?

У меня есть теория. Хотя я прочитал много «плохих комментариев» о SerialPort, я думаю, что причина должна быть найдена в том факте, что это происходит через USB (Arduino работает с USB 2.0). Я объяснил теорию на форуме Arduino (я даже немного исправил свое объяснение ниже):

Если я не ошибаюсь, устройства USB 2.0 HID (и я предполагаю, что CDC такой же) имеют частоту опроса 1 мс. Я работал с некоторыми микроконтроллерами PIC и создал свой собственный USB-стек на своем ПК для связи с ним, и у меня был некоторый предел в 1000 команд / сек или 500 наборов Tx / Rx в секунду. Как вы можете прочитать выше, самые быстрые результаты, которые я мог получить с SerialPort, составляли 488 наборов Tx / Rx в секунду, что немного медленнее, но довольно близко. Конечно, может быть некоторое «отставание от преобразования», что приводит к некоторой рассинхронизации между serial и USB, что приводит к отсутствию некоторых кадров USB. Ввод ответа «A n» в цикл () (не в ISR) дал мне только половину (244) команд. Я думаю, что ввод ответа в ISR заставил его реагировать так быстро, что данные все равно могли «вернуться» в следующий кадр. Если я помещу ответ в цикл (), он пропустил поезд и взял несколько более поздних кадров 1 мсек. В любом случае, на данный момент это просто ДИКИЕ ДОГАДКИ, поэтому я, конечно, могу быть СОВЕРШЕННО неправ.

Обновить:

Просто попытался поработать с некоторой обработкой файлов более низкого уровня в моей программе на C #, используя Kernel32.CreateFile, Kernel32.WriteFile и Kernel32.ReadFile, но получил точно такие же результаты, что означает 244 набора Tx / Rx в секунду. Даже пытался поиграть с таймингами (COMMTIMEOUTS), но ничего не изменилось.

Я также пробовал отправлять и получать большие строки символов, и кажется, что если я использую до 72 ‘ n’ как для передачи, так и для передачи данных, она остается стабильной. Использование большего количества символов приводит к удалению наборов Tx / Rx в секунду, пока я не использую 76 ‘ n’ как для передачи, так и для передачи. Затем он снова остается стабильным при 162 наборах Tx / Rx в секунду.

Для моего приложения 244 набора Tx / Rx в секунду достаточно хороши, но все же мне любопытно понять, откуда это ограничение.

Комментарии:

1. Просто попробовал, добавив _serialPort.BaseStream. Flush () после отправки моей строки, но безуспешно. Все еще на уровне 244 наборов Tx / Rx в секунду. Что также странно, так это то, что если я отправлю 70 символов вместо 10 ( ‘n’), я получу точно такие же результаты. Так что действительно кажется, что где-то система ожидает заполнения буфера, а затем решает передать после определенного тайм-аута. Разве это кому-то не напоминает?

2. Вы могли бы попробовать а) отправлять команды большими (r) блоками с одним ack для нескольких команд или б) реализовать полный дуплекс (отправка команд последовательно в одном потоке при асинхронном чтении ack в другом

3. Исходное приложение использовало async / await (TAP), в котором у меня был поток отправки и поток получения. Конечно, предполагается, что мое приложение ожидает, пока не будет получено подтверждение, чтобы отправить другую команду, но я построил это полностью асинхронно, используя отдельные потоки. Я получаю точно такие же результаты — 244 набора Tx / Rx в секунду. В любом случае, сегодня я собираюсь попробовать использовать stream, избавившись от SerialPort. Просто как вопрос эксперимента.

4. Похоже, никто не может дать ответ? Или мое объяснение слишком длинное? 🙂

Ответ №1:

Это старая тема, но я все равно комментирую. Я столкнулся с той же проблемой. Вы видите, что сообщения разрываются, затем удерживаются, затем разрываются и т.д. Проблема была решена на стороне arduino для меня, но поскольку я использую не Mega, а Teensy (с Teensyduino, расширением для Arduino), это решение, возможно, недоступно для вас. В любом случае, SerialUSB.send_now() решил проблему для меня. Я понятия не имею, что эта функция делает на низком уровне, но, возможно, это может быть отправной точкой.