C# 實現自定義的USB設備與上位機進行通信(上位機部分)


  因為以前沒用過USB,對USB也不了解,於是上網查了很多資料,不過網上的資料都是零零散散,不清不楚的,於是我自己總結了一下,下面幾個鏈接是網上這么多零散資料里,我覺得比較有參考意義的。

  USB設備連接思路參考:https://www.cnblogs.com/xyzyx/articles/2959610.html#undefined

  代碼參考:http://www.cnblogs.com/xidongs/archive/2011/11/28/2266100.html#

  收發數據參考:https://blog.csdn.net/zouwen198317/article/details/5814212

  整個思路概況為3步:1、識別設備;2、連接設備;3、數據傳輸。

  而我這里所有的步驟都是基於直接調用Windos的DLL實現的,調用了hid.dll、setupapi.dll、kernel32.dll,建立好工程后,可以從你自己的電腦里復制出這幾個文件到工程目錄下使用,調用DLL思路如下:

            /*          使用USB傳輸數據思路
             * 0.連接DALL的庫函數做好准備工作
             * 1.調用HidD_GetHidGuid獲取到GUID
             * 2.調用SetupDiGetClassDevs獲取全部的HID值
             * 3.調用SetupDiEnumDeviceInterfaces,填寫PSP_DEVICE_INTERFACE_DATA結構數據項,
             *   該結構用於識別一個HID設備接口,結構具體作用參考函數說明
             * 4.調用SetupDiGetDeviceInterfaceDetail獲取特定的HID路徑名
             * 5.調用CreateFile獲得設備HID句柄
             * 6.調用HidD_GetAttributes,填寫HID_ATTRIBUTES結構的數據項,
             *   該結構包含設備的廠商ID、產品ID和產品序列號,程序可比照這些獲取到的參數值確定該設備是否是查找的設備
             * 7.查找成功完成,未成功重復3 4 5 6 步
             * 8.與自己需要的USB設備連接成功后,調用WriteFile和ReadFile進行數據傳輸
             * 9.關閉時調用SetupDiDestroyDeviceInfoList(hDevInfo)和CloseHandle(HidHandle)斷開連接並關閉USB設備
             */

  接下來開始按上面的步驟來操作。

0、連接DALL的庫函數做好准備工作
/*  ======================聲明dll文件中需要調用的函數,以下是調用windows的API的函數======================= */

        //獲得USB設備的GUID
        [DllImport("hid.dll")]
        public static extern void HidD_GetHidGuid(ref Guid HidGuid);
        Guid guidHID = Guid.Empty;


        //獲得一個包含全部HID信息的結構數組的指針,具體配置看函數說明:https://docs.microsoft.com/zh-cn/windows/desktop/api/setupapi/nf-setupapi-setupdigetclassdevsw
        [DllImport("setupapi.dll", SetLastError = true)]
        public static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, uint Enumerator, IntPtr HwndParent, DIGCF Flags);
        IntPtr hDevInfo;

        public enum DIGCF   
        {
            DIGCF_DEFAULT = 0x1,
            DIGCF_PRESENT = 0x2,
            DIGCF_ALLCLASSES = 0x4,
            DIGCF_PROFILE = 0x8,
            DIGCF_DEVICEINTERFACE = 0x10
        }

        //該結構用於識別一個HID設備接口,獲取設備,true獲取到
        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern Boolean SetupDiEnumDeviceInterfaces(IntPtr hDevInfo, IntPtr devInfo, ref Guid interfaceClassGuid, UInt32 memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData);
        //SetupDiEnumDeviceInterfaces 識別出來的SP_DEVICE_INTERFACE_DATA結構,該結構標識滿足搜索參數的接口
        public struct SP_DEVICE_INTERFACE_DATA
        {
            public int cbSize;                     //SP_DEVICE_INTERFACE_DATA結構的大小
            public Guid interfaceClassGuid;        //設備接口所屬的類的GUID
            public int flags;                      //接口轉態標記
            public int reserved;                   //保留,不做使用
        }

        // 獲得一個指向該設備的路徑名,接口的詳細信息 必須調用兩次 第1次返回長度 第2次獲取數據 
        [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData,
                                                                   int deviceInterfaceDetailDataSize, ref int requiredSize, SP_DEVINFO_DATA deviceInfoData);

        [StructLayout(LayoutKind.Sequential)]
        public class SP_DEVINFO_DATA
        {
            public int cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
            public Guid classGuid = Guid.Empty; // temp
            public int devInst = 0; // dumy
            public int reserved = 0;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 2)]
        internal struct SP_DEVICE_INTERFACE_DETAIL_DATA
        {
            internal int cbSize;
            internal short devicePath;
        }
         
        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern Boolean SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);

        //獲取設備文件(獲取句柄)
        [DllImport("kernel32.dll", SetLastError = true)]
        //根據要求可在下面設定參數,具體參考參數說明:https://docs.microsoft.com/zh-cn/windows/desktop/api/fileapi/nf-fileapi-createfilea
        private static extern int CreateFile
            (
             string lpFileName,                             // file name 文件名
             uint   dwDesiredAccess,                        // access mode 訪問模式
             uint   dwShareMode,                            // share mode 共享模式
             uint   lpSecurityAttributes,                   // SD 安全屬性
             uint   dwCreationDisposition,                  // how to create 如何創建
             uint   dwFlagsAndAttributes,                   // file attributes 文件屬性
             uint   hTemplateFile                           // handle to template file 模板文件的句柄
            );
      
        [DllImport("Kernel32.dll", SetLastError = true)]  //接收函數DLL private static extern bool ReadFile
            (
                IntPtr hFile,
                byte[] lpBuffer,
                uint nNumberOfBytesToRead,
                ref uint lpNumberOfBytesRead,
                IntPtr lpOverlapped
            );

        [DllImport("kernel32.dll", SetLastError = true)] //發送數據DLL public static extern Boolean WriteFile
            (
                IntPtr hFile,
                byte[] lpBuffer,
                uint nNumberOfBytesToWrite,
                ref uint nNumberOfBytesWrite,
                IntPtr lpOverlapped
            );
        
        [DllImport("hid.dll")]       
        /*HidDeviceObject:指定頂級集合的打開句柄
          Attributes:指向調用者分配的HIDD_ATTRIBUTES結構的指針,該結構返回由HidDeviceObject指定的集合的屬性*/        
        private static extern Boolean HidD_GetAttributes(IntPtr hidDeviceObject, out HIDD_ATTRIBUTES HIDD_ATTRIBUTES);       
        //HidD_GetAttributes的調用者使用此結構來對比查找設備
        public unsafe struct HIDD_ATTRIBUTES
        {
            public int    Size;            //指定HIDD_ATTRIBUTES結構的大小(以字節為單位)
            public ushort VendorID;        //指定HID設備的供應商ID( VID )
            public ushort ProductID;       //指定HID設備的產品ID( PID )
            public ushort VersionNumber;   //指定HIDClass設備的制造商版本號
        }

        //自定義的結構體,用來存放自己要操作的設備信息
        public unsafe struct my_usb_id
        {
            public ushort my_vid;
            public ushort my_Pid;
            public ushort my_number;
        }

        /**/
        [DllImport("hid.dll")]
        /*HidP_GetCaps返回一個頂級集合的 HIDP_CAPS結構,獲取設備具體信息,這里暫時用不上
        PreparsedData:指向頂級集合的預分析數據的指針; Capabilities:指向調用程序分配的緩沖區的指針,該緩沖區用於返回集合的HIDP_CAPS結構*/
        private static extern uint HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities);
        [StructLayout(LayoutKind.Sequential)]
        public unsafe struct HIDP_CAPS
        {
            public ushort UsagePage;                                            //指定頂級集合的使用情況
            public uint   Usage;                                                //指定頂級集合的 使用ID
            public ushort InputReportByteLength;                                //指定所有輸入報告的最大大小(以字節為單位)
            public ushort OutputReportByteLength;                               //指定所有輸出報告的最大大小(以字節為單位)
            public ushort FeatureReportByteLength;                              //指定所有功能報告的最大長度(以字節為單位)
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]               //保留供內部系統使用數組
            public ushort NumberLinkCollectionNodes;                            //指定的數量HIDP_LINK_COLLECTION_NODE了為這個頂級集合返回的結構HidP_GetLinkCollectionNodes
            public ushort NumberInputButtonCaps;                                //指定HidP_GetButtonCaps返回的輸入HIDP_BUTTON_CAPS結構的數量
            public ushort NumberInputValueCaps;                                 //指定HidP_GetValueCaps返回的輸入HIDP_VALUE_CAPS結構的數量
            public ushort NumberInputDataIndices;                               //指定分配給所有輸入報告中的按鈕和值的數據索引數
            public ushort NumberOutputButtonCaps;                               //指定HidP_GetButtonCaps返回的輸出HIDP_BUTTON_CAPS結構的數量
            public ushort NumberOutputValueCaps;                                //指定HidP_GetValueCaps返回的輸出HIDP_VALUE_CAPS結構的數量
            public ushort NumberOutputDataIndices;                              //指定分配給所有輸出報告中的按鈕和值的數據索引數
            public ushort NumberFeatureButtonCaps;                              //指定HidP_GetButtonCaps返回的功能HIDP_BUTTONS_CAPS結構的總數
            public ushort NumberFeatureValueCaps;                               //指定HidP_GetValueCaps返回的功能HIDP_VALUE_CAPS結構的總數
            public ushort NumberFeatureDataIndices;                             //指定分配給所有要素報告中的按鈕和值的數據索引數
        }

        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr PreparsedData);     

        //釋放設備
        [DllImport("hid.dll")]
        static public extern bool HidD_FreePreparsedData(ref IntPtr PreparsedData);

        //關閉訪問設備句柄,結束進程的時候把這個加上保險點
        [DllImport("kernel32.dll")]
        static public extern int CloseHandle(int hObject);

        //查看數據傳輸異常函數
        [DllImport("kernel32.dll", EntryPoint = "GetProcAddress", SetLastError = true)]
        public static extern IntPtr GetProcAddress(int hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);
  上面的DLL調用在 public partial class Form1 : Form{}函數里直接聲明就好了
1~7連接USB設備
  連接前先定義一些要用到的變量,再調用 UsBMethod 函數連接,這個函數里面包含了1~7的所有步驟:
        //定於句柄序號和一些參數,具體可以去網上找這些API的參數說明
        int HidHandle = -1;
        int sele = 0;
        int usb_flag = 0;
        bool result;
        string devicePathName;

        //CreateFile參數配置
        public const uint GENERIC_READ = 0x80000000;
        public const uint GENERIC_WRITE = 0x40000000;
        public const uint FILE_SHARE_READ = 0x00000001;
        public const uint FILE_SHARE_WRITE = 0x00000002;
        public const int OPEN_EXISTING = 3;

        private void UsBMethod(int index)
        {
            //獲取USB設備的GUID
            HidD_GetHidGuid(ref guidHID);

            //Console.WriteLine(" GUID_HID = "+ guidHID);       //輸出guid信息調試用

            //獲取系統中存在的所有設備的列表,這些設備已從存儲卷設備接口類啟用了接口
            hDevInfo = SetupDiGetClassDevs(ref guidHID, 0, IntPtr.Zero, DIGCF.DIGCF_PRESENT | DIGCF.DIGCF_DEVICEINTERFACE);

            int bufferSize = 0;
            ArrayList HIDUSBAddress = new ArrayList();

            while (true)
            {

                //獲取設備,true獲取到
                SP_DEVICE_INTERFACE_DATA DeviceInterfaceData = new SP_DEVICE_INTERFACE_DATA();
                DeviceInterfaceData.cbSize = Marshal.SizeOf(DeviceInterfaceData);

                for (int i = 0; i < 3; i++)
                {
                    //識別HID設備接口,獲取設備,返回true成功
                    result = SetupDiEnumDeviceInterfaces(hDevInfo, IntPtr.Zero, ref guidHID, (UInt32)index, ref DeviceInterfaceData);
                }

                //Console.WriteLine(" 識別HID接口\t"+result);       //識別接口打印信息查看

                //第一次調用出錯,但可以返回正確的Size 
                SP_DEVINFO_DATA strtInterfaceData = new SP_DEVINFO_DATA();
                //獲得一個指向該設備的路徑名,接口的詳細信息 必須調用兩次 第1次返回路徑長度 
                result = SetupDiGetDeviceInterfaceDetail(hDevInfo, ref DeviceInterfaceData, IntPtr.Zero, 0, ref bufferSize, strtInterfaceData);

                //第二次調用傳遞返回值,調用即可成功 , 第2次獲取路徑數據 
                IntPtr detailDataBuffer = Marshal.AllocHGlobal(bufferSize);
                SP_DEVICE_INTERFACE_DETAIL_DATA detailData = new SP_DEVICE_INTERFACE_DETAIL_DATA();
                detailData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DETAIL_DATA));

                Marshal.StructureToPtr(detailData, detailDataBuffer, false);
                result = SetupDiGetDeviceInterfaceDetail(hDevInfo, ref DeviceInterfaceData, detailDataBuffer, bufferSize, ref bufferSize, strtInterfaceData);

                if (result == false)
                {
                    break;
                }

                //獲取設備路徑訪
                IntPtr pdevicePathName = (IntPtr)((int)detailDataBuffer + 4);
                devicePathName = Marshal.PtrToStringAuto(pdevicePathName);
                HIDUSBAddress.Add(devicePathName);

                //Console.WriteLine(" Get_DvicePathName = "+ devicePathName);     //打印路徑信息,調試用

                //連接USB設備文件
                int aa = CT_CreateFile(devicePathName);
                usb_flag = aa;
                if (aa == 1)                                    //設備連接成功               
                {
                    //獲取設備VID PID 出廠編號信息判斷是否跟自定義的USB設備匹配,匹配返回 1
                    usb_flag = HidD_GetAttributes(HidHandle);
                    if (usb_flag == 1) break;
                    else usb_flag = 0;
                }
                else usb_flag = 0;
                index++;
            }

        }

  上面的函數調用到了一個建立和設備的連接的函數,該函數主要是用來跟設備建立連接,函數如下:

  /*  =================建立和設備的連接==================    */
        public unsafe int CT_CreateFile(string DeviceName)
        {
            HidHandle = CreateFile
            (
                DeviceName,
                //GENERIC_READ |          // | GENERIC_WRITE,//讀寫,或者一起
                GENERIC_READ | GENERIC_WRITE,
                //FILE_SHARE_READ |       // | FILE_SHARE_WRITE,//共享讀寫,或者一起
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                0,
                OPEN_EXISTING,
                0,
                0
             );

            //Console.WriteLine(" IN_DeviceName = " + DeviceName);                    //查看參數是否傳入           

            if (HidHandle == -1) //INVALID_HANDLE_VALUE實際值等於-1,連接失敗
            {

                //Console.WriteLine(" 失敗 HidHandle = 0x" + "{0:x}",HidHandle );     //查看狀態,打印調試用
                return 0;
            }
            else    //連接成功
            {
                //Console.WriteLine(" 成功 HidHandle = 0x" + "{0:x}",HidHandle);      //查看狀態,打印調試用
                return 1;
            }
        }

  調用函數建立好連接后,就要判斷是否是自己需要的USB設備了,這里就調用了一個判斷函數:

 /*  ==============獲取設備的VID PID 出廠編號等信息,存放到HIDD_ATTRIBUTES==============   */
        public unsafe int HidD_GetAttributes(int handle)
        {
            HIDD_ATTRIBUTES HIDD_ATTRIBUTE = new HIDD_ATTRIBUTES();
            //handle是CreateFile函數返回一個有效的設備操作句柄,HIDD_ATTRIBUTES是函數返回的結構體信息(VID PID 設備號)
            bool sel = HidD_GetAttributes((IntPtr)handle, out HIDD_ATTRIBUTE);

            /*//打印VID、PID信息以16進制顯示調試用,打印數據前不能接+號,不然打印不出來,信息為0
            Console.Write("\t" + "VID:{0:x}", HIDD_ATTRIBUTE.VendorID );
            Console.Write("\t" + "PID:{0:x}", HIDD_ATTRIBUTE.ProductID);
            Console.WriteLine("\r\n");   */

            if (sel == true)  //獲取設備信息成功
            {
                //對自己定義的my_usb_id結構體賦值,輸入自己要操作的設備參數,用來跟讀取出來的設備參數比較
                my_usb_id my_usb_id = new my_usb_id();
                my_usb_id.my_vid = 4292;        //自定義的USB設備VID 0x10c4=4292
                my_usb_id.my_Pid = 33485;       //自定義的USB設備PID 0x82cd=33485

                if (my_usb_id.my_vid == HIDD_ATTRIBUTE.VendorID && my_usb_id.my_Pid == HIDD_ATTRIBUTE.ProductID) //判斷識別出來的是不是自定義的USB設備
                {
                    //Console.WriteLine("獲取VID PID成功"); //打印信息調試用
                    sele = 1;
                }
                else sele = 0;
                return sele;
            }
            else
            {
                //Console.WriteLine("獲取VID PID失敗");     //打印信息調試用
                return sele = 0;
            }
        }

  到這里就已經連接上USB設備了,接下來可以收發數據了。

8、調用WriteFile和ReadFile進行數據傳輸
  這里的發送函數使用的是同步發送,至於怎樣同步異步各位可以自行到https://docs.microsoft.com/zh-cn/windows/desktop/api/fileapi/nf-fileapi-writefile查看,或者看我前面的收發數據的參考鏈接
 private void button2_Click(object sender, EventArgs e)
        {
            if (usb_flag == 1)   //USB識別成功后發送數據
            {
                uint read = 0;
                byte[] src = { 1, 3,1 };
                bool isread = WriteFile((IntPtr)HidHandle, src, (uint)9, ref read, IntPtr.Zero);
                if (isread == false)
                {
                    int errCode = Marshal.GetLastWin32Error();
                    // Console.WriteLine("數據發送失敗!錯誤代碼:" + errCode);
                }
            }
        }
注意這里緩存區要比你的報文描述符多一個字節,不然會出錯,至於接收,使用異步接收,在接收前要做一個調用DLL聲明:
        /*構建一個Overlapped結構,異步通信用,
           internal是錯誤碼,internalHigh是傳輸字節,這個兩個是IO操作完成后需要填寫的內容*/
        [StructLayout(LayoutKind.Sequential)]
        public struct OVERLAPPED
        {
            public IntPtr Internal;         //I/O請求的狀態代碼
            public IntPtr InternalHigh;     //傳輸I/O請求的字節數
            public int Offset;              //文件讀寫起始位置
            public int OffsetHigh;          //地址偏移量
            public IntPtr hEvent;           //操作完成時系統將設置為信號狀態的事件句柄
        }

        /*監聽異步通信函數*/
        [DllImport("Kernel32.dll")]
        public static extern unsafe long WaitForSingleObject(IntPtr hHandle, long dwMilliseconds);
  聲明好后就開始寫接收函數了,接收函數怎么異步接收各位可以自行百度,或者官網查閱,也可以看窮前面的收發函數參考鏈接。
        public void redata()//USB異步接收數據
        {
            //初始化Overlapped
            OVERLAPPED overlap = new OVERLAPPED();
            overlap.Offset = 0;
            overlap.OffsetHigh = 0;

            //創建事件對象
            overlap.hEvent = CreateEvent(IntPtr.Zero, false, false, null);

            //接收數據緩存區:接收到的數據如果比這個小,則按實際數據大小,接收到一個ID+64個數據
            byte[] buffer = new byte[70];
            int dwRead = 0;
            Console.WriteLine("read... ");
            //while (true)
            {
                //讀設備
                bool re = ReadFile(HidHandle, buffer, 70, out dwRead, out overlap);
                if (re != false)
                {
                    SetupDiDestroyDeviceInfoList(hDevInfo);
                    for (int i = 0; i < 70; i++)
                    {
                        Console.WriteLine("i = " + i + " \t buffer = " + buffer[i]);
                    }
                    MessageBox.Show(" READ OK !");
                    //break;
                }
            }
            // long cc=WaitForSingleObject(overlap.hEvent, 5000);
        }

  這樣異步接收可以開一個定時器時不時檢測是否有數據接收,來實現,開定時是可以參考我的隱藏窗體功能的那篇文章:https://www.cnblogs.com/xingboy/p/10110443.html 不過用定時器做不定時檢測可能會出現丟包情況,最好的方法是多開一個線程一直等待接收,接收到立刻跳轉處理接收處理函數。

  收發完函數后,就可以關閉通道了,以便節約資源嘛。

9、關閉USB設備

 

  /*  釋放關閉USB設備   */
        public void Dispost()
        {
            //釋放設備資源(hDevInfo是SetupDiGetClassDevs獲取的)
            SetupDiDestroyDeviceInfoList(hDevInfo);
            //關閉連接(HidHandle是Create的時候獲取的)
            CloseHandle(HidHandle);
        }
        //===================================================

 

 以上就是USB通信上位機部分的內容了,這些函數都放在 public partial class Form1 : Form{ } 完成,至於下位機的部分各位可以參考:https://www.cnblogs.com/xingboy/p/9913963.html

補充一點:如果生成的程序在你的電腦可正常使用,在別的電腦不可以用的話,那可能是windows系統的DLL出了問題,解決方法可以參考我另一個文章:https://www.cnblogs.com/xingboy/p/9876812.html
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM