VC++ 串口開、關、讀、寫操作及注意事項

最近幫朋友做一款工具,設計到對作業系統串口的操作,雖然這個東西已經是歷史產物了,但是還有很多設備再用,索性從網路上找了一些程式碼最終完成這個小功能。下面資料將介紹串口在打開、關閉、讀和寫的時候一些注意事項以及參數的配置(程式碼中有詳細注釋。)

串口的開關

在串口打開的時候,我們要對串口做一些基礎的初始化,比如波特率、數據位、校驗位、停止位幾個參數,他們分別被聲明在 WinBase.h 頭文件中。

打開串口的程式碼如下:

bool SerialPortManager::Open(ReceiveDataCallback cb/* = nullptr*/)  {      if (serial_handle_ != NULL)      {          return false;      }        if (cb != nullptr)      {          cb_ = cb;      }        serial_handle_ = CreateFile(com_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);      if (serial_handle_ == INVALID_HANDLE_VALUE)      {          return false;      }        // 獲取舊的 dcb 數據      DCB dcb;      dcb.DCBlength = sizeof(DCB);      if (!GetCommState(serial_handle_, &dcb))      {          Close();          return false;      }        // 修改 dcb 數據然後設置埠屬性      // CBR_115200;      dcb.ByteSize = byte_size_;      dcb.BaudRate = baud_rate_;      dcb.StopBits = stop_bits_;      dcb.Parity = parity_;      dcb.fBinary = TRUE;      dcb.fParity = TRUE;      if (!SetCommState(serial_handle_, &dcb))      {          Close();          return false;      }        // 設置讀寫緩衝區大小      SetupComm(serial_handle_, 1024, 1024);        // 清空數據      PurgeComm(serial_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);        // 設置超時 10 秒      COMMTIMEOUTS to;      memset(&to, 0, sizeof(to));      to.ReadIntervalTimeout = 1000;      to.ReadTotalTimeoutMultiplier = 500;      to.ReadTotalTimeoutConstant = 5000; //設定寫超時      to.WriteTotalTimeoutMultiplier = 500;      to.WriteTotalTimeoutConstant = 2000;      SetCommTimeouts(serial_handle_, &to);        PostReadThread();        QLOG_APP(L"Serial port device is ready, serial: {0}, baud rate: {1}, byte size: {2}, parity: {3}, stop bits: {4}.")          << com_ << baud_rate_ << byte_size_ << parity_ << stop_bits_;        return true;  }

其中除了打開串口時傳遞的參數外,還包含了一些串口處理數據超時、讀寫緩衝區大小等屬性,需要用到的根據自己的環境來配置。 串口的關閉很簡單,只需要關閉掉 CreateFile 返回的句柄就可以了,這裡不多介紹。

串口讀寫

串口的讀寫可以同步也可以非同步,但是同步方式會造成一個問題就是當你調用了 ReadFile 在等待串口數據時,再去調用 WriteFile 就會被阻塞,因為 ReadFile 一直沒有返回。所以我還是推薦大家用非同步方式來讀寫串口,程式碼如下:

void SerialPortManager::ReadSerialPortThread()  {      QLOG_APP(L"PostReadThread is running....");      while (TRUE)      {          if (!serial_handle_)          {              QLOG_ERR(L"Failed to read data from serial port, serial port handle is null.");              break;          }            // 計算最小需要讀取的數據量          DWORD read_size = 1024;          // read_size = min(read_size, (DWORD)com_stat.cbInQue);            // 開始非同步讀取          OVERLAPPED over_lapped;          memset(&over_lapped, 0, sizeof(OVERLAPPED));          over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);            std::shared_ptr<BYTE> buffer;          buffer.reset(new BYTE[read_size]);          BOOL bReadStatus = ReadFile(serial_handle_, buffer.get(), read_size, &read_size, &over_lapped);          if (!bReadStatus) // 如果 ReadFile 函數返回 FALSE          {              DWORD last_error = GetLastError();              if (last_error == ERROR_IO_PENDING)              {                  QLOG_APP(L"Read file return ERROR_IO_PENDING..");                  BOOL bRet = GetOverlappedResult(serial_handle_, &over_lapped, &read_size, TRUE);                  if (bRet)                  {                      // 返回 true 代表讀取到了數據                      QLOG_APP(L"Read data {0}") << nbase::UTF8ToUTF16((char*)buffer.get());                      cb_((char*)buffer.get());                      PurgeComm(serial_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);                  }                  else                  {                      // 返回 false 可能是句柄已經被 close 了                      QLOG_APP(L"GetOverlappedResult returned false");                  }                    continue;              }                QLOG_ERR(L"Failed to read data from serial prot, error code = {0}") << last_error;          }      }        QLOG_APP(L"PostReadThread is quit....");  }
bool SerialPortManager::WriteData(const std::string& data)  {      QLOG_APP(L"Begin to write data [{0}] to serial port.") << data;        DWORD bytes_written = data.size() + 1;      OVERLAPPED over_lapped;      memset(&over_lapped, 0, sizeof(OVERLAPPED));      over_lapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);      BOOL write_stat = WriteFile(serial_handle_, data.c_str(), bytes_written, &bytes_written, &over_lapped);      if (!write_stat)      {          DWORD last_error = GetLastError();          if (last_error == ERROR_IO_PENDING)          {              WaitForSingleObject(over_lapped.hEvent, 2000);              return true;          }            return false;      }        QLOG_APP(L"Finished to write data.");      return true;  }

讀因為是非同步操作,我們需要傳一個 OVERLAPPED 結構體到 ReadFile 的最後一個參數。寫也是一樣,我們可以用 GetOverlappedResultWaitForSingleObject 來等待操作事件完成(記得要初始化 OVERLAPPED 否則會報錯的)。兩種方式讀寫我都做了演示,可以根據自己的需求改造。 讀寫操作的時候可以獲取當前返回值判斷是不是 ERROR_IO_PENDING 來確定是不是有數據還沒有讀取完成。

總結

串口的讀寫其實還是相對簡單的,上面程式碼基本上把可能出現問題的點都體現出來了,最後再來羅列一下注意事項。

  • 打開串口時要根據硬體情況初始化串口參數(在 WinBase.h 中有聲明)
  • 設置串口的緩衝區和超時
  • 非同步去讀寫串口通過返回值判斷是否讀寫成功
  • 不要忘記初始化 OVERLAPPAD 結構
  • 讀取完成後 PurgeComm 串口

相關