Qt usb通讯

Posted 凉天满月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Qt usb通讯相关的知识,希望对你有一定的参考价值。

一、前言

  Qt通讯方式有很多,如Tcp/Ip、串口等,但对Usb通讯支持较弱,此篇主要描述Qt与plc设备通过usb进行通讯的解决方法;

  开发环境:Qt5.5、VS2013

  优势:支持热插拔usb线

二、实现

  1、采用线程,通过hidapi方式读写usb信息

  1)hidapi源码下载地址:https://github.com/signal11/hidapi

  2)定义usb描述符

          hid_device *m_Handle;

  3)线程中,m_Handle默认为空,定时1秒检测是否有接入usb

 1 void SerialThread::run()
 2 {
 3     while(m_IsRun)
 4     {
 5         m_Mutex.lock();
 6         if(NULL == m_Handle)
 7             openUsb();
 8 
 9         if(m_StartSend)
10             sendData();
11         m_Mutex.unlock();
12 
13         if(NULL == m_Handle)
14             msleep(1000);
15         else
16             exec();
17     }
18 }
线程读写

  4)根据指定pid、vid,打开usb口

 1 void SerialThread::openUsb()
 2 {
 3     int result = hid_init();
 4 
 5     if(0 != result)
 6         emit funcSig(SERIAL_FIND, QVariantList() << 1);
 7     else
 8     {
 9         //打开pid=0x1FC9,vid=0x00A2的usb
10         m_Handle = hid_open(0x1FC9, 0x00A2, NULL);
11         if(NULL == m_Handle)
12         {
13             //判断是否第一次open,第一次需报错
14             if(m_IsSendError)
15             {
16                 m_IsSendError = false;
17                 emit funcSig(SERIAL_OPEN, QVariantList() << 1);
18             }
19         }
20         else
21         {
22             hid_set_nonblocking(m_Handle, 1);//非阻塞方式
23         }
24     }
25 }
打开usb

  5)使用函数hid_read/hid_write读写usb口,例如:写入开始测试

 1 void SerialThread::sendData()
 2 {
 3     int len = 0;
 4     int write = PACKET_LEN + 1;
 5     unsigned char buf[PACKET_LEN + 2] = {0};
 6 
 7     buf[0] = 0x00;
 8 
 9     buf[1] = 0xeb;
10     buf[2] = 0x90;
11     buf[3] = 0x02;
12     buf[4] = 0xff;
13 
14     buf[5] = 0xff;
15     buf[6] = 0x03;
16     buf[7] = 0xff;
17     buf[8] = 0x00;
18 
19     m_StartSend = false;
20     len = hid_write(m_Handle, buf, write);
21     if(write == len)
22         emit funcSig(SERIAL_SEND, QVariantList() << 0);
23     else
24         emit funcSig(SERIAL_SEND, QVariantList() << 1);
25 }
写usb口

  6)读取usb口数据

 1 void SerialThread::readUsb()
 2 {
 3     unsigned char buf[ONE_PACKET_LEN + 1] = {0};
 4 
 5     //读取usb数据
 6     int len = hid_read(m_Handle, buf, ONE_PACKET_LEN);
 7     if(len > 0)
 8     {
 9         //存入缓存
10         for(int index = 0;index < len;index++)
11             m_ByteArray.append(buf[index]);
12         //长度大于等于一条指令长度时,进行解析
13         if(m_ByteArray.length() >= ONE_PACKET_LEN)
14         {
15             parseData();
16             m_ByteArray.remove(0, ONE_PACKET_LEN);
17         }
18     }
19 }
读取usb

 

  2、解决粘包

  1)读取到的数据,先存在m_ByteArray中

  2)当m_ByteArray的长度大于等于一条指令长度时进行解析

  3)解析时注意先把char转为16进制,再进行数值提取,如下提取第5为数据

  QByteArray(1, bytes.at(4)).toHex().toInt(&ok, 16);

 

  3、usb热插拔

  1)QWidget对象中注册usb事件

 1 void Widget::registerDevice()
 2 {
 3     const GUID GUID_DEVINTERFACE_LIST[] = {
 4         { 0xA5DCBF10, 0x6530, 0x11D2, { 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED } }, //USB设备的GUID
 5         { 0x53f56307, 0xb6bf, 0x11d0, { 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b } }};
 6 
 7     HDEVNOTIFY hDevNotify;
 8     DEV_BROADCAST_DEVICEINTERFACE NotifacationFiler;
 9     ZeroMemory(&NotifacationFiler,sizeof(DEV_BROADCAST_DEVICEINTERFACE));
10     NotifacationFiler.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
11     NotifacationFiler.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
12 
13     for (int i = 0; i < sizeof(GUID_DEVINTERFACE_LIST)/sizeof(GUID); i++)
14     {
15         NotifacationFiler.dbcc_classguid = GUID_DEVINTERFACE_LIST[i];
16         hDevNotify = RegisterDeviceNotification((HANDLE)this->winId(), &NotifacationFiler, DEVICE_NOTIFY_WINDOW_HANDLE);
17         if (!hDevNotify)
18             qCritical() << QStringLiteral("注册失败!");
19     }
20 }
注册Usb事件

  2)继承QWidget的nativeEvent事件,原型如下

        bool nativeEvent(const QByteArray &eventType, void *message, long *result);

 1 bool Widget::nativeEvent(const QByteArray &eventType, void *message, long *result)
 2 {
 3     Q_UNUSED(eventType);
 4     Q_UNUSED(result);
 5 
 6     MSG *msg = reinterpret_cast<MSG *>(message);
 7 
 8     int msgType = msg->message;
 9     if (WM_DEVICECHANGE == msgType)
10     {
11         PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)msg->lParam;
12         switch (msg->wParam)
13         {
14         case DBT_DEVICEARRIVAL:
15         {
16             if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype)
17             {
18                 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
19                 if (0 == lpdbv->dbcv_flags)
20                     m_TipQlb->setText("已检测到USB设备插入");
21                 //else if (DBTF_MEDIA == lpdbv->dbcv_flags)
22                     //qDebug() << "CD_Arrived.";
23             }
24             else if (DBT_DEVTYP_DEVICEINTERFACE == lpdb->dbch_devicetype)
25             {
26                 PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb;
27                 QString name = QString::fromWCharArray(pDevInf->dbcc_name);
28 
29                 checkUsb(name);
30             }
31         }
32             break;
33         case DBT_DEVICEREMOVECOMPLETE:
34             if (DBT_DEVTYP_VOLUME == lpdb->dbch_devicetype)
35             {
36                 PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
37                 if (0 == lpdbv->dbcv_flags)
38                     m_TipQlb->setText("USB设备已拔出");
39 
40                 if (DBTF_MEDIA == lpdbv->dbcv_flags)
41                     m_TipQlb->setText("CD_Removed.");
42             }
43             break;
44         }
45     }
46 
47     return false;
48 }
插拔Usb检测

        3)检测到指定的pid、vid后,重新打开usb

 1 void Widget::checkUsb(const QString &name)
 2 {
 3     int errorCode = 0;
 4 
 5     if (name.contains("USB#"))
 6     {
 7         QStringList listAll = name.split(\'#\');
 8         QStringList listID = listAll.at(1).split(\'&\');
 9         QString vid = listID.at(0).right(4);
10         QString pid = listID.at(1).right(4);
11         if(0 == vid.compare("1FC9", Qt::CaseInsensitive) &&
12            0 == pid.compare("00A2", Qt::CaseInsensitive))
13         {
14             errorCode = m_SerialThread->resetOpen();
15             if(0 == errorCode)
16                 m_TipQlb->setText("重新打开USB,成功");
17             else if(1000 == errorCode)
18                 m_TipQlb->setText("初始化USB,失败");
19             else
20                 m_TipQlb->setText("重新打开USB,失败");
21         }
22     }
23 }
检查插入的usb

  4)线程中执行打开操作,创建新的usb描述符

 1 int SerialThread::resetOpen()
 2 {
 3     int errorCode = 0;
 4     int result = hid_init();
 5 
 6     if(0 != result)
 7         errorCode = 1000;
 8     else
 9     {
10         m_Handle = hid_open(0x1FC9, 0x00A2, NULL);
11         if(NULL == m_Handle)
12             errorCode = 1001;
13         else
14             hid_set_nonblocking(m_Handle, 1);
15     }
16 
17     return errorCode;
18 }
重新打开usb

  5)此时可继续读写usb口

USB通讯原理

USB是轮询总线,USB主机与设备之间的数据交换都是由主机发起的,设备端只能被动的响应。USB数据传入或传出 USB 设备中的端点。

USB 主机中的客户端将数据存储在缓冲区中,USB主机没有端点的概念。

USB Host 和外围 USB Device 有不同的层,如下图所示。各层之间的连接是每个水平层之间的逻辑主机-设备接口。在逻辑连接之间使用USB Pipes传输数据。

USB通讯过程

一次完整的通信分为三个过程:请求过程(令牌包)、数据过程(数据包)和状态过程(握手包),没有数据要传输时,跳过数据过程。
通信过程包含以下三种情况:


主机发送令牌包(Token)开始请求过程,如果请求中声明有数据要传输则有数据过程,最后由数据接收方(有数据过程)或从机(无数据过程)发起状态过程,结束本次通信。
与USB全速设备通信时,主机将每秒等分为1000个帧(Frame)。主机在每帧开始时,向所有从机广播一个帧起始令牌包(Start Of Frame,SOF包)。它的作用有两个:一是通知所有从机,主机的USB总线正常工作;二是从机以此同步主机的时序。
  与USB高速设备通信时,主机将帧进一步等分为8个微帧(Microframe),每个微帧占125μ \\muμs。在同一帧内,8个微帧的帧号都等于当前SOF包的帧号。

管道PIPE

管道分为两种类型:

  • 消息管道具有已定义的 USB 格式并受主机控制。消息管道允许数据双向流动并且仅支持控制传输
  • 流管道没有定义的 USB 格式,可以由主机或设备控制。数据流具有预定义的方向,即INOUT。流管道支持中断传输同步传输批量传输
    当 USB 设备连接到 USB 总线并由 USB 主机配置时,大多数管道就会存在。管道源自主机客户端内的数据缓冲区,并在 USB 设备内的端点处终止。

传输

传输(数据流类型)可以由一个或多个事务组成。管道仅支持以下传输类型之一:

  • 控制传输通常用于设置 USB 设备。他们总是使用 IN/OUT 端点 0。
  • 中断传输可用于定期发送数据的地方,例如状态更新。
  • 同步传输传输实时数据,例如音频和视频。它们有保证的固定带宽,但没有错误检测。
  • 批量传输可用于在时间不重要的情况下发送数据,例如发送到打印机。

事务

数据在所谓的事务中传输。通常,它们由三个数据包组成:

  • 令牌包是定义事务类型和方向、设备地址和端点的标头。
  • 数据以数据包的形式传输。
  • 交易的最终状态是握手包中的确认。

  • 在事务中,数据从 USB 主机传输到 USB 设备,反之亦然。传输方向在从 USB 主机发送的令牌包中指定。然后,源发送一个数据包或指示它没有数据要传输。一般情况下,目的地会以握手包进行响应,指示传输是否成功。

数据包

数据包可以被认为是数据传输的最小元素。每个数据包以当前传输速率传输整数个字节。数据包以同步模式开始,随后是数据包的数据字节,并以数据包结束 (EOP) 信号结束。所有 USB 数据包模式都先传输最低有效位。数据包前后,总线处于空闲状态。


一个特殊的数据包是将 USB 总线分成时间段的帧起始数据包 (SOF)。每个管道在每个帧中分配一个时隙。Start-of-Frame 数据包在全速链路上每 1ms 发送一次。在高速下,1ms 帧被分成 8 个微帧,每个微帧 125μs。Start-of-Frame 数据包在每个微帧的开头使用相同的帧号发送。帧号每 1ms 递增一次。

端点

端点可以描述为数据源或接收器,并且仅存在于 USB 设备中。存储在端点的数据可以从 USB 主机接收或等待发送到 USB 主机。端点可以配置为支持USB 规范中定义的四种传输类型(控制传输中断传输同步传输批量传输)。在硬件限制范围内,端点可以使用 USB 中间件进行配置(例如,将端点限制为某种传输类型)。

端点充当一种缓冲区。例如,USB 主机的客户端可以向端点 1 发送数据。来自 USB 主机的数据将被发送到OUT 端点 1. 微控制器上的程序将在准备好后立即读取数据。返回数据必须写入IN Endpoint 1,因为程序无法自由访问 USB 总线(USB 总线由 USB 主机控制)。IN Endpoint 1 中的数据一直保留在那里,直到主机向 Endpoint 1 发送 IN 数据包请求数据。

这些规则适用于所有微控制器设备:

  • 一个设备最多可以有16 个 OUT和16 个 IN端点。
  • 每个端点只能有一个 传输 方向。
  • 端点 0仅用于控制传输,不能分配给任何其他功能。

端点的总数和每个端点的数据大小由底层硬件定义。

  • OUT总是指从主机指向设备的方向。
  • IN总是指指向主机的方向。

¥打赏

以上是关于Qt usb通讯的主要内容,如果未能解决你的问题,请参考以下文章

安卓USB主机通讯

QT 实用代码片段

STM32 USB使用记录:使用CDC类虚拟串口(VCP)进行通讯

异常和TCP通讯

python 一个终端代码片段,在mac上生成可启动的usb live CD,以运行类似ubuntu或debian的内容。

qt usb通信添加动态库