具有虚拟COM端口的USB串行设备 - 如果使用带USB路径的

时间:2016-11-23 22:42:43

标签: c++ winapi usb

我有一个销售点应用程序,它使用串行通讯端口(RS-232)与称重产品的秤进行通信。我现在正致力于直接支持USB设备,而不是使用虚拟串行通信端口,因为它们有一种令人讨厌的移动趋势。

我们发现虽然Windows 7似乎会自动创建虚拟串行通信端口,但其他版本的Windows(如POS Ready 7)可能不会。我们怀疑这是由于POS Ready 7中缺少一个特定的.inf文件,但是有人确认了吗?

我有一个间歇性工作的USB示例应用程序。我在使用ReadFile() Windows API函数进行USB级别通信时遇到问题。我使用CreateFile()指定USB设备路径来获取I / O句柄,然后使用WriteFile()ReadFile()与比例进行通信。 ReadFile()在某些情况下不提供数据。

背景资料

我正在使用的特定规模,Brecknell 67xx工作台规模,使用虚拟串行通信端口直接开箱即用的销售点应用程序。我用USB电缆将秤连接到我的Windows 7 PC,Windows自动安装了驱动程序,在我的情况下创建了一个虚拟串口COM4。然后,我将应用程序配置为通过COM4与比例进行通信,一切正常。

使用比例的协议是将两个字节的命令“W \ r”(大写字母W后跟回车符)发送到比例,然后读取包含当前权重的16字节响应以及有关比例力学的状态信息,例如In Motion。

我正在学习的示例USB应用程序将成功提供重量。然后,它会在ReadFile()返回零字节读取的行为中停止正常工作。一旦它停止工作,它将继续无法提供来自ReadFile()的数据,即使我拔下并重新插上USB电缆或重新启动我的电脑。

学习应用程序的先前版本挂在ReadFile()上,当使用Visual Studio完成Break All时,会显示一个暂停,然后显示一条指示死锁的消息。但是,由于我开始在SetCommTimeouts()中使用ReadTotalTimeoutConstant超时值为5000毫秒,因此在ReadFile()返回之前看到一致的5秒暂停,读取零字节。

奇怪的是,如果我然后使用打开虚拟串行通信端口COM4的应用程序,该应用程序工作正常,并且比例报告项目的权重。

然后我可以返回使用直接USB而不是虚拟串行通信端口的示例应用程序,它将正常工作报告权重。

然而,如果我然后拔掉连接秤的USB线连接秤,它也会关闭秤,然后重新插入USB线,示例应用程序不再正常工作,我再次看到暂停超时。< / p>

然后我尝试使用原始销售点应用程序,该应用程序依赖于使用虚拟串行端口COM4的串行通信端口,并且该应用程序称重项目就好了。

然后,当我重试我的示例应用程序时,它也会报告项目权重。

我的问题。

如果USB设备在插入时创建了一个虚拟串行通信端口,那么是否只需要通过CreateFile()呼叫指定通信端口COM4(在我的情况下为COM4)来使用虚拟串行端口? / p>

如果设备导致Windows生成虚拟通信端口,如何使用CreateFile()与USB设备路径进行直接USB串行通信?

是否有某种方法可以指定任何版本的Windows在插入设备时自动为设备创建虚拟串行通信端口?

样本USB应用程序的源代码

使用Visual Studio 2005的示例USB Windows控制台应用程序的源代码如下,主要位于底部,其中大部分是查找特定USB设备然后允许ReadFile()和{ {1}}:

WriteFile()

开发的其他信息

Microsoft MSDN https://msdn.microsoft.com/en-us/windows/hardware/drivers/install/overview-of-inf-files

中的INF文件概述

Stackoverflow Do I need to write my own host side USB driver for a CDC device

Stackoverflow how to get vendor id and product id of a plugged usb device on windows

Is it possible to “transplant” drivers between machines?有一个指向文档Debugging USB Device Installation on Windows的链接,此帖子Remove Windows Device Class in Registry包含更多信息。

来自Microsoft的

USB serial driver (Usbser.sys)

来自Microsoft的

USB device class drivers included in Windows

3 个答案:

答案 0 :(得分:2)

运行Windows(USB主机)和秤(USB设备)的PC的通信遵循USB协议。如果您为Windows安装 libusb ,则可以在使用lsusb -v时获得与PC从USB设备获取的类似信息。 USB设备可以实现多个USB类。

如果USB设备创建了一个虚拟COM端口,它肯定会实现CDC ACM类(通信设备类抽象控制模型),它还可以实现其他USB类,如海量存储类,......

与USB设备的直接通信还取决于它实现的设备类及其接口和端点。如果USB设备实现了CDC ACM(虚拟COM),则使用特定的RS-232命令(即https://www.commfront.com/pages/3-easy-steps-to-understand-and-control-your-rs232-devices或将十六进制“D”发送到万用表以接收测量值)如果它实现了Mass Storage类您通常使用批量转移

要更改USB设备的模式,请使用控制传输(简要参见USB)

在此链接中,Win确定设备的USB类{}后,确定要加载哪个驱动程序https://msdn.microsoft.com/en-us/library/windows/hardware/ff538820%28v=vs.85%29.aspxhttps://msdn.microsoft.com/en-us/library/windows/hardware/jj649944%28v=vs.85%29.aspx

我不知道Brecknell如何实现作为虚拟COM的CDC ACM设备类,但是通常任何支持USB的Win版本都应该能够为CDC ACM设备类(虚拟COM)加载驱动程序,所以你这是正确的似乎是 .inf驱动程序文件的问题或驱动程序加载机制(可能是Brecknell CDC ACM实现的问题,但我不这么认为)

然后,如果Win加载了一个正常工作的驱动程序,那么就是你所做的:使用CreateFile()和分配给USB设备的COM。

奇怪的是,如果我然后使用打开虚拟串行通信端口COM4的应用程序,那个应用程序工作正常,并且比例报告项目的重量。&lt; - 这不是奇怪的是,有些Win版本无法识别CDC USB设备。 CDC设备的标准驱动程序似乎是USBser.syshttps://msdn.microsoft.com/de-de/library/windows/hardware/dn707976%28v=vs.85%29.aspx) 如果您搜索“Windows无法识别CDC设备”,您将获得结果

如果USB设备在插入时创建了一个虚拟串行通信端口,那么是否需要通过在CreateFile()调用中指定通信端口COM4(在我的情况下为COM4)来使用虚拟串行端口? 是的,如果USB设备实现虚拟COM,则使用此COM与此设备通信是最简单的方法

另请参见http://www.beyondlogic.org/usbnutshell/usb1.shtml USB简介

标准USB:设备描述符(类) - &gt;界面 - &gt; (配置) - &gt;端点

答案 1 :(得分:2)

使用修改后的USB串行示例应用程序进行测试表明,当拔出创建虚拟串行通信端口的USB设备时,创建的虚拟串行端口将被拆除,并从控制面板的设备管理器应用程序中的端口列表中消失。

当设备(在这种情况下为USB刻度)插入并打开时,虚拟串行通信端口会重新出现在设备管理器中。但是,在创建虚拟串行通信端口时,会使用默认串行端口设置(波特率,奇偶校验等)创建它,这些可能与实际设备不同。

总之,无论端口是作为COM端口打开还是USB设备路径名与CreateFile()一起使用,虚拟串行通信端口设置似乎都适用。

我仍在调查使用POS Ready 7时不会自动创建虚拟串行端口,并且在我知道更多后会更新此答案。但是,Windows 7和POS Ready 7之间的初步比较显示,我的Windows 7 PC上指定usbser.sys,mdmcpq.inf的文件不在文件夹C:\ Windows \中的POS Ready 7终端上INF。

有关.inf文件结构和各个部分的详细信息,请参阅The INF File。它有点陈旧,但似乎以可读的方式涵盖了基础知识。

我将问题中的函数CreateEndPoint()修改为以下内容以及对类和构造函数的更改,以便为我的比例创建一组默认通信端口设置。

类和构造函数现在包含一组通信端口设置的默认值(9600波特,7个数据位,一个停止位,甚至是标度的奇偶校验),如下所示:

class UsbSerialDevice
{
public:
    UsbSerialDevice();
    UsbSerialDevice(DWORD BaudRate, BYTE ByteSize = 8, BYTE Parity = NOPARITY, BYTE StopBits = ONESTOPBIT);
    ~UsbSerialDevice();
    int CreateEndPoint (wchar_t *wszVendorId);
    int SetCommPortSettings (DWORD BaudRate, BYTE ByteSize = 8, BYTE Parity = NOPARITY, BYTE StopBits = ONESTOPBIT);
    int CloseEndPoint ();
    int ReadStream (void *bString, size_t nBytes);
    int WriteStream (void *bString, size_t nBytes);
    int UpdateSettingsProxy (void);

    DWORD   m_dwError;          // GetLastError() for last action
    DWORD   m_dwErrorWrite;     // GetLastError() for last write
    DWORD   m_dwErrorRead;      // GetLastError() for last read
    DWORD   m_dwErrorCommState;
    DWORD   m_dwErrorCommTimeouts;
    DWORD   m_dwBytesWritten;
    DWORD   m_dwBytesRead;

    COMMTIMEOUTS  m_timeOut;           // last result from GetCommTimeouts(), updated by UpdateSettingsProxy()
    COMSTAT       m_statOut;           // last result from ClearCommError()
    DCB           m_commSet;           // last result from GetCommState(), updated by UpdateSettingsProxy()

private:
    HANDLE        m_hFile;
    DWORD         m_dwStatError;
    DCB           m_commSetDefault;    // the defaults used as standard
    wchar_t       m_portName[24];      // contains portname if defined for device in form \\.\\COMnn
};

UsbSerialDevice::UsbSerialDevice() :
    m_dwError(0),
    m_dwErrorWrite(0),
    m_dwErrorRead(0),
    m_dwBytesWritten(0),
    m_dwBytesRead(0),
    m_hFile(NULL)
{
    // initialize our COM port settings and allow people to change with 
    memset (&m_commSetDefault, 0, sizeof(m_commSetDefault));
    m_commSetDefault.DCBlength = sizeof(m_commSetDefault);
    m_commSetDefault.BaudRate = CBR_9600;
    m_commSetDefault.ByteSize = 7;
    m_commSetDefault.Parity = EVENPARITY;
    m_commSetDefault.StopBits = ONESTOPBIT;
    m_commSet.fDtrControl = DTR_CONTROL_DISABLE;
    m_portName[0] = 0;
}

修改了函数CreateEndPoint(),以便在使用USB设备的路径名执行CreateFile()打开USB设备后,它现在还将设置通信端口参数。

该方法的另一个实验性更改是检查是否还创建了通信端口名称,如果是,则生成与CreateFile()一起使用的正确COM端口规范。我计划将CreateEndPoint()方法拆分为两种方法,一种用于查找USB设备,另一种用于在我继续调查时实际打开。

COM端口大于COM9的CreateFile() COM端口说明符的格式似乎需要\\.\作为前缀。请参阅Microsoft支持部门的HOWTO: Specify Serial Ports Larger than COM9

CreateEndPoint()的新版本如下:

int UsbSerialDevice::CreateEndPoint (wchar_t *wszVendorId)
{
    HDEVINFO    hDevInfo;


    m_dwError = ERROR_INVALID_HANDLE;

    // We will try to get device information set for all USB devices that have a
    // device interface and are currently present on the system (plugged in).
    hDevInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, NULL, 0, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
    if (hDevInfo != INVALID_HANDLE_VALUE)
    {
        DWORD    dwMemberIdx;
        BOOL     bContinue = TRUE;
        SP_DEVICE_INTERFACE_DATA         DevIntfData;

        // Prepare to enumerate all device interfaces for the device information
        // set that we retrieved with SetupDiGetClassDevs(..)
        DevIntfData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
        dwMemberIdx = 0;

        // Next, we will keep calling this SetupDiEnumDeviceInterfaces(..) until this
        // function causes GetLastError() to return  ERROR_NO_MORE_ITEMS. With each
        // call the dwMemberIdx value needs to be incremented to retrieve the next
        // device interface information.
        for (BOOL bContinue = TRUE; bContinue; ) {
            PSP_DEVICE_INTERFACE_DETAIL_DATA  DevIntfDetailData;
            SP_DEVINFO_DATA    DevData;
            DWORD  dwSize;

            dwMemberIdx++;
            SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &GUID_DEVINTERFACE_USB_DEVICE, dwMemberIdx, &DevIntfData);

            if (GetLastError() == ERROR_NO_MORE_ITEMS) break;

            // As a last step we will need to get some more details for each
            // of device interface information we are able to retrieve. This
            // device interface detail gives us the information we need to identify
            // the device (VID/PID), and decide if it's useful to us. It will also
            // provide a DEVINFO_DATA structure which we can use to know the serial
            // port name for a virtual com port.

            DevData.cbSize = sizeof(DevData);

            // Get the required buffer size. Call SetupDiGetDeviceInterfaceDetail with
            // a NULL DevIntfDetailData pointer, a DevIntfDetailDataSize
            // of zero, and a valid RequiredSize variable. In response to such a call,
            // this function returns the required buffer size at dwSize.

            SetupDiGetDeviceInterfaceDetail(hDevInfo, &DevIntfData, NULL, 0, &dwSize, NULL);

            // Allocate memory for the DeviceInterfaceDetail struct. Don't forget to
            // deallocate it later!
            DevIntfDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
            DevIntfDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);

            if (SetupDiGetDeviceInterfaceDetail(hDevInfo, &DevIntfData, DevIntfDetailData, dwSize, &dwSize, &DevData))
            {
                // Finally we can start checking if we've found a useable device,
                // by inspecting the DevIntfDetailData->DevicePath variable.
                //
                // The DevicePath looks something like this for a Brecknell 67xx Series Serial Scale
                // \\?\usb#vid_1a86&pid_7523#6&28eaabda&0&2#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
                //
                // The VID for a particular vendor will be the same for a particular vendor's equipment.
                // The PID is variable for each device of the vendor.
                //
                // As you can see it contains the VID/PID for the device, so we can check
                // for the right VID/PID with string handling routines.

                // See https://github.com/Microsoft/Windows-driver-samples/blob/master/usb/usbview/vndrlist.h

                if (wcsstr (DevIntfDetailData->DevicePath, wszVendorId)) {
                    HKEY   hKey;

                    m_dwError = 0;
                    // To find out the serial port for our scale device,
                    // we'll need to check the registry:
                    hKey = SetupDiOpenDevRegKey(hDevInfo, &DevData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ);

                    if (hKey != INVALID_HANDLE_VALUE) {
                        DWORD    dwSize = 0, dwType = 0;
                        wchar_t  lpData[16] = {0};

                        dwType = REG_SZ;
                        dwSize = sizeof(lpData);
                        LONG queryStat = RegQueryValueEx(hKey, _T("PortName"), NULL, &dwType, (LPBYTE)&lpData[0], &dwSize);
                        RegCloseKey(hKey);
                        if (queryStat == ERROR_SUCCESS) {
                            wcscpy (m_portName, L"\\\\.\\");
                            wcsncat (m_portName, lpData, dwSize / sizeof(wchar_t));
                        }
                    } else {
                        m_dwError = GetLastError();
                    }

                    m_hFile = CreateFile (DevIntfDetailData->DevicePath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
                    if (m_hFile == INVALID_HANDLE_VALUE) {
                        m_dwError = GetLastError();
                    } else {
                        m_dwError = 0;
                        GetCommState (m_hFile, &m_commSet);
                        m_commSet = m_commSetDefault;
                        SetCommState (m_hFile, &m_commSet);
                        m_dwErrorCommState = GetLastError();
                        GetCommState (m_hFile, &m_commSet);

                        GetCommTimeouts (m_hFile, &m_timeOut);
                        m_timeOut.ReadIntervalTimeout = 0;
                        m_timeOut.ReadTotalTimeoutMultiplier = 0;
                        m_timeOut.ReadTotalTimeoutConstant = 5000;
                        SetCommTimeouts (m_hFile, &m_timeOut);
                        GetCommTimeouts (m_hFile, &m_timeOut);
                        m_dwErrorCommTimeouts = GetLastError();
                    }
                    bContinue = FALSE;    // found the vendor so stop processing after freeing the heap.
                }
            }

            HeapFree(GetProcessHeap(), 0, DevIntfDetailData);
        }

        SetupDiDestroyDeviceInfoList(hDevInfo);
    }

    return 0;
}

POS Ready 7调查

回顾一下似乎与规模兼容的Windows 7 PC,我们使用“控制面板”中的“设备管理器”查看了虚拟串行通信端口的驱动程序详细信息。驱动程序详细信息表明正在使用的驱动程序是www.winchiphead.com提供的CH341S64.SYS和Property&#34; Inf name&#34;值为oem50.inf。我找到了一个论坛帖子http://doityourselfchristmas.com/forums/showthread.php?14690-CH340-USB-RS232-Driver,其中提供了http://www.winchiphead.com/download/CH341/CH341SER.ZIP驱动程序下载的链接,但http://www.wch.cn/download/CH341SER_ZIP.html提供的其他版本可能更新。

将下载的zip文件CH341SER.ZIP从稍后放入POS Ready 7终端,解压缩内容并在文件夹SETUP.EXE中运行CH341SER(zip中有两个文件夹)文件和一个名为INSTALL的文件似乎用于设备开发),它显示了一个对话框并允许我安装CH341SER.INF。安装完成后,当我插入USB秤时,设备被识别并创建了一个虚拟串行通信端口,并且我的测试应用程序正常工作。

我确实找到了一些文件,但都是中文的。 Google Translate提供了USB设备文档的可读版本。当在使用中可能拔出/重新插入秤时,看起来还有其他工作要做设备管理。

另一个奇怪的事情是规模现在使用不同的COM端口名称COM5而不是COM4。查看高级设置,看来COM4是&#34;使用中&#34;虽然没有显示在端口列表中。进一步的实验表明,用于秤设备的COM端口名称取决于插入两个前面板USB端口中的哪一个。我最初插入左侧,今天插入正确的USB端口,其结果是使用新的COM端口名称创建了虚拟串行通信端口。

但是,由于我们在CreateFile()中使用USB路径,因此USB样本测试应用程序无需进行任何更改。

使用三条USB转串口转换器电缆进行POS Ready 7的进一步测试表明,不同供应商的电缆在USB路径中具有相同的供应商ID和产品代码。 USB路径也根据插入电缆的USB端口而改变。在某些情况下,只有路径名中的最后一位数字不同。一个有趣的实验是,如果USB集线器连接到USB端口,然后USB连接到集线器,那么路径名称是什么样的呢?

答案 2 :(得分:0)

你混淆了两个问题,我们可能不可能将它们区分开来。

我这样说是因为您将ReadFile问题与设备名称相关联。但是,ReadFile适用于 HANDLE 。获取名称并将其转换为HANDLE的函数称为CreateFile。这意味着ReadFile甚至知道关于它正在运行的名称。

这种误解也解释了其他一些行为。例如,当您拔下设备时,HANDLE变为无效,并且无效。重新插入设备可能会恢复名称,但不能恢复HANDLE