Чт. Окт 29th, 2026

Перевод

https://www.codeproject.com/Articles/992/Serial-library-for-C

Введение

Связь по ком порту нужна для специализированных приложений,  но API Win32 для его реализации является не самым простым в использовании. Это еще больше усложняется при использовании MFC. Приведенные здесь классы упрощают эту задачу. Связь по ком порту достаточна сложна, но знание ее реализации поможет в дальнейшей разработке…

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

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

Почему это сложно?

Последовательная связь в Win32 использует стандартные функции ReadFile/WriteFile для приема и передачи данных, поэтому почему последовательная связь сложнее, чем просто ввод / вывод? Есть несколько причин, которые я попытаюсь объяснить. Некоторые проблемы решены в этой библиотеке, но некоторые нет.

Скорость, четность, databits, handshaking, etc…

Последовательная связь использует разные форматы для передачи данных по проводу. Если оба устройства используют разные  параметры, вы получите неверные данные. К сожалению, ни один класс не поможет  вам решить эту ошибкой. Поэтому важно знать об этих настройках. Скорость передачи, четность, биты данных и стоп-биты часто легко установить, и если они совпадают, у вас не будет проблем.

Управление потоком намного сложнее, потому что обнаружить проблемы в этой области труднее. Управление потоком используется для управления объемом данных, которые могут быть переданы. Если передатчик быстрее отправляет данные, чем приемник может обработать, мы получим в  буфере приемника переполнение. Было бы неплохо, если бы приемник  мог сообщить передатчику прекратить передачу данных на некоторое время, чтобы она не переполнял буферы приемника. Этот процесс управления передачей данных называется управление потоком. Есть в три формы Управления потоком:

  • управлением потоком не используется, поэтому данные всегда отправляются, даже если получатель больше не может обрабатывать данные. Это может привести к потере данных, когда отправитель сможет быстрее передавать данные, чем может обрабатывать получатель. Конечно, эта опция не рекомендуется, но ее можно использовать для ситуаций, когда поток данных небольшой.
  • Аппаратное управлением потоком, когда линии RTS/CTS используются для указания, могут ли быть отправлены данные. Этот режим требует, чтобы оба порта и кабель поддерживали аппаратное подтверждение связи. Аппаратное подтверждение является наиболее надежной и эффективной формой связи, но зависит от аппаратного обеспечения. Убедитесь, что у вас используете подходящий кабель, который полностью подключен.
  • Программное управлением потоком, где символы XOFF / XON используются для управления данными. Основным недостатком этого метода является то, что эти символы не могут использоваться для данных. Символы XOFF / XON — это символы CTRL-S / CTRL-Q, которые больше нельзя использовать. Это делает программное обеспечение handshaking довольно бесполезным, когда вы хотите отправить бинарные данные. Для ASCII-данных это не представляет сложности. Он используется и на старых терминалах UNIX. Прокрутка начинается и останавливается с помощью CTRL-S / CTRL-Q на них, так что пользователь обеспечивает свое управлением потоком там (даже не зная об этом).

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

API Win32 предоставляет дополнительные параметры подтверждения связи, которые напрямую не поддерживаются этой библиотекой. Эти типы управлением потоком используются редко и их применение только усложнит классы. Если вам понадобятся эти  протоколы согласования, то вы можете использовать Win32 API для этого и по-прежнему использовать классы, предоставляемые библиотекой.

Asynchronous I/O makes things more complex

Файловый ввод / вывод относительно быстрый, поэтому, если вызов блокирует какое-то время, это, вероятно, будет только несколько миллисекунд, что приемлемо для большинства программ. Серийный ввод-вывод намного медленнее, что приводит к недопустимым задержкам в вашей программе. И другая проблема что вы не знаете, когда данные поступят, и  сколько данных поступит.

Win32 обеспечивает асинхронные вызовы функций (также называемые перекрывающимися операциями), чтобы решить эти проблемы. Асинхронное программирование часто является отличным способом повышения производительности, но увеличивает сложность. Из-за этого многие программы имеют ошибки в своих последовательных коммуникационных подпрограммах. Эта библиотека решает проблемы асинхронного ввода-вывода, позволяя программисту использовать смешанные по всему коду перекрывающиеся и не перекрывающиеся операции, что часто весьма удобно.

Модель программирования, управляемая событиями, не подходит

Программа усложняется  в GUI-приложениях, которые используют управляемую событиями модель, к которой они привыкли. Эта модель программирования является наследием старых 16-битных дней, и это даже не так уж плохо. Основное правило простое … Все события отправляются с помощью сообщения Windows, поэтому вам нужно как минимум одно окно для получения событий. Большинство GUI-приложений являются однопоточными (что часто является лучшим решением, позволяющим избежать большой сложности), и они используют следующий фрагмент кода в функции WinMain для обработки всех сообщений:

// Start the message-pump until a WM_QUIT is received
MSG msg;
while (::GetMessage(&msg,0,0,0))
{
    ::TranslateMessage(&msg);
    ::DispatchMessage(&msg);
}

Поскольку функция GetMessage блокируется до тех пор, пока в очереди сообщений не появится сообщение, невозможно получить данные при приходе  в сом порт. Конечно, можно установить таймер и проверять через него порты, но этот вид опроса плох и, конечно, плохо масштабируется. К сожалению, API последовательной связи Win32 не подходит для этой модели, управляемой событиями. В приложениях с графическим интерфейсом было бы проще, если бы API-интерфейс Win32 отправил сообщение в окно, когда произошло событие связи (именно так выглядела 16-разрядная реализация).

Если вы реализуете свой собственную отправку сообщений, вы можете использовать MsgWaitForMultipleObjects, чтобы дождаться сообщения Windows или объекта Windows, чтобы получить сигнал. Следующий код демонстрирует, как это делается (предполагается, что дескриптор события, который используется для асинхронных событий, хранится в переменной hevtCommEvent):

bool fQuit = false;
while (!fQuit)
{
    // Wait for a communication event or windows message
    switch (::MsgWaitForMultipleObjects(1,&hevtCommEvent,FALSE,INFINITE,QS_ALLEVENTS))
    {
    case WAIT_OBJECT_0:
        {
            // There is a serial communication event, handle it...
            HandleSerialEvent();
        }
        break;

    case WAIT_OBJECT_0+1:
        {
            // There is a windows message, handle it...
            MSG msg;
            while (::PeekMessage(&msg,0,0,0,PM_REMOVE))
            {
                // Abort on a WM_QUIT message
                if (msg.message == WM_QUIT) { fQuit = true; break; }

                // Translate and dispatch the message
                ::TranslateMessage(&msg);
                ::DispatchMessage(&msg);
            }
        }
        break;

    default:
        {
            // Error handling...
        }
        break;
    }
}

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

Использование последовательной связи в однопоточной программе, управляемой событиями, является трудным, как я только что объяснил, но вы, вероятно, сами это выяснили. Как мы можем решить эту проблему для этих типов приложений? Ответ находится в классе CSerialWnd, который отправляет сообщение в окно (как сообщение, так и окно может быть задано программистом) всякий раз, когда происходит последовательное событие. Это значительно облегчает использование последовательного порта в приложениях с графическим интерфейсом. Существует также очень тонкий класс-оболочка MFC, который называется CSerialMFC, но он настолько тонкий, что едва ли стоит его упоминать.

Эта библиотека не использует магию, поэтому как она может отправлять сообщения без блокировки сообщения? Ответ довольно прост. Он использует отдельный поток, который ожидает событий связи. Если такое событие произойдет, оно уведомит соответствующее окно. Это очень распространенный подход, который используется многими другими (serial) библиотеками. Это не лучшее решение (с точки зрения производительности), но оно подходит для 99% коммуникационных приложений на основе графического интерфейса. Поток связи полностью скрыт для программиста и не влиятет на вашу архитектуру.

Какой класс следует использовать в коде?

Текущая реализация содержит четыре разных класса, каждый из которых имеет свою собственную цель. Доступны следующие три класса.

  • CSerial — это базовый serial класс, который является оболочкой вокруг Win32 API. Он проще в использовании, потому что он объединяет все нужные вызовы в одном классе. Это позволяет программисту смешивать перекрывающиеся и не перекрывающиеся вызовы, обеспечивает настройки по умолчанию, лучшую читаемость и т. д.
  • CSerialEx добавляет дополнительный поток в serial класс, который может использоваться для обработки последовательных событий. Это освобождает основной поток графического интерфейса от serial нагрузки. Основным недостатком этого класса является то, что он использует потоки, что может быть трудно для некоторых людей.
  • CSerialWnd подходит для модели событий Windows. Всякий раз, когда происходит событие связи, сообщение отправляется в окно владельца, которое может обработать событие.
  • CSerialMFC — это обертка MFC вокруг CSerialWnd, которая делает serial  классы более удобными в программах на основе MFC.

Если вы не используете конвейер сообщений в потоке, который выполняет последовательную связь, то вы должны использовать классы CSerial или CSerialEx. Вы можете использовать блокирующие вызовы (самое простое решение) или одну из функций синхронизации (т. е. WaitForMultipleObjects), чтобы дождаться коммуникационных событий. Этот подход также используется в большинстве программ Unix, который имеет аналогичную функцию WaitForMultipleObjects, называемую ‘select’. Такой подход часто является лучшим решением в приложениях, не связанных с графическим интерфейсом, таких как службы NT.

CSerialEx добавляет еще один поток к serial объекту. Это освобождает основной поток от блокировки, ожидая последовательных событий. Эти события принимаются в контексте этого рабочего потока, поэтому программисту необходимо знать влияние многопоточности. Если вся обработка может быть выполнена в этом потоке, то это довольно эффективное решение. Вам нужна какая-то синхронизация потоков, когда вам нужно связаться с основным потоком GUI (для индикации работы порта). Если вам нужен интенсивный обмен с основным потоком GUI, тогда лучше использовать класс CSerialWnd. Однако, если вы не общаетесь с основным потоком, то класс CSerialEx может быть хорошим выбором.

Приложения GUI, которые хотят использовать модель программирования, управляемую событиями, для последовательной связи должны использовать CSerialWnd. Это менее эффективно, но снижение производительности минимально, если вы читаете порт эффективно. Поскольку он отлично вписывается в управляемую событиями парадигму, небольшое снижение производительности является минимальной жертвой. Обратите внимание, что CSerial можно использовать в приложениях на основе графического интерфейса (даже на основе MFC / WTL), но тогда вы можете заблокировать отправку сообщений. Это, конечно, плохая практика в коммерческом приложении (блокировка отправки сообщения тормозит приложение в течение некоторого времени). Пока вы знаете, какое влияние оказывает блокировка сообщения, вы можете сами решить, приемлемо ли оно в вашем случае (может быть хорошо для тестирования).

Приложение MFC должно использовать надстройка  CSerialMFC, если они хотят передавать указатели CWnd вместо дескрипторов. Поскольку эта надстройка очень тонкая, вы также можете использовать CSerialWnd напрямую.

Использование serial классов в вашей программе

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

Отправка данных

Начнем с классического примера из K & R и будьте вежливы и поздоровайтесь. Реализация очень проста и выглядит следующим образом (для простоты здесь нет проверки ошибок, она есть в реальном проекте):

 

#define STRICT
#include <tchar.h>
#include <windows.h>
#include "Serial.h"

int WINAPI _tWinMain
          (
           HINSTANCE /*hInst*/, 
           HINSTANCE /*hInstPrev*/, 
           LPTSTR    /*lptszCmdLine*/, 
           int       /*nCmdShow*/
          )
{
    CSerial serial;

    // Attempt to open the serial port (COM1)
    serial.Open(_T("COM1"));

    // Setup the serial port (9600,N81) using hardware handshaking
    serial.Setup(CSerial::EBaud9600,CSerial::EData8,CSerial::EParNone,CSerial::EStop1);
    serial.SetupHandshaking(CSerial::EHandshakeHardware);

    // The serial port is now ready and we can send/receive data. If
    // the following call blocks, then the other side doesn't support
    // hardware handshaking.
    serial.Write("Hello world");

    // Close the port again
    serial.Close();
    return 0;
}

Конечно, вам нужно включить заголовочный файл serial class. Убедитесь, что заголовочные файлы этой библиотеки находятся в пути включения компилятора. Все классы зависят от Win32 API, поэтому убедитесь, что вы также включили их. Я стараюсь, чтобы все мои программы совместимы с ANSI и Unicode, поэтому поэтому используется tchar.h.

Интересная часть находится внутри основного кода. Сначала мы объявляем серийную переменную, которая представляет собой ровно один COM-порт. Прежде чем вы сможете использовать его, вам нужно открыть порт. Конечно, в коде должна быть обработка ошибок, но это оставлено для читателя. Помимо указания COM-порта, вы также можете указать размеры буфера ввода и вывода. Если вы ничего не указываете, то используются размеры буфера ОС по умолчанию (более старые версии библиотеки использовали 2 КБ в качестве размера буфера по умолчанию, но это было изменено). Если вам нужны более крупные буферы, укажите их сами.

Настройка последовательного порта также довольно проста. Настройки с панели управления (или Диспетчера устройств) используются в качестве параметров по умолчанию для порта. Вызовите программу установки, если эти настройки не применимы к вашему приложению. Если вы предпочитаете использовать целые числа вместо перечисленных типов, просто передайте целое число требуемому типу. Таким образом, следующие две инициализации эквивалентны:

Setup(CSerial::EBaud9600,CSerial::EData8,CSerial::EParNone,CSerial::EStop1);
  
Setup(CSerial::EBaudrate(9600),
      CSerial::EDataBits(8),
      CSerial::EParity(NOPARITY),
      CSerial::EStopBits(ONESTOPBIT));

В последнем случае типы не проверяются. Поэтому убедитесь, что вы указываете соответствующие значения. Как только вы знаете, какой тип подтверждения вам нужен, просто вызовите SetupHandshaking с одним из соответствующих handshaking.

Написание данных также очень просто. Просто вызовите метод Write и укажите строку. Подпрограмма записи определит длину строки и отправит ее. Если вы написали приложения Unicode (как этот), вы могли заметить, что я не отправлял строку Unicode. Я думаю, что бесполезно отправлять строки в Unicode, поэтому вам нужно отправить их как двоичные или преобразовать обратно в ANSI самостоятельно. Поскольку мы используем аппаратное подтверждение связи и операция не перекрывается, метод Write не будет закрыт, пока все байты не будут отправлены получателю. Если приемника нет, то вы можете навсегда заблокировать ее на этом этапе.

Наконец, порт закрывается и программа завершает работу. Эта программа хорошо показывает, как легко открывать и настраивать последовательную связь, но это не очень полезно. Более интересные программы будут рассмотрены ниже.

Прием данных
Как и в реальной жизни, легче сказать что-то, что делать, а затем слушать другого и предпринимать соответствующие действия. То же самое относится и к последовательной связи. Как мы видели в примере Hello world, запись в порт проста, как запись в файл. Получение данных немного сложнее. Чтение данных не так сложно, но, зная, что есть данные и насколько это затрудняет работу. Вам придется подождать, пока появятся данные, и когда вы будете ждать, вы не сможете сделать что-то еще. Именно это и вызывает проблемы в однопоточных приложениях. Для решения этой проблемы существуют три общих подхода.

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

Второе решение — использовать объекты синхронизации Win32. Всякий раз, когда что-то происходит, запускаются соответствующие обработчики событий, и вы можете принять меры для обработки события. Этот метод доступен в большинстве современных операционных систем, но детали различаются. Системы Unix используют вызов select, где приложения Win32 в основном используют WaitForMultipleObjects или одну из связанных функций. Трюк заключается в том, чтобы вызывать функцию WaitEvent асинхронно, предоставляя перекрывающуюся структуру, которая содержит дескриптор, который будет посылать сигнал при событии. Используя WaitForMultipleObjects вы можете подождать, пока один из указатель не станет сигнальным. Я думаю, что это самый подходящий для большинства приложений без графического интерфейса. Когда вы решите использовать эту опцию, вы заметите, что serial классы — это всего лишь тонкий слой вокруг Win32 API.

Последнее решение — это то, которое будет оценено большинством программистов Windows с графическим интерфейсом. Всякий раз, когда что-то происходит, сообщение отправляется в очередь сообщений приложения, указывая, что произошло. Использование стандартного сообщения, отправляющего это сообщение, будет обработано в конечном итоге. Это решение идеально подходит в управляемой событиями среде программирования и поэтому полезно для большинства графических интерфейсов (как не MFC, так и MFC). К сожалению, Win32 API не предлагает никакой поддержки для достижения этого, что является основными причинами создания серийных классов. Старый Win16 API использует SetCommEventMask и EnableCommNotification для выполнения именно этого, но они были сброшены из Win32 API.

Block until something happens

Блокировка — самый простой способ подождать данных и поэтому будет обсуждаться в первую очередь. Класс CSerial предоставляет метод WaitEvent, который будет блокировать до тех пор, пока не будет получено событие. Вы можете (опционально) указать тайм-аут для этого вызова (если включен перекрываемый ввод-вывод), поэтому он не будет блокироваться навсегда, если данные больше не поступают. Метод WaitEvent может ожидать несколько событий, которые должны быть зарегистрированы во время установки. На COM-порт могут возникать следующие события:

  • EEventBreak отправляется всякий раз, когда на вход поступает перерывание.
  • EEventCTS означает, что изменился сигнал CTS (сигнал, передаваемый посылкой).
  • EEventDSR означает, что сигнал DSR (готовый к набору данных) изменился.
  • EEventError указывает, что произошла ошибка состояния линии.
  • EEventRing указывает, что индикатор звонка был установлен высоко. Только переходы с низкого на высокий будут генерировать это событие.
  • EEventRLSD означает, что сигнал RLSD (прием сигнала линейного сигнала) изменился. Обратите внимание, что этот сигнал часто называют CD (обнаружение несущей).
  • EEventRecv, вероятно, является одним из самых важных событий, поскольку сигнализирует, что получены данные.
  • EEventRcvEv указывает, что определенный символ (символ события) был получен. Этот символ можно установить с помощью метода SetEventChar.
  • EEventSend указывает, что весь выходной буфер был отправлен на получателю.

Когда последовательный порт открыт, регистрируются EEventBreak, EEventError и EEventRecv. Если вы хотите получать другие события, вам необходимо зарегистрировать их с помощью метода SetMask.

Теперь вы можете использовать метод WaitEvent для ожидания события. Затем вы можете вызвать GetEventType для получения фактического события. Эта функция сбрасывает событие, поэтому убедитесь, что вы вызываете его только один раз после каждого вызова WaitEvent. Несколько событий могут быть получены одновременно (то есть, когда принимается символ события, возвращается (EEventRecv | EEventRcvEv). Никогда не используйте оператор == для проверки событий, но используйте вместо этого оператор &.

Чтение может быть выполнено с использованием метода Read, но чтение становится сложнее, чем вы могли подумать вначале. Вы получаете только событие, которое содержит некоторые данные, но не сколько. Это может быть один байт, но это может быть и несколько килобайт. Существует только один способ справиться с этим. Просто прочитайте столько, сколько вы сможете обработать (эффективно) и обработать.

Сначала убедитесь, что порт находится в режиме EReadTimeoutNonblocking, выполнив следующий вызов:

// Use 'non-blocking' reads, because we don't know how many bytes
// will be received. This is normally the most convenient mode
// (and also the default mode for reading data).
serial.SetupReadTimeouts(CSerial::EReadTimeoutNonblocking);

Метод Read теперь будет читать как можно больше, но никогда не будет блокироваться. Если вы хотите блокировать чтение, то укажите EReadTimeoutBlocking. Чтение всегда возвращает число прочитанных байт, поэтому вы можете определить, прочитали ли вы весь буфер. Обязательно всегда читайте весь буфер после получения события EEventRecv, чтобы избежать потери данных. Типичный EEventRecv будет выглядеть примерно так:

// Read data, until there is nothing left
DWORD dwBytesRead = 0;
BYTE  abBuffer[100];
do
{
    // Read data from the COM-port
    serial.Read(abBuffer,sizeof(abBuffer),&dwBytesRead);
    if (dwBytesRead > 0)
    {
        // TODO: Process the data
    }
}
while (dwBytesRead == sizeof(abBuffer));

Пример Listener (включенный в ZIP-файл) демонстрирует технику, как описано выше. Весь пример кода не указан в этом документе, поскольку это займет слишком много места.

Using the Win32 synchronization objects

В большинстве случаев блокировка для отдельного события (как описано выше) не подходит. Когда приложение блокируется, оно полностью выходит из-под вашего контроля. Предположим, вы создали службу, которая прослушивает несколько COM-портов, а также контролирует событие Win32 (используется для указания на то, что служба должна останавливаться). В этом случае вам понадобятся многопоточность, очереди сообщений или функция Win32 для синхронизации. Объекты синхронизации — это наиболее эффективный способ реализации этого, поэтому я попытаюсь объяснить их. Прежде чем продолжить чтение, я предполагаю, что вы немного знакомы с использованием объектов синхронизации и перекрывающихся операций. Если это не так, сначала прочтите раздел о синхронизации в Win32 API.

Единственный вызов, который блокирует довольно длительное время, — это метод WaitEvent. В следующих параграфах я покажу вам, как реализовать этот вызов, используя объекты синхронизации Win32 (все другие перекрывающиеся вызовы работают одинаково). Полная реализация может быть найдена в проекте Overlapped, который очень похож на проект Listener, но теперь использует перекрывающиеся операции ввода-вывода.

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

// Create a handle for the overlapped operations
HANDLE hevtOverlapped = ::CreateEvent(0,TRUE,FALSE,0);;

// Open the "STOP" handle
HANDLE hevtStop = ::CreateEvent(0,TRUE,FALSE,_T("Overlapped_Stop_Event"));

// Setup the overlapped structure
OVERLAPPED ov = {0};
ov.hEvent = hevtOverlapped;

Все события были правильно настроены, и структура перекрытия была инициализирована. Теперь мы можем вызывать метод WaitEvent в перекрывающемся режиме.

// Wait for an event
serial.WaitEvent(&ov);

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

// Setup array of handles in which we are interested
HANDLE ahWait[2];
ahWait[0] = hevtOverlapped;
ahWait[1] = hevtStop;

// Wait until something happens
switch (::WaitForMultipleObjects(2,ahWait,FALSE,INFINITE))
{
case WAIT_OBJECT_0:
    // Serial port event occurred
    ...

case WAIT_OBJECT_0+1:
    // Stop event raised
    ...
}

Это все, что вам нужно сделать, если вы хотите использовать последовательный класс в режиме перекрытия ввода-вывода. N

Using Windows messages
Большинство разработчиков Windows используют сообщения Windows, когда происходит событие. Это вписывается в управляемую событиями Windows модель, но API Win32 не предоставляет такой механизм для serial связи. Эта библиотека включает в себя класс CSerialWnd, который будет отправлять сообщение при наступлении serial события. Это просто, если вы знакомы с управляемой событиями моделью программирования Windows.

Вместо использования класса CSerial вы должны использовать класс CSerialWnd (который на самом деле является производным от CSerial). CSerialWnd работает так же, как CSerial, но есть некоторые различия в открытии порта и ожидании его событий. Обратите внимание, что CSerialWnd не имеет самого окна, и вы не должны его выводить, если хотите его использовать. Просто определите переменную-член и используйте ее из своего окна.

Поскольку CSerialWnd отправляет свои сообщения в окно, ему требуется дополнительная информация. Поэтому метод Open принимает три дополнительных параметра, которые определяют дескриптор окна, сообщение и необязательный аргумент. Прототип выглядит следующим образом:

LONG Open (
           LPCTSTR lpszDevice,
           HWND    hwndDest,
           UINT    nComMsg    = WM_NULL,
           LPARAM  lParam     = 0,
           DWORD   dwInQueue  = 0,
           DWORD   dwOutQueue = 0
          )

LpszDevice, dwInQueue и dwOutQueue используются как в CSerial. Аргумент hwndDest указывает окно, куда должно быть отправлено сообщение. Во время запуска библиотека регистрирует сообщение по умолчанию, которое может использоваться в большинстве случаев. Просто передайте WM_NULL, чтобы использовать это сообщение. Значение этого сообщения хранится в переменной CSerialWnd :: mg_nDefaultComMsg, которая представляет собой статическую переменную-член CSerialWnd. Если вы предпочитаете одно из своих сообщений, вы можете использовать его вместо этого. Необязательный аргумент lParam отправляется как второй параметр (lParam) в каждом сообщении, отправляемом CSerial. Последовательная библиотека ничего не делает с этим значением, поэтому можете использовать его по своему усмотрению.

Передача данных и настройка последовательного порта в точности совпадают с CSerial, поэтому я не буду обсуждать это снова. Самое большое различие — способ получения событий, но именно поэтому вы хотите использовать этот класс в любом случае.

Если все в порядке, то вы зарегистрировали все интересные события с помощью метода SetMask. Всякий раз, когда происходит одно из этих событий, указанное сообщение будет отправлено в окно, которое вы зарегистрировали ранее. WParam будет содержать событие и код ошибки. LParam содержит все, что вы передали CSerialWnd :: Open, когда вы открыли порт. Типичный обработчик для этих сообщений выглядит так:

LRESULT CALLBACK MyWndProc (HWND hwnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
   if (nMsg == CSerialWnd::mg_nDefaultComMsg)
   {
       // A serial message occurred
       const CSerialWnd::EEvent eEvent = CSerialWnd::EEvent(LOWORD(wParam));
       const CSerialWnd::EError eError = CSerialWnd::EError(HIWORD(wParam));

       switch (eEvent)
       {
       case CSerialWnd::EEventRecv:
           // TODO: Read data from the port
           break;

           ...
       }
         
       // Return successful
       return 0;
   }

   // Perform other window processing
   ...
}

Методы WaitEvent, GetEventType и GetError из CSerial скрыты в классе CSerialWnd, потому что они больше не могут использоваться. Вся информация передается в оконном сообщении, поэтому нет необходимости использовать их больше.

Using the library with MFC

Лично мне не нравится MFC, но я знаю, что многие люди используют его, поэтому есть поддержка в этой библиотеке для MFC. Вместо использования CSerialWnd вы можете использовать CSerialMFC. Он работает точно так же, но может также обрабатывать указатель CWnd, и он предоставляет макрос, который можно использовать в карте сообщений для лучшей читаемости. Карта сообщений окна, которая может принимать события от CSerialMFC, должна выглядеть так:

BEGIN_MESSAGE_MAP(CMyClass,CWnd)
//{{AFX_MSG_MAP(CMyClass)
    ...
//}}AFX_MSG_MAP
...
    ON_WM_SERIAL(OnSerialMsg)
    ...
END_MESSAGE_MAP()

Обратите внимание, что макрос ON_WM_SERIAL находится вне блока AFX_MSG_MAP, в противном случае мастер классов MFC будет запутан. Сам обработчик выглядит примерно так:

 

afx_msg LRESULT CMyClass::OnSerialMsg (WPARAM wParam, LPARAM lParam)
{
    const CSerialMFC::EEvent eEvent = CSerialMFC::EEvent(LOWORD(wParam));
    const CSerialMFC::EError eError = CSerialMFC::EError(HIWORD(wParam));

    switch (eEvent)
    {
    case CSerialMFC::EEventRecv:
        // TODO: Read data from the port
        break;
        ...
    }
   
    // Return successful
    return 0;
}

Полная версия, включая листы свойств для настройки COM-порта, поставляется с этой библиотекой. Посмотрите на проект SerialTestMFC для примера, как использовать эту библиотеку в своих программах MFC.

Интеграция этой библиотеки в свой код.

Эта библиотека очень легкая, поэтому ее можно легко интегрировать в ваше приложение без использования отдельной DLL. Я использовал статическую библиотеку для CSerial (и производных) классов, потому что я думаю, что это именно то, для чего предназначена библиотека. Просто вставьте Serial-проект в ваше рабочее пространство и установите зависимость от него. Затем компоновщик автоматически скомпилирует и привяжет последовательные классы к вашему приложению. Некоторые люди не любят библиотеки. В этом случае вы можете просто добавить Серийные файлы в свой проект и перекомпилировать.

Если вы используете предварительно скомпилированные заголовки, вам нужно удалить следующие строки из Serial.cpp и SerialWnd.cpp:

#define STRICT
#include <crtdbg.h>
#include <tchar.h>
#include <windows.h>

Замените эти строки следующей строкой:

#include "StdAfx.h"

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

Серийная библиотека поставляется с некоторыми образцами программ, которые могут быть скомпилированы из рабочего пространства Serial.dsw. Убедитесь, что вы всегда открываете файл Serial.dsw при создании библиотеки и / или примеров приложений. Открытие отдельных .dsp-файлов не будет работать, поскольку эти файлы не имеют зависимостей, что приводит к нераскрытым внешним.

Обратите внимание, что все сэмплы могут быть скомпилированы как версии ANSI или Unicode. Версии Unicode не запускаются на платформах Windows 95/98 / ME, так как они не поддерживают Unicode. В примере SerialTestMFC используется версия библиотеки MFC в Unicode (когда она скомпилирована с поддержкой Unicode). Параметры установки по умолчанию Visual C ++ v6.0 не устанавливают библиотеки Unicode MFC, поэтому может появиться сообщение об ошибке, из-за которого mfc42ud.dll или mfc42u.dll не могут быть найдены.

Windows 95 support and CancelIo

Многим людям по-прежнему необходимо поддерживать среду Windows 95, которая не поддерживает функцию CancelIo. Если время перекрытия операции истекло, отложенный вызов должен быть отменен вызовом CancelIo. Поэтому тайм-ауты (кроме 0 и INFINTE) не могут использоваться, если включена совместимость с Windows 95. К счастью, CSerialEx, CSerialWnd и CSerialMFC не полагаются на этот вызов, поэтому эти классы можно использовать в Windows 95 без каких-либо ограничений. Если вы определяете символ SERIAL_NO_CANCELIO, то отмена ввода-вывода отключена, поэтому вы всегда должны определять этот символ, когда нацеливаетесь на Windows 95.

Другие проблемы, характерные для Windows 95/98 / ME, — это размеры очереди. Если размер буфера слишком мал, вы можете получить синий экран при приеме или передаче данных. Настройки по умолчанию должны быть точными для обычной последовательной связи.

Windows CE support and the lack of overlapped I/O
У меня много запросов о реализации версии библиотеки CE для Windows CE. В старой версии этой библиотеки всегда использовался перекрытый ввод-вывод, поэтому он не работал в Windows CE. Из-за огромного количества запросов я начал работать над этой проблемой. Я переписал класс CSerialEx, поэтому он не полагается на внутренние операции ввода-вывода. Отмена WaitCommEvent теперь использует документированный трюк, который устанавливает маску события в его текущее значение. Это эффективно отменяет метод WaitCommEvent и делает ненужным применение перекрывающихся операций ввода-вывода в CSerialEx.

SetCommMask, когда он уже ждет событие (используя WaitCommEvent). Это сделало бы этот метод бесполезным на платформе Windows CE, потому что он не поддерживает перекрывающиеся операции ввода-вывода. К счастью, последовательный драйвер реализован как внутренне перекрываемый драйвер в Windows CE, поэтому он допускает несколько вызовов (описанных в KB, статья Q175551). Используя эту функцию Windows CE, вы также можете использовать все классы на платформе Windows CE.
Чтобы включить эту библиотеку в ваш проект Windows CE, просто добавьте файл проекта Serial.dsp в ваше собственное рабочее пространство. Если вы используете eMbedded Visual C ++, файл проекта автоматически преобразуется. Убедитесь, что вы определяете символ SERIAL_NO_OVERLAPPED, чтобы избежать использования перекрывающихся операций ввода-вывода.

Porting issues

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

  • [Общие] Не делается никаких попыток сделать бинарные библиотеки совместимыми между разными сборками. Убедитесь, что вы всегда используете одни и те же настройки компилятора и файлы заголовков для библиотеки и вашего приложения, чтобы гарантировать правильную работу.
  • [Август 2002 г. и позже] По умолчанию изменены размеры входных и выходных очередей. Раньше размер по умолчанию составлял 2 КБ, но теперь используется размер ОС по умолчанию.
  • [Август 2002 г. и позже] Настройки связи по умолчанию были изменены. Ранее установка 9600,8N1 использовалась как настройка по умолчанию. В новой версии используются значения из панели управления (или Диспетчера устройств).
  • [Август 2002 г. и позже] Добавлен класс CSerialEx, который может лучше соответствовать вашим потребностям.

Virtual COM ports (USB dongles, Bluetooth dongles, …)

Некоторые люди используют виртуальные COM-порты для эмуляции обычного последовательного порта (например, USB-серийный ключ, Bluetooth-ключ, …). Эти так называемые виртуальные COM-порты используют разные драйверы, а затем обычные последовательные порты. К сожалению, большинство драйверов довольно глючные. В большинстве случаев нормальная связь с использованием настроек по умолчанию работает довольно хорошо, но вы сталкиваетесь с проблемами, когда вам требуется более сложная связь.

Большинство виртуальных COM-портов испытывают трудности с функцией CancelIo. В некоторых случаях процесс полностью зависает (и даже не может быть убит диспетчером задач), и я даже видел перезагрузку систем Windows XP после вызова CancelIo. Некоторые драйверы могут обрабатывать CancelIo для перекрывающегося вызова ReadFile, но не сбрасываются при отмене запроса WaitCommEvent. Завершение потока с ожидающим запросом ввода-вывода также подразумевает неявный вызов метода CancelIo, поэтому если у вас возникли проблемы с завершением потока, у вас могут быть ожидающие запросы, которые не могут быть отменены. CSerialEx (и производные классы) эффективно отменяет ожидающее WaitEvent, поэтому вы должны быть в полной безопасности, когда используете эти классы.

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

Некоторые виртуальные COM-порты также имеют проблемы, когда вы не используете общие 8-разрядные, нечетные и 1 стоповые. Отправка перерыва также проблематична для некоторых виртуальных COM-портов. Для этих ошибок нет стандартного обхода, поэтому убедитесь, что вы тестируете эти функции, когда собираетесь использовать свое приложение с виртуальным COM-портом.

Некоторые люди утверждают, что ключ и его драйвер хороши, потому что HyperTerminal работает нормально. В большинстве случаев это неверно. HyperTerminal использует только небольшое подмножество Win32 API для последовательной связи и, похоже, не очень использует функцию CancelIo. Самый важный урок из последних нескольких параграфов состоит в том, что вам нужно очень тщательно протестировать приложение с каждым драйвером, который можно использовать. Хотя Win32 API одинаков для каждого (виртуального) COM-порта, реализация под API может быть совершенно иной.

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

References

Конечно, первое место для поиска информации о последовательных коммуникациях занимает раздел Platform SDK «Службы Windows Base, файлы и ввод-вывод, связь». Там, вероятно, достаточно, чтобы реализовать собственную последовательную связь, но для лучшего объяснения прочитайте статью Аллена Денвера «Serial Communications in Win32», которая находится в технических статьях MSDN.

От sapfir63