前面我寫了一篇《boost開發網絡庫》一文,該文章介紹了使用boost庫開發一個高效、穩定的網絡通訊庫,其中用到了c++準標準庫boost的asio網絡通訊模塊,本文將要講的是使用boost開發usb串口,正好也用到了asio,我以前文章中說過asio不只僅包含網絡通訊,還包括串口,接下來我將帶你們講解使用boost庫實現串口的通訊。(固然,咱們徹底可使用windows本地api實現相似功能) linux
串口是一種很是通用的設備通訊的協議(不要與通用串行總線Universal Serial Bus(USB)混淆)。大多數計算機包含兩個基於RS232的串口。串口同時也是儀器儀表設備通用的通訊協議;不少GPIB兼容的設備也帶有RS-232口。同時,串口通訊協議也能夠用於獲取遠程採集設備的數據。 ios
串口通訊的概念很是簡單,串口按位(bit)發送和接收字節。儘管比按字節(byte)的並行通訊慢,可是串口能夠在使用一根線發送數據的同時用另外一根線接收數據。它很簡單而且可以實現遠距離通訊。好比IEEE488定義並行通行狀態時,規定設備線總長不得超過20米,而且任意兩個設備間的長度不得超過2米;而對於串口而言,長度可達1200米。
典型地,串口用於ASCII碼字符的傳輸。通訊使用3根線完成:(1)地線,(2)發送,(3)接收。因爲串口通訊是異步的,端口可以在一根線上發送數據同時在另外一根線上接收數據。其餘線用於握手,但不是必須的。串口通訊最重要的參數是波特率、數據位、中止位和奇偶校驗。對於兩個進行通行的端口,這些參數必須匹配。c++
首先,咱們要了解串口通訊的幾個概念:windows
這是一個衡量符號傳輸速率的參數。它表示每秒鐘傳送的符號的個數。例如300波特表示每秒鐘發送300個符號。當咱們提到時鐘週期時,咱們就是指波特率,例如若是協議須要4800波特率,那麼時鐘是4800Hz。這意味着串口通訊在數據線上的採樣率爲4800Hz。一般電話線的波特率爲14400,28800和36600。波特率能夠遠遠大於這些值,可是波特率和距離成反比。高波特率經常用於放置的很近的儀器間的通訊,典型的例子就是GPIB設備的通訊。api
這是衡量通訊中實際數據位的參數。當計算機發送一個信息包,實際的數據不會是8位的,標準的值是五、六、7和8位。如何設置取決於你想傳送的信息。好比,標準的ASCII碼是0~127(7位)。擴展的ASCII碼是0~255(8位)。若是數據使用簡單的文本(標準 ASCII碼),那麼每一個數據包使用7位數據。每一個包是指一個字節,包括開始/中止位,數據位和奇偶校驗位。因爲實際數據位取決於通訊協議的選取,術語「包」指任何通訊的狀況。緩存
用於表示單個包的最後一位。典型的值爲1,1.5和2位。因爲數據是在傳輸線上定時的,而且每個設備有其本身的時鐘,極可能在通訊中兩臺設備間出現了小小的不一樣步。所以中止位不只僅是表示傳輸的結束,而且提供計算機校訂時鐘同步的機會。適用於中止位的位數越多,不一樣時鐘同步的容忍程度越大,可是數據傳輸率同時也越慢。微信
在串口通訊中一種簡單的檢錯方式。有四種檢錯方式:偶、奇、高和低。固然沒有校驗位也是能夠的。對於偶和奇校驗的狀況,串口會設置校驗位(數據位後面的一位),用一個值確保傳輸的數據有偶個或者奇個邏輯高位。例如,若是數據是011,那麼對於偶校驗,校驗位爲0,保證邏輯高的位數是偶數個。若是是奇校驗,校驗位爲1,這樣就有3個邏輯高位。高位和低位不是真正的檢查數據,簡單置位邏輯高或者邏輯低校驗。這樣使得接收設備可以知道一個位的狀態,有機會判斷是否有噪聲干擾了通訊或者是否傳輸和接收數據是否不一樣步。 網絡
當咱們打開串口的時候就要指定通訊的波特率、數據位、中止位、奇偶校驗位等參數,它是經過核心的類serial_port實現的異步
接下來,咱們就用boost的asio實現串口的通訊,由於boost庫是跨平臺的庫,因此,咱們只需稍加改造就能夠運行在linux下。 async
首先,個人目的很簡單,就是實現串口鏈接、關閉、發送數據、接收數據,因此個人接口也是這幾個功能:
// ------------------------------------------------------------------------------ // Summary: // open com port // Parameters: // [in]port : com port value as SERIAL_PORT specified // [in]baut_rate : baut rate // [in]parity : parity // [in]stop_bit : stop bit // [in]data_bit : data bit length // [out]lHandle : if success return handle on the com port else return the value < 0 // Return: // SERIAL_RETURN as description // ------------------------------------------------------------------------------ SERIALPORT_API SERIAL_RETURN OpenSerialPort(SERIAL_PORT port, BAUT_RATE baut_rate, PARITY_TYPE parity, STOP_BIT_TYPE stop_bit, DATA_BIT_TYPE data_bit, long* lHandle); // ------------------------------------------------------------------------------ // Summary: // register data callback // Parameters: // [in]lHandle : OpenSerialPort returned // [in]pCallBack : data callback // [in]pContext : user context // Return: // NULL // ------------------------------------------------------------------------------ SERIALPORT_API SERIAL_RETURN SetDataCallBack(long lHandle, ReadCallBack pCallBack, void* pContext); // ------------------------------------------------------------------------------ // Summary: // register disconnect callback // Parameters: // [in]lHandle : OpenSerialPort returned // [in]pCallBack : data callback // [in]pContext : user context // Return: // NULL // ------------------------------------------------------------------------------ SERIALPORT_API SERIAL_RETURN SetDisconnectCallBack(long lHandle, DisConnectCallBack pCallBack, void* pContext); // ------------------------------------------------------------------------------ // Summary: // close com port // Parameters: // [in]lHandle : OpenSerialPort returned // Return: // NULL // ------------------------------------------------------------------------------ SERIALPORT_API void CloseSerialPort(long lHandle); // ------------------------------------------------------------------------------ // Summary: // close com port // Parameters: // [in]lHandle : OpenSerialPort returned // [in]pData : send data content // [in]nLen : send data size // Return: // NULL // ------------------------------------------------------------------------------ SERIALPORT_API SERIAL_RETURN SendSerialData(long lHandle, unsigned char* pData, int nLen);
接下來,咱們針對性的重點介紹幾個接口的實現,內部的實現類爲CSerialPortInst,如下爲封裝類實現
#pragma once #include "SerialPort.h" #include "Buffer.h" using namespace SerialPort; class CSerialPortInst : public boost::enable_shared_from_this<CSerialPortInst> { typedef boost::shared_ptr<serial_port> SerialPortPtr; typedef boost::shared_ptr<boost::asio::io_service::work> IO_Work; public: CSerialPortInst(); virtual ~CSerialPortInst(void); public: void SetDataCallBack(ReadCallBack pCallBack, void* pContext); void SetConnectCallBack(DisConnectCallBack pCallBack, void* pContext); long GetHandle(); public: SERIAL_RETURN Open(SERIAL_PORT port, BAUT_RATE baud_rate, PARITY_TYPE parity, STOP_BIT_TYPE stop_bits, DATA_BIT_TYPE size); SERIAL_RETURN Send(unsigned char* pData, int nLen); void Close(); protected: bool AsynRead(); void ReadHandler(const boost::system::error_code err, const size_t nTransferedSize); void AsyncSend(); void WriteHandler(CBuffer* pBuffer, const boost::system::error_code err, const size_t nTransferedSize); std::string GetPortText(SERIAL_PORT port); protected: ReadCallBack m_pDataCB; // 數據回調 void* m_pDataCBUser; // 數據上下文 DisConnectCallBack m_pDisconectCB; // 斷開回調 void* m_pDisconectCBUser; // 斷開上下文 public: boost::thread m_thread; // 服務線程 IO_Work m_work; // 保活 io_service m_ios; // IO 服務 SerialPortPtr m_pPort; // 端口對象 CBuffer m_read_buffer; // 讀緩存 CSafeBuffer m_write_buffer; // 寫緩存 boost::mutex m_buffer_lock; // 發送標誌 bool m_send_finish; protected: SERIAL_PORT m_port; // COM口 BAUT_RATE m_baud_rate; // 波特率 PARITY_TYPE m_parity; // 奇偶校驗 1 odd 0 even -1 none STOP_BIT_TYPE m_stop_bits; // 中止位 DATA_BIT_TYPE m_size; // 數據位 }; typedef boost::shared_ptr<CSerialPortInst> SerialPortPtr;
首先,咱們看打開串口核心實現:
try { boost::system::error_code ec; m_pPort->open(GetPortText(port), ec); if(0 != ec) { return SERIAL_INIPORT_ERR; } // 設置波特率 m_pPort->set_option(serial_port::baud_rate((unsigned int)baud_rate), ec); // 流量控制 m_pPort->set_option(serial_port::flow_control(serial_port::flow_control::none), ec); // 奇偶校驗 serial_port::parity::type etype = serial_port::parity::none; if(PARITY_TYPE_ODD == parity) etype = serial_port::parity::odd; else if(PARITY_TYPE_EVEN == parity) etype = serial_port::parity::even; m_pPort->set_option(serial_port::parity(etype), ec); // 中止位 m_pPort->set_option( serial_port::stop_bits((serial_port::stop_bits::type)stop_bits), ec); // 數據位 m_pPort->set_option( serial_port::character_size( (unsigned int)size ) ); m_port = port; m_baud_rate = baud_rate; m_parity = parity; m_stop_bits = stop_bits; m_size = size; m_work.reset(new boost::asio::io_service::work(m_ios)); m_thread = boost::thread(boost::BOOST_BIND(&boost::asio::io_service::run, &m_ios)); // 讀取數據 AsynRead(); return SERIAL_SUCCESS; } catch (...) { }
咱們設置了串口相關參數,並啓動了一個線程不斷處理io_service的事件,由於io_service提供了任務隊列和任務分發功能且serial_port綁定到io_service中,因此當有讀取事件的時候,咱們能在回掉中讀取到。
如下爲信息讀取處理函數
void CSerialPortInst::ReadHandler(const boost::system::error_code err, const size_t nTransferedSize) { try { if (!err) { if (NULL != m_pDataCB && nTransferedSize > 0) { m_pDataCB(GetHandle(), m_read_buffer.m_pData, nTransferedSize, m_pDataCBUser); AsynRead(); } } else { if (NULL != m_pDisconectCB) { m_pDisconectCB(GetHandle(), 0, m_pDisconectCBUser); } } } catch (std::exception& ) { if (NULL != m_pDisconectCB) { m_pDisconectCB(GetHandle(), 0, m_pDisconectCBUser); } } }
很簡單,當讀取到數據的時候,我直接將數據回調到外部,讓外部程序處理該數據便可。
boost::mutex::scoped_lock a_lock(m_buffer_lock); if (m_send_finish) { // 獲取當前發送buffer CBuffer* pBuffer = m_write_buffer.GetFullBuffer(); // 無可發送的buffer if (NULL == pBuffer) return; if (pBuffer->m_nDataSize > 0) { m_send_finish = false; unsigned int nSendLen = INT_MAX_SEND_PAKAGE_TCP; if (nSendLen > pBuffer->m_nDataSize) { nSendLen = pBuffer->m_nDataSize; } m_pPort->async_write_some(buffer(pBuffer->m_pData, nSendLen), bind(&CSerialPortInst::WriteHandler, shared_from_this(), pBuffer, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } }
除了讀取數據以外,重點就是數據的發送,同網絡庫同樣,咱們使用了一個環形緩衝區,將數據直接寫入緩衝區而後由他asio庫發送,每次咱們最多發送一段數據,當一段數據發送完成後咱們繼續發送知道該緩衝區數據發送完成後在回收到環形緩衝區中
if (pBuffer) { pBuffer->PopData(nTransferedSize); } // 數據發送完成則回收到環形緩衝區 if (pBuffer->m_nDataSize <= 0) { // 標誌當前幀發送結束 { boost::mutex::scoped_lock a_lock(m_buffer_lock); m_send_finish = true; } // 將發送完的buffer返回隊列中 m_write_buffer.AddEmptyBuffer(pBuffer); // 準備發送下一幀數據 AsyncSend(); } else { // 發送剩餘數據 unsigned int nSendLen = INT_MAX_SEND_PAKAGE_TCP; if (nSendLen > pBuffer->m_nDataSize) { nSendLen = pBuffer->m_nDataSize; } m_pPort->async_write_some(buffer(pBuffer->m_pData, nSendLen), bind(&CSerialPortInst::WriteHandler, shared_from_this(), pBuffer, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); }
關於關閉串口實現就更簡單了,只須要關閉serial_port並中止io_service便可:
m_pPort->close(); m_work.reset(); m_ios.stop(); m_thread.join();
關於串口的測試就稍微有點麻煩,由於通常的機器如今不多有串口了,爲了測試,咱們可使用串口模擬輔助工具:
虛擬串口模擬工具,能夠經過該軟件在本機添加一個串口對,而後你的程序就能夠模擬鏈接該COM口了
串口測試工具,爲了查看發送的數據,咱們能夠從另外一個串口中讀取數據,在測試以前,咱們先看看個人庫的調用流程
OpenSerialPort打開串口=>SetDataCallBack設置數據回調=>SetDisconnectCallBack設置鏈接斷開回調=>SendSerialData發送數據給串口=>CloseSerialPort關閉串口
具體demon的使用代碼, 鏈接串口(注意我默認是打開COM5口的):
BOOL CSerialDemonDlg::OnInitDialog() { CDialog::OnInitDialog(); if (OpenSerialPort(SERIAL_COM5, BAUT_9600, PARITY_TYPE_NONE, STOP_BIT_1, DATA_BIT_8, &m_lHandle) != SERIAL_SUCCESS) { AfxMessageBox(_T("打開COM5串口失敗!")); } else { SetDataCallBack(m_lHandle, DataCallBack, this); SetDisconnectCallBack(m_lHandle, DisConnectCB, this); } return TRUE; }
發送數據:
void CSerialDemonDlg::OnBnClickedOk() { if (m_lHandle > 0) { UpdateData(TRUE); SendSerialData(m_lHandle, (unsigned char *)(LPTSTR)(LPCTSTR)m_strText, m_strText.GetLength()); } }
接收數據處理(這裏僅僅將數據打印到控制檯):
void DataCallBack(long lHandle,unsigned char* pData, int nLen, void* pContext) { unsigned char szData[1024] = {0}; memcpy(szData, pData, nLen); TRACE("%d:%s\n", nLen, szData); } void DisConnectCB(long lHandle, long lType, void* pContext) { TRACE("連接斷開\n"); }
關閉串口:
void CSerialDemonDlg::OnClose() { if (m_lHandle > 0) { CloseSerialPort(m_lHandle); m_lHandle = -1; } CDialog::OnClose(); }
個人demon啓動界面以下:
首先,咱們使用窗口工具建立一個串口對,我這裏使用的串口是COM5,因此我創建了一個串口對COM5和COM6
添加完成後能夠看到機器上多了兩個虛擬串口COM5和COM6,那麼接下來咱們就能夠鏈接這兩個串口來發送和讀取數據了,首先咱們測試數據發送。
在COM5上發送數據,在COM6上接收數據, 首先,咱們使用第三方的調試工具amcktszs在COM6上讀取數據
此時右側是沒有任何數據的,咱們啓動個人demon,而後發送數據12345,查看測試工具上右側讀取的數據
能夠看到測試工具已經讀取到了我發送的123456字符串,它是以16進制顯示的,正好對應爲"31 32 33 34 35"
接下來,咱們在COM6上發送數據--測試工具,而後在COM5上讀取數據-個人demon,看看demon控制檯打印:
以上就是使用boost庫實現了一個跨平臺的串口通訊庫的開發,該庫能夠支持多個com口同時通訊,若是須要該庫的源碼請聯繫我的付費獲取,若是須要技術交流,請進入羣聊。
源碼獲取、合做、技術交流請獲取以下聯繫方式:
QQ交流羣:961179337
微信帳號:lixiang6153 公衆號:IT技術快餐 電子郵箱:lixx2048@163.com