-
Юрий К (14.02.19 10:22) [20]Вообщем так. Следуя принципу "лучшее - враг хорошего" я пока реализовал работу с портом так ( фактически копируя пример гуру вашего форума ( по моему Rouse )
В проекте создан отдельный юнит для порта в нем несколько глобальных переменных и процедур и функций:var
PortMode : TConnectMode = cmDisconnect; //состояние порта
FormHandle : THandle; // Хенндл формы основного приложения, которой будут отправляться сообщения при чтении из порта данных
CommHandle : integer; // Хендл порта
DCB : TDCB;
Ovr : TOverlapped;
Stat : TComStat;
CommThread : THandle; // Хендл потока чтения
hEvent : THandle;
TransMask : DWORD;
Errs : DWORD;
Kols : DWORD;
RxBuf : array[0..4096] of byte; // Входной буфер данных, заведомо большего размера, чем максимальная длинна входящих сообщений
Rx_i : integer = 0; // Счетчик ( куда записываем входящие данные )
Rx_i_start : integer = -1; // позиция с которой начинается сообщение, о приходе которого информируем основную форму приложения
Есть правило, которое я запомнил, пытаясь баловаться программированием: "нельзя вынуждать пользователя выполнять какие-то действия вынужденно.
Ниже функция - опрашивающая доступные в системе порты. Результат возвращает в виде набора строк с именами доступных портов.
Дабы не плодить экземпляры класса типа TStrings в основном приложении создаю внутри процедуры свой TStringList и пишу данные в него. параметр Items - может быть любым свойством визуального компонента основного приложения, где будут отображаться доступные порты. В моем случае это ComboBox.Items
Не знаю, корректно ли реализовал или нет. Покритикуйте.function FindPorts( Items : TStrings ) : boolean;
var i : byte;
h : THandle;
List : TStringList;
begin
Result := false;
List := TStringList.Create;
for i := 1 to 255 do
begin
h := CreateFile(PChar('COM'+inttostr(i)),GENERIC_READ or GENERIC_WRITE,0,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL or FILE_FLAG_OVERLAPPED,0);
if h <> INVALID_HANDLE_VALUE then
begin
List.Add('COM'+inttostr(i));
Result := true;
end;
CloseHandle(h);
end;
if Assigned(Items) then Items.Assign(List);
List.Free;
end;
Процедура открытия порта.
Процедуре передается два параметра: имя порта и хендл основной формы приложения, которая будет принимать и обрабатывать сообщения от потока чтения данных. Процедуре чтения данных назначаем режим RXFLAG ( т.е. по приходу определенного символа во входящий буфер ) в моем случае это FRAME_FLAG = $E7. Но этот флаг выставляется в начале и в конце сообщения, поэтому надо уметь различать где начало, а где конец.procedure PortInit(Port:String; hHandle : THandle);
var ThreadID:dword;
begin
CommHandle := CreateFile(PChar(Port),GENERIC_READ or GENERIC_WRITE,0,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL or FILE_FLAG_OVERLAPPED,0);
if CommHandle = INVALID_HANDLE_VALUE then begin CloseHandle(CommHandle); Exit; end;
SetCommMask(CommHandle,EV_RXFLAG);
GetCommState(CommHandle,DCB);
DCB.BaudRate:=CBR_9600;
DCB.Parity:=NOPARITY;
DCB.ByteSize:=8;
DCB.StopBits:=OneStopBit;
DCB.EvtChar:= chr(FRAME_FLAG);
SetCommState(CommHandle,DCB);
CommThread := CreateThread(nil,0,@ReadComm,nil,0,ThreadID);
FormHandle := hHandle;
PortMode := cmConnect;
end;
Ко всему прочему: я пришел к выводу, что сообщение не всегда приходит полностью за один проход цикла. Поэтому событие, что получено сообщение от контроллера генерируется при соблюдении нескольких условий ( см. код )
Процедура чтения данных:
Чтобы не тормозить поток чтения данных из порта ( на время обработки данных ). Передаю ссылки на считанные данные параметрами wParam, lParam процедуры PostMessage, которая возвращает управление потоку не дожидаясь выполнения. ( и вот тут и пригодилась ссылка на хендл формы основного приложения )
Я рассматривал разные варианты... например PostThreadMessage или ThreadList но мне сложно было это реализовать. Я решил, что пока меня устроит приведенный ниже алгоритм. Главное - пока для меня сделать, чтобы не глючило. Насколько профессионально написано - вопрос второй и для меня сейчас он менее важен. Возможно мое мнение ошибочно.procedure ReadComm;
var Resive : array[0..255] of byte;
i : byte;
begin
while true do
begin
TransMask:=0;
WaitCommEvent(CommHandle,TransMask,@Ovr);
if (TransMask and EV_RXFLAG)=EV_RXFLAG then
begin
ClearCommError(CommHandle,Errs,@Stat);
Kols := Stat.cbInQue;
ReadFile(CommHandle,Resive,Kols,Kols,@Ovr);
for i := 0 to Kols - 1 do
begin
RxBuf[Rx_i] := Resive[i];
if ( Resive[i] = FRAME_FLAG ) then
begin
if Rx_i_start < 0 then Rx_i_start := Rx_i
else begin
PostMessage(FormHandle,WM_PORT_READ,Rx_i_start,Rx_i);
Rx_i_start := -1;
end;
end;
inc(Rx_i);
if Rx_i > 4096 then Rx_i := 0;
end;
PurgeComm(CommHandle, PURGE_RXCLEAR);
end;
end;
end;
Процедура записи в порт простаprocedure WriteComm(Buf : array of byte);
var KolByte : DWORD;
begin
KolByte:=length(Buf);
WriteFile(CommHandle,Buf,KolByte,KolByte,@Ovr);
end;
Процедура остановки потока и закрытия порта тоже
procedure KillComm;
begin
TerminateThread(CommThread,0);
CloseHandle(CommHandle);
PortMode := cmDisconnect;
end;
Покритикуйте плз. -
Юрий К (14.02.19 10:22) [21]Дальше в основном приложении объявляем процедуру, открытия порта по нажатию кнопкиprocedure TForm1.Button1Click(Sender: TObject);
var data : array of byte;
begin
if ComboBOx1.Text = '' then exit;
PortInit(Combobox1.Text,Form1.Handle);
end;
И процедуру обработки входящих данных в которой:
а) копируем сообщение из глобального RxBuf в локально объявленный буфер, парсим его, проверяя корректность пришедших данных, и
фактически задача данной процедуры перемаршрутизировать сообщение "заинтересованному лицу", (процедуре/функции/классу) которое работает с контроллером, приславшим сообщение.procedure CommRead ( var msg : TMessage ); message WM_PORT_READ;
...
procedure TForm1.CommRead(var msg: TMessage);
var TxMsg : TArduinoMsg;
begin
if msg.LParam - msg.WParam <= 0 then exit;
TxMsg := ParseMsg(msg.wParam, msg.lParam);
Case TxMsg.Error of
meNoError :; //Тут обрабатываем сообщение, пришедшее без ошибок
meErrorLength:; //Тут обрабатываем сообщение, длинна которого не соответствует заявленной
meErrorCRC :; //Тут обрабатываем сообщение у которого контрольная сумма не совпадает с вычисленной
meErrorFlag:; //Тут обрабатываем сообщение в котором где-то потерялись флаги начала и конца сообщения
meErrorMsg :; //Тут обрабатываем сообщение, которое не задекларировано в списке констант
meErrorSid :; //Тут обрабатываем сообщение у которого ошибочный ID мастера.
end;
end;
Покритекуйте плз. -
RWolf © (14.02.19 11:37) [22]Ошибочных кейсов как-то многовато. Я бы оставил просто признак «корректный/некорректный пакет», под который попадали бы все перечисленные нарушения структуры пакета.
> if msg.LParam - msg.WParam <= 0 then exit;
Как только Rx_i перейдёт через 4096, это условие остановит обмен. -
Юрий К (14.02.19 16:18) [23]Да. я заметил, что при переполнении значения Rx_i - будет ошибка.
Ниже код функции ParseMsg в которой это учтено + попытка посчитать "на ходу" crc16.
Код расчета сrc16 тупо скомуниздил с интернета, у меня есть некоторые размышления по поводу crc16.
В реальных железяках, занимающихся передачей данных, времени рассчитывать crc тупо нет. Железяки молотят на скоростях до 10-100 Гб/с и никаких процессоров обсчитать контрольную сумму не хватит. Следовательно сrc вычисляется аппаратно = регистры сдвига + логика И, НЕ, Искл. ИЛИ. На той скорости, с какой работает девайс на передачу.
Следовательно алгоритм расчета crc заточен под аппаратную реализацию.
В языках высокого уровня реализовать что-то подобное, мне кажется, не получится. Компилятор преобразует код цикла расчета crc в приблизительно в такой алгоритм:
1. загрузить в регистр а число №1
2. загрузить в регистр в число №2
3. произвести операцию ( например xor или сдвиг влево/вправо ).
4. Записать результат в ячейку памяти числа №1
5. Перейти к п. 1
В результате куча тактов расходуется на перезапись значений из регистров в переменные и обратно.
Но что-то подобное, мне кажется, должен уметь делать ассамблер потому как он работает напрямую с регистрами процессора. К сожалению ассамблер для меня - темный лес.
Поэтому пока так. Сейчас буду тестить вычисление crc сравнивая с онлайн ресурсами... будет ли давать правильный результат. Потом буду думать о скорости вычислений.
function ParseMsg ( a,b : integer ) : TArduinoMsg;
var i,j,len : integer;
crc16 : word;
crc1,crc2 : byte;
begin
if a < b then len := b - a else len := 4096 - a + b;
if len >= 8 then SetLength(Result.data,len - 8) else
begin
Result.Error := meErrorLength;
Exit;
end;
i := a + 1;
j := 1;
crc16 := $FFFF;
while i <> b - 3 do
begin
case j of
1: Result.sid := RxBuf[i];
2: Result.rid := RxBuf[i];
3: Result.len := RxBuf[i];
4: Result.msg := RxBuf[i];
else Result.data[j+5] := RxBuf[i];
end;
inc(j);
inc(i);
if i > 4096 then i := 0;
crc16 := Calccrc16( RxBuf[i],crc16 );
end;
case b of
0: begin crc1 := RxBuf[4096]; crc2 := RxBuf[0]; end;
1: begin crc1 := RxBuf[0]; crc2 := RxBuf[1]; end;
else begin crc1 := RxBuf[b-2]; crc2 := RxBuf[b-1]; end;
end;
Result.Error := meNoError;
if Result.sid <> SID then Result.Error := meErrorSid;
if ( crc1 <> Lo(crc16)) OR ( crc2 <> Hi(crc16)) then Result.Error := meErrorCrc;
if ( RxBuf[a] <> FRAME_FLAG ) OR ( RxBuf[b] <> FRAME_FLAG ) then Result.Error := meErrorFlag;
if Result.len <> len then Result.Error := meErrorLength;
end;
Функция вычисления crc для одного байтаFunction CalcCRC16( B : byte; CRC16 : Word) : Word;
Var a : Word;
Begin
crc16 := crc16 xor B;
a := ( crc16 xor ( crc16 shr 4)) and $00FF;
Result := ( crc16 shr 8 ) xor ( a shl 8 ) xor ( a shl 3 ) xor ( a shr 4 );
End; -
RWolf © (15.02.19 11:35) [24]Нет смысла думать о производительности CRC на компьютере. Даже ардуина считает его достаточно быстро, чтобы это не мешало обмену — не те скорости.
-
> Юрий К (14.02.19 10:22) [20]
>
> Вообщем так. Следуя принципу "лучшее - враг хорошего" я
> пока реализовал работу с портом так ( фактически копируя
> пример гуру вашего форума ( по моему Rouse )
Во-первых.
Не надо упоминать имя гуру, если вы не можете дать ссылку на его пример!
Во-вторых я уже не понимаю ваши вопросы.