А теперь перейдём к конкретике.
Со стороны Arduino я вижу вот такое ожидание того, что что-то пришло:
- Код: Выделить всё
while(Serial.available()==0)
{
}
Но надо понимать, что протокол RS-232 не гарантирует, что данные придут сразу и целиком. Он вообще не подразумевает такого понятия как «целиком»: он лишь умеет передавать по одному байту друг за другом. В случае с Arduino, реальная передача идёт через USB. USB пакетный протокол, но нас это мало волнует. Данные, который были отправлены в сторону Arduino, попадают с приёмный буфер чипа FT232RL. Если буфер чипа переполнен, чип в сторону компьютера шлёт сообщения «хватит пока слать», и компьютер временно перестаёт. Вне зависимости от того, переполнился ли приёмный буфер чипа, чип попавшие туда данные начинает по очереди и с чётко установленной скоростью (в нашем случае — 9600) выдавать через UART-интерфейс, а точнее на свою ногу TX, которая подключена к ноге RX микроконтроллера.
У микроконтроллера свой маленький буфер (однобайтный!), и как только все 10 бодов UART-символа получены, в микроконтроллере срабатывает прерывание, обработчик которого должен из маленького однобайтного
аппаратного буфера данные
срочно забрать, чтобы микроконтроллер мог быть готовым к приёму по UART следующего символа, потому что чип FT232 не будет ждать.
В принципе (теоретически), обработчик прерывания в микроконтроллере (AVR) мог бы не освобождать аппаратный приёмный буфер максимально срочно, а «потянуть резину», а чтобы ничего не потерялось, заставить FT232 временно ничего не передавать ему (ему = МК). Сделать это можно было бы с помощью всё тех же сигналов квитирования (RTS/CTS/RTR/CTR), достаточно было бы на нужном входе квитирования чипа FT232 выставить нужный уровень, и FT232 перестал бы слать МК по UART-у новые байты, а лишь накапливал бы получаемое по USB в своём аппаратном буфере (у FT232 он больше, чем у AVR, но не сильно), а когда аппаратный буфер FT232 заполнился бы, FT232 послал бы компу по USB отмашку, что пока хватит слать, что «пробка»/«затор». МК мог бы сколько угодно тянуть резину, а когда обрабатал бы принятый байт, поменял бы уровень на линии квитирования, и FT232 тут же послал бы следующий байт из своего приёмного буфера, и всё бы повторилось. Но в Arduino этот принцип не применяется и применить его невозможно даже чисто их схемотехнических причин: входы «RTS#», «CTS#» и прочие у чипа FT232
не соединены с пинами МК и МК не имеет рычага воздействия на FT232 и не может заставить этот чип «сделать паузу».
Вместо этого обработчик прерывания Arduino переносит принятый байт из аппаратного буфера в программный и выставляет флаг, что следующий байт может быть принят аппаратно через UART-интерфейс.
Так вот, метод
Serial.available() всего лишь возвращает кол-во байт, накопленных в программном приёмном буфере. Если мы послали что-то в сторону Arduino, например послали туда 20 или 2018 байтов, то конечно рано или поздно случится такое, что метод
Serial.available() вернёт ненулевое значение, но нет абсолютно никакой гарантии, что он вернёт именно число 20 или число 2018. То, что мы послали в адрес Arudino, может разбиться на куски произвольного размера (для передаче по шине USB). Эти куски могут дойти до чипа с непредсказуемой задержкой, и каждый кусок может иметь непредсказуемый размер. В случае, если мы пошлём 2018 байтов разом (со стороны компа), можно гарантированно сказать, что метод никогда не вернёт число 2018. Хотя бы потому, что столько не может вместить ни программный приёмный буфер Ардуино (его размер — 64 байта), ни аппаратный буфер чипа FT232RL (у этого чипа буферы 256 и 128 байтов на приём и передачу). Поэтому к моменту, когда в буфере окажется ненулевое кол-во байтов и вышеуказанный цикл прервётся, реальное число байтов в буфере может быть любым. И один байт, и 5, и 15, и 7.
И не нужно делать никаких предположений, что когда вот этот цикл с проверкой
Serial.available()==0 закончится, мы приняли всё сообщение (всю строку целиком). Никто не даёт таких гарантий, а строка вообще может быть больше, чем способен вместить буфер.
Поэтому, когда со стороны Arduino мы что-то приняли, надо принятое рассматривать как исключительно сырые данные, которые поступили частично. И на наших плечах лежит ответственность за тем, чтобы во-первых сырые фрагменты склеивать в что-то большее, а, во-вторых, чтобы в склеенном находить нечто цельное и обарабывать как цельное. Забегая вперёд скажу, что то же спреведливо и для программирования на стороне ПК.
На практике это означает, что помимо встроенного (и невидимого для нас) буфера Arduino (который всего 64 байта размером) нам скорее всего понадобится свой собственный буфер, который будет больше. В котором мы будем склеивать принимаемые куски, искать в скленном цельное сообщение, блок, команду, пакет в зависимости от способа, который мы установили нашим протоколом.
Когда мне потребовалось с компьютера управлять Arduino-устройством через терминал путём ввода команда вида
COMMAND param1, param2, ..., я решил использовать типичный и традиционный протокол, где команда передаётся текстом, а конец команды знаменуется передачей символа перевода строки (т.е. нажатие клавиши Enter в терминале).
Я сделал свой собственный приёмный буфер для команды и счётчик принятых символов:
- Код: Выделить всё
char CmdBuffer[64];
unsigned char cchCommand = 0;
и написал вот такой цикл, отвечающий за приём данных от компьютера:
- Код: Выделить всё
void loop()
{
char sym;
if(Serial.available() == 0) return;
sym = Serial.read();
if((sym == '\r') || (sym == '\n'))
{
if(cchCommand <= sizeof(CmdBuffer))
{
DispatchCommand(CmdBuffer, cchCommand);
}
else
{
RipError(F("Too long command!"));
}
cchCommand = 0;
}
else
{
if(cchCommand < sizeof(CmdBuffer))
{
CmdBuffer[cchCommand++] = sym;
}
else
{
cchCommand = sizeof(CmdBuffer) + 1;
}
}
}
Из этого кода видно, что мы, если в родном приёмном буфере нет вообще ничего (
Serial.available() == 0), сразу возвращаемся из функции
loop, а если есть (не важно сколько), достаём 1 байт и начинаем его обрабатывать: если это не символ перевод строки, то этот символ пишем в наш собственный буфер (в котором, по мере прихода новых байтов, растёт и формируется строка, означающая команду), если же это символ перевод строки (т.е. нажат Enter и мы получили команду целиком к этому моменту), то мы вызываем функцию
DispatchCommand, которая принятую целиком команду обработает, проанализирует и что-то сделает.
Причём, обращаю внимание, что предусмотрена ситуация, что символы некой длинной команды всё приходят и приходят, а символа перевода строки всё никак не поступает, и при таком сценарии мы рискуем исчерпать всё место в нашем собственном буфере, и нам будет некуда писать новые поступающие символы.
На этот случай сделаны в обоих ветках наружного if-условия проверки на переполнение: если пришёл очередной символ команды, а командный буфер уже заполнен до предела, то новый символ никуда не пишется, а просто игнорируется, а счётчик принятых символов (он равен длине принятой команды) устанавливается на величину, на 1 большую, чем размер буфера. Таким значением сигнализируется, что не только буфер заполнен до предела, но и что мы как минимум один символ команды потеряли. Если же место в буфере есть, то новый символ записывается в буфер (приклеивается в конец строки, которая там уже успела накопиться) и счётчик принятых символов увеличивается на 1 (этот же счётчик служит как указатель позиции записи в буфер).
Если же принятый символ соответствует символу перевода строки, мы опять же анализируем: к этому моменту произошло ли переполнение буфера (а значит в буфере мы имеем только начальную часть команды, концовка была вынужденно проигнорирована нами, потому что её некуда было писать), и если так, то тупо жалуемся, что команда была слишком длинной. Если всё нормально, то вызываем функцию, которая уже разделывается с командой.
Хочу заметить, что размер буфера
CmdBuffer[64]; в 64 байта никак не связан с тем, что встроенный приёмный буфер имет размер 64 байта. Мой собственный приёмный буфер мог бы быть любого размера, например 220 байт. Просто я сделал его таким, каким сделал, из экономии SRAM и исходя из того, что длинных команд у меня не было.
Также обращаю внимание, что вне зависимости от того, смогла ли функцию
DispatchCommand разделаться с командой и выполнить её, и независимо от того, дошло ли вообще дело до вызова этой функции или мы упёрлись в ограничение по размеру буфера для команд, после того, как был принят символ перевода строки и была сделана какая-то реакция на это, мы обнуляем счётчик принятых байтов (он же указатель на позицию следующего свободного символа в приёмном буфере) и после этого наш собственный буфер готов к приёму следующей команды (которая, возможно, в этот момент уже давно лежит в штатном приёмном буфере Arduino или буфере чипа).
Не имеет никакого отношения к вопросу, но всё-таки покажу, как выглядит функция
DispatchCommand —она полностью абстрагирована от того, по какому алгоритму происходит приём команд (и вообще, абстрагирована даже от того факта, что команды приходят по UART-у, ибо она принимает на вход уже готовые цельные команды) и занимается синтаксическим разбором команды (либо выводит сообщение «Неизвестная команда»):
- Код: Выделить всё
void DispatchCommand(const char* Command, unsigned char cmdLength)
{
//
// Skipping space characters
//
SkipSpaces(&Command, &cmdLength);
if(cmdLength == 0) return;
for(unsigned int i = 0; i < sizeof(rgCommands) / sizeof(rgCommands[0]); i++)
{
if(ProbeToken(&Command,
&cmdLength,
rgCommands[i].pszCmd,
rgCommands[i].cchCmd))
{
rgCommands[i].Dispatcher(Command, cmdLength);
return;
}
}
RipError(F("Unknown command"));
}
_______________________
Абсолютно аналогичным образом обстоят дела со стороны компьютера и VB6.
Никто (имеется в виду ни протокол RS-232, ни компонент MSComm) не даёт гарантии, что в свойстве Input окажется что-то цельное и законченное, а не какой-то жалкий фрагмент, потому что с точки зрения протокола и компонента (и COM-порта) нет никаких критериев цельности и законченности, есть просто непонятный поток байтов, и байты могут приходить с любыми произвольными задержками между любыми двумя отдельными байтами.
Поэтому в свойстве Input может оказаться сколько угодно байтов, в том числе и 1, а может и 4, даже если со стороны Arduino была отправка строки
HELLO WORLD разом (то есть одним вызовом). Всё зависит от скоростей работы устройств, от скоростей передачи, от размеров буферов, от загруженности контроллеров и промежуточных шин (в нашем случае USB). Картина, в целом, непредсказуемая.
Поэтоу правильный подход — это со стороны VB6 точно так же иметь свой собственный буфер. По мере прихода новых порций данных добавлять принятые порции в этот буфер, и в зависимости от того, как протокол устанавливает границы пакетов/фрагментов/сообщений, искать в собственном приёмном буфере где кончается один пакет и начинается другой (либо по маркеру конца пакета/команды/сообщения, либо по длине пакета, указанной в начале пакета — тут всё зависит от протокола).
Поделюсь, опять же, примером. В одном проекте Arudino была в роли сниффера, которая мониторила некую засекреченную шину (типа IIC/TWI) и нужные пакеты отправляла на компьютер, обернув содежимое пакетов в «собственную обёртку». Со стороны компьютера нужно было принимать пакеты уже от Arduino и выводить захваченные сниффером данные/события.
Для этой цели использовался VB6 и MSComm, как наиболее быстрое и простое решение.
- Во-первых, в форме, где лежал принимающий MSComm, был сделан приёмный буфер:
- Код: Выделить всё
Private m_InputBuffer As String
- Момент, когда что-то приходило в наш COM-порт — отслеживался, и пришедшее (сколько бы байтов там ни было, пусть даже 1) добавлялся в собственный приёмный буфер (m_InputBuffer). Отслеживался этот момент, конечно, не таким уродским способом, как у тебя, а с помощью события OnComm:
- Код: Выделить всё
Private Sub ComPort_OnComm()
m_InputBuffer = m_InputBuffer + ComPort.Input
CheckIncomingQueue
End Sub
Использование события позволяет не крутить бешеный бесконечный цикл Do/Loop, у которого 9999 из 10000 итераций проходят бестолку и который грузит ядро процессора на 100%, а «просыпаться» только тогда, когда есть на что реагировать. Принятое сначала складывается в приёмный буфер, а затем вызывается CheckIncomingQueue, которая проверяет, а не накопилось ли в приёмном буфере (с учётом возможности поступления данных по байту в секунду) достаточной длины сообщения, чтобы как-то всё это можно было обработать. - Сама процедура устроена довольно просто:
- Код: Выделить всё
Private Sub CheckIncomingQueue()
Dim pos As Long
Do
pos = InStr(1, m_InputBuffer, vbNewLine)
If pos = 0 Then Exit Do
ProcessIncomingMessage Left$(m_InputBuffer, pos - 1)
m_InputBuffer = Mid$(m_InputBuffer, pos + 2)
Loop
End Sub
Она пытается в принятом найти символ переноса строки, и если он там есть, то всё, что стоит слева от него — считается за целиком принятое сообщение и передаётся в процедуру ProcessIncomingMessage которая уже имеет дело с отдельно взятыми сообщениями (ей ничего не нужно ни склеивать, ни разрезать). Причём, с учётом того, что байты могут идти сначала в час по чайной ложке, а потом привалит целый килобайт данных одним мигом, процедура CheckIncomingQueue делает проверку на наличие символа перевода строки в цикле, исходя из того, что в приёмном буфере разом может оказаться сразу несколько сообщений, например сразу 4 штуки, плюс первые символы пятой (незавершённой). Так что она крутит цикл и по очереди обрабатывает каждое принятое сообщение, пока в буфере не останется ни одного целого сообщения (там может остаться кусок сообщения либо пустота).
Мораль — для правильной работы нужно:
- Рабобраться с протоколом (придумать его, если нужно) и чётко установить его
- Использовать подход с буфером (работающим как очередь), в конец которого принятые фрагменты (любой длины) дописываются, а из начала которого принятые фрагменты (цельные блоки, сообщения, команды) извлекаются, как только они оказываются полностью сформированными в буфере. Как опредеть момент, когда нечто цельное накопилось в приёмном буфере — всецело зависит от протокола.