文章目录
前言
因为平时需要配合硬件开发的做程序升级,提到了一些问题,之前每次用串口升级的时候,都需要安装MSComm控件,在新的设备工控机上安装比较繁琐,另外对之前的下载升级程序效率上要有所提升。
下面写这篇文章也是总结下遇到的问题,希望对看到的朋友有帮助。MFC程序实现
一、MFC串口通信问题
相信看到的朋友,应该都用过MSComm控件写串口通信,也能找到很多的例子程序。还要一种方式用windows api的方式,完成串口通信操作。具体细节往下看。
二、MSComm控件的串口通信形式
1.添加active控件
在项目菜单栏中选择“工具”菜单,在弹出的窗口中选择“选择工具箱选项”在“COM组件”中选择,然后点击“确定”按钮将MSComm添加到工具箱。
如下图所示是已经添加完成MSComm控件后的工具箱,工具箱中会出现一个电话式样的图标,该图标就是MSComm控件。
拖进去就能添加控件,右键添加控件变量后就能使用了。
2.添加Combo box控件,获取当前主机可用串口
获取当前主机可用串口号,添加到Combo box控件变量中:
// 初始化端口号
void CSerialMSCommDlg::GetCom()
{
//程序启动时获取全部可用串口
HANDLE hCom;
int i, k;
CString str;
BOOL flag;
//((CComboBox *)GetDlgItem(IDC_COMBO1))->ResetContent();
m_ComboSerial.ResetContent();
flag = FALSE;
num = 0;
for (i = 1; i <= 16; i++)
{//此程序支持16个串口
str.Format(L"\\\\.\\COM%d", i);
hCom = CreateFile(str, 0, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (INVALID_HANDLE_VALUE != hCom)
{//能打开该串口,则添加该串口
CloseHandle(hCom);
str = str.Mid(4);
m_ComboSerial.AddString(str);
//((CComboBox *)GetDlgItem(IDC_COMBO1))->AddString(str);
if (flag == FALSE)
{
flag = TRUE;
num = i;
}
}
}
i = m_ComboSerial.GetCount();
if (i == 0)
{//若找不到可用串口则禁用“打开串口”功能
m_ComboSerial.EnableWindow(FALSE);
}
else
{
k = m_ComboSerial.GetCount();
m_ComboSerial.SetCurSel(k-1);
}
}
3.打开串口、关闭串口、发送和接收串口数据代码实现
打开串口代码实现:
if (!m_mscom.get_PortOpen())
{
try
{
m_ComboSerial.GetLBText(m_ComboSerial.GetCurSel(), strCBText);
int iabc = _ttoi(strCBText.Right(1));
m_mscom.put_CommPort(_ttoi(strCBText.Right(1))); //选择串口
}
catch (CException* e)
{
m_mscom.put_OutBufferCount(0);
AfxMessageBox(L"打开串口 失败");
return;
}
m_mscom.put_InputMode(1); //设置输入方式为二进制方式
m_mscom.put_Settings(_T("9600,n,8,1")); //设置串口参数,波特率,无奇偶校验,位停止位,位数据位
m_mscom.put_InputLen(1024); //设置当前接收区数据长度为1024
m_mscom.put_RThreshold(1); //接收缓冲区有1个及1个以上字符时,触发OnComm事件
m_mscom.put_RTSEnable(1); //设置RT允许
m_mscom.put_PortOpen(true); //打开串口
if (m_mscom.get_PortOpen())
{
bIsloop = true;
str = _T("关闭串口");
UpdateData(true);
h1->SetWindowText(str); //改变按钮名称为‘’关闭串口”
}
}
关闭串口代码:
if (m_MSCommControl.get_PortOpen())
{
m_mscom.put_PortOpen(false); //关闭串口
if (str != _T("打开串口"))
{
bIsloop = false;
str = _T("打开串口");
UpdateData(true); //将控件的状态传给其关联的变量
h1->SetWindowText(str); //改变按钮名称为打开串口
}
}
接收串口返回数据时,可以添加【串口事件】
为MSComm控件增加OnComm处理函数:在类向导中选择“命令”、选择ID为IDC_MSCOMMPORT的控件为其增加OnComm消息处理函数。具体在函数中怎么处理,大家可以自己搜一下,或者看下面的例子(我没测试哦!)
void CMFCApplicationMSCommPortDlg::OnOncommMscommport()
{
VARIANT m_Variant_Rec;
COleSafeArray m_SafeArray_Rec;
LONG m_DataLenth, nCount;
const int m_RecByteLenth = 2048;
BYTE m_RecDataByte[m_RecByteLenth];
CString m_TemDataStr, strRevBuff, strSendBuff,strRevByte,strSendByte,strRxCount;
if (m_MSCommControl.get_CommEvent() == 1)
{
//strSendByte.Format(_T("%d"), m_MSCommControl.get_OutBufferCount());
//printf("get_OutBufferCount=%d\n", m_MSCommControl.get_OutBufferCount());
//m_SendByteCount.SetWindowText(_T("get_OutBufferCount:") + strSendByte);
m_SendMsg = TRUE;
PostMessage(WM_SENDMSG, WPARAM(m_SendMsg), 0);
printf("get_CommEvent=%d\n", m_MSCommControl.get_CommEvent());
}
else if (m_MSCommControl.get_CommEvent() == 2)
{
m_SendMsg = FALSE;
PostMessage(WM_SENDMSG, WPARAM(m_SendMsg), 0);
}
if (m_MSCommControl.get_CommEvent()==2)
{
strRevBuff.Format(_T("%d"), m_MSCommControl.get_InBufferSize());
printf("get_InBufferSize=%d\n", m_MSCommControl.get_InBufferSize());
strSendBuff.Format(_T("%d"), m_MSCommControl.get_OutBufferSize());
printf("get_OutBufferSize=%d\n", m_MSCommControl.get_OutBufferSize());
strRevByte.Format(_T("%d"), m_MSCommControl.get_InBufferCount());
printf("get_InBufferCount=%d\n", m_MSCommControl.get_InBufferCount());
InCount= InCount+m_MSCommControl.get_InBufferCount();
printf("InCount=%I64u\n", InCount);
strRxCount.Format(_T("%I64u"), InCount);
}
}
自己的代码实例中要求一直循环读取气象设备数据,所以写了个死循环的线程函数,
while (true)
{
CByteArray HexDataBuf;
int i = 0;
BYTE SendBuf[128]={0};
BYTE GetData[256]={0};
int SendLen = 0;
int GetLen = 0;
CString csEdit;
HexDataBuf.RemoveAll(); //清空数组
// 气象
//01 03 00 00 00 05 85 C9
pMainDlg->SetDlgItemTextW(IDC_EDIT1,_T("01 03 00 00 00 05 85 C9"));
//pMainDlg->UpdateData(TRUE);//获取编辑框内容
pMainDlg->GetDlgItemTextW(IDC_EDIT1,csEdit);
GetLen = csEdit.GetLength();
for(i=0; i<GetLen; i++)
{
GetData[i] = (BYTE)csEdit.GetBuffer()[i];
}
StringtoHex(GetData, GetLen, SendBuf, &SendLen);//将字符串转化为字节数据
HexDataBuf.SetSize(SendLen); //设置数组大小为帧长度
for(i=0; i<SendLen; i++)
{
HexDataBuf.SetAt(i,SendBuf[i]);
}
pMainDlg->m_mscom.put_Output(COleVariant(HexDataBuf)); //发送十六进制数据
// 空气
// 01 03 00 00 00 06 C5 C8
// 01 03 00 00 00 07 04 08
Sleep(100); //延时毫秒
//------------------------------------
VARIANT variant_inp;
COleSafeArray safearray_inp;
long len, k;
byte rxdata[1024]; //设置 BYTE 数组
CString strtemp, buffer;
variant_inp = pMainDlg->m_mscom.get_Input(); //读缓冲区消息
safearray_inp = variant_inp; ///变量转换
len = safearray_inp.GetOneDimSize(); //得到有效的数据长度
//将数组转换为 CString 型变量
for (k = 0; k < len; k++)
{safearray_inp.GetElement(&k, rxdata + k);
strtemp.Format(_T("%02X"), *(rxdata + k)); buffer += strtemp;}
CStringA cstrNum, cstrNum1, cstrNum2, cstrNum3, cstrNum4;
// 温度
cstrNum2 = buffer.Mid(6,4);
// 湿度
cstrNum3 = buffer.Mid(10,4);
// 气压
cstrNum4 = buffer.Mid(14,4);
// 风速
cstrNum = buffer.Mid(18,4);
// 风向
cstrNum1 = buffer.Mid(22,4);
UINT sum4 = strtoul(cstrNum4.GetBuffer(), 0, 16);
CString cstFengsu, cstFengxiang, cstShidu, cstWendu, cstQiya;
cstFengsu.Format(_T("风速 m/s: %d.%d"),strtoul(cstrNum.GetBuffer(), 0, 16) / 100,strtoul(cstrNum.GetBuffer(), 0, 16) % 100);
cstFengxiang.Format(_T(" 风向 °: %d"),strtoul(cstrNum1.GetBuffer(), 0, 16));
if (strtoul(cstrNum2.GetBuffer(), 0, 16) > 1000)
{
cstWendu.Format(_T(" 温度 °C: -%d.%d"),(65535-strtoul(cstrNum2.GetBuffer(), 0, 16))/10, (65535-strtoul(cstrNum2.GetBuffer(), 0, 16))%10);
}
else
{
cstWendu.Format(_T(" 温度 °C: %d.%d"),strtoul(cstrNum2.GetBuffer(), 0, 16)/10, strtoul(cstrNum2.GetBuffer(), 0, 16)%10);
}
cstShidu.Format(_T(" 湿度 %%RH: %d.%d"),strtoul(cstrNum3.GetBuffer(), 0, 16)/10, strtoul(cstrNum3.GetBuffer(), 0, 16)%10);
cstQiya.Format(_T(" 气压 hPa: %d.%d"),strtoul(cstrNum4.GetBuffer(), 0, 16)/10, strtoul(cstrNum4.GetBuffer(), 0, 16)%10);
pMainDlg->GetDlgItem(IDC_QIYA)->SetWindowTextW(cstFengsu+cstFengxiang+cstWendu+cstShidu+cstQiya);
pMainDlg->GetDlgItem(IDC_QIYA)->UpdateData(false); //更新到控件
//------------------------------------
Sleep(2000); //滞后2秒
}
完整代码参见:MFC串口通信发送和接收+MSCOMM注册组件控件.rar
三、Windows API方式串口通信
1.获取当前主机可用串口信息,打开串口(采用异步的方式)
部分代码可参见上面的获取串口信息
闭坑指南:注意串口号如果大于COM9应该在前面加上\.\,比如COM10表示为"\\.\COM10",这里都用“ \\.\%d ”
BOOL CDlg::OpenCom()
{
CString csCOMMnum;
m_ComboSerial.GetLBText(m_ComboSerial.GetCurSel(), csCOMMnum); // 获取Combo box控件选中的串口号
csCOMMnum = "\\\\.\\" + csCOMMnum;
hCom = CreateFile(csCOMMnum, //COM1口 注意串口号如果大于COM9应该在前面加上\\.\,比如COM10表示为"\\\\.\\COM10"
GENERIC_READ | GENERIC_WRITE, //允许读和写
0, //独占方式
NULL,
OPEN_EXISTING, //打开而不是创建
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, //0:同步方式,FILE_FLAG_OVERLAPPED:异步方式
NULL);
if (hCom == (HANDLE)-1)
{
return FALSE;
}
return TRUE;
}
2.配置串口参数信息
void CDlg::InitCom()
{
SetupComm(hCom, 1024, 1024); //输入缓冲区和输出缓冲区的大小都是1024
COMMTIMEOUTS TimeOuts; //设定读超时
TimeOuts.ReadIntervalTimeout = 1000;
TimeOuts.ReadTotalTimeoutMultiplier = 500;
TimeOuts.ReadTotalTimeoutConstant = 5000; //设定写超时
TimeOuts.WriteTotalTimeoutMultiplier = 500;
TimeOuts.WriteTotalTimeoutConstant = 2000;
SetCommTimeouts(hCom, &TimeOuts); //设置超时
DCB dcb;
GetCommState(hCom, &dcb);
dcb.BaudRate = 9600; //波特率为9600
dcb.ByteSize = 8; //每个字节有8位
dcb.Parity = NOPARITY; //无奇偶校验位
dcb.StopBits = TWOSTOPBITS; //2个停止位 TWOSTOPBITS ONE5STOPBITS
SetCommState(hCom, &dcb);
PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR); // PurgeComm()函数清空缓冲区
}
3.发送串口信息
闭坑指南:WriteFile发送串口数据时,总返回错误码 6, 查了好多资料,才知道是参数没初始化的问题。
OVERLAPPED m_OsWrite;
memset(&m_OsWrite, 0, sizeof(OVERLAPPED)); // 不初始化,发送结果返回错误码 6
WriteFile(hCom, m_Sendinfo.GetBuffer(), m_Sendinfo.GetLength(), &dwBytesWritten, &m_OsWrite);
BOOL CDlg::Sendcomminfo()
{
//char buffer[1024] = ;
DWORD dwBytesWritten = 1024;
DWORD dwErrorFlags;
COMSTAT ComStat;
OVERLAPPED m_OsWrite;
BOOL bWriteStat;
memset(&m_OsWrite, 0, sizeof(OVERLAPPED)); // 不初始化,发送结果返回错误码 6
memset(&m_odWrite, 0, sizeof(OVERLAPPED));
m_odWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
PurgeComm(hCom, PURGE_RXABORT | PURGE_TXABORT | PURGE_RXCLEAR | PURGE_TXCLEAR);
if (m_checkHEX.GetCheck()) // 添加复选框,如果是HEX数据,则转换格式
{
char csSendBuff[2048];
//memset(csSendBuff, 0xFF, 2048);
int ilen = String2Hex(m_Sendinfo, csSendBuff);
bWriteStat = WriteFile(hCom, csSendBuff, ilen, &dwBytesWritten, &m_OsWrite);
}
else
{
bWriteStat = WriteFile(hCom, m_Sendinfo.GetBuffer(), m_Sendinfo.GetLength(), &dwBytesWritten, &m_OsWrite);
}
if (!bWriteStat)
{
if (GetLastError() == ERROR_IO_PENDING)
{
WaitForSingleObject(m_OsWrite.hEvent, 1000);
CString csline;
csline.Format("发送串口数据:%s", m_Sendinfo);
EditShowData(csline);
return TRUE;
}
CString cserror;
cserror.Format("err = %d\n ", GetLastError());
EditShowData(cserror);
return FALSE;
}
CString csline;
csline.Format("发送串口数据:%s", m_Sendinfo);
EditShowData(csline);
return TRUE;
}
4.接收串口信息
这里// 添加复选框,如果是HEX数据,则转换格式,具体实现函数可以参见完整程序代码,放文末了。
BOOL CDlg::Receivedatayibu()
{
char lpInBuffer[1024] = {0};
DWORD dwBytesRead = 1024;
BOOL bReadStatus;
DWORD dwErrorFlags;
COMSTAT ComStat;
OVERLAPPED m_osRead;
memset(&m_osRead, 0, sizeof(OVERLAPPED));//注意每次读取串口时都要初始化OVERLAPPED
m_osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
ClearCommError(hCom, &dwErrorFlags, &ComStat);
if (!ComStat.cbInQue) return 0; //读缓冲区没有字节
dwBytesRead = min(dwBytesRead, (DWORD)ComStat.cbInQue); //防止读取字节数超过数组最大值
bReadStatus = ReadFile(hCom, lpInBuffer, dwBytesRead, &dwBytesRead, &m_osRead);
if (!bReadStatus)
{ //如果ReadFile函数返回FALSE
if (GetLastError() == ERROR_IO_PENDING)
{
GetOverlappedResult(hCom, &m_osRead, &dwBytesRead, TRUE); // GetOverlappedResult函数的最后一个参数设为TRUE,函数会一直
//等待,直到读操作完成或由于错误而返回。
}
return FALSE;
}
else
{
if (m_checkHEX.GetCheck()) // 添加复选框,如果是HEX数据,则转换格式
{
CString csbuffer, strtemp;
for (int k = 0; k < dwBytesRead; k++)
{
strtemp.Format(_T("%02X"), *(lpInBuffer + k));
csbuffer += strtemp.Mid(0, 2);
csbuffer += " ";
}
EditShowData(csbuffer);
if (csbuffer == "12 FF 00 30 ") // 这里设置了一个标志,HEX文件下发定义了1024个字节下发,等待下位机返回参数后,再继续下发
{
m_bNextLine = TRUE;
}
}
else
{
EditShowData(lpInBuffer);
}
}
return TRUE;
}
5.关闭串口通信
void CDlg::OnBnClickedClosecom()
{
// TODO: 在此添加控件通知处理程序代码
//::WaitForSingleObject(m_MyThreadComm->m_hThread, INFINITE);
m_bOpencomm = FALSE;
GetDlgItem(BTN_UPDATECOMM)->EnableWindow(TRUE);
GetDlgItem(BEN_OPENCOM)->EnableWindow(TRUE);
m_ComboSerial.EnableWindow(TRUE);
if (CloseHandle(hCom))
{
EditShowData("关闭串口连接成功...");
if (m_MyThreadDownHEX != NULL)
{
TerminateThread(m_MyThreadDownHEX->m_hThread, 0); // 这里选择强制关闭线程
}
}
else
{
EditShowData("关闭串口连接失败...");
}
}
四、下载HEX文件到单片机
if (strDlg->m_csFilePath.Right(4) == ".hex")
{
// 按1024字节大小读取
CStdioFile file;
CString csLineAll, csline;
if (file.Open((LPCTSTR)strDlg->m_csFilePath, CStdioFile::modeReadWrite))
{
strDlg->EditShowData("开始下载HEX文件...");
int iLineNum = 0;
CString csStartID; // 保存hex文件 第一行标志
// 获取文件总自己长度,每小包1024字节,计算总包数//
while (file.ReadString(csline))
{
iLineNum += 1;
if (iLineNum == 1)
{
csStartID = csline.Mid(9, csline.GetLength() - 11); // 保存hex文件 第一行标志
continue;
}
//csline = csline.Mid(9);
csline = csline.Mid(9, csline.GetLength() - 11);
if (csStartID == csline.Mid(0, 4)) // 判断hex第一行标志,是否到文件结尾
{
break;
}
else
{
csLineAll += csline; // 把每行截取到的数据段都存入 csLineAll 中
}
}
file.Close(); // 读取hex文件结束,释放文件句柄
//----------------------------------------------------------------------------------
int iCount = csLineAll.GetLength() / 2048 + 1; // 计算文件总包数 每包1024字节长度
for (size_t i = 0; i < iCount; i++) // 拆分小包下载到单片机
{
while (1)
{
if (strDlg->m_bNextLine == FALSE)
{
Sleep(100);
}
else
{
break;
}
}
char cBuffLine[2048];
memset(cBuffLine, 0xFF, 2048);
// 添加包头 包尾
char cBuffPage[1034] = { 0xFE, 0xFE };
cBuffPage[2] = (uint16_t)iCount & 0xff;
cBuffPage[3] = (uint16_t)iCount >> 8;
cBuffPage[4] = (uint16_t)i & 0xff;
cBuffPage[5] = (uint16_t)i >> 8;
cBuffPage[1032] = 0xEF;
cBuffPage[1033] = 0xEF;
//将字符串转化为字节数据
csline = csLineAll.Mid(0, 2048);
int istrlen = strDlg->String2Hex(csline, cBuffLine);
for (size_t i = 0; i < 1024; i++)
{
cBuffPage[6 + i] = cBuffLine[i];
}
// 添加crc32 校验
uint16_t crc16;
crc16 = crc16_modbus((uint8_t*)cBuffPage, 1030);
cBuffPage[1030] = crc16 & 0xff;
cBuffPage[1031] = crc16 >> 8;
//------------------------------------------------------------
DWORD dwBytesWritten = 1034;
DWORD dwErrorFlags;
COMSTAT ComStat;
OVERLAPPED m_OsWrite;
BOOL bWriteStat;
memset(&m_OsWrite, 0, sizeof(OVERLAPPED)); // 不初始化,发送结果返回错误码 6
memset(&strDlg->m_odWrite, 0, sizeof(OVERLAPPED));
strDlg->m_odWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
PurgeComm(strDlg->hCom, PURGE_RXABORT | PURGE_TXABORT | PURGE_RXCLEAR | PURGE_TXCLEAR);
bWriteStat = WriteFile(strDlg->hCom, cBuffPage, 1034, &dwBytesWritten, &m_OsWrite);
if (!bWriteStat)
{
if (GetLastError() == ERROR_IO_PENDING)
{
WaitForSingleObject(m_OsWrite.hEvent, 1000);
CString cslineshow;
cslineshow.Format("发送串口数据:%s", csline);
csLineAll = csLineAll.Mid(2048);
strDlg->EditShowData(cslineshow);
strDlg->m_bNextLine = FALSE;
}
else
{
CString cserror;
cserror.Format("err = %d\n ", GetLastError());
strDlg->EditShowData(cserror);
}
}
else
{
CString cslineshow;
cslineshow.Format("发送串口数据:%s", csline);
strDlg->EditShowData(cslineshow);
csLineAll = csLineAll.Mid(2048);
strDlg->m_bNextLine = FALSE;
}
}
return 1;
}
五、HEX文件格式
hex文件格式是可以烧写到单片机中,被单片机执行的一种文件格式,生成Hex文件的方式有很多种,可以通过不同的编译器将C程序或者汇编程序编译生成hex。
Hex文件格式解析
Hex文件如果用特殊的程序来查看(一般记事本就可以实现)。打开后可发现,整个文件以行为单位,每行以冒号开头,内容全部为16进制码(以ASCII码形式显示)。Hex文件可以按照如下的方式进行拆分来分析其中的内容:
例如:
:020000040000FA , 我把它看做 0x02 0x00 0x00 0x04 0x00 0x00 0xFA
第一个 0x02 为数据长度。
紧跟着后面的0x00 0x00 为地址。
再后面的0x04为数据类型,类型共分以下几类:
‘00’ Data Record//数据记录
‘01’ End of File Record//文件结束记录
‘02’ Extended Segment Address Record//扩展段地址记录
‘03’ Start Segment Address Record//开始段地址记录
‘04’ Extended Linear Address Record//扩展线性地址记录
‘05’ Start Linear Address Record//开始线性地址记录
然后,接着0x04后面的两个 0x00 0x00就是数据。最后一个0xFA是校验码。
第一行,是Extended Linear Address Record,里面的数据,也就是基地址是0x0004,第二行是Data Record,里面的地址值是0x0000。那么数据18F09FE518F09FE518F09FE518F09FE5要写入FLASH中的地址为 (0x0004 << 16) | 0x0000,也就是写入FLASH的0x40000这个地址。同样,第三行的数据的写入地址为0x40010。当一个HEX文件的数据超过7k的时候,文件中就会出现多个Extended Linear Address Record。
End of File Record 行是每一个HEX文件的最后一行。例如:
:00000001FF
这样的一行数据内容是固定的,数据长度为0,地址为0。
校验值:每一行的最后一个值为此行数据的校验和。例如:
:1000000018F09FE518F09FE518F09FE518F09FE5C0 这行中的 0xC0
:1000100018F09FE5805F20B9F0FF1FE518F09FE51D 这行中的 0x1D
校验和的算法为:计算从0x3A 以后(不包括0x3A)的所有各字节的和模256的余。即各字节二进制算术和,不计超过256的溢出值,然后用0x100减去这个算数累加和,得出得值就是此行得校验和。
工程代码参见:
Windows API串口异步通信实例代码:需要的自己下载吧,有问题可以一起交流。
有部分TCP通信的代码实现,也可以参考下。
总结
以上就是今天要讲的内容了,希望本文能够帮助您更好地学习技术知识,并且让它实现更多的价值。如果您想要获得更多关于技术文章信息,欢迎关注我或者一起探讨相关问题,感谢支持。