Организация обмена с приборами через последовательный порт под Windows

+7 (383) 358-68-69; semico@mail.ru |  Контакты  |  Прайс-лист

Главная / Лабораторное оборудование / ПО

Здесь рассмотрен пример организации обмена с приборами серии МУЛЬТИТЕСТ в программе на языке Си/C++ под управлением операционной системы Windows различных версий. В примере приведены необходимые и достаточные для работы с приборами классы и функции. Обмен данными с приборами производится через последовательный порт компьютера по интерфейсу RS-232C.

Пример может быть полезен для самостоятельной разработки программ, работающих с последовательным портом в ОС Windows.

По ряду причин, непосредственное взаимодействие с портами и другим оборудованием под Windows недостаточно хорошо описано в литературе по программированию. Вероятно, в первую очередь, потому что Windows не является системой реального времени, в полной мере пригодной для решения задач сбора данных, мониторинга и управления объектами. Хотя ее широкая распространенность привела к необходимости разработки прикладных программ и для этих применений.

Пример является консольным приложением Win32 и запускается в окне DOS под Windows 95/98/Millenium, а также под Windows NT/2000/XP. Консольное приложение выбрано в качестве примера для того, чтобы не загромождать программу реализацией оконного интерфейса. Приведенные ниже классы и функции можно использовать для связи с приборами в любых приложениях Windows. Для работы с портом используется коммуникационный API Win32, поэтому пример может быть легко адаптирован под другие языки программирования, позволяющие его использовать. Например, для Delphi или Visual Basic.

Для упрощения, обмен данными ведется в синхронном режиме. Это значит, что выполнение основной программы прерывается на время обмена, но во многих случаях это можно допустить. В асинхронном режиме возврат из функции происходит немедленно, сам обмен происходит в фоновом режиме, а по окончании обмена ОС сообщает об этом приложению. Этот режим предпочтительнее, но чуть сложнее. С небольшими изменениями приведенный пример может быть использован и для работы в асинхронном режиме.

В примере открытие и закрытие коммуникационного порта происходит при каждом обмене данными. Такая реализация делает функцию обмена с приборами самодостаточной и облегчает понимание последовательности действий. Но это незначительно ограничивает скорость обмена. Для ускорения работы открытие и закрытие порта следует выделить в отдельные функции. При этом желательно не забывать их вызывать соответственно после запуска программы и перед выходом из нее.

Пример программы

#include "stdafx.h"
#include <afxwin.h>
#include <conio.h>

HANDLE hPort=NULL; /* Дескриптор порта, создается при открытии и используется для всех операций с портом */
DCB dcb; /* Device Control Block - установки, управляющие работой порта */
COMMTIMEOUTS cto; /* Структура, задающая тайм-ауты последовательного порта */

double dd; /* Принятое от прибора значение */
int f_dd; /* Наличие принятого значения 1-есть, 0-нет */

char str[1024]; /* Принятая от прибора строка */
int f_str; /* Наличие принятой строки 1-есть, 0-нет */

char strdos[1024]; /* Буфер строки для перекодировки */
int lenstrdos=1024; /* Размер буфера строки */

/* Класс, который будет использоваться для управления прибором */
class CMtest
{
public:
CMtest(); // конструктор класса

BYTE kbuf[1032]; // буфер передаваемой команды
BYTE dbuf[1032]; // буфер данных передаваемой команды
BYTE rbuf[1032]; // буфер принимаемого ответа

int maxlbuf; // максимальная длина пакета - зависит от размера буферов
int lenkbuf; // количество передаваемых байт
int lenrbuf; // количество принимаемых и принятых байт

int com; // номер последовательного порта 1 - COM1 и т.д.

int kom_mtest(int, int, int, int, long); // функция обмена данными с прибором
int print (int); // печать данных или кода ошибки
};

////////////////////////////////////////////////////////////////
int printfdos(char* sss)
{
/* приложение исполняется в консольном режиме, поэтому для вывода русского текста функцией printf требуется предварительно выполнить преобразование строки в кодировку DOS (866). Если в консольном режиме используется другая кодировка, следует изменить и эту функцию

Можно, конечно, сразу записать строки в нужной кодировке, но в этом листинге они бы отображались абракадаброй и затруднили восприятие текста.

Этой функцией перекодируются только простые строки, форматированный вывод и подстановку переменных выполнять при помощи printf.
*/
int i,j;
int c;

// Проверить размер буфера для перекодировки
j=strlen(sss);
if (j>=lenstrdos-1) sss[lenstrdos-1]=0;

// перенести строку в буфер
strcpy(strdos,sss);
j=strlen(strdos);

for (i=0; i<j; i++)
{
c=(int)strdos[i];
if (c<0) c+=256; // если char знаковый
// из кодировки Windows (1251) в DOS (866)
if (c>=128)
{if ((c>=192)&&(c<240)) {c-=64; goto m2;}
if (c>=240) {c-=16; goto m2;}
if (c==164) {c=253; goto m2;} // клоп - солнышко
if (c==168) {c=240; goto m2;} /* Ё */
if (c==176) {c=248; goto m2;} // градус Цельсия
if (c==183) {c=250; goto m2;} // точка в центре
if (c==184) {c=241; goto m2;} /* ё */
if (c==185) {c=252; goto m2;} // номер №
c=32;
}
m2:strdos[i]=(char)c;
}

printf(strdos);

return(j);
}

/////////////////////////////////////////////////////////////////
CMtest::CMtest()
{
int i;
/* Инициализация буферов, хотя этого можно и не делать */
for (i=0; i<1024; i++) {rbuf[i]=0; kbuf[i]=0; dbuf[i]=0;}
/* От размеров буферов зависит и максимальная длина команды. Запас 8 байт сделан специально для уменьшения количества проверок при приеме пакета
*/
maxlbuf=1024;
}

int CMtest::kom_mtest(int n, int k, int z, int r, long len)
{
/* Используемые при вызове переменные
n - сетевой номер прибора должен быть от 0 до 255
k - соответствует коду типа пакета "K" протокола
(см. протокол связи - документ 421598.100 Д1, далее протокол).
Для команд запроса данных - 0x10.
z,r - соответствуют кодам группы параметров "Z" и коду параметра "R", см. протокол.
Определяют формат и назначение передаваемых данных или запрашиваемой величины.
len - длина передаваемых данных D1...DN из буфера dbuf в прибор . Для команд запроса данных - нуль.
*/
int re=0;
/* Код возврата из функции 0 - OK, 256-выход по тайм-ауту, 257-ошибочная контрольная сумма или формат команды, 258 - последовательный порт не открыт или не обнаружен.
1-255 коды ошибок, возвращаемые прибором, см. протокол.
*/
int i; // Вспомогательные переменные
int l,l1; // Вспомогательные переменные
CString strPort="\\\\.\\COM"; // Строка, используемая при открытии порта
char temp[32]; // буфер для преобразования чисел
DWORD d; // переменная для подсчета реально переданных и принятых байт
float* f; // для разбора принятого числа в формате float

f_dd=0; // обнулить флаг наличия данных в dd
f_str=0; // обнулить флаг наличия строки в str

/* Начало формирования команды в буфере kbuf[],
формат команды здесь подробно не рассматривается. См. в протоколе
*/

kbuf[0]=0; // адрес группы, для отдельного прибора равен нулю
kbuf[1]=n%256; // адрес (сетевой номер)
kbuf[2]=(len+4)%256; // длина пакета, мл. байт
kbuf[3]=((len+4)/256)%256; // длина пакета, ст. байт
kbuf[4]=k%256; // код типа пакета
kbuf[5]=z%256; // код группы параметров
kbuf[6]=r%256; // код параметра

/* Перенос данных, передаваемых в прибор (если есть) */

if (len!=0)
{
for (l=0; l<len; l++) kbuf[l+7]=dbuf[l];
l1=len+7;
}
else l1=7;

/* Подсчет контрольной суммы */

kbuf[l1]=0;
for (l=0; l<l1; l++)
{kbuf[l1]+=kbuf[l];
kbuf[l1]%=256;
}

lenkbuf=l1+1;
/* Команда сформирована в буфере kbuf[] - далее открытие порта */
itoa(com,temp,10); /* Преобразование номера последовательного порта в строку */
strPort+=temp; /* Добавление номера к существующей строке */

/* Открытие коммуникационного порта для ввода - вывода */

hPort=::CreateFile(strPort, GENERIC_READ|GENERIC_WRITE,0,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

/*
hPort - дескриптор;
strPort - строка с наименованием порта COM1...COM9 к которому при помощи кабеля связи подключен прибор. Естественно, порт с указанным номером должен физически присутствовать в компьютере. Будтье внимательны, в "современных" материнских платах может оказаться всего один COM-порт или вовсе их не быть. В таком случае следует купить и установить в компьютер специальную плату с последовательными портами.

Если вместо FILE_ATTRIBUTE_NORMAL указать FILE_FLAG_OVERLAPPED порт будет открыт в асинхронном режиме, но это выходит за рамки данного примера.
*/

if((hPort==INVALID_HANDLE_VALUE)||(hPort==NULL))
{
/* Если порт не открывается - выход с кодом ошибки 258 */
re=258;
goto end;
}

/* Установка параметров обмена */

memset(&dcb,0,sizeof(dcb)); /* Выделение места в памяти */

dcb.DCBlength=sizeof(dcb);
dcb.BaudRate=CBR_9600; /* Скорость обмена 9600 */
/* 8 бит, 1 стоп бит, без контроля четности */
dcb.ByteSize=8;dcb.Parity=0;dcb.StopBits=ONESTOPBIT;

dcb.fBinary=TRUE; /* Обмен двоичными числами */
dcb.fOutxCtsFlow=FALSE; /* Сигнал CTS не отслеживать */
dcb.fOutxDsrFlow=FALSE; /* Сигнал DSR не отслеживать */
dcb.fDtrControl=DTR_CONTROL_DISABLE; /* Контроль с помощью сигнала DTR запретить */
dcb.fDsrSensitivity=FALSE; /* Сигнал DSR на коммуникационный драйвер не влияет */

dcb.fOutX=FALSE; /* Управление выходным потоком символами XON/XOFF отключить */
dcb.fInX=FALSE; /* Управление входным потоком символами XON/XOFF отключить */

dcb.fErrorChar=FALSE; /* Ошибочные байты не заменять */
dcb.fNull=FALSE; /* Нулевые байты не отбрасывать */
dcb.fRtsControl=RTS_CONTROL_DISABLE; /* Запретить RTS */
dcb.fAbortOnError=FALSE; /* Не прекращать ввод-вывод при ошибках */
dcb.wReserved=0; /* Зарезервировано - должен быть 0 */

::SetCommState(hPort,&dcb); /* Установить параметры порта с дескриптором hPort из структуры dcb */

cto.ReadIntervalTimeout=40; /* Максимальное время между чтением байт, мс */
cto.ReadTotalTimeoutMultiplier=0; /* Множитель для вычисления общего времени чтения */
cto.ReadTotalTimeoutConstant=200; /* Время ожидания начала приема, мс */
cto.WriteTotalTimeoutMultiplier=10; /* Множитель для вычисления общего времени записи, мс */
cto.WriteTotalTimeoutConstant=100; /* Время ожидания начала записи, мс */

::SetCommTimeouts(hPort,&cto); /* Установить тайм-ауты из структуры cto */

::SetupComm(hPort,maxlbuf+8,maxlbuf+8); /* Очистить внутренние буферы порта и установить их длину maxbuf+8 байт */

for (i=0; i<maxlbuf; i++) rbuf[i]=0; /* Очистить входной буфер */

lenrbuf=0;

if(hPort!=INVALID_HANDLE_VALUE && hPort!=NULL)
{
/* Если порт успешно открыт */
/* Передать lenkbuf байт из буфера kbuf[] */
::WriteFile(hPort,(LPVOID)kbuf,lenkbuf,&d,NULL);
/* Прочитать заголовок пакета (4 байта) в буфер rbuf[] */
::ReadFile(hPort,(LPVOID)rbuf,4,&d,NULL);
/* Определить длину пакета */
lenrbuf=rbuf[2]+256*rbuf[3];
if ((lenrbuf>0)&&(lenrbuf<maxlbuf))
{
/* Прочитать пакет в буфер rbuf[] начиная с rbuf[4] */
::ReadFile(hPort,(LPVOID)(rbuf+4),lenrbuf,&d,NULL);
}
/* Закрыть порт */
CloseHandle(hPort);
}
/* В принципе, открытие порта можно делать один раз при запуске,
а закрытие - один раз при выходе из программы.
*/

/* Проверка количества принятых байт */
if (lenrbuf<=0) {re=256; goto end;}
lenrbuf+=4; /* прибавить длину заголовка */

/* Проверка контрольной суммы принятого от прибора ответа */
l=0;
for (i=0; i<lenrbuf-1; i++) {l+=rbuf[i]; if (l>=256) l-=256;}

if (rbuf[lenrbuf-1]!=(BYTE)l) {re=257; goto end;}

/* Проверка формата команды - наличие в ответе обязательных полей */

if ((rbuf[0]!=0)||(rbuf[1]!=n)) {re=257; goto end;}

if ((rbuf[5]!=z) || (rbuf[6]!=r)) {re=257; goto end;}

if (rbuf[4]==0x40) {re=rbuf[7]; goto end;} /* принят пакет с сообщением об ошибке */

if ((z>=0x10)&&(r!=0)&&(lenrbuf==13))
{
/* если был запрос числовой величины и длина пакета соответствует типу float, выполнить соответствующее преобразование и перенести принятые данные в глобальную переменную dd
*/
for (i=7; i<lenrbuf-1; i++) temp[i-7]=rbuf[i];
f=(float*)temp;
dd=(double)*f;
f_dd=1;
}

if ((z<0x10)&&(r==0)&&(lenrbuf>7))
{
for (i=7; i<lenrbuf-1; i++)
{
/* заменить непечатаемые символы пробелом и переместить конец строки */
if (rbuf[i]>=32) str[i-7]=(char)rbuf[i]; else str[i-7]=' ';
str[i-6]=0;
}
f_str=1;
}

end: return(re);
}

//////////////////////////////////////////////
int CMtest::print(int err)
{
int i;
char temp[32]; // буфер для преобразования чисел

if (err<256)
{
/* Если нет ошибок порта - распечатать буферы в шестнадцатиричном виде */
for (i=0; i<lenkbuf; i++)
{
if (i!=0) printf(",");
else printf("> ");
if (kbuf[i]<16) printf("0");
printf("%X",kbuf[i]);
}

printf("\r\n");
for (i=0; i<lenrbuf; i++)
{
if (i!=0) printf(",");
else printf("< ");
if (rbuf[i]<16) printf("0");
printf("%X",rbuf[i]);
}
printf("\r\n");
if (f_dd!=0)
{
/* распечатать принятые данные из формата float */
gcvt(dd,8,temp);
printfdos("Данные (float): ");
printf("%s\n",temp);
f_dd=0;
}
if (f_str!=0)
{
/* распечатать принятые данные из формата строки ASCII */
printfdos("Данные (строка ASCII): ");
printf("%s\n",str);
f_str=0;
}
}

if (err!=0)
{
/* Если код ошибки не равен нулю - напечатать сообщение */
printfdos ("Ошибка ");
printf("%d",err);
switch (err)
{
/* ошибки прибора */
case 2: printfdos(" - неверный формат данных"); break;
case 3: printfdos(" - ошибка в параметрах.\r\nОписание параметра Z,R в приборе отсутствует \r\nили операция над параметром не поддерживается."); break;
case 4: printfdos(" - данные не готовы. \r\nНапример, при запросе pX/pH прибор не градуирован."); break;

case 255: printfdos(" - прибор не исправен.\r\nВыдается при срабатывании системы самодиагностики."); break;
/* ошибки обмена*/
case 256: printfdos(" - нет связи"); break;
case 257: printfdos(" - ошибка пакета "); break;
case 258: printfdos(" - ошибка COM порта "); break;
}
}
printf("\r\n");
return 0;
}

/////////////////////////////////////////////////////////

int main(int argc, char* argv[])
{
int i,j; // вспомогательные переменные
int n=-1; // номер прибора

CMtest Mtest;

SetConsoleTitle("Связь с приборами МУЛЬТИТЕСТ");
Mtest.com=1; /* Использовать порт COM1 */

printfdos("Поиск прибора в последовательном порту ");
printf("%d: \r\n", Mtest.com);

for (i=0; i<256; i++)
{ /* запрос наименования - есть во всех приборах */
j=Mtest.kom_mtest(i,0x10,0,0,0);
if (j==0)
{ /* прибор обнаружен */
n=i;
printfdos("\r\nОбнаружен прибор номер ");
printf("%d \r\n\n",n);
goto m1;
}
else printf("."); /* Выход до окончания поиска не предусмотрен, чтобы не загромождать текст программы. */
}

printfdos ("прибор не обнаружен \r\n");

m1: if ((n>=0)&&(n<256))
{
printfdos("Наименование прибора (Z=0, R=0):\r\n");
Mtest.print(j); /* сама команда не нужна - запрос только что был выполнен */
Sleep(100);

printfdos("ЭДС первого канала (Z=10h, R=10h):\r\n");
j=Mtest.kom_mtest(n,0x10,0x10,0x10,0);
Mtest.print(j);
Sleep(100);

printfdos("pX/pH первого канала (Z=10h, R=30h):\r\n");
j=Mtest.kom_mtest(n,0x10,0x10,0x30,0);
Mtest.print(j);
Sleep(100);

printfdos("Температура (Z=0A0h, R=20h):\r\n");
j=Mtest.kom_mtest(n,0x10,0xA0,0x20,0);
Mtest.print(j);
Sleep(100);
}

printfdos("\r\nНажмите любую клавишу для выхода из программы ");
getch();
return 0;
}



Для компиляции программы следует в компиляторе C++ создать консольное приложение Windows (Win32 Console Application) с именем, например MTest1. Для упрощения работы следует создать заготовку приложения (A simple application). Затем скопировать в файл mtest1.cpp приведенный выше текст и откомпилировать его. Если все сделано правильно - получится файл mtest1.exe.

После запуска программа начнет поиск подключенного прибора Мультитест. Если программа его обнаруживает, то выполняет несколько запросов данных.

пример организации обмена данными с приборами через последовательный порт RS-232C под Windows

Для записи результатов в файл перенаправьте вывод программы, для этого наберите в командной строке: "mtest1.exe > mtest1.txt". После запуска в каталоге программы появится текстовый файл mtest1.txt, в который будут записаны команды, ответы и сообщения программы (на экран они в этом случае не выводятся). Обратите внимание, что для выхода из программы потребуется нажать какую-нибудь клавишу, но соответствующая надпись тоже будет выведена в файл и не видна на экране.


Пример текстового файла

Поиск прибора в последовательном порту 1:
.....
Обнаружен прибор номер 5

Наименование прибора (Z=0, R=0):
> 00,05,04,00,10,00,00,19
< 00,05,0B,00,20,00,00,49,50,4C,31,30,31,31,D8
Данные (строка ASCII): IPL1011

ЭДС первого канала (Z=10h, R=10h):
> 00,05,04,00,10,10,10,39
< 00,05,09,00,20,10,10,00,7A,94,C3,FD,1C
Данные (float): -296.95313

pX/pH первого канала (Z=10h, R=30h):
> 00,05,04,00,10,10,30,59
< 00,05,05,00,40,10,30,04,8E
Ошибка 4 - данные не готовы.
Например, при запросе pX/pH прибор не градуирован.

Температура (Z=0A0h, R=20h):
> 00,05,04,00,10,A0,20,D9
< 00,05,09,00,20,A0,20,00,00,C8,41,00,F7
Данные (float): 25.


Нажмите любую клавишу для выхода из программы


В примере к порту COM1 был подключен pH-метр ⁄ иономер ⁄ титратор МУЛЬТИТЕСТ ИПЛ-101-1 с установленным сетевым номером 5.


НПП "СЕМИКО" (383) 271-01-25 (многоканальный)