一、前言
前面我写了一篇《boost开发网络库》一文,该文章介绍了使用boost库开发一个高效、稳定的网络通信库,其中用到了c++准标准库boost的asio网络通信模块,本文将要讲的是使用boost开发usb串口,正好也用到了asio,我之前文章中说过asio不仅仅包含网络通信,还包括串口,接下来我将带大家讲解使用boost库实现串口的通信。(当然,我们完全可以使用windows本地api实现类似功能)
串口是一种非常通用的设备通信的协议(不要与通用串行总线Universal Serial Bus(USB)混淆)。大多数计算机包含两个基于RS232的串口。串口同时也是仪器仪表设备通用的通信协议;很多GPIB兼容的设备也带有RS-232口。同时,串口通信协议也可以用于获取远程采集设备的数据。
串口通信的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。比如IEEE488定义并行通行状态时,规定设备线总长不得超过20米,并且任意两个设备间的长度不得超过2米;而对于串口而言,长度可达1200米。
典型地,串口用于ASCII码字符的传输。通信使用3根线完成:(1)地线,(2)发送,(3)接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但不是必须的。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通行的端口,这些参数必须匹配。
首先,我们要了解串口通信的几个概念:
- 波特率
这是一个衡量符号传输速率的参数。它表示每秒钟传送的符号的个数。例如300波特表示每秒钟发送300个符号。当我们提到时钟周期时,我们就是指波特率,例如如果协议需要4800波特率,那么时钟是4800Hz。这意味着串口通信在数据线上的采样率为4800Hz。通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信。
- 数据位
这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据不会是8位的,标准的值是5、6、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下。
首先,我的目的很简单,就是实现串口连接、关闭、发送数据、接收数据,所以我的接口也是这几个功能:
// ------------------------------------------------------------------------------
// 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();
三、测试
关于串口的测试就稍微有点麻烦,因为一般的机器现在很少有串口了,为了测试,我们可以使用串口模拟辅助工具:
- Configure Virtual Serial Port Driver
虚拟串口模拟工具,可以通过该软件在本机添加一个串口对,然后你的程序就可以模拟连接该COM口了
- amcktszs
串口测试工具,为了查看发送的数据,我们可以从另一个串口中读取数据,在测试之前,我们先看看我的库的调用流程
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。