參考鏈接:https://blog.csdn.net/songyi160/article/details/50754705
1、新建項目
建立好的項目界面如下:
接着在解決方案中找到【頭文件】然后右擊選擇【添加】》【新建項】,在彈出的添加新項對話框中進行如下選擇:
繼續按上面的方法在解決方案中找到【源文件】然后右擊選擇【添加】》【新建項】,在彈出的添加新項對話框中進行如下選擇:
項目准備建好了,現在開始編程了。
2 編程實現串口通信
我們先編寫剛才建立好的 “ WzSerialPort.h ” 文件,該文件主要是做一些函數聲明:
#pragma once #ifndef _WZSERIALPORT_H #define _WZSERIALPORT_H class WzSerialPort { public: WzSerialPort(); ~WzSerialPort(); // 打開串口,成功返回true,失敗返回false // portname(串口名): 在Windows下是"COM1""COM2"等,在Linux下是"/dev/ttyS1"等 // baudrate(波特率): 9600、19200、38400、43000、56000、57600、115200 // parity(校驗位): 0為無校驗,1為奇校驗,2為偶校驗,3為標記校驗(僅適用於windows) // databit(數據位): 4-8(windows),5-8(linux),通常為8位 // stopbit(停止位): 1為1位停止位,2為2位停止位,3為1.5位停止位 // synchronizeflag(同步、異步,僅適用與windows): 0為異步,1為同步 bool open(const char* portname, int baudrate, char parity, char databit, char stopbit, char synchronizeflag = 1); //關閉串口,參數待定 void close(); //發送數據或寫數據,成功返回發送數據長度,失敗返回0 int send(const void *buf, int len); //接受數據或讀數據,成功返回讀取實際數據的長度,失敗返回0 int receive(void *buf, int maxlen); private: int pHandle[16]; char synchronizeflag; }; #endif
接着編寫 “ WzSerialPort.cpp ” 的文件,該文件主要實現串口打開關閉以及數據傳輸函數:
#include "WzSerialPort.h" #include <stdio.h> #include <string.h> #include <WinSock2.h> #include <windows.h> WzSerialPort::WzSerialPort() { } WzSerialPort::~WzSerialPort() { } bool WzSerialPort::open(const char* portname, int baudrate, char parity, char databit, char stopbit, char synchronizeflag) { this->synchronizeflag = synchronizeflag; HANDLE hCom = NULL; if (this->synchronizeflag) { //同步方式 hCom = CreateFileA(portname, //串口名 GENERIC_READ | GENERIC_WRITE, //支持讀寫 0, //獨占方式,串口不支持共享 NULL,//安全屬性指針,默認值為NULL OPEN_EXISTING, //打開現有的串口文件 0, //0:同步方式,FILE_FLAG_OVERLAPPED:異步方式 NULL);//用於復制文件句柄,默認值為NULL,對串口而言該參數必須置為NULL } else { //異步方式 hCom = CreateFileA(portname, //串口名 GENERIC_READ | GENERIC_WRITE, //支持讀寫 0, //獨占方式,串口不支持共享 NULL,//安全屬性指針,默認值為NULL OPEN_EXISTING, //打開現有的串口文件 FILE_FLAG_OVERLAPPED, //0:同步方式,FILE_FLAG_OVERLAPPED:異步方式 NULL);//用於復制文件句柄,默認值為NULL,對串口而言該參數必須置為NULL } if (hCom == (HANDLE)-1) { return false; } //配置緩沖區大小 if (!SetupComm(hCom, 1024, 1024)) { return false; } // 配置參數 DCB p; memset(&p, 0, sizeof(p)); p.DCBlength = sizeof(p); p.BaudRate = baudrate; // 波特率 p.ByteSize = databit; // 數據位 switch (parity) //校驗位 { case 0: p.Parity = NOPARITY; //無校驗 break; case 1: p.Parity = ODDPARITY; //奇校驗 break; case 2: p.Parity = EVENPARITY; //偶校驗 break; case 3: p.Parity = MARKPARITY; //標記校驗 break; } switch (stopbit) //停止位 { case 1: p.StopBits = ONESTOPBIT; //1位停止位 break; case 2: p.StopBits = TWOSTOPBITS; //2位停止位 break; case 3: p.StopBits = ONE5STOPBITS; //1.5位停止位 break; } if (!SetCommState(hCom, &p)) { // 設置參數失敗 return false; } //超時處理,單位:毫秒 //總超時=時間系數×讀或寫的字符數+時間常量 COMMTIMEOUTS TimeOuts; TimeOuts.ReadIntervalTimeout = 1000; //讀間隔超時,該時間為串口每次接收等待的時間間隔,數據不多可以把該時間改小,這里每次等待1000mS間隔 TimeOuts.ReadTotalTimeoutMultiplier = 500; //讀時間系數 TimeOuts.ReadTotalTimeoutConstant = 5000; //讀時間常量 TimeOuts.WriteTotalTimeoutMultiplier = 500; // 寫時間系數 TimeOuts.WriteTotalTimeoutConstant = 2000; //寫時間常量 SetCommTimeouts(hCom, &TimeOuts); PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);//清空串口緩沖區 memcpy(pHandle, &hCom, sizeof(hCom));// 保存句柄 return true; } void WzSerialPort::close() { HANDLE hCom = *(HANDLE*)pHandle; CloseHandle(hCom); } int WzSerialPort::send(const void *buf, int len) { HANDLE hCom = *(HANDLE*)pHandle; if (this->synchronizeflag) { // 同步方式 DWORD dwBytesWrite = len; //成功寫入的數據字節數 BOOL bWriteStat = WriteFile(hCom, //串口句柄 buf, //數據首地址 dwBytesWrite, //要發送的數據字節數 &dwBytesWrite, //DWORD*,用來接收返回成功發送的數據字節數 NULL); //NULL為同步發送,OVERLAPPED*為異步發送 if (!bWriteStat) { return 0; } return dwBytesWrite; } else { //異步方式 DWORD dwBytesWrite = len; //成功寫入的數據字節數 DWORD dwErrorFlags; //錯誤標志 COMSTAT comStat; //通訊狀態 OVERLAPPED m_osWrite; //異步輸入輸出結構體 //創建一個用於OVERLAPPED的事件處理,不會真正用到,但系統要求這么做 memset(&m_osWrite, 0, sizeof(m_osWrite)); m_osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, L"WriteEvent"); ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通訊錯誤,獲得設備當前狀態 BOOL bWriteStat = WriteFile(hCom, //串口句柄 buf, //數據首地址 dwBytesWrite, //要發送的數據字節數 &dwBytesWrite, //DWORD*,用來接收返回成功發送的數據字節數 &m_osWrite); //NULL為同步發送,OVERLAPPED*為異步發送 if (!bWriteStat) { if (GetLastError() == ERROR_IO_PENDING) //如果串口正在寫入 { WaitForSingleObject(m_osWrite.hEvent, 1000); //等待寫入事件1秒鍾 } else { ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通訊錯誤 CloseHandle(m_osWrite.hEvent); //關閉並釋放hEvent內存 return 0; } } return dwBytesWrite; } } int WzSerialPort::receive(void *buf, int maxlen) { HANDLE hCom = *(HANDLE*)pHandle; //if (this->synchronizeflag) //{ // //同步方式,這里因為發送用了同步,接收想用異步,又沒有重新初始化串口打開,就直接注釋掉用串口異步接收了 // DWORD wCount = maxlen; //成功讀取的數據字節數 // BOOL bReadStat = ReadFile(hCom, //串口句柄 // buf, //數據首地址 // wCount, //要讀取的數據最大字節數 // &wCount, //DWORD*,用來接收返回成功讀取的數據字節數 // NULL); //NULL為同步發送,OVERLAPPED*為異步發送 // if (!bReadStat) // { // return 0; // } // return wCount; //} //else { //異步方式,用同步會阻塞 DWORD wCount = maxlen; //成功讀取的數據字節數 DWORD dwErrorFlags; //錯誤標志 COMSTAT comStat; //通訊狀態 OVERLAPPED m_osRead; //異步輸入輸出結構體 //創建一個用於OVERLAPPED的事件處理,不會真正用到,但系統要求這么做 memset(&m_osRead, 0, sizeof(m_osRead)); m_osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, L"ReadEvent"); ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通訊錯誤,獲得設備當前狀態 if (!comStat.cbInQue)return 0; //如果輸入緩沖區字節數為0,則返回false BOOL bReadStat = ReadFile(hCom, //串口句柄 buf, //數據首地址 wCount, //要讀取的數據最大字節數 &wCount, //DWORD*,用來接收返回成功讀取的數據字節數 &m_osRead); //NULL為同步發送,OVERLAPPED*為異步發送 if (!bReadStat) { if (GetLastError() == ERROR_IO_PENDING) //如果串口正在讀取中 { //GetOverlappedResult函數的最后一個參數設為TRUE //函數會一直等待,直到讀操作完成或由於錯誤而返回 GetOverlappedResult(hCom, &m_osRead, &wCount, TRUE); } else { ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通訊錯誤 CloseHandle(m_osRead.hEvent); //關閉並釋放hEvent的內存 return 0; } } return wCount; } }
再接着,我們編寫 “ USER_COM.cpp ” 文件,該文件主要是實現把 “ WzSerialPort.cpp ” 文件里的串口函數進行一次封裝,封裝聲明成可供DLL外部調用的函數:
// USER_COM_DLL.cpp : 定義 DLL 應用程序的導出函數。 // #include "stdafx.h" #include "USER_COM.h" #include <iostream> #include "WzSerialPort.h" #include "Windows.h" using namespace std; /*類重命名*/ WzSerialPort w; /************************************************* 函數名:bool OpenCOM() 功 能:打開串口 傳入值:無 返回值:串口打開成功返回true,串口打開失敗返回false *************************************************/ bool OpenCOM() { return w.open("COM1", 115200, 0, 8, 1); //這里配置打開串口1,配置波特率為115200,數據位為8位,奇偶校驗位為0,停止位為1,最后一位是同步異步選擇位(隱藏)
}
/************************************************* 函數名:void COM_Send(unsigned char cmd, unsigned char data1, unsigned char data2) 功 能:發送數據 傳入值:num為要發送的數據量大小,Send_buff為發送數據的數組,數組大小要跟num大小一致 返回值:發送成功返回發送數據長度,發送失敗返回0 *************************************************/ int COM_Send(int num ,uint8_t* Send_buff) { sen_len = w.send(Send_buff, num); if (sen_len==0) return 0; else return sen_len ; } /************************************************* 函數名:void Close_COM() 功 能:關閉串口 傳入值:無 返回值:無 *************************************************/ void Close_COM() { w.close(); } /************************************************* 函數名:void COM_RX(uint8_t* Rx_buff) 功 能:接收數據數據 傳入值:uint8_t* Rx_buff 接收的串口數據的緩存BUFF數組指針, 接收到數據后,直接把數據填到該Rx_buff,默認可接收的數據最大長度為255 返回值:int len 返回值為實際接收到的數據長度,返回0則代表沒有接收到數據或者數據校驗出錯 *************************************************/ int COM_RX(uint8_t* Rx_buff) { //該函數編譯X86的時候,調用時使用X64編譯器無法調用,調用編譯時要調用的話就要用X64編譯 uint8_t buff[255];int i = 0; memset(buff, 0, 255); int len = w.receive(buff, 255); //參數:接收的數據buff;接收的最大數據長度;返回值為實際接收到的數據長度,其他APP使用該函數時使用新線程調用for (i = 0; i < len; i++) Rx_buff[i] = buff[i];return len; }
最后我們在頭文件那里,新建一個 “ USER_COM.h ” 文件實現把 “ USER_COM.cpp ” 文件里的函數聲明為DLL的外部接口:
#ifdef USER_COM_EXPORTS #define USER_COM_API __declspec(dllexport) //聲明為DLL導出函數的宏定義 #else #define USER_COM_API __declspec(dllimport) #endif #include "stdint.h" extern "C" USER_COM_API bool OpenCOM(); extern "C" USER_COM_API int COM_Send(unsigned char cmd, unsigned char data1, unsigned char data2); extern "C" USER_COM_API int COM_RX(uint8_t* Rx_buff); extern "C" USER_COM_API void Close_COM();
注意:__stdcall定義導出函數入口點調用約定為_stdcall
extern "C" 說明導出函數使用C編譯器,則函數名遵循C編譯器的函數名修飾規則,不加extern "C"說明使用C++編譯器的函數名修飾 規則,兩種規則區別如下:
(1)C編譯器的函數名修飾規則
對於__stdcall調用約定,編譯器和鏈接器會在輸出函數名前加上一個下划線前綴,函數名后面加上一個“@”符號和其參數的字節數。
例如 _functionname@number。__cdecl調用約定僅在輸出函數名前加上一個下划線前綴,例如_functionname。__fastcall調用約定在輸出函數名前加上一個“@”符號,后面也是一個“@”符號和其參數的字節數,例如@functionname@number
(2)C++編譯器的函數名修飾規則
C++的函數名修飾規則有些復雜,但是信息更充分,通過分析修飾名不僅能夠知道函數的調用方式,返回值類型,甚至參數個數、參數類 型。不管__cdecl,__fastcall還是__stdcall調用方式,函數修飾都是以一個“?”開始,后面緊跟函數的名字,再后面是參數表的開始標識 和按照參數類型代號拼出的參數表。對於__stdcall方式,參數表的開始標識是“@@YG”,對於__cdecl方式則是“@@YA”,對於__fastcall方式則是“@@YI”。
使用 extern "C" 跟不使用 extern "C" 的形成的DLL函數名差異如下圖測試DLL函數名所示:
由圖可以看出兩者之間最終形成的名字會有差異的,到這里編譯就已經可以建成串口通信的 DLL 了,不過形成的的DLL通過函數查看器還會有一些除函數名以外的符號,最后進行函數命名規范就好了。“ .h ” 頭文件的作用僅僅能導出動態庫、明確編譯鏈接方式及確定入口點約定,還一個重要作用是打包給開發者,使其了解動態庫導出的函數及對應的的參數,為了確保導出函數名及入口點函數不變,此時需添加.def文件。
3 添加 def 文件,保持函數名以及入口點函數不變
在解決方案中找到【源文件】右擊選擇【添加】》【新建項】,在彈出的添加新項對話框中進行如下圖所示選擇:
然后編寫 “ USER_COM.def ” 文件,使用def文件的意義:將編譯器生成的函數修飾去掉,用更加自然、更加容易理解、更加容易記憶的名字來命名函數,而不是一串人一看就嚇一跳的 修飾名字。
LIBRARY "USER_COM" EXPORTS OpenCOM @ 1 COM_Send @ 2 COM_RX @ 3 Close_COM @ 4
4 編譯形成DLL
最后編譯就可以形成對應的DLL了,要是要編譯32位的DLL就選擇X86,要是要選擇64位的DLL就選擇X64即可。
用函數查看器看,函數名都正常了: