COM-порт в Windows (программирование)
Написать программу, управляющую устройством через COM-порт, для MS-DOS не так сложно.
С платформой Win32 дело обстоит сложнее. Но только на первый взгляд. Конечно напрямую работать с регистрами портов нельзя, Windows это не позволяет, зато можно не обращать внимания на тонкости различных реализаций (i8251, 16450, 16550A) и не возиться с обработкой прерываний.
С последовательными и параллельными портами в Win32 работают как с файлами. Для открытия порта используется функция CreateFile
. Эта функция предоставляется Win32 API. Ее прототип выглядит так:
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile );
Указатель на строку с именем открываемого или создаваемого файла. Формат этой строки может быть очень «хитрым». В частности можно указывать сетевые имена для доступа к файлам на других компьютерах. Можно открывать логические разделы или физические диски и работать в обход файловой системы.
Последовательные порты имеют имена «COM1», «COM2», «COM3», «COM4», «COM5», «COM6», «COM7», «COM8», «COM9». Для доступа к портам, чей номер больше 9, необходимо указывать имя порта как «\\.\COMx», где x — номер порта. Например, «\\.\COM72» (в нотации языка C/C++ строка будет выглядеть «\\\\.\\COM72»). Такой синтаксис подходит для любого номера порта. Точно так же они назывались в MS-DOS. Параллельные порты называются «LPT1», «LPT2» и так далее.
Задает тип доступа к файлу. Возможно использование следующих значений:
0
Опрос атрибутов устройства без получения доступа к нему.GENERIC_READ
Файл будет считываться.GENERIC_WRITE
Файл будет записываться.GENERIC_READ|GENERIC_WRITE
Файл будет и считываться и записываться.
Задает параметры совместного доступа к файлу. Коммуникационные порты нельзя делать разделяемыми, поэтому данный параметр должен быть равен 0.
Задает атрибуты защиты файла. Поддерживается только в Windows NT. Однако при работе с портами должен в любом случае равняться NULL
.
Управляет режимами автосоздания, автоусечения файла и им подобными. Для коммуникационных портов всегда должно задаваться OPEN_EXISTING
.
Задает атрибуты создаваемого файла. Также управляет различными режимами обработки. При работе с портом этот параметр должен быть или равным 0
, или FILE_FLAG_OVERLAPPED
. Нулевое значение используется при синхронной работе с портом, а FILE_FLAG_OVERLAPPED
при асинхронной, или, другими словами, при фоновой обработке ввода/вывода. Подробнее про асинхронный ввод/вывод я расскажу позже.
Задает описатель файла-шаблона. При работе с портами всегда должен быть равен NULL
.
При успешном открытии файла, в данном случае порта, функция возвращает дескриптор (HANDLE
) файла. При ошибке [[|INVALID HANDLE VALUE
]]. Код ошибки можно получитить вызвав функцию [[|GetLastError
]].
Открытый порт должен быть закрыт перед завершением работы программы. В Win32 закрытие объекта по его дескриптору выполняет функция CloseHandle
:
BOOL CloseHandle( HANDLE hObject );
При успешном завершении функция возвращает не нулевое значение, при ошибке нуль.
Пример открытия/закрытия на языке C
[править]
#include <windows.h> //. . . HANDLE Port; //. . . Port = CreateFile(L"\\\\.\\COM2", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (Port == INVALID_HANDLE_VALUE) { MessageBox(NULL, "Невозможно открыть последовательный порт", "Error", MB_OK); ExitProcess(1); } //. . . CloseHandle(Port); //. . .
В данном примере открывается порт СОМ2
для чтения и записи, используется синхронный режим обмена. Проверяется успешность открытия порта, при ошибке выводится сообщение и программа завершается. Если порт открыт успешно, то он закрывается.
Основные параметры последовательного порта описываются структурой DCB
. Временные параметры — структурой COMMTIMEOUTS
. Существует еще несколько информационных и управляющих структур, но они используются реже. Настройка порта заключается в заполнении управляющих структур и последующем вызове функций настройки.
Основную информацию содержит структура DCB
:
typedef struct _DCB { DWORD DCBlength; // sizeof(DCB) DWORD BaudRate; // current baud rate DWORD fBinary:1; // binary mode, no EOF check DWORD fParity:1; // enable parity checking DWORD fOutxCtsFlow:1; // CTS output flow control DWORD fOutxDsrFlow:1; // DSR output flow control DWORD fDtrControl:2; // DTR flow control type DWORD fDsrSensitivity:1; // DSR sensitivity DWORD fTXContinueOnXoff:1; // XOFF continues Tx DWORD fOutX:1; // XON/XOFF out flow control DWORD fInX:1; // XON/XOFF in flow control DWORD fErrorChar:1; // enable error replacement DWORD fNull:1; // enable null stripping DWORD fRtsControl:2; // RTS flow control DWORD fAbortOnError:1; // abort reads/writes on error DWORD fDummy2:17; // reserved WORD wReserved; // not currently used WORD XonLim; // transmit XON threshold WORD XoffLim; // transmit XOFF threshold BYTE ByteSize; // number of bits/byte, 4-8 BYTE Parity; // 0-4=no,odd,even,mark,space BYTE StopBits; // 0,1,2 = 1, 1.5, 2 char XonChar; // Tx and Rx XON character char XoffChar; // Tx and Rx XOFF character char ErrorChar; // error replacement character char EofChar; // end of input character char EvtChar; // received event character WORD wReserved1; // reserved; do not use } DCB;
Эта структура содержит почти всю управляющую информацию, которая в реальности располагается в различных регистрах последовательного порта.
Задает длину, в байтах, структуры DCB
. Используется для контроля корректности структуры при передаче ее адреса в функции настройки порта.
Скорость передачи данных. Возможно указание следующих констант: CBR_110, CBR_300, CBR_600, CBR_1200, CBR_2400, CBR_4800, CBR_9600, CBR_14400, CBR_19200, CBR_38400, CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000
. Эти константы соответствуют всем стандартным скоростям обмена. На самом деле, это поле содержит числовое значение скорости передачи, а константы просто являются символическими именами. Поэтому можно указывать, например, и CBR_9600
, и просто 9600
. Однако рекомендуется указывать символические константы, поскольку при компиляции программы проверяется корректность их имен.
Включает двоичный режим обмена. Win32 не поддерживает недвоичный режим, поэтому данное поле всегда должно быть равно 1
, или логической константе TRUE
(что предпочтительней). В Windows 3.1, если это поле было равно FALSE
, включался текстовый режим обмена. В этом режиме поступивший на вход порта символ, заданный полем EofChar
, свидетельствовал о конце принимаемых данных.
Включает режим контроля четности. Если это поле равно TRUE
, то выполняется проверка четности, при ошибке, в вызывающую программу, выдается соответствующий код завершения.
Включает режим слежения за сигналом [[|CTS
]]. Если это поле равно [[|TRUE
]] и сигнал [[|CTS
]] сброшен, передача данных приостанавливается до установки сигнала CTS
. Это позволяет подключеному к компьютеру прибору приостановить поток передаваемой в него информации, если он не успевает ее обрабатывать.
Включает режим слежения за сигналом [[|DSR
]]. Если это поле равно TRUE
и сигнал DSR
сброшен, передача данных прекращается до установки сигнала DSR
.
Задает режим управления обменом для сигнала [[|DTR
]]. Поле может принимать следующие значения:
- DTR_CONTROL_DISABLE Сигнал DTR снимается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
- DTR_CONTROL_ENABLE Сигнал DTR устанавливается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
- DTR_CONTROL_HANDSHAKE Сигнал DTR автоматически устанавливается/снимается в ходе работы с портом. Не может быть изменён функцией EscapeCommFunction.
Задает чувствительсть коммуникационного драйвера к состоянию линии [[|DSR
]]. Если это поле равно TRUE
, то все принимаемые данные игнорируются драйвером (коммуникационный драйвер расположен в операционной системе), за исключением тех, которые принимаются при установленом сигнале DSR
.
Задает, прекращается ли передача при переполнении приемного буфера и передаче драйвером символа XoffChar
. Если это поле равно TRUE
, то передача продолжается, несмотря на то, что приемный буфер содержит более XoffLim
символов и близок к переполнению, а драйвер передал символ XoffChar
для приостановления потока принимаемых данных. Если поле равно FALSE
, то передача не будет продолжена до тех пор, пока в приемном буфере не останется меньше XonLim
символов и драйвер не передаст символ XonChar
для возобновления потока принимаемых данных. Таким образом это поле вводит некую зависимость между управлением входным и выходным потоками информации.
Задает использование XON/XOFF
управления потоком при передаче. Если это поле равно TRUE
, то передача останавливается при приеме символа XoffChar
, и возобновляется при приеме символа XonChar
.
Задает использование XON/XOFF
управления потоком при приеме. Если это поле равно TRUE
, то драйвер передает символ XoffChar
, когда в приемном буфере находится более XoffLim
, и XonChar
, когда в приемном буфере остается менее XonLim символов.
Указывает на необходимость замены символов с ошибкой четности на символ задаваемый полем ErrorChar
. Если это поле равно TRUE
, и поле fParity
равно TRUE
, то выполняется замена.
Определяет действие выполняемое при приеме нулевого байта. Если это поле TRUE
, то нулевые байты отбрасываются при передаче.
Задает режим управления потоком для сигнала RTS. Поле может принимать следующие значения:
- RTS_CONTROL_DISABLE Сигнал RTS снимается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
- RTS_CONTROL_ENABLE Сигнал RTS устанавливается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
- RTS_CONTROL_HANDSHAKE Сигнал RTS автоматически устанавливается/снимается в ходе работы с портом. Не может быть изменён функцией EscapeCommFunction. Сигнал RTS устанавливается, когда приемный буфер заполнен менее, чем на половину, и снимается, когда буфер заполняется более чем на три четверти.
- RTS_CONTROL_TOGGLE Задаёт, что сигнал RTS установлен, когда есть данные для передачи. Когда все символы из передающего буфера переданы, сигнал снимается.
Задает игнорирование всех операций чтения/записи при возникновении ошибки. Если это поле равно TRUE
, драйвер прекращает все операции чтения/записи для порта при возникновении ошибки. Продолжать работать с портом можно будет только после устранения причины ошибки и вызова функции ClearCommError.
Зарезервировано и не используется.
Не используется, должно быть установлено в 0
.
Задает минимальное число символов в приемном буфере перед посылкой символа XON
.
Определяет максимальное количество байт в приемном буфере перед посылкой символа XOFF
. Максимально допустимое количество байт в буфере вычисляется вычитанием данного значения из размера приемного буфера в байтах.
Определяет число информационных бит в передаваемых и принимаемых байтах. Число информационных бит может быть в диапазоне от 4
до 8
.
Определяет выбор схемы контроля четности. Данное поле должно содержать одно из следующих значений:
- EVENPARITY Дополнение до четности
- MARKPARITY Бит четности всегда 1
- NOPARITY Бит четности отсутствует
- ODDPARITY Дополнение до нечетности
- SPACEPARITY Бит четности всегда 0
Задает количество стоповых бит. Поле может принимать следующие значения:
- ONESTOPBIT Один стоповый бит
- ONE5STOPBIT Полтора стоповых бита
- TWOSTOPBITS Два стоповых бита
Задает символ XON
используемый как для приема, так и для передачи. Обычно 0x11
(17
).
Задает символ XOFF
используемый как для приема, так и для передачи. Обычно 0x13
(19
).
Задает символ, использующийся для замены символов с ошибочной четностью.
Задает символ, использующийся для сигнализации о конце данных.
Задает символ, использующийся для сигнализации о событии.
Зарезервировано и не используется.
Если структура DCB
содержит конфигурацию для последовательного порта, совместимого с 8250, то к значениям полей ByteSize
и StopBits
применяются следующие ограничения:
- Количество информационных бит должно быть от
5
до8
. - Не допускается использование
5
информационных бит с2
стоповыми битами, также как6
,7
или8
информационных бит с1,5
стоповыми битами.
winbase.h
typedef struct _COMMTIMEOUTS { DWORD ReadIntervalTimeout; /* Maximum time between read chars. */ DWORD ReadTotalTimeoutMultiplier; /* Multiplier of characters. */ DWORD ReadTotalTimeoutConstant; /* Constant in milliseconds. */ DWORD WriteTotalTimeoutMultiplier; /* Multiplier of characters. */ DWORD WriteTotalTimeoutConstant; /* Constant in milliseconds. */ } COMMTIMEOUTS,*LPCOMMTIMEOUTS;
ReadIntervalTimeout — время в миллисекундах, задающее максимальное время, для интервала между поступлением двух символов по линии связи. Если интервал между поступлением каких-либо двух символов будет больше этой величины, операция ReadFile завершается и любые буферизированные данные возвращаются.
Чтобы операция ReadFile немедленно возвращала управление со всеми полученными данными (асинхронный режим) следует задавать следующие значения:
ReadIntervalTimeout=0xFFFFFFFF; ReadTotalTimeoutConstant=0; ReadTotalTimeoutMultiplier=0;
ReadTotalTimeoutMultiplier — Множитель, используемый, чтобы вычислить полный период времени простоя для операций чтения, в миллисекундах. Для каждой операции чтения, это значение умножается на затребованное число байтов, которые читаются.
ReadTotalTimeoutConstant — Константа, используемая, чтобы вычислить полный (максимальный) период времени простоя для операций чтения, в миллисекундах. Для каждой операции чтения, это значение добавляется к произведению члена структуры ReadTotalTimeoutMultiplier и прочитанного числа байтов.
Значение нуля и для члена ReadTotalTimeoutMultiplier, и для члена ReadTotalTimeoutConstant указывает, что полное время простоя не используются для операций чтения.
WriteTotalTimeoutMultiplier — Множитель, используемый, чтобы вычислить полный период времени простоя для операций записи, в миллисекундах. Для каждой операции записи, это значение умножается на число записываемых байтов.
WriteTotalTimeoutConstant — Константа, используемая, чтобы вычислить полный период времени простоя для операций записи, в миллисекундах. Для каждой операции записи, это значение добавляется к произведению члена структуры WriteTotalTimeoutMultiplier и записанного числа байтов.
Значение нуля и для члена WriteTotalTimeoutMultiplier, и для члена WriteTotalTimeoutConstant указывает, что полное время простоя не используются для операций записи.
Заполнение структуры COMMTIMEOUTS
[править]
Вариант 1: (максимальная задержка при чтении и записи = TIMEOUT)
COMMTIMEOUTS CommTimeOuts; CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF; CommTimeOuts.ReadTotalTimeoutMultiplier = 0; CommTimeOuts.ReadTotalTimeoutConstant = TIMEOUT; CommTimeOuts.WriteTotalTimeoutMultiplier = 0; CommTimeOuts.WriteTotalTimeoutConstant = TIMEOUT;
Вариант 2: Инициализация значениями (без задержки при чтении)
COMMTIMEOUTS CommTimeOuts={0xFFFFFFFF,0,0,0,1500};
Стандартный диалог настройки порта
[править]
Для настройки параметров COM — порта может быть вызвано штатное окно Windows. Вызов осуществляется функцией CommConfigDialog(), которая в качестве параметров принимает имя настраиваемого порта, хендл родительского окна и указатель на структуру COMMCONFIG.
Следует отметить, что для корректного вызова окна, структура COMMCONFIG должна быть заполнена значениями заранее.
Настройку структуры можно выполнить вручную или при помощи функции GetCommConfig().
Например:
/* Получение существующих настроек */ unsigned long new_size = 0; if (!GetCommConfig(port->handle, &port->settings,&new_size)) goto error; /* Вызов окна и установка новых параметров */ if (CommConfigDialog(port->name, 0, &port->settings)) { if (SetCommConfig(port->handle, &port->settings, port->settings.dwSize)) return 1; goto error; }
Выделение памяти для структуры COMMPORT
[править]
Прием и передача данных для последовательного порта может выполнятся в синхронном или асинхронном режимах. Асинхронный режим позволяет реализовать работу по событиям, в то время как синхронный лишен этой возможности, но является более простым в реализации.
Для работы в синхронном режиме, порт должен быть открыт следующим образом:
CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
Предпоследний параметр dwFlagsAndAttributes должен быть равен 0.
После успешного открытия порта, данные могут быть считаны или записаны при помощи функций ReadFile() и WriteFile().
HANDLE port = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); unsigned char dst[1024] = {0}; unsigned long size = sizeof(dst); if(port!= INVALID_HANDLE_VALUE) if(ReadFile(port,dst,size, &size,0)) printf("\nRead %d bytes",size);
Функция ReadFile/WriteFile осуществляет чтение/запись из файла (устройства) начиная с текущей позиции после окончания чтения обновляет указатель в файле.
BOOL ReadFile( HANDLE hFile, // хендл файла LPVOID lpBuffer, //указатель на буфер DWORD nNumberOfBytesToRead, // размер данных LPDWORD lpNumberOfBytesRead, //размер считанных данных LPOVERLAPPED lpOverlapped //структура OVERLAPPED );
Недостатком этого способа является то, что вызывая функцию ReadFile(), мы не знаем есть ли данные для чтения. Можно циклически проверять их наличие, но это приводит к дополнительным расходам времени ЦП.
Поэтому на практике часто удобней использовать асинхронный режим. Для этого при вызове функции CreateFile() параметр dwFlagsAndAttributes должен быть равен FILE_FLAG_OVERLAPPED.
CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
Далее, необходимо настроить реакцию порта на события при помощи функции SetCommMask() и используя функции WaitCommEvent() и WaitForSingleObject() ожидать событие или тайм аут.
Например:
const int READ_TIME = 100; OVERLAPPED sync = {0}; int reuslt = 0; unsigned long wait = 0, read = 0, state = 0; /* Создаем объект синхронизации */ sync.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL); /* Устанавливаем маску на события порта */ if(SetCommMask(port, EV_RXCHAR)) { /* Связываем порт и объект синхронизации*/ WaitCommEvent(port, &state, &sync); /* Начинаем ожидание данных*/ wait = WaitForSingleObject(sync.hEvent, READ_TIME); /* Данные получены */ if(wait == WAIT_OBJECT_0) { /* Начинаем чтение данных */ ReadFile(port, dst, size, &read, &sync); /* Ждем завершения операции чтения */ wait = WaitForSingleObject(sync.hEvent, READ_TIME); /* Если все успешно завершено, узнаем какой объем данных прочитан */ if(wait == WAIT_OBJECT_0) if(GetOverlappedResult(port, &sync, &read, FALSE)) reuslt = read; } } CloseHandle(sync.hEvent);
Пример настройки порта и выполнения чтения/записи данных
[править]
Код для работы с COM-портом. Многострадальный, соответственно относительно простой и понятный, при этом обходит основные подводные камни. Надеюсь, может быть полезен.
#ifndef TTY_H #define TTY_H #define NOMINMAX //иначе API windows определит макросы min и max, конфликтующие с std::max и std::min в vector #include <windows.h> #include <vector> #include <string> using namespace std; struct TTY { TTY(); virtual ~TTY(); bool IsOK() const; void Connect(const string& port, int baudrate); void Disconnect(); virtual void Write(const vector<unsigned char>& data); virtual void Read(vector<unsigned char>& data); HANDLE m_Handle; }; struct TTYException { }; #endif
#include "tty.h" #include <iostream> #include <assert.h> #include <windows.h> using namespace std; static int TIMEOUT = 1000; TTY::TTY() { m_Handle = INVALID_HANDLE_VALUE; } TTY::~TTY() { Disconnect(); } bool TTY::IsOK() const { return m_Handle != INVALID_HANDLE_VALUE; } void TTY::Connect(const string& port, int baudrate) { Disconnect(); m_Handle = CreateFile( port.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(m_Handle == INVALID_HANDLE_VALUE) { throw TTYException(); } SetCommMask(m_Handle, EV_RXCHAR); SetupComm(m_Handle, 1500, 1500); COMMTIMEOUTS CommTimeOuts; CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF; CommTimeOuts.ReadTotalTimeoutMultiplier = 0; CommTimeOuts.ReadTotalTimeoutConstant = TIMEOUT; CommTimeOuts.WriteTotalTimeoutMultiplier = 0; CommTimeOuts.WriteTotalTimeoutConstant = TIMEOUT; if(!SetCommTimeouts(m_Handle, &CommTimeOuts)) { CloseHandle(m_Handle); m_Handle = INVALID_HANDLE_VALUE; throw TTYException(); } DCB ComDCM; memset(&ComDCM,0,sizeof(ComDCM)); ComDCM.DCBlength = sizeof(DCB); GetCommState(m_Handle, &ComDCM); ComDCM.BaudRate = DWORD(baudrate); ComDCM.ByteSize = 8; ComDCM.Parity = NOPARITY; ComDCM.StopBits = ONESTOPBIT; ComDCM.fAbortOnError = TRUE; ComDCM.fDtrControl = DTR_CONTROL_DISABLE; ComDCM.fRtsControl = RTS_CONTROL_DISABLE; ComDCM.fBinary = TRUE; ComDCM.fParity = FALSE; ComDCM.fInX = FALSE; ComDCM.fOutX = FALSE; ComDCM.XonChar = 0; ComDCM.XoffChar = (unsigned char)0xFF; ComDCM.fErrorChar = FALSE; ComDCM.fNull = FALSE; ComDCM.fOutxCtsFlow = FALSE; ComDCM.fOutxDsrFlow = FALSE; ComDCM.XonLim = 128; ComDCM.XoffLim = 128; if(!SetCommState(m_Handle, &ComDCM)) { CloseHandle(m_Handle); m_Handle = INVALID_HANDLE_VALUE; throw TTYException(); } } void TTY::Disconnect() { if(m_Handle != INVALID_HANDLE_VALUE) { CloseHandle(m_Handle); m_Handle = INVALID_HANDLE_VALUE; } } void TTY::Write(const vector<unsigned char>& data) { if(m_Handle == INVALID_HANDLE_VALUE) { throw TTYException(); } DWORD feedback; if(!WriteFile(m_Handle, &data[0], (DWORD)data.size(), &feedback, 0) || feedback != (DWORD)data.size()) { CloseHandle(m_Handle); m_Handle = INVALID_HANDLE_VALUE; throw TTYException(); } // In some cases it's worth uncommenting //FlushFileBuffers(m_Handle); } void TTY::Read(vector<unsigned char>& data) { if(m_Handle == INVALID_HANDLE_VALUE) { throw TTYException(); } DWORD begin = GetTickCount(); DWORD feedback = 0; unsigned char* buf = &data[0]; DWORD len = (DWORD)data.size(); int attempts = 3; while(len && (attempts || (GetTickCount()-begin) < (DWORD)TIMEOUT/3)) { if(attempts) attempts--; if(!ReadFile(m_Handle, buf, len, &feedback, NULL)) { CloseHandle(m_Handle); m_Handle = INVALID_HANDLE_VALUE; throw TTYException(); } assert(feedback <= len); len -= feedback; buf += feedback; } if(len) { CloseHandle(m_Handle); m_Handle = INVALID_HANDLE_VALUE; throw TTYException(); } } using namespace std; int main(int argc, char *argv[]) { TTY tty; tty.Connect("COM4",9600); for(int i=0;i<1000;i++) { std::vector<unsigned char> the_vectsor; the_vectsor.push_back(5); tty.Read(the_vectsor); std::cout << (char)(the_vectsor[0]); //output text } system("PAUSE"); return EXIT_SUCCESS; }
Часто бывает необходимо, чтобы работа с устройством поддерживалась как в windows, так и в linux. В моем случае нужно было обеспечить работу com-порта в приложении, написанном на с/с++ с использованием кроссплатформенной библиотеки QT. Штатной поддержки программирования портов в QT нет (да и ни к чему это). Поэтому в win32 для работы с портом будем использовать средства WinAPI. В linux системах же, как известно для работы с устройствами используются специальные файлы.
Итак, взяв на вооружение всем знакомый gcc и его windows-аналог mingw, напишем нехитрый код.
Код будет довольно таки искусственным, т.к. ставится цель не более, чем показать принципы работы с com-портами в двух операционках. Для управления компилятором будем использовать директивы определения компилятора и ОС (__MINGW32__ и __linux). Com-портом в нашем случае является устройство dev/ttyS0 и COM1.
Инициализация
#ifdef __MINGW32__
#include <windows.h>
#endif
#ifdef __linux
#include <sys/types.h>
#include <fcntl.h>
#endif
#include <stdio.h>
int main() {
#ifdef __MINGW32__
HANDLE hSerial = CreateFile
("COM1",GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
if (hSerial == INVALID_HANDLE_VALUE) {
printf("Error opening port\r\n");
return -1;
}
#endif
#ifdef __linux
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY );
if (fd <0) {
printf("Error opening port\n");
return -1;
}
#endif
...
Здесь в win32 используется функция CreateFile, параметры по ссылке в коде. Функция возвращает хендл на на устройство, в которым мы дальше будем работать. Аналогично работает и open в linux.
Запись
char * data; // данные записи в порт
data[0] = 0x01;
#ifdef __MINGW32__
DWORD dwBytesWrite = 0; // кол-во записанных байтов
if(!WriteFile
(hSerial, data, 1, &dwBytesWrite, NULL)){
printf("write error\r\n");
}
#endif
#ifdef __linux
int iOut = write(fd, data, 1);
if (iOut < 0){
printf("write error\n");
}
#endif
Для записи в порт используем ранее возвращенный хендл и пишем соответствующими функциями.
Процедура чтения выглядит аналогично.
Чтение
char * buffer;
#ifdef __MINGW32__
if(!ReadFile
(hSerial, buffer, 1, &dwBytesWrite, NULL)){
printf("read error\r\n");
}
#endif
#ifdef __linux
int iOut = read(fd, buffer, 1);
if (iOut < 0){
printf("read error\n");
}
#endif
Закрытие порта
#ifdef __MINGW32__
CloseHandle(hSerial);
#endif
#ifdef __linux
close(fd);
#endif
Кроме того, существуют функции для настройки самих портов. Об их использовании я, возможно, напишу в следующих статьях. В нашем примере все работает по стандартным настройкам системы.
Осталось дело за малым — скомпилировать код под каждую из ОС.
Надеюсь, что эта практика пригодится кому-то, кроме меня самого.
Приятного си-программирования!
Main features of Serial Port Reader for Windows:
• Reading COM port activity
This software utility allows you to read RS232 data from a designated port and monitor it even if another application had already opened it. Captured serial data can be displayed in various formats, and the opportunity of real-time monitoring is a great feature for problem resolution.
The received data can be saved to a file of your choice or copied to the clipboard. The tool displays and files input/output control codes (IOCTLs) along with their complete parameters. Sessions can be saved by Com Port Reader and can be reloaded if required.
• Working with multiple ports in one session
Multiple serial ports can be read simultaneously by this software tool. This feature is very useful when comparing data collected from different COM ports that are interacting with the same application within monitoring session. In this case, all data is received and stored in a single log file on a first-in-first-out basis.
• Multiple views for sniffed data
Serial Port Reader allows you to choose the way that collected data is displayed on your computer. Four different view are available: table, line, dump, or terminal. You have the option of monitoring all view modes at the same time.
• Emulating serial communication
An option in terminal mode allows simulated data transmission from a serial application to a monitored COM port. Various data formats, such as string, binary, octal, decimal, hexadecimal, or mixed, can be used to test the COM port or its attached device’s reactions.
• Capturing Modbus data
COM Port Reader’s powerful filters enable you to read serial data transmitted over Modbus RTU and Modbus ASCII. The utility is fully compatible with these protocols, as well as those employed in RS-232, RS-485, and RS-422 interfaces
• Repetitive data exchange
Sending the same command from a serial app to a monitored serial port multiple times can give a clearer picture of the port’s behavior. This serial port utility simplifies that task with a playback feature that can display differences between sessions automatically.
Give your session a meaningful name so you can return to it for later analysis
Advanced Troubleshooting and Optimization
Troubleshooting common serial communication issues
- Data corruption may result from bad cables or dissimilar baud rates.
- Unresponsive devices are often the result of incorrect COM port and parameter settings. Some devices may require flow control for smooth data transfer.
Error Detection and Handling
Using error detection techniques such as conducting a Cyclic Redundancy Check (CRC) or checking parity bits can identify transmission errors often associated with long cables or noisy operational environments.
Optimizing baud rate
Choosing the right baud rate is crucial for consistent and reliable communication. A high baud rate may deliver data too quickly for a device to handle effectively, resulting in data corruption or loss. Serial Port Monitor lets you try different baud rates to optimize data transfer with your serial device.
The RS232 Connection and Signals
• DTE and DCE
DTE stands for Data Terminal Equipment. An example of a DTE is a computer. DCE stands for Data Communication Equipment. A modem is an excellent example of a DCE.
A DTE normally comes with a Male Connector, while a DCE comes with a Female Connector. This is not always the case. Here is a simple test to confirm device type. Using a voltmeter, measure Pin 3 and Pin 5 of a DB-9 Connector. DTE devices will indicate a voltage of -3V to -15V. DCE devices will have the voltage on Pin 2.
Note: The result for a DB-25 Connector is reversed (Please refer to DB-9 to DB-25 conversion table below).
• DB-9 to DB-25 Conversion
• RS-232 Connections
Straight-through cables are used to connect a DTE (e.g. computer) to a DCE (e.g. modem), with all signals in one side connected to the corresponding signals in the other side in a corresponding one-to-one basis. When connecting two DTE devices directly with no modem in between, a crossover, or null-modem cable is used. This type of cable cross transmits and receives data signals between the two sides. There is no standard and many variations on how the other control signals are wired. Below is an example of one of them:
• RS-232 Signals
The graphic above illustrates a typical RS-232 logic waveform (Data format: 1 Start bit, 8 Data bits, No Parity, 1 Stop bit). Data transmission begins with a Start bit, followed by the data bits (LSB sent first and MSB sent last), and ends with a «Stop» bit.
The voltage of Logic «1» (Mark) is between -3VDC to -15VDC, while the Logic «0» (Space) is between +3VDC to +15VDC.
RS-232 connects the Ground of 2 different devices together, which is the so-called «Unbalanced» connection. Unbalanced connections have a distance limitation of 50 ft (approximately 15 meters) and are very susceptible to noise.
Applications that Rely on Reading Serial Data
Embedded Systems
Embedded systems often provide serial ports used by engineers to program and debug their microcontrollers. Communication issues between a microcontroller and a computer are typically diagnosed by reading serial data. An example can be seen in this tutorial on reading serial data from an Arduino.
Industrial Automation
Industrial facilities often utilize serial communication to control and monitor equipment used in automated procedures. Analyzing serial data can be instrumental in diagnosing problems and optimizing system performance. Feedback from real-time data sensors can be used to make immediate adjustments in machinery parameters to address fluctuating conditions in an automated assembly line.
RS232 Pinout
An RS232 connection between a Data Terminal Equipment (DTE) and a Data Communications Equipment (DCE) requires the use of DB9 or DB25 connectors. The DTE is the male port, while the DCE is typically female. As their namesake, a DB9 connector has 9 pins, while a DB25 has 25 pins. Each pin in the connectors has a determined purpose.
The RS232 serial interface has nine pins and can be obtained in male or female type configurations. RS232C is an upgraded version of RS233 and it’s exactly the same with the only exception that it makes use of a 25 pin connector. Irrespective of whether the connector is 9 or 25 pins, only 3 of those pins are actually used when connecting a terminal device.
Apart from defining electrical characteristics, each signal in RS232 data transmission has a specific function that is determined by the RS232 protocol. These functions include control and timing signals, common ground, and data signals. Refer to the image below that illustrates the signals and functions that make up the RS232 pinout.
RS232 also includes a number of complementary secondary signals that can be applied when configuring DCE and DTE connections. These signals include TxD (Transmit Data), RxD((Receive Data), DTE (Data Terminal Equipment), RTS (Request To Send), and DCD (Data Carrier Detect).
Sometimes we require to communicate with an external device like a printer, microcontroller board or any serial device using the serial port of a windows machine. There is a lot of serial application available like Hercules, HyperTerminal, Docklight, ..etc.
We can use any one of them for serial communication but sometimes we require to create our own custom serial application for communication. In windows, it is easy to create the custom serial application using the win32 API.
In this blog post, we will learn serial port programming using the Win32 API. In Windows, serial port programming is very easy, MSDN provide all the required win32 API information which require for the serial port programming.
You can also see the below articles that how to change the properties of com-port like baud rate, parity using the Windows API.
- Change properties of COM Port Using win32 API
- Get COM PORT of USB Serial Device using the VID and PID
Finding COM port Number of a Device
In windows, the serial device will display in com port section of device manager with name as COM1, COM2, COM3, COM4.. etc. Generally, COM1 and COM2 refer to the hardware serial ports present in the PC and another com port number is export when any serial device or USB to serial device attached to PC. It also possible that com id could be virtual ( for example static virtual com port).
In my laptop, I have attached an Arduino board (Atmega 2560) and its COM id would be shown in com port section (Control Panel > Device Manager > Ports).
Compilers and IDE’s used
Here I have used Visual Studio 2013. You can also use MinGW (Minimalist GNU for Windows) an Open Source programming tool. Here I am assuming that you know how to create the console application using the visual studio and familiar with win32 API.
Opening a Serial Port
In Windows using the CreateFile(), we can open the serial port. The CreateFile() is a Win32 API that creates or opens a file or I/O device.
On success CreateFile() returns a handle that can be used to access the file or device depending on the flags and attributes specified.
HANDLE CreateFile( LPCTSTR lpFileName, // pointer to name of the file DWORD dwDesiredAccess, // access (read-write) mode DWORD dwShareMode, // share mode LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to security attributes DWORD dwCreationDisposition, // how to create DWORD dwFlagsAndAttributes, // file attributes HANDLE hTemplateFile // handle to file with attributes to // copy );
Closing a Serial Port
You should remember that after opening the com port using the CreateFile(), you have to close it by calling the CloseHandle() otherwise it will be unavailable to other applications.
BOOL CloseHandle( HANDLE hObject );
On success CloseHandle() returns zero.
Serial port communication Application
Let see an example code where I am talking with a Mega2560 Arduino device. I have written a small code for mega2560 in which it receives serial data. If received data is “aticleworld” then it will send a message “Welcome to AticleWorld !” or otherwise it will send the message “Please Send Correct Message”.
Mega2560 serial code,
#include <SoftwareSerial.h> void setup() { // initialize serial ports Serial.begin(9600); // USB serial port 0 } void loop() { String msg = ""; // check for data byte on USB serial port if (Serial.available()) { // get byte from USB serial port while(Serial.available()) { msg = Serial.readString();// read the incoming data as string } if( msg == "aticleworld") { //Send data to usb serial port Serial.write(" Welcome to AticleWorld !"); } else { //Send data to usb serial port Serial.write("Please Send Correct Message"); } } }
You can purchase Mega2560 from Amazon, Click here
Serial Application for the console,
In the below console application, I am using the win32 API to open the com port and sending the data to the open COM port. See the below video where I have downloaded the Arduino code in the Arduino board and communicating with this board using the console application.
#include "stdafx.h" #include <Windows.h> #include <stdio.h> #include <string.h> int main(void) { HANDLE hComm; // Handle to the Serial port BOOL Status; // Status DCB dcbSerialParams = { 0 }; // Initializing DCB structure COMMTIMEOUTS timeouts = { 0 }; //Initializing timeouts structure char SerialBuffer[64] = { 0 }; //Buffer to send and receive data DWORD BytesWritten = 0; // No of bytes written to the port DWORD dwEventMask; // Event mask to trigger char ReadData; //temperory Character DWORD NoBytesRead; // Bytes read by ReadFile() unsigned char loop = 0; wchar_t pszPortName[10] = { 0 }; //com port id wchar_t PortNo[20] = { 0 }; //contain friendly name //Enter the com port id printf_s("Enter the Com Port: "); wscanf_s(L"%s", pszPortName, (unsigned)_countof(pszPortName)); swprintf_s(PortNo, 20, L"\\\\.\\%s", pszPortName); //Open the serial com port hComm = CreateFile(PortNo, //friendly name GENERIC_READ | GENERIC_WRITE, // Read/Write Access 0, // No Sharing, ports cant be shared NULL, // No Security OPEN_EXISTING, // Open existing port only 0, // Non Overlapped I/O NULL); // Null for Comm Devices if (hComm == INVALID_HANDLE_VALUE) { printf_s("\n Port can't be opened\n\n"); goto Exit2; } //Setting the Parameters for the SerialPort dcbSerialParams.DCBlength = sizeof(dcbSerialParams); Status = GetCommState(hComm, &dcbSerialParams); //retreives the current settings if (Status == FALSE) { printf_s("\nError to Get the Com state\n\n"); goto Exit1; } dcbSerialParams.BaudRate = CBR_9600; //BaudRate = 9600 dcbSerialParams.ByteSize = 8; //ByteSize = 8 dcbSerialParams.StopBits = ONESTOPBIT; //StopBits = 1 dcbSerialParams.Parity = NOPARITY; //Parity = None Status = SetCommState(hComm, &dcbSerialParams); if (Status == FALSE) { printf_s("\nError to Setting DCB Structure\n\n"); goto Exit1; } //Setting Timeouts timeouts.ReadIntervalTimeout = 50; timeouts.ReadTotalTimeoutConstant = 50; timeouts.ReadTotalTimeoutMultiplier = 10; timeouts.WriteTotalTimeoutConstant = 50; timeouts.WriteTotalTimeoutMultiplier = 10; if (SetCommTimeouts(hComm, &timeouts) == FALSE) { printf_s("\nError to Setting Time outs"); goto Exit1; } printf_s("\n\nEnter your message: "); scanf_s("%s", SerialBuffer, (unsigned)_countof(SerialBuffer)); //Writing data to Serial Port Status = WriteFile(hComm,// Handle to the Serialport SerialBuffer, // Data to be written to the port sizeof(SerialBuffer), // No of bytes to write into the port &BytesWritten, // No of bytes written to the port NULL); if (Status == FALSE) { printf_s("\nFail to Written"); goto Exit1; } //print numbers of byte written to the serial port printf_s("\nNumber of bytes written to the serail port = %d\n\n", BytesWritten); //Setting Receive Mask Status = SetCommMask(hComm, EV_RXCHAR); if (Status == FALSE) { printf_s("\nError to in Setting CommMask\n\n"); goto Exit1; } //Setting WaitComm() Event Status = WaitCommEvent(hComm, &dwEventMask, NULL); //Wait for the character to be received if (Status == FALSE) { printf_s("\nError! in Setting WaitCommEvent()\n\n"); goto Exit1; } //Read data and store in a buffer do { Status = ReadFile(hComm, &ReadData, sizeof(ReadData), &NoBytesRead, NULL); SerialBuffer[loop] = ReadData; ++loop; } while (NoBytesRead > 0); --loop; //Get Actual length of received data printf_s("\nNumber of bytes received = %d\n\n", loop); //print receive data on console printf_s("\n\n"); int index = 0; for (index = 0; index < loop; ++index) { printf_s("%c", SerialBuffer[index]); } printf_s("\n\n"); Exit1: CloseHandle(hComm);//Closing the Serial Port Exit2: system("pause"); return 0; }
Recommended Posts for you:
- Best 5 C Books.
- Get COM PORT of USB Serial Device using the VID and PID.
- Reading And Writing Windows Registry Using WinAPI
- Install the port monitor silently without user interaction.
- C++ Interview Questions with Answers.
- C-Sharp Interview Questions.
- Python Interview Questions with Answer.
- Memory Layout in C.
- 100 C interview questions, your interviewer might ask.
- C Interview Questions for the experience.
- 10 questions about dynamic memory allocation
- File handling in C, in few hours.