WPF制作简易串口调试助手(上位机部分)

项目要求:下位机使用单片机,不断发送一个随机数值给上位机,上位机收到数据,显示在显示框中。
实验环境:vs2013
参考文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.io.ports?view=netframework-4.7.2
下位机部分:https://blog.csdn.net/weixin_42462552/article/details/85561632



一、窗体程序制作:

(一)UI界面

1.首先我们参照网上的串口调试助手将UI界面的大体框架完成。
界面图片
2.其中数据接收区域和发送区域的文本需要实现自动换行功能,根据网上资料,需要设置TextWrapping的属性为"Wrap"。
3.接下来是对串口号、波特率、校验位等的一些内容设置,其中较复杂的是串口号的设置,需要根据电脑的端口实际情况来确定串口数量,不能同设置波特率属性一般,直接在程序里指定唯一值。

string[] ports = SerialPort.GetPortNames();//获取当前计算机的串行端口名的数组。
            for(int index =0;index<ports.Length;index++)
            {
                cb_SerialPortNumber.Items.Add(ports[index]);//添加item
                cb_SerialPortNumber.SelectedIndex = index; //设置显示的item索引
            }

在官方文档中找到获取当前计算机的串行端口名的数组的函数GetPortNames (),接着使用循环,利用comboBox的Items.Add方法将数组内的元素添加到comboBox中。

UI界面xml代码
<Window x:Class="SerialDebuggingAssistant.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SerialDebuggingAssistant"
        Title="串口调试助手" Height="400" Width="600" ResizeMode="CanMinimize" Closing="Window_Closing">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="192*"/>
            <RowDefinition Height="82*"/>
            <RowDefinition Height="97*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="266*"/>
            <ColumnDefinition Width="327*"/>
        </Grid.ColumnDefinitions>
        <GroupBox Header="串口参数设置" BorderBrush="Black">
            <StackPanel >
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="串口号" Width="auto" Margin="5" Padding="0,6,0,0"/>
                    <ComboBox Name="cb_SerialPortNumber" Width="65" Margin="5,10,5,10"/>
                    <TextBlock Text="波特率" Width="auto" Margin="5" Padding="0,6,0,0"/>
                    <ComboBox Name="cb_BaudRate" Width="68" Margin="10,10,0,10" SelectedIndex="8">
                        <ComboBoxItem Content="600"/>
                        <ComboBoxItem Content="1200"/>
                        <ComboBoxItem Content="2400"/>
                        <ComboBoxItem Content="4800"/>
                        <ComboBoxItem Content="9600"/>
                        <ComboBoxItem Content="14400"/>
                        <ComboBoxItem Content="19200"/>
                        <ComboBoxItem Content="28800"/>
                        <ComboBoxItem Content="38400"/>
                        <ComboBoxItem Content="57600"/>
                        <ComboBoxItem Content="115200"/>
                        <ComboBoxItem Content="230400"/>
                        <ComboBoxItem Content="460800"/>
                    </ComboBox>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="校验位" Width="auto" Margin="5" Padding="0,6,0,0"/>
                    <ComboBox Width="65" Margin="5,10,5,10" SelectedIndex="0">
                        <ComboBoxItem Content="None"/>
                    </ComboBox>
                    <TextBlock Text="数据位" Width="auto" Margin="5" Padding="0,6,0,0"/>
                    <ComboBox Width="68" Margin="10,10,0,10" SelectedIndex="0">
                        <ComboBoxItem Content="8"/>
                    </ComboBox>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="停止位" Width="auto" Margin="10,10,0,10" Padding="0,3,0,0"/>
                    <ComboBox Width="65" Margin="5,10,5,10" SelectedIndex="0">
                        <ComboBoxItem Content="one"/>
                    </ComboBox>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Name="tb_switchStatus" Text="串口为关闭状态" Width="auto" Margin="10" Padding="0,2,0,0"/>
                    <Ellipse Name="e_status" Fill="#000000" Height="10" Stroke="Black" Margin="5,3,5,0" Width="10" />
                    <Button Name="bt_SerialSwitch" Content="打开串口" Margin="10" Padding="5,0,5,0" Click="bt_SerialSwitch_Click"/>
                   
                </StackPanel>
            </StackPanel>
        </GroupBox>


        <GroupBox Header="接收数据设置" Grid.Row="1" BorderBrush="Black">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <Button Name="ClearReceiveData" Content="清空接收数据" Margin="20" Click="ClearReceiveData_Click"/>
                <Button Name="bt_stopSend" Content="停止接收" Margin="20" Click="bt_stopSend_Click"/>
            </StackPanel>
        </GroupBox>


        <GroupBox Header="发送数据设置" Grid.Row="2" BorderBrush="Black">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <Button Name="bt_ClearSendData" Content="清空发送数据" Margin="20,30,20,25" Click="bt_ClearSendData_Click"/>
                <Button Name="bt_send" Content="手动发送" Margin="20,30,20,25" Click="bt_send_Click"/>
            </StackPanel>
        </GroupBox>
        <GroupBox Header="接收数据区" Grid.Column="1" Grid.RowSpan="2" BorderBrush="Black"  Margin="5">
            <ScrollViewer VerticalScrollBarVisibility="Auto">
                <TextBlock Name="tb_receiveData" TextWrapping="Wrap"/>
            </ScrollViewer>
        </GroupBox>
        <GroupBox Header="发送数据区" Grid.Row="2" Grid.Column="1"  BorderBrush="Black">
            <ScrollViewer VerticalScrollBarVisibility="Auto">  <!--滚动条-->
                <TextBox Name="tb_SendData" TextWrapping="Wrap"/>
            </ScrollViewer>
        </GroupBox>
    </Grid>
</Window>

(二)后台逻辑代码(.cs代码部分)

1.添加using引用
using System.IO.Ports;
2.新建串口对象
public SerialPort _serialPort = new SerialPort ();
3.设置一些简单控件的逻辑功能

(1)“清空接收数据”和“清空发送数据”按钮的点击事件,只需要把对应区域的Text属性设为空,Text = “” 。

		private void bt_ClearSendData_Click(object sender, RoutedEventArgs e)//清空发送区域
        {
            tb_SendData.Text = "";
        }

        private void ClearReceiveData_Click(object sender, RoutedEventArgs e)//清空接受数据
        {
            tb_receiveData.Text = "";
        }
4.设置串口参数

(1)如数据位、奇偶校验位、停止位和波特率等,在官方文档中说明使用PortName、BaudRate、DataBits、StopBits、Parity分别可以设置串口、波特率、数据位、停止位和校验位,其中串口、波特率是根据实际情况来设定的,数据位、停止位和校验位因为基本上在使用时不会去做更改,所以指定唯一值。

		public void initialize()//初始化
        {
                _serialPort.PortName = cb_SerialPortNumber.SelectedItem.ToString();//串口号
                ComboBoxItem seletedItem = (ComboBoxItem)this.cb_BaudRate.SelectedItem;
                _serialPort.BaudRate = Convert.ToInt32(seletedItem.Content.ToString());//波特率
                _serialPort.DataBits = 8;//数据位
                _serialPort.StopBits = StopBits.One;//停止位
                _serialPort.Parity = Parity.None;//校验位
        }
5.编写“打开串口”按钮的点击事件

(1)这里主要实现的功能是打开和关闭串口,添加数据接收事件和取消数据接收事件。开关串口需要使用两个函数open()和close()。

		private void bt_SerialSwitch_Click(object sender, RoutedEventArgs e)//串口开关
        {
            initialize();//初始化
            string strContent = this.bt_SerialSwitch.Content.ToString();
            if (strContent == "打开串口")
            {
                    _serialPort.Open();
                    _serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);//添加数据接收事件
                    //_serialPort.DataReceived += DataReceivedHandler;
                    bt_SerialSwitch.Content = "关闭串口";
                    tb_switchStatus.Text = "串口为打开状态";
                    bt_send.IsEnabled = true;
                    bt_stopSend.IsEnabled = true;
                    e_status.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF0000"));
            }
            else
            {
                    _serialPort.DataReceived -= DataReceivedHandler;
                    _serialPort.Close();
                    bt_SerialSwitch.Content = "打开串口";
                    tb_switchStatus.Text = "串口为关闭状态";
                    e_status.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#000000"));
                    bt_send.IsEnabled = false;
                    bt_stopSend.IsEnabled = false;
            }
        }
6.添加数据接收事件

(1)官方文档上说明需要使用SerialErrorReceivedEventHandler(注解原文:“创建SerialErrorReceivedEventHandler委托时,需要标识将处理该事件的方法。若要将事件与事件处理程序关联,请将该委托的一个实例添加到事件中”)。在文档中找到了DataReceived,具体写法是:
SerialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);后来发现可以简写 serialPort1.DataReceived += new DataReceivedHandler; 其中的DataReceivedHandler就是数据接收的具体实现方法。

//在串口打开点击事件中添加
_serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);//添加数据接收事件
7.数据接收

(1)获取数据

  • 编写数据接收事件DataReceivedHandler,功能是读取下位机的数据,显示在数据接收区中。首先是使用BytesToRead获取接收缓冲区中数据的字节数,然后根据获取到的字节数来创建一个对应大小的byte数组,再使用Read函数将获取的数据放入byte数组中,然后还要对byte数组的数据转换成十六进制数,使用到了这个函数BitConverter.ToString(官方解释是“将指定的字节子数组的每个元素的数值转换为它的等效十六进制字符串表示形式”)。
    (2)更新UI界面
  • 将处理好的数据加载到Text属性上,Text += strData;不过访问UI组件必须放在主线程中运行,不然会抛异常“调用线程无法访问此对象,因为另一个线程拥有该对象”。
  • 窗体控件是由主线程执行的,因此在多线程中试图更改控件的内容比如覆盖或追加Textblock的内容,是无法在分线程中执行的,因为Textblock是由UI界面线程控制的,因此采用Dispatcher.Invoke(() =>{ ……});里面调用Textblock控件。
 		public void DataReceivedHandler(object sender,SerialDataReceivedEventArgs e)//读取下位机的数据,显示在textBlock中
        {          
            int len = this._serialPort.BytesToRead;
            byte[] buffer = new byte[len];
            this._serialPort.Read(buffer, 0, len);         
            string strData = BitConverter.ToString(buffer, 0, len);
            Dispatcher.Invoke(() =>
            {
                this.tb_receiveData.Text += strData;
                this.tb_receiveData.Text += "-";//字符分隔-
            });
        }
8.数据发送

(1)使用Write()方法发送数据

  • 数据发送跟数据接收的原理差不多,只不过不需要使用线程方式。首先将数据发送区域TextBox的Text属性值赋值到一个string类型的变量中,然后使用一个byte数组储存数据,因为输入的字符是十六进制数,所以这边做了个循环用Substring()切割字符串方法,每次取两位字符组成一个16进制,接着使用Write()方法发送数据。
		private void bt_send_Click(object sender, RoutedEventArgs e)//发送按钮
        {
            string SendData = tb_SendData.Text;
            byte[] Data = new byte[20];
            for (int i = 0; i < SendData.Length / 2; i++)
            {
                    //每次取两位字符组成一个16进制
                  Data[i] = Convert.ToByte(tb_SendData.Text.Substring(i * 2, 2), 16);
            }
            this._serialPort.Write(Data, 0, Data.Length);   
        }

(2)“ 停止接收数据”和“继续接收数据”

  • 这里是分别通过发送0x99和0x66指令到单片机中实现停止和继续发送数据功能,同时还使用取消和添加数据接收事件DataReceivedHandler的方式,其实这两种方式也就是主动和被动的不同罢了,第一种方式更为真实一些。
		private void bt_stopSend_Click(object sender, RoutedEventArgs e)//停止接收数据
        {
            string strContent = this.bt_stopSend.Content.ToString();
            if (strContent == "停止接收")
            {
                byte[] data = { 0x99 };
                _serialPort.DataReceived -= DataReceivedHandler;
                this._serialPort.Write(data, 0, data.Length);
                bt_stopSend.Content = "继续接收";
            }else
            {
                byte[] data = { 0x66 };
                this._serialPort.Write(data, 0, data.Length);
                _serialPort.DataReceived += DataReceivedHandler;
                bt_stopSend.Content = "停止接收";
            }
        }
9.程序关闭事件

(1)当用户将程序关闭时,如果串口处于开启状态时,应该在关闭程序时将串口关闭。

		private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            string strContent = this.bt_SerialSwitch.Content.ToString();
            if (strContent == "关闭串口")
            {
                _serialPort.Close();
            }
        }
10.测试时引发的异常

(1)我们发现当串口还没被打开时,如果点击“停止接收”和“手动发送”按钮,会出现异常,导致程序无法进行,所以用了这个办法,当串口还没别打开,“停止接收”和“手动发送”按钮的IsEnabled属性就处于false状态,直到串口打开,然后才恢复true状态。
(2)当打开串口后,点击关闭串口按钮时,也会抛异常,并且提示在初始化那一块和关闭串口事件那块,所以我们将这两个地方加了try……catch,让程序不去理他继续运行。

三、实验结果

(一)当程序运行,用户将串口号和波特率设置完成,点击“打开串口”按钮时,“接收数据区域”会显示从下位机传来的数据。
(二)当点击“停止接收”按钮时,下位机停止发送数据,“接收数据区域”停止显示,按钮上的文本变成“继续接收”,当再次点击按钮时,下位机继续发送数据,“接收数据区域”继续显示数据,按钮文本变成“停止接收”。
(三)当点击“清空接收数据”和“清空发送数据”时,对应区域的文本将会清空。
(四)当在“发送数据区域”输入“99”,点击“手动发送”按钮时,下位机停止发送数据,“接收数据区域”停止显示。发送“66”时,下位机继续发送数据,“接收数据区域”继续显示数据。

完整代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
//上面的是系统自动生成的
using System.IO.Ports;//这是手动添加的
namespace SerialDebuggingAssistant
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    /// 
   
    public partial class MainWindow : Window
    {
        public SerialPort _serialPort = new SerialPort ();
        
        public MainWindow()
        {
            InitializeComponent();
            
            string[] ports = SerialPort.GetPortNames();//获取当前计算机的串行端口名的数组。
            for(int index =0;index<ports.Length;index++)
            {
                cb_SerialPortNumber.Items.Add(ports[index]);//添加item
                cb_SerialPortNumber.SelectedIndex = index; //设置显示的item索引
            }
            bt_send.IsEnabled = false;
            bt_stopSend.IsEnabled = false;
        }
        public void initialize()//初始化
        {
            //关闭串口时回抛异常
            try
            {
                _serialPort.PortName = cb_SerialPortNumber.SelectedItem.ToString();//串口号
                ComboBoxItem seletedItem = (ComboBoxItem)this.cb_BaudRate.SelectedItem;
                _serialPort.BaudRate = Convert.ToInt32(seletedItem.Content.ToString());//波特率
                _serialPort.DataBits = 8;//数据位
                _serialPort.StopBits = StopBits.One;//停止位
                _serialPort.Parity = Parity.None;//校验位
            }
            catch
            {

            }

        }
        private void bt_ClearSendData_Click(object sender, RoutedEventArgs e)//清空发送区域
        {
            tb_SendData.Text = "";
        }

        private void ClearReceiveData_Click(object sender, RoutedEventArgs e)//清空接受数据
        {
            tb_receiveData.Text = "";
        }
        
        private void bt_SerialSwitch_Click(object sender, RoutedEventArgs e)//串口开关
        {
            initialize();//初始化
            string strContent = this.bt_SerialSwitch.Content.ToString();
            if (strContent == "打开串口")
            {
                try
                {
                    _serialPort.Open();
                    _serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);//添加数据接收事件
                    //_serialPort.DataReceived += DataReceivedHandler;
                    bt_SerialSwitch.Content = "关闭串口";
                    tb_switchStatus.Text = "串口为打开状态";
                    bt_send.IsEnabled = true;
                    bt_stopSend.IsEnabled = true;
                    e_status.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF0000"));
                }
                catch { }
            }
            else
            {
                try
                {
                    _serialPort.DataReceived -= DataReceivedHandler;
                    _serialPort.Close();
                    bt_SerialSwitch.Content = "打开串口";
                    tb_switchStatus.Text = "串口为关闭状态";
                    e_status.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#000000"));
                    bt_send.IsEnabled = false;
                    bt_stopSend.IsEnabled = false;
                }
               catch
                {

                }
            }
        }
        public void DataReceivedHandler(object sender,SerialDataReceivedEventArgs e)//读取下位机的数据,显示在textBlock中
        {          
            int len = this._serialPort.BytesToRead;
            byte[] buffer = new byte[len];
            this._serialPort.Read(buffer, 0, len);         
            string strData = BitConverter.ToString(buffer, 0, len);
            Dispatcher.Invoke(() =>
            {
                this.tb_receiveData.Text += strData;
                this.tb_receiveData.Text += "-";
            });
        }
        
        private void bt_send_Click(object sender, RoutedEventArgs e)//发送按钮
        {
            string SendData = tb_SendData.Text;
            byte[] Data = new byte[20];
            for (int i = 0; i < SendData.Length / 2; i++)
            {
                    //每次取两位字符组成一个16进制
                  Data[i] = Convert.ToByte(tb_SendData.Text.Substring(i * 2, 2), 16);
            }
            this._serialPort.Write(Data, 0, Data.Length);   
        }
        private void bt_stopSend_Click(object sender, RoutedEventArgs e)//停止接收数据
        {
            string strContent = this.bt_stopSend.Content.ToString();
            if (strContent == "停止接收")
            {
                byte[] data = { 0x99 };
                _serialPort.DataReceived -= DataReceivedHandler;
                this._serialPort.Write(data, 0, data.Length);
                bt_stopSend.Content = "继续接收";
            }else
            {
                byte[] data = { 0x66 };
                this._serialPort.Write(data, 0, data.Length);
                _serialPort.DataReceived += DataReceivedHandler;
                bt_stopSend.Content = "停止接收";
            }
        }
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            string strContent = this.bt_SerialSwitch.Content.ToString();
            if (strContent == "关闭串口")
            {
                _serialPort.Close();
            }
        }
    }
}

  • 23
    点赞
  • 131
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值