串口是串行接口(serial port)的简称,也称为 串行通信接口 或 COM接口。
串口通信(serial communication)是指采用串行通信协议在一条信号线上将数据一个比特一个比特地逐位进行传输的通信模式。
串口按电气标准及协议来划分,包括RS-232-C、RS-422、RS485等。
在串行通信中,数据在1位宽的单条线路上进行传输,一个字节的数据要分为8次,由低位到高位按顺序一位一位的进行传送。
串行通信的数据是逐位传输的,发送方发送的每一位都具有固定的时间间隔,这就要求接收方也要按照发送方同样的时间间隔来接收每一位。不仅如此,接收方还必须能够确定一个信息组的开始和结束。
常用的两种基本串行通信方式包括同步通信和异步通信。
同步通信(SYNC:synchronous data communication)是指在约定的通信速率下,发送端和接收端的时钟信号频率和相位始终保持一致(同步),这样就保证了通信双方在发送和接收数据时具有完全一致的定时关系。
同步通信把许多字符组成一个信息组(信息帧),每帧的开始用同步字符来指示,一次通信只传送一帧信息。在传输数据的同时还需要传输时钟信号,以便接收方可以用时针信号来确定每个信息位。
优点:传送信息的位数几乎不受限制,一次通信传输的数据有几十到几千个字节,通信效率较高。
缺点:要求在通信中始终保持精确的同步时钟,即发送时钟和接收时钟要严格的同步(常用的做法是两个设备使用同一个时钟源)。
异步通信(ASYNC:asynchronous data communication),又称为起止式异步通信,是以字符为单位进行传输的,字符之间没有固定的时间间隔要求,而每个字符中的各位则以固定的时间传送。
在异步通信中,收发双方取得同步是通过在字符格式中设置起始位和停止位的方法来实现的。具体来说就是,在一个有效字符正式发送之前,发送器先发送一个起始位,然后发送有效字符位,在字符结束时再发送一个停止位,起始位至停止位构成一帧。停止位至下一个起始位之间是不定长的空闲位,并且规定起始位为低电平(逻辑值为0),停止位和空闲位都是高电平(逻辑值为1),这样就保证了起始位开始处一定会有一个下跳沿,由此就可以标志一个字符传输的起始。而根据起始位和停止位也就很容易的实现了字符的界定和同步。
显然,采用异步通信时,发送端和接收端可以由各自的时钟来控制数据的发送和接收,这两个时钟源彼此独立,可以互不同步。
下面简单的说说异步通信的数据发送和接收过程。
在介绍异步通信的数据发送和接收过程之前,有必要先弄清楚异步通信的数据格式。
异步通信规定传输的数据格式由起始位(start bit)、数据位(data bit)、奇偶校验位(parity bit)和停止位(stop bit)组成,如图1所示(该图中未画出奇偶校验位,因为奇偶检验位不是必须有的,如果有奇偶检验位,则奇偶检验位应该在数据位之后,停止位之前)。
起始位:起始位必须是持续一个比特时间的逻辑0电平,标志传输一个字符的开始,接收方可用起始位使自己的接收时钟与发送方的数据同步。
数据位:数据位紧跟在起始位之后,是通信中的真正有效信息。数据位的位数可以由通信双方共同约定,一般可以是5位、7位或8位,标准的ASCII码是0 ~127(7位),扩展的ASCII码是0 ~255(8位)。传输数据时先传送字符的低位,后传送字符的高位。
奇偶校验位:奇偶校验位仅占一位,用于进行奇校验或偶校验,奇偶检验位不是必须有的。如果是奇校验,需要保证传输的数据总共有奇数个逻辑高位;如果是偶校验,需要保证传输的数据总共有偶数个逻辑高位。
举例来说,假设传输的数据位为01001100,如果是奇校验,则奇校验位为0(要确保总共有奇数个1),如果是偶校验,则偶校验位为1(要确保总共有偶数个1)。
由此可见,奇偶校验位仅是对数据进行简单的置逻辑高位或逻辑低位,不会对数据进行实质的判断,这样做的好处是接收设备能够知道一个位的状态,有可能判断是否有噪声干扰了通信以及传输的数据是否同步。
停止位:停止位可以是是1位、1.5位或2位,可以由软件设定。它一定是逻辑1电平,标志着传输一个字符的结束。
空闲位:空闲位是指从一个字符的停止位结束到下一个字符的起始位开始,表示线路处于空闲状态,必须由高电平来填充。
清楚了异步通信的数据格式之后,就可以按照指定的数据格式发送数据了,发送数据的具体步骤如下:
初始化后或者没有数据需要发送时,发送端输出逻辑1,可以有任意数量的空闲位。
当需要发送数据时,发送端首先输出逻辑0,作为起始位。
接着就可以开始输出数据位了,发送端首先输出数据的最低位D0,然后是D1,最后是数据的最高位。
如果设有奇偶检验位,发送端输出检验位。
最后,发送端输出停止位(逻辑1)。
如果没有信息需要发送,发送端输出逻辑1(空闲位),如果有信息需要发送,则转入步骤(2)。
在异步通信中,接收端以接收时钟和波特率因子决定每一位的时间长度。下面以波特率因子等于16(接收时钟每16个时钟周期使接收移位寄存器移位一次)为例来说明。
开始通信,信号线为空闲(逻辑1),当检测到由1到0的跳变时,开始对接收时钟计数。
当计到8个时钟的时候,对输入信号进行检测,若仍然为低电平,则确认这是起始位,而不是干扰信号。
接收端检测到起始位后,隔16个接收时钟对输入信号检测一次,把对应的值作为D0位数据。
再隔16个接收时钟,对输入信号检测一次,把对应的值作为D1位数据,直到全部数据位都输入。
检验奇偶检验位。
接收到规定的数据位个数和校验位之后,通信接口电路希望收到停止位(逻辑1),若此时未收到逻辑1,说明出现了错误,在状态寄存器中置“帧错误”标志;若没有错误,对全部数据位进行奇偶校验,无校验错时,把数据位从移位寄存器中取出送至数据输入寄存器,若校验错,在状态寄存器中置“奇偶错”标志。
本帧信息全部接收完,把线路上出现的高电平作为空闲位。
当信号再次变为低时,开始进入下一帧的检测。
串口、UART口、COM口、USB口是指的物理接口形式(物理硬件)。
而TTL、RS-232、RS-485是指的电平标准(电信号)。
串口:串口是一个泛称,UART,TTL,RS232,RS485都遵循类似的通信时序协议,因此都被通称为串口。
UART接口:通用异步收发器(Universal Asynchronous Receiver/Transmitter),UART是串口收发的逻辑电路,这部分可以独立成芯片,也可以作为模块嵌入到其他芯片里,单片机、SOC、PC里都会有UART模块。
COM口:特指台式计算机或一些电子设备上的D-SUB外形(一种连接器结构,VGA接口的连接器也是D-SUB)的串行通信口,应用了串口通信时序和RS232的逻辑电平。
TTL,RS232,RS485都是一种逻辑电平的表示方式
TTL:TTL指双极型三极管逻辑电路,市面上很多“USB转TTL”模块,实际上是“USB转TTL电平的串口”模块。这种信号0对应0V,1对应3.3V或者5V。与单片机、SOC的IO电平兼容。不过实际也不一定是TTL电平,因为现在大部分数字逻辑都是CMOS工艺做的,只是沿用了TTL的说法。我们进行串口通信的时候从单片机直接出来的基本是都是 TTL 电平。
TTL电平:全双工(逻辑1: 2.4V–5V; 逻辑0: 0V–0.5V)
硬件框图如下,TTL用于两个MCU间通信
‘0’和‘1’表示
RS232:是电子工业协会(Electronic Industries Association,EIA) 制定的异步传输标准接口,同时对应着电平标准和通信协议(时序),其电平标准:+3V~+15V对应0,-3V~-15V对应1。
RS232 的逻辑电平和TTL 不一样但是协议一样。
RS-232电平:全双工(逻辑1:-15V–5V 逻辑0:+3V–+15V)
硬件框图如下,RS232用于MCU与PC机之间通信
‘0’和‘1’表示
RS485:RS485是一种串口接口标准,为了长距离传输采用差分方式传输,传输的是差分信号,抗干扰能力比RS232强很多。两线压差为-(26)V表示0,两线压差为+(26)V表示1
RS-485:半双工、(逻辑1:+2V–+6V 逻辑0:-6V—2V)这里的电平指AB 两线间的电压差。
串口协议是一种基于串行通信的数据传输协议。它通过串口接口将数据以串行的方式传输。串口协议通常包括物理层、数据链路层和应用层三个部分,其中物理层主要定义了串口接口的电气特性,数据链路层定义了数据的传输方式和错误检测机制,应用层定义了具体的数据格式和通信协议。
串口协议的物理层主要定义了串口接口的电气特性,包括传输速率、数据位、停止位、奇偶校验等。常见的串口接口有RS-232、RS-422、RS-485等。
串口协议的数据链路层定义了数据的传输方式和错误检测机制。串口通信采用异步传输方式,即每个数据字节之间没有固定的时间间隔。在数据传输时,每个字节都以一个起始位和一个或多个停止位作为帧定界符,以便接收端能够识别每个字节的开始和结束。
串口协议还包括奇偶校验机制和流控制机制。奇偶校验机制可以检测数据传输过程中的错误,流控制机制可以控制数据的传输速率,防止数据丢失或溢出。
串口协议的应用层定义了具体的数据格式和通信协议。常见的串口协议有Modbus(ascii,RTU)、三菱FX系列编程口专用协议、西门子S7-200系列PLC PPI专用协议、西门子USS协议(USS协议(USS Protocol)是西门子公司推出的用于控制器(PLC/PG/PC)与驱动装置之间数据交换的通信协议)、自定义协议等等。不同的应用场景需要使用不同的串口协议。
三菱FX3u编程口用于PLC程序的下载和上传、程序调试监控、和其它设备(变频器、上位机、打印机等等)的通信,数据通信使用三菱FX3u编程口专用协议。
通过专用协议,可以实现:
根据《三菱FX编程口协议.pdf》文档,可以手动封装各读写命令数据帧,也可以使用第三方通信库。
这里使用HslCommunication通信库,官方网址链接,能够实现三菱PLC的读写、西门子PLC的读写、欧姆龙PLC读写、Modbus Tcp读写、Modbus服务器开发、日志记录功能。
HslCommunication通信库的使用可以参考1)C# Demo源代码地址;2)通讯库使用说明blog。
(1). 新建工程
打开vs2019,新建windowsForm应用。
(2). 使用nuget添加HslCommunication通信库
打开Form1.cs代码编辑器界面,添加HslCommunication相关命名空间的引用
using HslCommunication;
using HslCommunication.Profinet.Melsec;
(3). UI界面设计
在UI界面设计器中,使用label、Textbox、ComboBox、button、groupBox设计如下的界面。
其中端口号使用textbox控件,使用时根据使用计算机com口实际使用端口号,手动输入。
波特率comboBox手动添加两个item,9600和115200。
(4) 添加全局字段
//定义PLC对象
private MelsecFxSerial FxSerial = new MelsecFxSerial();
//定义obj用于进程锁
private object obj = new object();
//定义定时器,用于周期读取X、Y、M区变量
System.Timers.Timer timer = new System.Timers.Timer();
为“连接”按钮添加如下代码:
///
/// 连接串口
///
///
///
private void button1_Click(object sender, EventArgs e)
{
FxSerial.SerialPortInni(sp =>
{
sp.PortName = tbx_port.Text;
sp.BaudRate = int.Parse(cbx_baud.Text) ;
sp.DataBits = 7;
sp.StopBits = System.IO.Ports.StopBits.One;
sp.Parity = System.IO.Ports.Parity.Even;
}
);
try
{
FxSerial.Open();
timer.Start();
MessageBox.Show("串口打开!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
为“断开”按钮添加如下代码:
///
/// 关闭串口
///
///
///
private void button2_Click(object sender, EventArgs e)
{
FxSerial.Close();
FxSerial.Dispose();
timer.Stop();
MessageBox.Show("串口关闭");
}
主要完成两个任务:1)定时器初始化;2)3组label的初始化(因为相关属性基本一致,使用代码完成属性设置更快;这里每个label的mouseDown事件调用同一个方法)。
X、Y、M三组label的text属性和变量地址对应
private void Form1_Load(object sender, EventArgs e)
{
//定时器初始化
timer.Elapsed += new System.Timers.ElapsedEventHandler(onTimerTrig);
timer.AutoReset = true;
timer.Interval = 500;
timer.Enabled = false;
lbl_y0.BackColor = Color.Red;
lbl_y1.BackColor = Color.Red;
lbl_y2.BackColor = Color.Red;
lbl_y3.BackColor = Color.Red;
lbl_y4.BackColor = Color.Red;
lbl_y5.BackColor = Color.Red;
lbl_y6.BackColor = Color.Red;
lbl_y7.BackColor = Color.Red;
lbl_y0.MouseDown += Lbl_MouseDown;
lbl_y1.MouseDown += Lbl_MouseDown;
lbl_y2.MouseDown += Lbl_MouseDown;
lbl_y3.MouseDown += Lbl_MouseDown;
lbl_y4.MouseDown += Lbl_MouseDown;
lbl_y5.MouseDown += Lbl_MouseDown;
lbl_y6.MouseDown += Lbl_MouseDown;
lbl_y7.MouseDown += Lbl_MouseDown;
lbl_m0.BackColor = Color.Red;
lbl_m1.BackColor = Color.Red;
lbl_m2.BackColor = Color.Red;
lbl_m3.BackColor = Color.Red;
lbl_m4.BackColor = Color.Red;
lbl_m5.BackColor = Color.Red;
lbl_m6.BackColor = Color.Red;
lbl_m7.BackColor = Color.Red;
lbl_m0.MouseDown += Lbl_MouseDown;
lbl_m1.MouseDown += Lbl_MouseDown;
lbl_m2.MouseDown += Lbl_MouseDown;
lbl_m3.MouseDown += Lbl_MouseDown;
lbl_m4.MouseDown += Lbl_MouseDown;
lbl_m5.MouseDown += Lbl_MouseDown;
lbl_m6.MouseDown += Lbl_MouseDown;
lbl_m7.MouseDown += Lbl_MouseDown;
}
Lbl_MouseDown方法代码:
private void Lbl_MouseDown(object sender, MouseEventArgs e)
{
if(e.Button == MouseButtons.Right)
{
//根据label的text属性值作为变量地址,根据背景色决定即将写入的bool值
Write_Bool(((Label)sender).Text, (((Label)sender).BackColor == Color.Red) ? true : false);
}
}
private bool Write_Bool(string addr,bool bVal)
{
bool res = false;
//FxSerial同一时间只能有一个线程独占使用,使用lock语句获取并独占对FxSerial的使用权
lock (obj)
{
if (FxSerial.IsOpen() == true)
{
OperateResult Write_res = FxSerial.Write(addr, bVal);
if (Write_res.IsSuccess)
{
//MessageBox.Show("写入成功");
res= true;
}
}
else
{
timer.Stop();
MessageBox.Show("串口已关闭");
}
return res;
}
}
///
/// X区、Y区、M区 lbl背景色更新(用来指示各存储区位变量状态)
///
///
///
public void UpdateForm(bool[] bool_res, string addrType)
{
switch (addrType)
{
case "X":
if (this.IsHandleCreated)
{
this.Invoke(new Action(() =>
{
foreach (var c in groupBox3.Controls)
{
if (c is Label)
{
((Label)c).BackColor = bool_res[int.Parse(((Label)c).Text.Substring(1, 1))] ? Color.Green : Color.Red;
}
}
}));
}
break;
case "M":
if (this.IsHandleCreated)
{
this.Invoke(new Action(() =>
{
foreach (var c in groupBox5.Controls)
{
if (c is Label)
{
((Label)c).BackColor = bool_res[int.Parse(((Label)c).Text.Substring(1, 1))] ? Color.Green : Color.Red;
}
}
}));
}
break;
case "Y":
if (this.IsHandleCreated)
{
this.Invoke(new Action(() =>
{
foreach (var c in groupBox4.Controls)
{
if (c is Label)
{
((Label)c).BackColor = bool_res[int.Parse(((Label)c).Text.Substring(1, 1))] ? Color.Green : Color.Red;
}
}
}));
}
break;
default:
break;
}
}
///
/// 声明一个带参数的委托
///
///
///
public delegate void MyInvoke(bool[] bool_res,string addrType);
///
/// 定时器事件处理程序
///
///
///
private void onTimerTrig(object sender, ElapsedEventArgs e)
{
//周期读取变量
//这里的sw用于计算以下代码执行时间
sw.Restart();
lock (obj)
{
if (FxSerial.IsOpen() == true)
{
// 读M区
try
{
OperateResult<bool[]> Read_res = FxSerial.ReadBool("m0", 8);
if (Read_res.IsSuccess)
{
bool[] read_bool_res = Read_res.Content;
MyInvoke mi = new MyInvoke(UpdateForm);
this.BeginInvoke(mi, new Object[] { read_bool_res, "M" });
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
// 读Y区
try
{
OperateResult<bool[]> Read_res = FxSerial.ReadBool("y0", 8);
if (Read_res.IsSuccess)
{
bool[] read_bool_res = Read_res.Content;
MyInvoke mi = new MyInvoke(UpdateForm);
this.BeginInvoke(mi, new Object[] { read_bool_res, "Y" });
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
// 读X区
try
{
OperateResult<bool[]> Read_res = FxSerial.ReadBool("x0", 8);
if (Read_res.IsSuccess)
{
bool[] read_bool_res = Read_res.Content;
MyInvoke mi = new MyInvoke(UpdateForm);
this.BeginInvoke(mi, new Object[] { read_bool_res, "X" });
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
else
{
MessageBox.Show("串口已关闭");
timer.Stop();
}
}
//这里的sw用于计算以下代码执行时间
sw.Stop();
Console.WriteLine("time= {0}", sw.ElapsedMilliseconds.ToString());
}
运行后,初始化界面如下:
运行操作动画:
主要演示了PLC的连接和关闭;
PLC改变存储区变量值,上位机可以同步修改;在上位机右键单击Y区和M区label,对应的label标签背景色相应反转,同时PLC对应地址值也发生变化。操作过程中界面无卡顿。
通过输出窗口,可以看到定时器事件处理程序一次执行大概消耗130ms。
单击打开源码下载链接
不定期分享c#、wpf上位机开发学习经验,欢迎交流。
点赞、收藏加关注,让你永远不迷路。