Мой Kbyte.Ru
Рассылка Kbyte.Ru
Группы на Kbyte.Ru
Партнеры Kbyte.Ru
Реклама
Сделано руками
Сделано руками
> Статьи - Ярослав (comexe) Филипченко -

Delphi - Железо и устройства

Все статьи / Железо и устройства

Работа с протоколом Modbus RTU в среде Delphi 7. Часть 2.

Автор: Ярослав (comexe) Филипченко | добавлено: 23.09.2014, 09:53 | просмотров: 4327 (0+) | комментариев: 10 | рейтинг: *x4

Введение

Сразу оговорюсь: я не программист наивысшей квалификации и не отрицаю, что в программе имеется, как проггеры говорят, быдлокод и костыли. Но программа, написанная мной ,выполняет возложенные на нее задачи и имеет гибкий функционал.

Итак, при написании новой программы на Дельфи было решено полностью отказаться от mscomm32.ocx, а использовать API и потоки. Исследуя вопросы по данной теме, я использовал статью Терехова Александра, с которой можно ознакомиться и даже скачать пример здесь. В целом же, остальную часть кода после определенной оптимизации я перенес из старой программы.

Потоки

Все функционирование программы будет содержаться в двух файлах с расцирением *.pas. Это comportacc.pas (основные функции) и Unit1.pas(интерфейс, вывод на экран). Объяснять действия буду по ходу листинга.

unit ComPortAcc;

interface

uses Windows, SysUtils, Classes, RusErrorStr, Dialogs, Unit1;

type

  //определим тип TComThread - наследника класса TThread

  TCommThread = class(TThread)

  private

    { Private declarations }

  //процедура, занимающаяся опросом порта

    Procedure QueryPort;

  protected

  //переопределим метод запуска потока

     Procedure Execute; override;

  end;

Procedure StartService;

 Procedure StopService;

 Procedure WriteStrToPort(Str:String);

//глобальные переменные

Var

//необходимые переменные

CommThread:TCommThread; //наш поток, в котором будет работать процедура опроса порта

hPort:Integer;//дескриптор порта

implementation

//уже знакомый массив подсчета CRC16

CONST crctab: ARRAY[0..255] OF WORD = (

    $0000, $C0C1, $C181, $0140, $C301, $03C0, $0280, $C241,

    $C601, $06C0, $0780, $C741, $0500, $C5C1, $C481, $0440,

    $CC01, $0CC0, $0D80, $CD41, $0F00, $CFC1, $CE81, $0E40,

    $0A00, $CAC1, $CB81, $0B40, $C901, $09C0, $0880, $C841,

    $D801, $18C0, $1980, $D941, $1B00, $DBC1, $DA81, $1A40,

    $1E00, $DEC1, $DF81, $1F40, $DD01, $1DC0, $1C80, $DC41,

    $1400, $D4C1, $D581, $1540, $D701, $17C0, $1680, $D641,

    $D201, $12C0, $1380, $D341, $1100, $D1C1, $D081, $1040,

    $F001, $30C0, $3180, $F141, $3300, $F3C1, $F281, $3240,

    $3600, $F6C1, $F781, $3740, $F501, $35C0, $3480, $F441,

    $3C00, $FCC1, $FD81, $3D40, $FF01, $3FC0, $3E80, $FE41,

    $FA01, $3AC0, $3B80, $FB41, $3900, $F9C1, $F881, $3840,

    $2800, $E8C1, $E981, $2940, $EB01, $2BC0, $2A80, $EA41,

    $EE01, $2EC0, $2F80, $EF41, $2D00, $EDC1, $EC81, $2C40,

    $E401, $24C0, $2580, $E541, $2700, $E7C1, $E681, $2640,

    $2200, $E2C1, $E381, $2340, $E101, $21C0, $2080, $E041,

    $A001, $60C0, $6180, $A141, $6300, $A3C1, $A281, $6240,

    $6600, $A6C1, $A781, $6740, $A501, $65C0, $6480, $A441,

    $6C00, $ACC1, $AD81, $6D40, $AF01, $6FC0, $6E80, $AE41,

    $AA01, $6AC0, $6B80, $AB41, $6900, $A9C1, $A881, $6840,

    $7800, $B8C1, $B981, $7940, $BB01, $7BC0, $7A80, $BA41,

    $BE01, $7EC0, $7F80, $BF41, $7D00, $BDC1, $BC81, $7C40,

    $B401, $74C0, $7580, $B541, $7700, $B7C1, $B681, $7640,

    $7200, $B2C1, $B381, $7340, $B101, $71C0, $7080, $B041,

    $5000, $90C1, $9181, $5140, $9301, $53C0, $5280, $9241,

    $9601, $56C0, $5780, $9741, $5500, $95C1, $9481, $5440,

    $9C01, $5CC0, $5D80, $9D41, $5F00, $9FC1, $9E81, $5E40,

    $5A00, $9AC1, $9B81, $5B40, $9901, $59C0, $5880, $9841,

    $8801, $48C0, $4980, $8941, $4B00, $8BC1, $8A81, $4A40,

    $4E00, $8EC1, $8F81, $4F40, $8D01, $4DC0, $4C80, $8C41,

    $4400, $84C1, $8581, $4540, $8701, $47C0, $4680, $8641,

    $8201, $42C0, $4380, $8341, $4100, $81C1, $8081, $4040 );

 //уже знакомая функция нахождения контрольной суммы

function crc16(twoSym:array of Word; size:Word):Word;

var

 i:Integer;

 crc:Word;

begin

  crc:=$FFFF;

for i:=0 to ((size div 2)-1) do

  begin

    crc:=  (crc shr 8) xor CrcTab[(crc and $FF) xor twoSym[i]];

  end;

   Result:=(crc shr 8) or (crc shl 8);

end;

//преобразование string в byte

 function StrToByte(p1:string):integer;

const hex:array['A'..'F'] of Word=(10,11,12,13,14,15);

var

 Int,i:Integer;

begin

   Int:=0;

       for i := 1 to Length(P1) do

        if P1[i] < 'A' then Int := Int * 16 + ORD(P1[i]) - 48

        else Int := Int * 16 + HEX[p1[i]];

   Result:=int;

end;

Procedure StartComThread;

//инициализация нашего потока

Begin {StartComThread}

//пытаемся инициализировать поток

CommThread:=TCommThread.Create(False);

//проверяем получилось или нет

If CommThread = Nil Then

Begin {Nil}

//ошибка, все выключаем и выходим

SysErrorMessage(GetLastError);

Exit;

End; {Nil}

End; {StartComThread}

//запустим процедуру опроса порта в нашем потоке

Procedure TCommThread.Execute;

Begin {Execute}

Repeat

QueryPort;//процедура опроса порта будет производиться пока поток не будет прекращен

Until Terminated;

End;  {Execute}


//******************************************************************************

//процедура опроса порта

Procedure TCommThread.QueryPort;

Var

f:integer;

temp :real;

tempar: array[1..2] of real;

MyBuff:Array[0..1023] Of Char;//буфер для чтения данных

ByteReaded:Cardinal; //количество считанных байт

Str:String;         //вспомогательная строка

Status:DWord;       //статус устройства 

Begin {QueryPort}

//получим статус COM-порта устройства 

If Not GetCommModemStatus(hPort,Status) Then

Begin {ошибка при получении статуса модема}

//ошибка, все выключаем и выходим

SysErrorMessage(GetLastError);

Exit;

End;  {ошибка при получении статуса модема}

//читаем буфер из Com-порта

If Not ReadFile(hPort,MyBuff,SizeOf(MyBuff),ByteReaded,Nil) Then

Begin {ошибка при чтении данных}

//ошибка, все закрываем и уходим

SysErrorMessage(GetLastError);

Exit;

End; {ошибка при чтении данных}

//если данные пришли

If ByteReaded>0 Then

Begin {ByteReaded>0}

//отправим строку на просмотр, предварительно преобразловав в читабельный вид

for f := 0 to 6 do begin                        //если прибор оперирует шестизначными сообщениями

str :=str+IntToHex(ord(mybuff[f]),2);//представляем строчку в читабельный вид

end;

 form1.edit3.text:=str; //в этом поле ответ прибора в текстовой форме

//Мы используем тот же прибор МВ110-224-2А, поэтому декодируем температуру

 if (ord(mybuff[3]) <= $FF) and (ord(mybuff[3]) > $32)  then

temp:= ((ord(mybuff[3])*256 +ord(mybuff[4]))/10)-6553.5 // если значение отрицательное

  else    temp:= (ord(mybuff[3])*256 +ord(mybuff[4]))/10;// если значение положительное

  {выводим на экран интерфейса значения}

 form1.sg1.Cells[2,cnt]:=floattostr(temp);

 form1.sg1.Row:=cnt;

 tempar[cnt]:=round(temp);

  form1.pb1.Position:=round(tempar[1]);

  form1.pb2.Position:=round(tempar[2]);

   form1.Series1.AddXY(time,tempar[1]) ;

  form1.Series2.AddXY(time,tempar[2]) ;

End; {ByteReaded>0}

End; {QueryPort}

//******************************************************************************

//Инициализируем порт

Procedure InitPort;

Var

DCB: TDCB;         //структура для хранения настроек порта

CT: TCommTimeouts; //стркутура для хранения тайм-аутов

Begin {InitPort}

//создаем порт COM1 - порт, который уже есть в системе и назначен для обмена данными по RS-485

hPort := CreateFile(PChar('COM1'),

                        GENERIC_READ or GENERIC_WRITE,

                        FILE_SHARE_READ or FILE_SHARE_WRITE,

                        nil, OPEN_EXISTING,

                        FILE_ATTRIBUTE_NORMAL, 0);

If (hPort < 0) //не удалось создать файл (инициализировать порт)

Or Not SetupComm(hPort, 2048, 2048)//не удалось установить размерность входящих - выходящих буферов

Or Not GetCommState(hPort, DCB) Then //не удалось получить данные порта

Begin {ошибка}

SysErrorMessage(GetLastError);

Exit;

End;  {ошибка}

//параметры порта

DCB.BaudRate := CBR_115200; //скорость (обязательно 14400 и выше, на меньших скоростях получится белиберда)

DCB.StopBits := 1;        //стоповые биты (0 - 1, 1 - 1,5, 2 - 2)

DCB.Parity := 0;          //проверка четности

DCB.ByteSize := 8;        //биты данных

If Not SetCommState(hPort, DCB) Then //не удалсь установить параметры порта

Begin {ошибка}

SysErrorMessage(GetLastError);

Exit;

End; {ошибка}

//устанавливаем параметры тайм-аутов

If Not GetCommTimeouts(hPort, CT) Then //не удалось получить значения тайм-аутов

Begin  {ошибка}

SysErrorMessage(GetLastError);

Exit;

End; {ошибка}

//тайм-ауты установлены для работы в асинхронном режиме

CT.ReadTotalTimeoutConstant := 0;

CT.ReadIntervalTimeout := maxDWORD;

CT.ReadTotalTimeoutMultiplier := 0;

CT.WriteTotalTimeoutMultiplier := 0;

CT.WriteTotalTimeoutConstant :=0;

If Not SetCommTimeouts(hPort, CT) Then //не удалось установить тайм-ауты

Begin {ошибка}

SysErrorMessage(GetLastError);

Exit;

End; {ошибка}

End;{InitPort}


//******************************************************************************

//пишем данные в порт

Procedure WriteStrToPort(Str:String);

Var

i,j,m,n,len :shortint; //вспомогательные переменные циклов

ByteWritten:Cardinal;

b: array[1..100] of Word;

MyBuff:Array [0..1023] Of byte;

crc:word;

data: string;

Begin {WriteStrToPort}

//готовим буфер для передачи

FillChar(MyBuff,SizeOf(MyBuff),#0);

 If str = '' Then

    ShowMessage('Поле данных пусто!')

  Else

    Begin

      n:=1;

     for m := 1 to Length(str) do

     begin

       b[m]:=StrToByte(str[n]+str[n+1]);

       n:=n+2;

     end;

     crc:= crc16(b, Length(str));

     data :=str + IntToHex(crc,4);//прикручиваем к дейтаграмме её CRC

       Begin

      j:=1;

     for i := 0 to Length(data) do

     begin

       mybuff[i]:=StrToByte(data[j]+data[j+1]);

       j:=j+2;

     end;

     end;

     end;

     len :=length(data) div 2 ;

    //передаем данные

If Not WriteFile(hPort,MyBuff,len,ByteWritten,Nil) Then

Begin {ошибка}

SysErrorMessage(GetLastError);

Exit;

End; {ошибка}

End; {WriteStrToPort}

//******************************************************************************

//останавливаем нашу "службу" и убиваем дескриптор

Procedure StopService;

Begin {StopService}

CloseHandle(hPort); //закрываем порт

CommThread.free;    //"отпускаем" поток

End; {StopService}

//******************************************************************************

//запускаем нашу "службу"

Procedure StartService;

Begin {StartService}

InitPort;       //инициализируем порт

StartComThread; //запускаем поток

End;  {StartService}

end.

О том, как осуществлять проектирование интерфейса,  я рассказывать не буду. Проверку CRC входящего сообщения я не делал, в виду отсутствия свободного времени. Можете сделать сами. Заметьте, что программа на скоростях ниже 14400 кб/с принимает неверные данные. По крайней мере, у меня так. Если используете таймер со скоростью такта меньше 200 мс, целесообразно повысить приоритет приложения:

procedure TForm1.FormCreate(Sender: TObject);

//чтобы не было заеданий, устанавливаем высокий приоритет программы

var

  ProcessID: DWORD;

  ProcessHandle: THandle;

  ThreadHandle: THandle;

begin

  ProcessID := GetCurrentProcessID;

  ProcessHandle := OpenProcess(PROCESS_SET_INFORMATION,

    false, ProcessID);

  SetPriorityClass(ProcessHandle, REALTIME_PRIORITY_CLASS);

  ThreadHandle := GetCurrentThread;

  SetThreadPriority(ThreadHandle, THREAD_PRIORITY_TIME_CRITICAL);

end;

Пример рабочей программы, работающей с прибором ОВЕН МВ110-224-2А, выкладываю в приложенных файлах.

+ Добавить в избранное
    ? Помощь
Об авторе

Ярослав (comexe) Филипченко

Инженер по промышленной автоматизации

См. также:
Профиль автора
Ярослав (comexe) Филипченко
Последние комментарии (всего: 10)

Добавлять комментарии могут только зарегистрированные пользователи сайта.
Если у Вас уже есть учетная запись на Kbyte.Ru, пройдите процедуру авторизации OpenID.
Если Вы еще не зарегистрированы на Kbyte.Ru - зарегистрируйтесь.

Архив с рабочими исходниками можно взять по адресу: https://cloud.mail.ru/public/91535d28a7e0%2FDelphi%20table%20Threads.rar
Здравствуйте Ярослав.
Было бы неплохо если бы вы доделали программу, т.е внесли блок проверки CRC входящего пакета. Сам бьюсь над этим уже неделю но все что приходит на ум выглядит не очень изящно.Также интересует как в Delphi отследить конец пакета по промежутку 3,5 символа. Также как и Вы пишу для опроса прибора Овен (Модуль ввода сигналов тензодатчиков МВ110-224.1ТД.). Прешел на win API когда не смог сделать как у Вас в первой статье. Только у меня чтение в потоке асинхронное.Отсюда вопрос, какова загрузка процессора программой? У меня при работе в "синхроне" она была 10-20%. Собственно поэтому и перешел на асинхронный метод.
А что в этом CRC сложного? Берете целиком принятое сообщение, отсекаете от него последние биты CRC, Проворачиваете это отсеченное сообщение через ту же функцию CRC16, и сравниваете: если равны, то окей, если неравны, то сообщение плохое.
Что касается отслеживания конца пакета, то winapi скорей всего на это не рассчитан. Сообщение от прибора поступает в приемный буфер порта целиком, и как только система обращается за истребованием имеющейся в буфере информации, он очищается, ожидая следующего сообщения. Может быть, на ассемблере это и можно сделать, но на Высоком уровне - нет.
Скачайте мою программу, в среде программирования измените содержимое ячейки sg1.Cells[1,1]:= '100300010001'; на то, которое Вам нужно, например, если адрес прибора тоже 16, то sg1.Cells[1,1]:= '1003003E0001'; а ячейки sg1.Cells[1,2]:= '1003003F0001'; И посмотрите, что будет ответом. Если ответа вообще нет, значит неправильно настроено соединение, либо не тот адрес у прибора.
Что же касается загрузки процессора, то при скорости 19200, периоде таймера опроса 100мс, опросе четырех переменных на максимальном приоритете, ну собственно, та программа что я залил, то загрузка процессора колеблется от 0 до 35% (Celeron 900 Penryn 2.20 GHz).
Вчера возился с кодом...
Ну, вывод из потока в форму - ладно, но это очень большая потеря времени... Но не самая критичная точка в программе...

Хрень вышла в паре мест...
n:=1;
for m := 1 to Length(str) do
begin
b[m]:=StrToByte(str[n]+str[n+1 ]);
n:=n+2;
end;
crc:= crc16(b, Length(str));
data :=str + IntToHex(crc,4);//прикручиваем к дейтаграмме её CRC
(ниже аналогичный кусок с той-же багой)
Фигня в этом фрагменте в том, что цикл надо делать не по ВСЕЙ длине исходной строке, а только по ПОЛОВИНЕ: иначе в результирующую прилетает в два раза больше данных со всяким мусором!!!

Вторая ошибка - при инициализации COM-порта, когда задаём вторичные параметры типа скорости, чётности и т.п.
Это даже не ошибка, просто практика работы с портом:
// PortParam: string;
PortParam := 'baud=' + IntToStr(57600) + ' parity=n' +
' data=' + IntToStr(8) + ' stop=' + IntToStr(1) + ' ' +
'octs=off';
if BuildCommDCB(PChar(PortParam), DCB) then
begin
SetCommState(hPort, DCB);
end;
Причина: ошибки в реализации TDCB, из-за которых можно хорошо налететь...
Siorinex, спасибо за замечания. Действительно, в указанном цикле частенько программа вылетает в AV. Не могли бы Вы пояснить, что значит "цикл надо делать не по ВСЕЙ длине исходной строке, а только по ПОЛОВИНЕ"? Я как нуб в этом плане не очень понял, что следует делать.
По поводу второго замечания - немножко погуглил, почитал, попробую реорганизовать параметрирование порта по той более корректной схеме, предложенной Вами.
Что касается вывода из потока в форму, то что посоветуете? Сделать Synchronize?
Все комментарии (всего: 10)
Авторизация
 
OpenID
Зарегистрируйся и получи 10% скидку на добавление своего сайта в каталоги! Подробнее »
Поиск по сайту
Реклама
Счетчики