Конференция "Начинающим" » Работа с сом портом в асинхронном режиме
 
  • Юрий К (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 на компьютере. Даже ардуина считает его достаточно быстро, чтобы это не мешало обмену — не те скорости.
  • Германн © (16.02.19 03:00) [25]

    > Юрий К   (14.02.19 10:22) [20]
    >
    > Вообщем так. Следуя принципу "лучшее - враг хорошего" я
    > пока реализовал работу с портом так ( фактически копируя
    > пример гуру вашего форума ( по моему Rouse )

    Во-первых.
    Не надо упоминать имя гуру, если вы не можете дать ссылку на его пример!
    Во-вторых я уже не понимаю ваши вопросы.
 
Конференция "Начинающим" » Работа с сом портом в асинхронном режиме
Есть новые Нет новых   [99016   +12][b:0.005][p:0.018]