C#实现串口通讯程序

308 阅读5分钟
  • 摘要:本文描述如何使用C#实现串口通信,使用系统原生类SerialPort实现。
  • 微信公众号: [编程笔记in]
  • 导航mp.weixin.qq.com/s/Jvs2NsTyf…

  • 前言

    • 基本步骤:

      • 1、自动获取本地串口。
      • 2、根据传入串口基本属性参数打开串口。
      • 3、串口数据的发送接收功能(ASCII和HEX格式)。
      • 4、ASCII和HEX相互转换。
    • SerialPort类:

      • 属性

        • PortName: 获取或设置通信端口。
        • BaudRate: 获取或设置波特率。
        • Parity: 获取或设置校验协议。
        • DataBits: 获取或设置每个字节的标准数据位长度。
        • StopBits: 获取或设置每个字节的标准停止位数。
        • IsOpen: 获取一个值,指示 SerialPort 对象的打开或关闭状态。
      • 方法

        • Open(): 打开一个新的串行端口连接。
        • Close(): 关闭端口连接。
        • ReadExisting(): 读取 SerialPort 对象的流和输入缓冲区中所有立即可用的字节。
        • ReadLine(): 一直读取到输入缓冲区中的 NewLine 值。
        • Write(string text): 将字符串写入串行端口。
        • WriteLine(string text): 将字符串和 NewLine 值写入输出缓冲区。
    • 具体实现:

      • 案例内容大概如下:
        • 1、创建一些使用到的字段
        • 2、窗体初始化、加载
        • 3、创建方法实现通讯状态、数据接收、消息更新、控件状态更新
        • 4、创建按钮事件实现:打开串口、发送数据
        • 5、发送格式变更、以ASCII或HEX格式发送
        • 6、创建串口参数变更方法
        • 7、创建数据发送HEX、数据转换的方法
        • 8、创建自定义控件、用于显示串口打开状态

  • 运行环境

    • 操作系统: Win11
    • 编程软件: Visual Studio 2022
    • .Net版本:.Net Framework 4.8.0

  • 一、预览

    • 运行效果

image.png

  • 二、代码

    • (一)使用到的字段

    #region  字段
    SerialPort serialPortTool;                   //串口
    System.Windows.Forms.Timer timer;           //状态定时器
    private bool IsConnected = false;            //连接状态
    private string portName = "COM1";            // 串口号
    private int baudRate = 9600;                 // 波特率
    private Parity parity = Parity.None;         // 校验位
    private int dataBits = 8;                    // 数据位
    private StopBits stopBits = StopBits.One;    // 停止位
    //波特率数组
    public int[] _BaudRateArray = { 9600, 14400, 19200, 38400, 57600, 115200};
    //数据位数组
    public int[] _DataBitArray = { 8, 7, 6, 5 };
    States runState = States.None;
    #endregion
    
  • (二)窗体初始化、加载

     #region 窗体初始化、加载
     public FrmSerialPort()
     {
         InitializeComponent();
         this.CenterToParent();
     }
     private void FrmSerialPort_Load(object sender, EventArgs e)
     {
         Initialize();
     }
     /// <summary>
     /// 初始化
     /// </summary>
     private void Initialize()
     {
         cbx_SerialPort.DataSource = GetPortArray();
         cbx_BaudRate.DataSource = GetBaudRateArray();
         cbx_Parity.DataSource = GetParityArray();
         cbx_DataBits.DataSource = GetDataBitArray();
         cbx_StopBits.DataSource = GetStopBitArray();
         cbx_StopBits.SelectedIndex = 1;
         cbx_BaudRate.SelectedIndex = 0;
         serialPortTool = new SerialPort();
         if (timer == null) timer = new System.Windows.Forms.Timer();
         timer.Tick += Timer_Tick;
         timer.Interval = 200;
     }
     #endregion
    
  • (三)通讯状态、数据接收、消息更新、控件状态更新

    #region 通讯状态、数据接收、消息更新
    /// <summary>
    /// 定时器
    /// </summary>
    private void Timer_Tick(object sender, EventArgs e)
    {
        if (!IsConnected) return;
        rsc_RunState.Invoke(new Action(() => {
            if (runState == States.Running)
            {
                rsc_RunState.State = rsc_RunState.State == States.None ? States.Running : States.None;
                Task.Delay(200);
            }
            else if (runState == States.Stop)
            {
                rsc_RunState.State = rsc_RunState.State == States.None ? States.Stop : States.None;
                Task.Delay(200);
            }
            else
            {
                rsc_RunState.State = States.None;
            }
        }));
    }
    
    /// <summary>
    /// 数据接收处理
    /// </summary>
    private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
    {
        if (radioBtn_ReceiveEncodingASCII.Checked)
        {
            HexToString(((SerialPort)sender).ReadExisting() , out string data);
            MessageUpdate($"{data}", Color.Blue, "# RECV ASCII>");
        }
        else if (radioBtn_ReceiveEncodingHEX.Checked)
        {
            string hexString = StringToHex(((SerialPort)sender).ReadExisting());
            MessageUpdate($"{hexString}", Color.Blue, "# RECV HEX>");
        }
    }
    private void MessageUpdate(string data, Color color, string appendText = null,bool isAppendTime = true)
    {
        rtbx_Message.BeginInvoke(new Action(() => {
            if (isAppendTime)
            {
                rtbx_Message.AppendText($"[{DateTime.Now.ToString()}]");
            }
            if (appendText!=null)
            {
            rtbx_Message.AppendText($"{appendText}{Environment.NewLine}");
            }
            int startIndex = rtbx_Message.Text.Length;
            rtbx_Message.AppendText($"{data}{Environment.NewLine}");
            SetTextColor(rtbx_Message, startIndex, data.Length, color);
        }));
    }
    /// <summary>
    /// 设置指定范围内的文本颜色
    /// </summary>
    private void SetTextColor(RichTextBox rtb, int startIndex, int length, Color color)
    {
        rtb.Invoke(new Action(() => {
            // 保存当前选择状态
            int originalStart = rtb.SelectionStart;
            int originalLength = rtb.SelectionLength;
            // 设置新选择范围
            rtb.Select(startIndex, length);
            // 更改选中文本的颜色
            rtb.SelectionColor = color;
            // 恢复原始选择状态
            rtb.Select(originalStart, originalLength);
        }));
    }
    /// <summary>
    /// 控件状态更新
    /// </summary>
    private void ControlStateUpdate()
    {
        cbx_SerialPort.Enabled = !IsConnected;
        cbx_BaudRate.Enabled = !IsConnected;
        cbx_Parity.Enabled = !IsConnected;
        cbx_DataBits.Enabled = !IsConnected;
        cbx_StopBits.Enabled = !IsConnected;
    }
    #endregion
    
  • (四)按钮事件:打开串口、发送数据

    #region 按钮事件
    private void btn_Open_Click(object sender, EventArgs e)
    {
        try
        {
            if (!IsConnected)
            {
                serialPortTool.Close();
                timer.Stop();
                serialPortTool = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
                serialPortTool.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
                serialPortTool.Open();
                IsConnected = true;
                timer.Start();
                btn_Open.Text = "关闭";
                runState = States.Running;
            }
            else
            {
                serialPortTool.Close();
                timer.Stop();
                runState = States.Stop;
                btn_Open.Text = "打开";
                IsConnected = false;
            }
        }
        catch (Exception)
        {
            serialPortTool.Close();
            runState = States.Stop;
            IsConnected = false;
            btn_Open.Text = "打开";
        }
        rsc_RunState.State = runState;
        ControlStateUpdate();
    }
    private void btn_SendData_Click(object sender, EventArgs e)
    {
        if (serialPortTool == null || !serialPortTool.IsOpen) return;
        string data = rtbx_SendData.Text;
        if(radioBtn_SendEncodingASCII.Checked)
        {
            MessageUpdate($"{data}", Color.Green, "# SEND ASCII>");
            serialPortTool.Write(data);
        }
        else if (radioBtn_SendEncodingHEX.Checked)
        {
            //string hexString = StringToHex(data);
            //MessageUpdate($"{hexString}", Color.Green, "# SEND HEX>");
            MessageUpdate($"{data}", Color.Green, "# SEND HEX>");
            SendHex(data);
        }
    }
    private void btn_ClearMessage_Click(object sender, EventArgs e)
    {
        rtbx_Message.Clear();
    }
    private void btn_ClearSendData_Click(object sender, EventArgs e)
    {
        rtbx_SendData.Clear();
    }
    #endregion
    
  • (五)串口参数变更

     #region 串口参数变更
     private void cbx_SerialPort_SelectedIndexChanged(object sender, EventArgs e)
     {
         portName = cbx_SerialPort.SelectedItem.ToString();
     }
     private void cbx_BaudRate_SelectedIndexChanged(object sender, EventArgs e)
     {
         if (int.TryParse(cbx_BaudRate.SelectedItem.ToString(), out int result))
         {
             baudRate = result;
         }
         else
         {
             cbx_BaudRate.SelectedText = baudRate.ToString();
         }
    
     }
     private void cbx_Parity_SelectedIndexChanged(object sender, EventArgs e)
     {
         if (Enum.TryParse(cbx_Parity.SelectedItem.ToString(),out Parity result))
         {
             parity = result;
         }
         else
         {
             cbx_Parity.SelectedText = parity.ToString();
         }
     }
     private void cbx_DataBits_SelectedIndexChanged(object sender, EventArgs e)
     {
         if (int.TryParse(cbx_DataBits.SelectedItem.ToString(), out int result))
         {
             dataBits = result;
         }
         else
         {
             cbx_DataBits.SelectedText = dataBits.ToString();
         }
     }
     private void cbx_StopBits_SelectedIndexChanged(object sender, EventArgs e)
     {
         if (Enum.TryParse(cbx_StopBits.SelectedItem.ToString(), out StopBits result))
         {
             stopBits = result;
             if (result == StopBits.None)
             {
                 stopBits = StopBits.One;
                 cbx_StopBits.SelectedIndex =1;
             }
         }
         else
         {
             cbx_StopBits.SelectedText = dataBits.ToString();
         }
     }
     #endregion
    
  • (六)获取串口参数数据源

    #region 获取串口参数数据源
    public  Array GetPortArray()
    {
        return SerialPort.GetPortNames();
    }
    public  Array GetBaudRateArray()
    {
        return _BaudRateArray;
    }
    public  Array GetDataBitArray()
    {
        return _DataBitArray;
    }
    public  Array GetParityArray()
    {
        return Enum.GetValues(typeof(Parity));
    }
    public  Array GetStopBitArray()
    {
        return Enum.GetValues(typeof(StopBits));
    }
    #endregion
    
  • (六)设置变更:发送ASCII、发送HEX

    #region 设置变更事件
    private void radioBtn_SendEncodingASCII_CheckedChanged(object sender, EventArgs e)
    {
        if (radioBtn_SendEncodingASCII.Checked)
        {
            HexToString(rtbx_SendData.Text, out string data);
            rtbx_SendData.Clear();
            rtbx_SendData.Text = data;
        }
    }
    private void radioBtn_SendEncodingHEX_CheckedChanged(object sender, EventArgs e)
    {
        if (radioBtn_SendEncodingHEX.Checked)
        {
            rtbx_SendData.Text = StringToHex(rtbx_SendData.Text);
        }
    }
    #endregion
    
  • (七)数据发送HEX、数据转换

    #region 数据发送
    /// <summary>
    /// 发送HEX格式数据
    /// </summary>
    public void SendHex(string hexData)
    {
        if (serialPortTool != null && serialPortTool.IsOpen)
        {
            try
            {
                // 移除空格
                hexData = hexData.Replace(" ", "");
                // 检查是否为有效的HEX字符串
                if (hexData.Length % 2 != 0)
                {
                    Debug.WriteLine("HEX数据长度必须为偶数");
                    return;
                }
                byte[] buffer = new byte[hexData.Length / 2];
                for (int i = 0; i < hexData.Length; i += 2)
                {
                    buffer[i / 2] = Convert.ToByte(hexData.Substring(i, 2), 16);
                }
                serialPortTool.Write(buffer, 0, buffer.Length);
                Debug.WriteLine($"已发送HEX数据: {BitConverter.ToString(buffer).Replace("-", " ")}");
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"发送HEX数据失败: {ex.Message}");
            }
        }
    }
    /// <summary>
    /// 文本转HEX
    /// </summary>
    public string StringToHex(string text)
    {
        byte[] bytes = Encoding.ASCII.GetBytes(text);
        return BitConverter.ToString(bytes).Replace("-", " ");
    }
    /// <summary>
    /// HEX转字符串,如果不是Hex,返回输入值
    /// </summary>
    public void HexToString(string hexInput,out string data)
    {
        // 先检查是否是有效的HEX字符串(不含冒号)
        bool isPureHex = IsPureHexString(hexInput.Replace(" ", "").Replace("-", ""));
        if (isPureHex)
        {
            // 处理纯HEX字符串
            hexInput = hexInput.Replace(" ", "").Replace("-", "");
            if (hexInput.Length % 2 != 0)
            {
                data = hexInput;
                return;
            }
            byte[] bytes = new byte[hexInput.Length / 2];
            for (int i = 0; i < hexInput.Length; i += 2)
            {
                string hexByte = hexInput.Substring(i, 2);
                bytes[i / 2] = Convert.ToByte(hexByte, 16);
            }
            data = Encoding.ASCII.GetString(bytes);
        }
        else
        {
            // 如果不是纯HEX字符串,可能是已经解码的字符串(如URL)
            data = hexInput;
        }
    }
    /// <summary>
    /// 验证是否为有效HEX
    /// </summary>
    private bool IsPureHexString(string input)
    {
        if (string.IsNullOrEmpty(input)) return false;
        // 有效的HEX字符:0-9, A-F, a-f
        foreach (char c in input)
        {
            if (!((c >= '0' && c <= '9') ||
                  (c >= 'A' && c <= 'F') ||
                  (c >= 'a' && c <= 'f')))
            {
                return false;
            }
        }
        return true;
    }
    #endregion
    
  • (八)自定义控件:状态显示

    public class RunStateControl : Control
    {
        private int radius =30;
        private States state =  States.None;
        private Color fillColor = Color.Gray;
        [Category("UserProperty")]
        [Description("轮廓半径")]
        public int Radius
        {
            get => radius;
            set
            {
                if (radius != value)
                {
                    radius = value;
                    Size = new Size(value - 3, value - 3);    
                    this.Invalidate();
                }
            }
        }
        [Category("UserProperty")]
        [Description("状态")]
        public States State { 
            get => state;
            set
            {
                if (state != value)
                {
                    state = value;
                    switch (value)
                    {
                        case States.None:
                            this.FillColor = Color.Gray;
                            break;
                        case States.Running:
                            this.FillColor = Color.LightSeaGreen;
                            break;
                        case States.Stop:
                            this.FillColor = Color.Red;
                            break;
                        default:
                            this.FillColor = Color.Gray;
                            break;
                    }
                    this.Invalidate();
                }
            }
        }
        public Color FillColor { 
            get => fillColor; 
            set => fillColor = value; 
        }
        public RunStateControl()
        {
            this.DoubleBuffered = true;
            this.State = States.None;
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            Graphics g = e.Graphics;
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            // 计算圆的尺寸(考虑Padding)
            int diameter = Math.Min(this.Width, this.Height) - 4;
            Rectangle circleRect = new Rectangle(
                (this.Width - diameter) / 2,
                (this.Height - diameter) / 2,
                diameter, diameter);
            // 绘制背景圆
            using (Pen pen = new Pen(FillColor, 2))
            {
                g.DrawEllipse(pen, circleRect);
            }
            // 绘制填充部分// 从顶部开始绘制扇形
            using (Brush brush = new SolidBrush(FillColor))
            {
                g.FillPie(brush, circleRect, -90, 360f);
            }
        }
    }
    public enum States
    {
        None,
        Running,
        Stop,
    }
    

  • 结语

    • 上面的案例通过Winform实现了串口通讯功能,参考了一些串口通讯助手的设计,是个不错的练手小案例。

image.png

  • 最后

    • 如果你觉得这篇文章对你有帮助,不妨点个赞再走呗!
    • 如有其他疑问,欢迎评论区留言讨论!
    • 也可以关注微信公众号 [编程笔记in] ,一起交流学习!