MFC串口通信编程之字符串发送十六进制发送

01MFC/VS2013编程 专栏收录该内容
21 篇文章 0 订阅

1.串口通信基础

通信协议:物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输;协议层规定通讯逻辑,统一收发双方的数据打包、解包标准。

串口通讯物理层之RS-232

RS-232标准主要规定了信号用途、通讯接口和信号电平标准。如图9-1为常见的设备间串口通讯结构图。
在这里插入图片描述
如上图所示,每个设备都有一个DB9接口,通过DB9接口连接RS-232标准的串口线进行数据传输。由于通过RS-232标准传输的电平信号到达设备后,不能直接被识别,所以会通过电平转换芯片(例如MAX3232、SP3232芯片等)转换成能识别的TTL电平信号,实现通讯。

相互通讯的两个设备间,一个设备发送数据,一个设备接收数据。一般情况下,发送数据的设备称为DTE,如计算机;接收数据的设备称为DCE,如调制解调器。

串口通讯协议层

协议层规定了数据包的内容,内容包含了波特率、起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据。
在这里插入图片描述

  • 波特率

串口异步通讯中由于没有时钟信号,所以通讯双方需要约定好波特率,即每个码元的长度,以便对信号进行解码。常见的波特率有4800、9600、115200等。

  • 起始位、停止位

数据包从起始位开始,到停止位结束。起始信号用逻辑0的数据位表示,停止信号由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。

  • 有效数据

起始位之后便是传输的主体数据内容了,也称为有效数据,其长度一般被约定为5、6、7或8位长。

  • 数据校验

由于在通讯过程中易受到外部干扰导致传输数据出现偏差,所以在有效数据之后加上校验位解决。校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)及无校验(noparity)。

奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个8位长的有效数据为:01101001,此时共有4个“1”,为达到奇校验效果,校验位为“1”,最后传输的是8位有效数据加1位校验位,共9位。

而偶校验刚好相反,要求有效数据和校验位的“1”数量为偶数,则此时为达到偶校验效果,校验位为“0”。

而0校验则无论有效数据中是什么数据内容,校验位总是为“0”,1校验校验位总是为“1”。

2.串口通信校验方式

利用串口传输数据时,近距离传输还好,远距离传输由于线路长度影响,可能会使信号在传输过程中出现不可预知的错误,为了达到通信的稳定性,在远距离通信时一般要引入一种校验方式来去除干扰。

方便简单的奇偶校验

奇偶校验需要一位校验位,即使用串口通信的方式2或方式3(8位数据位+1位校验位)。

奇校验(odd parity):让传输的数据(包含校验位)中1的个数为奇数。

偶校验(even parity):让传输的数据(包含校验位)中1的个数为偶数。
数据和校验位发送给接受方后,接收方再次对数据中1的个数进行计算,如果为奇数则校验通过,表示此次传输过程未发生错误。如果不是奇数,则表示有错误发生,此时接收方可以向发送方发送请求,要求重新发送一遍数据。

  • 优缺点

奇偶校验的检错率只有50%,因为只有奇数个数据位发生变化能检测到,如果偶数个数据位发生变化则无能为力了。

奇偶校验每传输一个字节都需要加一位校验位,对传输效率影响很大。

奇偶校验只能发现错误,但不能纠正错误,也就是说它只能告诉你出错了,但不能告诉你怎么出错了,一旦发现错误,只好重发。

虽然奇偶校验有很多缺点,但因为其使用起来十分简单,故目前仍被广泛使用。

  • 应用

如何用编程确定一个字节中“1”个数的奇偶性?我们可以利用二进制数相加的特点:

0+0=0、1+0=1、1+1=0

可以看出,如果我们将一个字节的所有位相加

有奇数个“1”的字节的和为1
有偶数个“1”的字节的和为0
由此即可通过编程完成判断。实际应用中,实现方法很多,但这是相对简单的一种,这里不再赘述。

累加和校验

所谓的累加和校验有很多种,最常见的一种是在每次通信数据包最后都加一个字节的校验数据,这个校验字节里的数据是通信数据包里所有数据的不进位累加和。
接收方接收到数据后同样对一个数据包的数据进行不进位累加和计算,如果累加出的结果与校验位相同的话就认为传输的数据没有错误。

  • 优缺点

实现起来方便简单,被广泛运用。
检错率一般,例如一个字节多1,一个字节少1,则会出现误判。
和奇偶校验一样,只能发现错误,但不能纠正错误。
另外,累加和校验的数据传输格式可以表示为:通讯数据+校验数据。这与我们之后要说的CRC循环冗余码校验是相同的。

循环冗余校验

CRC全称:Cyclic Redundancy Check

我们在工作中,很多场合都会用到CRC。像做视频编解码,就需要时刻校验每一帧的CRC,验证解码或者编码出来的码流是否正确。
发送码串的时候,在原始码串后面加上CRC。CRC一般有8bit,16bit,和32bit。

  • 生成CRC码的基本原理

发送方通过指定的g(x)生成多项式产生CRC码字,接收方则通过该g(x)生成多项式来验证收到的CRC码字。

例如:信息字段代码为: 1011001;对应m(x)=x6+x4+x3+1

​ 假设生成多项式为:g(x)=x4+x3+1;则对应g(x)的代码为: 11001

​ x4*m(x)=x10+x8+x7+x4 对应的代码记为:10110010000;

采用多项式除法: 得余数为: 1010 (即校验字段为:1010)

发送方:发出的传输字段为: 1 0 1 1 0 0 1 1 0 10

接收方:使用相同的生成码计算CRC码字进行校验;接收到的字段/生成码(模2除法) ,能够除尽,则正确。

在这里插入图片描述

3.MFC串口通信编程

一般来说,计算机都有一个或多个串行端口,这些串口提供了外部设备与PC进行数据传输和通信的通道,在CPU和外设之间充当解释器的角色。当字符数据从CPU发送给外设时,这些字符数据将被转换成串行比特流数据;当接收数据时,比特流数据被转换为字符数据传递给CPU,再进一步说,在操作系统方面,Windows用通信驱动程序(COMM.DRV)调用API函数发送和接收数据;当用通信控件或声明调用API函数时,它们由COMM.DRV解释并传递给设备驱动程序。作为一个程序员,要编写通信程序,只需知道通信控件提供的Windows API通信函数的接口即可,换句话说,只需设定和监视通信控件的属性和事件即可。

串口通信方法一般有以下几种:

  1. 利用Windows API通信函数;
  2. 利用Visual利用Visual C++的标准通信函数inp、inpw、inpd、outp、outpw、outpd等直接对串口进行操作;
  3. 通过微软的串口通信控件MSComm,它是一种ActiveX控件;
  4. 利用第3方编写的通信类,比如MuMega利用第3方编写的通信类,比如MuMega Technologies公司提供的CSerail类;

win32 API函数和mscomm组件使用示例:https://blog.csdn.net/greless/article/details/77532552

重点介绍MSComm控件

MSComm 是 Microsoft 公司为简化Windows下串行端口编程而提供的ActiveX控件,它提供了一系列标准通讯命令的使用接口。MSComm 控件通过串行端口(serial port)传送和接收数据,为应用程序提供了串行通讯功能。在可视化编程盛行的今天,我们可以很方便的在Visual Basic(VB)、Visual C++(VC)、Delphi等语言及开发平台中应用。处理数据的方式有事件驱动(Event-driver)、查询法(Inquire)两种。

事件驱动法:在使用事件驱动法设计程序时,每当有新字符到达、端口状态变化或发生错误时,MSComm控件将触发OnComm事件,而应用程序在捕获该事件后,通过检查MSComm控件的CommEvent属性可以获知所发生的事件或错误,从而采取相应的操作。这种方法的优点是程序响应及时,可靠性高。

查询法:这种方法适合于较小的应用程序。在这种情况下,每当应用程序执行完某一串行口操作后,将不断检查MSComm控件的CommEvent属性以检查执行结果或者检查某一事件是否发生。例如,当程序向串行设备发送了某个命令后,可能只是在等待收到一个特定的响应字符串,而不是对收到的每一个字符都立刻响应并处理。

使用的每个MSComm控件都与一个串口对应。如果在应用程序中需要访问多个串口,必须使用多个MSComm控件,可以在Windows 控制面板中修改串口地址的中断地址。

  • MSComm控件的常用属性

CommPort属性:设置或返回通讯端口号,可以设置为1到16之间的任何值;
Settings属性:以字符串形式设置或返回波特率、奇偶校验、数据位和停止位;
PortOpen属性:设置或返回通讯口的状态以及打开和关闭端口,可通过把该属性设置为true或者false来打开或者关闭端口;
InBufferSize和OutBufferSize属性:分别设置接收和发送缓冲区分配的内存数量,单位为字节,缺省值分别为1024byte和512byte;
InputLen属性:确定希望从接收缓冲区移出的字符数量,当InputLen=0时,一次把接收缓冲区的字符全部移出;
Input属性:从接收缓冲区中读出数据,然后将该数据从缓冲区移走。
OutPut属性:向发送缓冲区传递待发送的数据。
InBufferCount和OutBufferCount属性:分别确定当前驻留在接收缓冲区等待被取出和发送缓冲区准备发送的字符数量,这两个属性设置为0,接收和发送缓冲区的内容将被清除;
InputMode属性:设置接收传入数据的格式,设置为0采用文本形式,设置为1采用二进制格式;
SThreshold属性:保存一个产生发送OnComm事件的界限值,本系统设置该属性为0,发送数据时不产生OnComm事件;
RThreshold属性:设定当接收几个字符时触发OnComm事件,本系统设置该属性为1,每接收一个字符就产生一个OnComm事件;

  • MSComm控件的事件

MSCOMM控件只使用一个事件OnComm,用属性CommEvent的17个值来区分不同的触发时机,主要有以下几个:

CommEvent=1时:传输缓冲区中的字符个数已少于Sthreshold(可设置的属性值)个;
CommEvent=2时:接收缓冲区中收到Rthreshold(可设置的属性值)个字符,利用此事件可编写接收数据的过程;
CommEvent=3时:CTS线发生变化;
CommEvent=4时:DSR线发生变化;
CommEvent=5时:CD线发生变化;
CommEvent=6时:检测到振铃信号;
另外十种情况是通信错误时产生,即错误代码。

  • 基于VS2010下MFC的MSComm串口程序的实现

1、注册MSComm控件

2、添加MSComm控件

3、添加控件变量

4、串口信息配置及打开串口

5、串口数据的读写

MSComm 类的读写函数比较简单:get_Input()和put_Output()。函数原形分别为VARIANT get_Input()和void put_Output(const VARIANT newValue),均使用VARIANT类型。但PC机发送和接收数据时习惯用字符串形式。MSDN中查阅VARIANT类型,可以用BSTR表示字符串,但所有的BSTR都包含宽字符,而只有Windows NT支持宽字符,Windows 9X并不支持。所以要完成一个适应各平台的串口应用程序必须解决这个问题,这里使用CByteArray解决之。

添加接收数据函数,在对话框中双击Comm Control,接受默认函数,则对话框类的成员函数为OnCommMscomm(),其大致代码如下:

	CDataTypeConverter DTC;
    //电话图标可能有一半白边去不了,右击电话图标点击edit control就可以去掉
    if(m_ctrlComm.get_CommEvent()==2)//事件值为2表示接收事件
    {
        BYTE rxdata[255]={0};//设置BYTE数组
        VARIANT variant_inp=m_ctrlComm.get_Input();//读缓冲区
        COleSafeArray safearray_inp = variant_inp;//VARIANT型变量转换为COleSafeArray变量
        long len=safearray_inp.GetOneDimSize();//得到有效数据长度
        for(long k=0;k<len;k++)
            safearray_inp.GetElement(&k,rxdata+k);//转换为BYTE数组
        m_ctrlComm.put_OutBufferCount(0);//清空发送缓冲区
        m_ctrlComm.put_InBufferCount(0);//滑空接收缓冲区
        safearray_inp.Clear();
        for(long k=0;k<len;k++)
        {
        BYTE bt = *(char*)(rxdata+k);//字符型
        short int intDec=(int)bt;
        CString strtemp=DTC.Dec2Hex(intDec);
        m_strDataRXTemp+=strtemp;//加入接收编辑框对应字符串
        }
        m_strDataRX=m_strDataRXTemp;
        m_strDataRXTemp="";
   }

其中,Dec2Hex()函数的代码如下:

CString CDataTypeConverter::Dec2Hex(unsigned int intDec)
{
    CString strHex;
    char charHex[255];
    sprintf(charHex,"%x",intDec);
    strHex=charHex;
    if(strHex.GetLength()==1)
        strHex=_T("0")+strHex;
    return strHex;
}

发送数据的代码大致如下:

//UpdateData(true);//读取编辑框内容m_strDataTX//发送的字符串上表面为十六进制格式
CString m_strCtrlLightBL;
m_strCtrlLightBL="55AA0AAA6B4310100000";//"55aa0aaa6b4310100000"
    
CDataTypeConverter DTC;
COleVariant m_OleVariant=DTC.HexM2OleVariant(m_strCtrlLightBL);
m_ctrlComm.put_Output(m_OleVariant);//发送数据

其中,HexM2OleVariant()函数定义如下:

COleVariant CDataTypeConverter::HexM2OleVariant(CString strHexM)
{
    BYTE bt[255];
    short int len=strHexM.GetLength();
    short int length=0;
    short int intDec;
    for(int n=0,i=0;n<len-1;n+=2,i++)
    {        
        intDec=Hex2Dec(strHexM.Mid(n,2));
        bt[i]=char(intDec);
        length=i+1;
    }
    CByteArray m_Array;
    m_Array.RemoveAll();
    m_Array.SetSize(length);
    for(int i=0;i<length;i++)
        m_Array.SetAt(i,bt[i]);
    return COleVariant(m_Array);
}

注意:接收数据时,RThreshold属性很重要,因为它影响着OnComm事件的触发条件,在程序中可以通过put_RThreshold()函数来设定RThreshold属性。

4.基本数据类型int、float、char

CPU 只能进行数值运算,那么计算机怎么显示出字符

严格的来说,计算机就是是无法显示出字符的……。显示字符的是计算机的外设产品如显示器、投影仪、终端等等……现代计算机显示内容的方式通常是将要显示的图像数据存放在指定的内存区域(显存),再由视频输出设备(显卡)按照某些格式(VGA、DVI等)将图像编码传输给视觉呈现设备(显示器、投影仪等),显示器解码并通过相应的硬件(阴极射线管、液晶等)还原成人类视觉可以读取的图像信息。显示字符和打印文档本质上是一回事儿,纯粹的计算机既不能显示字符也不能打印文档,这些都需要外部设备的支持。

每种高级语言,数据类型都各有特色,而基本数据类型,都是相似的
高级语言的发展,是一个历史过程。最初,计算机能做的事情很简单,编程方式就简单粗暴0101。随着人类的发展、科技的进步,各类电子元件革新,计算机性能更佳,所能承担的任务更多。简单粗暴的编程不再满足。

为了让计算机能最大程度的,帮人类解决问题。编程必须可以更加复杂。

数据类型,就要满足编程需求。

人类社会有的数据类型,计算机要有。人类社会没有的数据类型,如果方便,计算机也要有。

C语言基本的数据类型:整型int、浮点型float、字符型char。

数据类型的使用方法不难理解。

第一步:声明。int i;告诉计算机,整型数i。声明过程,计算机分配一段内存,用于存储i。

第二步:赋值。i=0;把i的值,赋成0。赋值过程,改变此内存中的值。

两步可以放一起写,int i=0;。注:“;”代表一句指令结束。

除了int,还有long int、short int、unsigned int、unsigned long int、unsigned short int。

unsigned是表示无符号,不能表示负数。不加unsigned,是默认signed。

我们要理解它们之间的区别是,占内存大小不同。

通常用操作符sizeof(),计算数据类型所占字节数。

某些数据类型,针对不同位数的操作系统,占字节数不同。

并且,它们的取值范围不同。

float的原理和使用方法和int一致,关键在于小数点的处理。

float类型采用科学记数法,a*10^b。

整个内存被“截”为两部分,一部分表示基数a,另一部分表示指数b。

char,字符型,非常有意思。

char在内存中只占1个字节,取值范围是-128~127。

int、float用作表示数据,char用作表示字符,共计128个,负值未使用。

其中包含26个英文字母(大小写区分),数字0-9,换行符、制表符、回车等。

数字0-9,可以整型,也可以是字符型。该如何区分?

字符型数字:在内存中占1个字节,实际存储值是48-57,并且字符赋值时加单引号,例如’0’。

整型数字:在内存中占4个字节,实际存储值是实际数值。

ASCII码(美国标准信息交换码),对应0~127的编码,就是char的对应字节。

程序,是由一条条指令组成,而指令,则由符号组成。

计算机只识别01,对符号进行信息转换为01的过程,就叫做编码。

注意区分编译,编码是在编写程序之前定义的转换规则,编译是程序编写完成后的转换过程。

计算机所显示的,任何数字、字母、符号都是字符。

汉字也不例外,但是汉字却不能用char表示。因为C语言使用的是ASCII码,没有对汉字进行编码。

编码是个历史遗留问题。

对英语系国家来说,128个编码,足够表达程序和语言。随着发展,汉语圈及其他语种进入,128个编码是完全不够的。

ASCII码单字节编码效率高,无法舍弃,汉语圈需求要满足。只能另外增添新的编码方式,双字节编码,UNICODE横空出世,几乎覆盖所有人类语言。

现在所有显示汉语的界面,采用的UTF-8编码,就是UNICODE转换而来。

5.字符串发送与十六进制发送的区别

在使用串口助手调试,TCP/UDP测试等工具时,经常会在主界面上的数据发送区和数据接收区有一个“按十六进制”的选项,在数据发送区,如果不选择此项,在输入框中输入要发送的字母或者数字,点击“发送”时,这时将按字符的方式发送,即输入的每一个数字或者字母将转换成ASCii码发送,如1a,实际发送的是1的ACSII值49,a的ACSII值97,发送的是两个字符的ASCII值,如果选择了此项,那么点击“发送”按钮时将直接发送1a而非每一个字符的ASCII值;在数据接收区,该功能类似。

注意:由于char类型的取值范围为-128-+127,所以在接收十六进制发送的数据时为防止溢出,接收数据类型最好定义为unsigned char,其取值范围为0-255。

在计算机中,数据是以二进制的形式存储的,例如十进制 1(10进制)在计算机中用 0000 0001(2进制)来表示。我们在用串口发送数据的时候首先将待数据转换为对应的ASCII码,然后再将这些ASCII码按照二进制的方式一位一位的发送出去。

例如我们要发送一串数据“A852010100000000A91A”,以字符串和十六进制两种方式发送:

(1)字符串发送

     串口以字符串发送数据,首先将字符串转化为二进制,格式如下:

      0100 0001     0011 1000    0011 0101    0011 0010    0011 0000   00110001   0011 0000    0011 0001 

            A(0x41)          8                  5                      2                    0                1                    0                    1




     0011 0000     0011 0000   0011 0000    0011 0000     0100 0001    0011 1001    0011 0001    0100 0001

                0                    0                     0                   0                    A                 9                   1                   A

     然后按照8位(串口设置数据位为8位)形式将数据发送出去。

(2)十六进制发送数据

      串口以十六进制发送数据,首先将数据转化为:

      1010 1000     0110 0010    0000 0001     0000 0001    0000 0000    0000 0000     1010 1001     0001 1010

          0XA8            0X52               0X01             0X01              0X00           0X00                0XA9            0X1A 

     然后按照8位(串口设置数据位为8位)形式将数据发送出去。

     串口接收的数据格式如下:

6.串口通信程序中十六进制格式发送和接收实现

上位机软件(MFC)发送给三轴步进电机控制器的指令是用hex方式(也就是16进制方式传送的,而不是Ascii码的形式传送的,比如说‘0’,按照Ascii码的方式传送就是48,而以hex的方式传送就是0)

刚刚用MFC编写了一个采集和设定中央空调控制板上参数的应用程序,控制板和PC机之间通过485转串口和串口转USB电路实现通信。程序设计中碰到一个问题是PC端对发送和接收数据格式的处理,控制板可以读懂的协议是一组16进制数,如“66 03 0C 00 01 00 01 00 00 00 00 00 3C 00 3E F5 94”,PC端应用程序可以解析的也是由控制板发送的一组16进制数,而串口通信是二进制字节流进行,发送和接收缓冲区均为char型的数组,发送时如何将字符串类型的16进制数转换为对应大小的10进制数并存进缓冲数组呢?下面的函数可供参考:

HexChar函数的功能是将16进制字符由ASCII码转为相应大小的16进制数
//也就是说利用串口调试助手,选中以Hex方式发送到的复选框,则文本框中的字符都是16进制的,在进行传送带之前需要将这些字符转化为10进制的形式,然后在进行传送,也就是HexChar()函数所实现的功能。

char HexChar(char c)
    {
        if((c>='0')&&(c<='9')) 
            return c-'0';//16进制中的,字符0-9转化成10进制,还是0-9
        else if((c>='A')&&(c<='F'))
            return c-'A'+10;//16进制中的A-F,分别对应着11-16
        else if((c>='a')&&(c<='f'))
            return c-'a'+10;//16进制中的a-f,分别对应也是11-16,不区分大小写
        else
            return 0x10;   // 其他返回0x10
    }

Str2Hex函数的功能则是将如“66 03 …”形式的字符串以空格为间隔转换为对应的16进制数并存放在BYTE型(typdef unsigned char BYTE)数组中,data数组作为发送缓冲数组写入串口即可。 实际应用中将BYTE数据类型修改为char。也是可以使用的。

下面的函数是将文本框中的字符串,去除空格,然后将其他的字符转换为char型数据和长度,放到数组data中,以方便下面的串口传送。

int Str2Hex(CString str, BYTE *data)
{
    int t,t1;
    int rlen=0,len=str.GetLength();
    if(len==1)
    {
        char h=str[0];
        t=HexChar(h);
        data[0]=(BYTE)t;
        rlen++;
    }

    //data.SetSize(len/2);
    for(int i=0;i<len;)
    {
        char l,h=str[i];
        if(h==' ')
        {
            i++;
            continue;
        }
        i++;
        if(i>=len)
            break;
        l=str[i];
        t=HexChar(h);
        t1=HexChar(l);
        if((t==16)||(t1==16))//判断为非法的16进制数
           break;
        else
            t=t*16+t1;
        i++;
        data[rlen]=(BYTE)t;
        rlen++;
    }
    return rlen;
}

对于接收到的数据,位于接收缓冲区的BYTE数组RecBuf中,如果要以相应大小的16进制形式显示,刚可以将数组中每一个元素以下列格式转换并放入字符串RecText中,即可实现以16进制显示。

下面函数的功能是将char的数据转换为16进制输出。

CString RecText,str;
for(int i=0;i<Rlen;i++)
    {
     str.Format("%02X ",RecBuf[i]);//将接收到的BYTE型数据转换为对应的十六进制
     RecText.Append(str);
    }

7.参考资料

https://zhuanlan.zhihu.com/p/24858971
https://zhuanlan.zhihu.com/p/29693940
https://www.zhihu.com/question/20303082
https://www.cnblogs.com/gaohongchen01/p/4432922.html
https://blog.csdn.net/u010154491/article/details/58592831
https://www.zhihu.com/question/21323244/answer/181518447
https://zhuanlan.zhihu.com/p/33103684

https://blog.csdn.net/fightingforcv/article/details/78386201

  • 7
    点赞
  • 0
    评论
  • 53
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值