参考链接: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 = ); //关闭串口,参数待定
void close(); //发送数据或写数据,成功返回发送数据长度,失败返回0
int send(const void *buf, int len); //接受数据或读数据,成功返回读取实际数据的长度,失败返回0
int receive(void *buf, int maxlen); private:
int pHandle[];
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, //支持读写
, //独占方式,串口不支持共享
NULL,//安全属性指针,默认值为NULL
OPEN_EXISTING, //打开现有的串口文件
, //0:同步方式,FILE_FLAG_OVERLAPPED:异步方式
NULL);//用于复制文件句柄,默认值为NULL,对串口而言该参数必须置为NULL
}
else
{
//异步方式
hCom = CreateFileA(portname, //串口名
GENERIC_READ | GENERIC_WRITE, //支持读写
, //独占方式,串口不支持共享
NULL,//安全属性指针,默认值为NULL
OPEN_EXISTING, //打开现有的串口文件
FILE_FLAG_OVERLAPPED, //0:同步方式,FILE_FLAG_OVERLAPPED:异步方式
NULL);//用于复制文件句柄,默认值为NULL,对串口而言该参数必须置为NULL
} if (hCom == (HANDLE)-)
{
return false;
} //配置缓冲区大小
if (!SetupComm(hCom, , ))
{
return false;
} // 配置参数
DCB p;
memset(&p, , sizeof(p));
p.DCBlength = sizeof(p);
p.BaudRate = baudrate; // 波特率
p.ByteSize = databit; // 数据位 switch (parity) //校验位
{
case :
p.Parity = NOPARITY; //无校验
break;
case :
p.Parity = ODDPARITY; //奇校验
break;
case :
p.Parity = EVENPARITY; //偶校验
break;
case :
p.Parity = MARKPARITY; //标记校验
break;
} switch (stopbit) //停止位
{
case :
p.StopBits = ONESTOPBIT; //1位停止位
break;
case :
p.StopBits = TWOSTOPBITS; //2位停止位
break;
case :
p.StopBits = ONE5STOPBITS; //1.5位停止位
break;
} if (!SetCommState(hCom, &p))
{
// 设置参数失败
return false;
} //超时处理,单位:毫秒
//总超时=时间系数×读或写的字符数+时间常量
COMMTIMEOUTS TimeOuts;
TimeOuts.ReadIntervalTimeout = ; //读间隔超时,该时间为串口每次接收等待的时间间隔,数据不多可以把该时间改小,这里每次等待1000mS间隔
TimeOuts.ReadTotalTimeoutMultiplier = ; //读时间系数
TimeOuts.ReadTotalTimeoutConstant = ; //读时间常量
TimeOuts.WriteTotalTimeoutMultiplier = ; // 写时间系数
TimeOuts.WriteTotalTimeoutConstant = ; //写时间常量
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 ;
}
return dwBytesWrite;
}
else
{
//异步方式
DWORD dwBytesWrite = len; //成功写入的数据字节数
DWORD dwErrorFlags; //错误标志
COMSTAT comStat; //通讯状态
OVERLAPPED m_osWrite; //异步输入输出结构体 //创建一个用于OVERLAPPED的事件处理,不会真正用到,但系统要求这么做
memset(&m_osWrite, , 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, ); //等待写入事件1秒钟
}
else
{
ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通讯错误
CloseHandle(m_osWrite.hEvent); //关闭并释放hEvent内存
return ;
}
}
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, , sizeof(m_osRead));
m_osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, L"ReadEvent"); ClearCommError(hCom, &dwErrorFlags, &comStat); //清除通讯错误,获得设备当前状态
if (!comStat.cbInQue)return ; //如果输入缓冲区字节数为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 ;
}
}
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", , , , ); //这里配置打开串口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==)
return ;
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[];int i = ;
memset(buff, , );
int len = w.receive(buff, ); //参数:接收的数据buff;接收的最大数据长度;返回值为实际接收到的数据长度,其他APP使用该函数时使用新线程调用for (i = ; 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 @
COM_Send @
COM_RX @
Close_COM @

4 编译形成DLL

  最后编译就可以形成对应的DLL了,要是要编译32位的DLL就选择X86,要是要选择64位的DLL就选择X64即可。

  用函数查看器看,函数名都正常了:

  

VS2015 编写C++的DLL,并防止DLL导出的函数名出现乱码(以串口通信为例,实现串口通信)的更多相关文章

  1. Delphi 调用C/C++的Dll(stdcall关键字, 会导致函数名分裂. 此时函数名变成_stdadd@8)

    delphi调用C++写的Dll, 当然这个Dll要求是非MFC的Dll, 这样子才能被delphi调用. 根据C++定义函数的情况, Delphi有不同的相对应的处理方法.1. 声明中不加__std ...

  2. C++ 编写的DLL导出的函数名乱码含义解析

    C++编译时函数名修饰约定规则: __stdcall调用约定:   1.以"?"标识函数名的开始,后跟函数名:     2.函数名后面以"@@YG"标识参数表的 ...

  3. DLL DEF文件编写方法 VC++ 调用、调试DLL的方法 显式(静态)调用、隐式(动态)调用

    DLL 文件编写方法: 1.建立DLL工程 2.声明.定义要导出的函数 BOOL WINAPI InitDlg( HWND hTabctrl,TShareMem* pTshare,CRect* prc ...

  4. MinGW g++.exe 编译 DLL 时,导出函数名带@的问题

    今天尝试用CodeBlocks写了一个简单的Dll,发现生成的 dll 文件导出的函数名后面都有一个 @xxx 从生成的 libDll2.def 中看到: EXPORTS DllMain@ @ Max ...

  5. 在VS2015中用C++编写可被C#调用的DLL

    VS2015用C++创建动态库DLL步骤如下: (1)启动VS2015-->文件-->新建-->项目,按图二进行选择,选择Win32项目,弹出创建窗口,如第二张图.注意.net版本根 ...

  6. C++编写 动态链接库dll 和 调用dll

    参考:https://jingyan.baidu.com/article/ff42efa92c49cfc19e2202fd.html 和htps://jingyan.baidu.com/article ...

  7. 四种DLL:NON-MFC DLL, Regular DLL Statically/Dynamically Linked to MFC, MFC Extension DLL

    参考资料: https://msdn.microsoft.com/en-us/library/30c674tx.aspx http://www.cnblogs.com/qrlozte/p/484442 ...

  8. vc编译器 msvcr.dll、msvcp.dll的含义和相关错误的处理

    转自:http://blog.csdn.net/sptoor/article/details/6203376 很久没有写程式设计入门知识的相关文章了,这篇文章要来谈谈程式库 (Library) 连结, ...

  9. dll的概念 dll导出变量 函数 类

    1. DLL的概念 DLL(Dynamic Linkable Library),动态链接库,可以向程序提供一些函数.变量或类.这些可以直接拿来使用. 静态链接库与动态链接库的区别:   (1)静态链接 ...

随机推荐

  1. activity知识点

    一:activity的理解 1.活动:四大应用组件之一 2.作用:提供能让用户操作并与之交互的界面 3.组件的特点: 它的类必须实现特定接口或继承特定类 需要在配置文件中配置全类名 它的对象不是通过n ...

  2. Kernel Page Global Directory (PGD) of Page table of Process created in Linux Kernel

    Kernel Page Global Directory (PGD) of User process created 在早期版本: 在fork一个进程的时候,必须建立进程自己的内核页目录项(内核页目录 ...

  3. git-vi

    VI命令可以说是Unix/Linux世界里最常用的编辑文件的命令了,但是它的命令集太多,所以要想精通他,也是一件很不容易的事情,除了专业SA,对于我们开发人员而已只需要掌握一些最最常见的用法应该就可以 ...

  4. 搭建阿里云服务器(centos,jdk和Tomcat版本)

    1.购买服务器(登录阿里云,购买服务器,并进入控制台,查看自己的服务器实例 2.域名注册(这步可以省略,直接IP地址访问,因为域名需要备案),购买域名的需要进行解析以及绑定自己的服务器 3.可以准备一 ...

  5. elasticsearch 基础 —— Explain、Version、min_score、query  rescorer

    Explain 相关度得分计算: GET /_search { "explain": true, "query" : { "term" : ...

  6. elasticsearch 深入 —— Search After实时滚动查询

    Search After 一般的分页需求我们可以使用form和size的方式实现,但是这种分页方式在深度分页的场景下应该是要避免使用的.深度分页会随着请求的页次增加,所消耗的内存和时间的增长也是成比例 ...

  7. 20180305-Python中迭代器和生成器

    一.迭代器 迭代器是访问集合元素的一种方式.迭代器从访问到集合的第一个元素开始访问,直到所有元素被访问结束.而且迭代器只能往前访问,不能后退.另外迭代器的另一个优点,不会事先准备好访问的集合的所有元素 ...

  8. Tomcat的用途

    总结: 这篇文章主要反思了Tomcat的作用.本文主要是自己的一个思考过程,不是严谨地介绍和详细总结Tomcat使用方法的文章.最后尝试利用tomcat的知识,以URL的形式来访问文件夹(在浏览器的地 ...

  9. Linux学习笔记5(2)-CentOS7中Tomcat8修改jvm内存配置

    1.进入tomcat的bin目录,比如我的在 /usr/local/apache-tomcat-8.5.16/bin 2.创建新的文件setenv.sh vi setenv.sh 并在此文件中添加以下 ...

  10. 二、RabbitMQ操作

    1.RabbitMQ发送与接收. 2.RabbitMQ发送与接收. 3.RabbitMQ发送与接收.