前言
本着学习研究的态度,用c#语言实现简单的串口通信工具。
一、串口通信原理
串口通信
串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但不是必须的。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配。
常用术语介绍
波特率:这是一个衡量符号传输速率的参数。指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps。一般调制速率大于波特率,比如曼彻斯特编码)。通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信。
数据位:这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位)。扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语"包"指任何通信的情况。
停止位:用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
奇偶校验位:在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。
串口引脚图解
1 载波检测(DCD) 2 接受数据(RXD) 3 发出数据(TXD) 4 数据终端准备好(DTR)
5 信号地线(SG) 6 数据准备好(DSR) 7 请求发送(RTS) 8 清除发送(CTS) 9 振铃指示(RI)
二、使用System.IO.Port.SerialPort类实现串口通信
System.IO.Port.SerialPort类介绍
System.IO.Port.SerialPort是.NET Framework提供的操作串行端口的类,里面提供了一些方法、属性和和事件供开发者调用操作串口。
调用流程
1. 直接调用SerialPort的静态方法GetPortNames()获取当前计算机的串行端口名称数组
2.根据串口名称,初始化SerialPort对象,设置参数,调用Open()方法打开串口
3.调用Write()方法发送数据
4.注册接收数据的监听,获取数据(或者另起线程循环读取接收数据,本文使用注册监听方式接收数据)
具体代码实现
using System; using System.IO.Ports; using System.Text; namespace PortControlDemo { public class PortControlHelper { #region 字段/属性/委托 /// <summary> /// 串行端口对象 /// </summary> private SerialPort sp; /// <summary> /// 串口接收数据委托 /// </summary> public delegate void ComReceiveDataHandler(string data); public ComReceiveDataHandler OnComReceiveDataHandler = null; /// <summary> /// 端口名称数组 /// </summary> public string[] PortNameArr { get; set; } /// <summary> /// 串口通信开启状态 /// </summary> public bool PortState { get; set; } = false; /// <summary> /// 编码类型 /// </summary> public Encoding EncodingType { get; set; } = Encoding.ASCII; #endregion #region 方法 public PortControlHelper() { PortNameArr = SerialPort.GetPortNames(); sp = new SerialPort(); sp.DataReceived += new SerialDataReceivedEventHandler(DataReceived); } /// <summary> /// 打开端口 /// </summary> /// <param name="portName">端口名称</param> /// <param name="boudRate">波特率</param> /// <param name="dataBit">数据位</param> /// <param name="stopBit">停止位</param> /// <param name="timeout">超时时间</param> public void OpenPort(string portName , int boudRate = 115200, int dataBit = 8, int stopBit = 1, int timeout = 5000) { try { sp.PortName = portName; sp.BaudRate = boudRate; sp.DataBits = dataBit; sp.StopBits = (StopBits)stopBit; sp.ReadTimeout = timeout; sp.Open(); PortState = true; } catch (Exception e) { throw e; } } /// <summary> /// 关闭端口 /// </summary> public void ClosePort() { try { sp.Close(); PortState = false; } catch (Exception e) { throw e; } } /// <summary> /// 发送数据 /// </summary> /// <param name="sendData"></param> public void SendData(string sendData) { try { sp.Encoding = EncodingType; sp.Write(sendData); } catch (Exception e) { throw e; } } /// <summary> /// 接收数据回调用 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void DataReceived(object sender, SerialDataReceivedEventArgs e) { byte[] buffer = new byte[sp.BytesToRead]; sp.Read(buffer, 0, buffer.Length); string str = EncodingType.GetString(buffer); if (OnComReceiveDataHandler != null) { OnComReceiveDataHandler(str); } } #endregion } }
三、编写Winform串口通信工具界面
界面预览
操作介绍
本界面主要功能是操作两个串口,一个发送数据另一个接收数据。左侧设置两串口的一些参数,设置完成后点击"打开发送接收串口",如两串口成功打开,右侧便可操作发送和接受数据。
具体代码实现
using System; using System.Windows.Forms; namespace PortControlDemo { public partial class FrmPortControl : Form { #region 字段/属性 int[] BaudRateArr = new int[] { 110, 300, 1200, 2400, 4800, 115200 }; int[] DataBitArr = new int[] { 6, 7, 8 }; int[] StopBitArr = new int[] { 1, 2, 3}; int[] TimeoutArr = new int[] { 500, 1000, 2000, 5000, 10000 }; object[] CheckBitArr = new object[] { "None"}; private bool ReceiveState = false; private PortControlHelper pchSend; private PortControlHelper pchReceive; #endregion #region 方法 /// <summary> /// 初始化控件 /// </summary> private void InitView() { cb_portNameSend.DataSource = pchSend.PortNameArr; cb_portNameReceive.DataSource = pchReceive.PortNameArr; cb_baudRate.DataSource = BaudRateArr; cb_dataBit.DataSource = DataBitArr; cb_stopBit.DataSource = StopBitArr; cb_checkBit.DataSource = CheckBitArr; cb_timeout.DataSource = TimeoutArr; FreshBtnState(pchSend.PortState && pchReceive.PortState); } /// <summary> /// 刷新按钮状态 /// </summary> /// <param name="state"></param> private void FreshBtnState(bool state) { if (state) { Btn_open.Text = "关闭发送接收串口"; Btn_send.Enabled = true; Btn_receive.Enabled = true; } else { Btn_open.Text = "打开发送接收串口"; Btn_send.Enabled = false; Btn_receive.Enabled = false; } } #endregion #region 事件 public FrmPortControl() { InitializeComponent(); pchSend = new PortControlHelper(); pchReceive = new PortControlHelper(); InitView(); } /// <summary> /// 点击 发送数据 按钮,发送文本内数据 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Btn_send_Click(object sender, EventArgs e) { pchSend.SendData(tb_send.Text); } /// <summary> /// 点击 开始接收 按钮,开始监听串口接收入口数据 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Btn_receive_Click(object sender, EventArgs e) { if (ReceiveState) { pchReceive.OnComReceiveDataHandler -= new PortControlHelper.ComReceiveDataHandler(ComReceiveData); Btn_receive.Text = "开始接收"; ReceiveState = false; } else { pchReceive.OnComReceiveDataHandler += new PortControlHelper.ComReceiveDataHandler(ComReceiveData); Btn_receive.Text = "停止接收"; ReceiveState = true; } } /// <summary> /// 开启或关闭 两个通信的串口,刷新按钮状态 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Btn_open_Click(object sender, EventArgs e) { if (pchSend.PortState) { pchSend.ClosePort(); pchReceive.ClosePort(); } else { pchSend.OpenPort(cb_portNameSend.Text, int.Parse(cb_baudRate.Text), int.Parse(cb_dataBit.Text), int.Parse(cb_stopBit.Text), int.Parse(cb_timeout.Text)); pchReceive.OpenPort(cb_portNameReceive.Text, int.Parse(cb_baudRate.Text), int.Parse(cb_dataBit.Text), int.Parse(cb_stopBit.Text), int.Parse(cb_timeout.Text)); } FreshBtnState(pchSend.PortState && pchReceive.PortState); pchReceive.OnComReceiveDataHandler += new PortControlHelper.ComReceiveDataHandler(ComReceiveData); Btn_receive.Text = "停止接收"; ReceiveState = true; } /// <summary> /// 接收到的数据,写入文本框内 /// </summary> /// <param name="data"></param> private void ComReceiveData(string data) { this.Invoke(new EventHandler(delegate { tb_receive.AppendText($"接收:{data} "); })); } #endregion } }
总结
有框架提供的帮助类,我们实现串口通信虽然不难,但是还是有很多细节要注意的。写好任何一样东西都不容易,一起加油吧!
附录
搭建一个串口调试环境的工具和本文源码地址,有需要的筒靴自提吧 (ง •̀_•́)ง
串口调试Demo源码地址:https://files.cnblogs.com/files/ElijahZeng/PortControlDemo.rar