这一篇来做一个简单的串口上位机程序,配合【STM32F103笔记】中的串口程序使用,后续还可以在这个串口小程序的基础上添加更多功能,可以根据预先设计的数据格式,将串口小程序接收到的数据进行不同的显示,并根据接收到的数据向STM32发送控制指令,比如上位机PID控制STM32电机调速或者转角控制等等,会很有意思。
笔者也是刚开始学C#,就当做和大家一起学习进步啦。
作为C#小程序开发的第一篇,首先介绍一下C#的开发环境——Visual Studio(VS),是美国微软公司的开发工具包系列产品,这里给出官方下载地址(https://visualstudio.microsoft.com/zh-hans/downloads/):
可以看到VS共有三个发行版本:Community、Professional、Enterprise,作为个人开发一般选择Community就已经非常够用啦,如果你的网络不太好的话,可以点击下面的如何离线安装进行参考,具体安装就不再介绍了。
打开VS,点击左上角文件->新建->项目,弹出新建项目对话框:
在左侧选择编程语言,这里笔者的默认编程语言设置的是C++,需要在其他语言里找到Visual C#,选择Window 桌面->Windows 窗体应用,然后在下方设置好项目名称、位置等,点击确定就可以了:
左侧是项目的树形结构选项卡,右侧为工具箱和属性设置等选项卡,这些选项卡都可以自己调整位置,这里按默认就很舒服,正中间就是我们的窗体了。
拖动工具箱中的控件就可以在窗体中摆放,还请大家先了解一下C#的基本编程操作,这里就不展开说明了。(这个系列前几篇的程序都会详细给出并加上注释,可以就此了解C#程序设计的基本操作)
按照下面图中设置好窗体控件:
左侧主要为用户操作区,包括串口设置、信息显示、发送数据以及程序介绍,为了控件的分块整齐,这里使用了GroupBox控件放置同一功能区的控件。(这里所有的操作都放置在相近的地方(左侧),便于使用);右侧为显示区域,用于显示串口接收到的数据或者文本。
简单介绍一下:
在设计程序之前,应该对窗体各个位置的分布进行设计,尽量操作方便且美观。
界面设计完成后,应该对各个控件进行命名,按照其控件类型与对应的功能,如上图中的“打开”按钮,将其命名为Btn_OpenPort,其中Btn为Button缩写,表示为按钮控件(控件的常用缩写可以在网上查到),OpenPort表示其功能为打开对应的串口端口号。
各个控件设计运行的思路是:
添加控件的事件处理函数
添加控件的事件处理函数,一般双击控件可以添加默认的事件处理函数,对于其它事件处理函数需要在属性选项卡中的事件列表下双击添加:
在窗体上双击或者在属性选项卡的事件下的Load事件右侧双击,如上图,自动生成窗体加载程序,即窗体加载时运行的程序:
// 串口变量,这里需要添加命名空间using System.IO.Ports
private SerialPort _serial;
// 数据接收、端口关闭标志
private bool _receiving, _closing;
// 接收、发送计数
private int _receiveCount, _sendCount;
// 窗体加载程序
private void MainForm_Load(object sender, EventArgs e)
{
// 调用InitializeSettings函数对控件进行初始化
InitializeSettings();
// 变量赋初值
_receiving = false; _closing = false;
_receiveCount = 0; _sendCount = 0;
// 在串口没有打开之前不允许发送
Btn_Send.Enabled = false;
}
///
/// 控件初始化函数,设置串口的端口号及波特率
///
private void InitializeSettings()
{
// 获取所有存在的端口号
string[] ports = SerialPort.GetPortNames();
// 排序一下,显得整齐一点
Array.Sort(ports);
// 重新添加端口号下拉选框控件的内容
cb_PortNum.Items.Clear();
cb_PortNum.Items.AddRange(ports);
// 选择第一个端口为默认端口
cb_PortNum.SelectedIndex = cb_PortNum.Items.Count > 0 ? 0 : -1;
// 默认波特率
string[] baudrate = { "2400", "4800", "9600", "19200", "38400", "57600", "115200" };
// 设置波特率下拉选框的内容
cb_Baudrate.Items.Clear();
cb_Baudrate.Items.AddRange(baudrate);
// 默认波特率选择115200
cb_Baudrate.SelectedIndex = cb_Baudrate.Items.IndexOf("115200");
// _serial赋初值
_serial = new SerialPort
{
NewLine = "\n",
//RtsEnable = false,
//DtrEnable = false
};
// 添加串口数据接收事件处理函数
_serial.DataReceived += _serial_DataReceived;
}
MainForm_Load() 函数会在窗体加载(也就是显示窗口出来)的过程中运行,主要是检测系统中已经存在的串口的端口号,填入cb_PortNum(端口号下拉选框控件)中,同理初始化cb_Baudrate波特率下拉选框控件;
并初始化串口变量_serial(这里初始化了换行控制符,以及添加数据接收处理函数),初始化其它后续需要使用的变量。
双击检测按钮(Btn_Detect),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:
private void Btn_Detect_Click(object sender, EventArgs e)
{
// 调用InitializeSettings函数进行检测并设置
InitializeSettings();
}
这里检测按钮功能是重新检测系统中已经存在的串口的端口好,并初始化端口下拉选框和波特率下拉选框,因此直接调用InitializeSettings()函数就可以了。
双击打开按钮(Btn_OpenPort),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数;打开按钮用于打开或者关闭串口:
private void Btn_OpenPort_Click(object sender, EventArgs e)
{
// 判断串口端口是否已经打开
// 如果已经打开了,那么应该将其关闭
if (_serial.IsOpen)
{
// 正在关闭串口的标志
_closing = true;
// 若此时正在接收数据,那么等待这次的接收完成再关闭
while (_receiving)
Application.DoEvents();
try
{
// 关闭串口
_serial.Close();
}
catch (Exception ex)
{
// 如果出错则提示错误信息
// 一般为串口端口号不存在(因为串口可能在别处已经被关闭或者占用)
_serial = new SerialPort();
MessageBox.Show(ex.Message);
}
// 关闭过程结束,关闭标志清除(置false)
_closing = false;
}
else
{ // 若串口没有打开则应该打开串口
// 首先判断串口端口号是否设置好
if (!string.IsNullOrEmpty(cb_PortNum.Text))
{
// 将_serial的端口号、波特率复制
_serial.PortName = cb_PortNum.Text;
_serial.BaudRate = int.Parse(cb_Baudrate.Text);
try
{
// 尝试打开串口端口
_serial.Open();
}
catch (Exception ex)
{
// 打开失败则输出错误信息
_serial = new SerialPort();
MessageBox.Show(ex.Message);
}
}
else
MessageBox.Show("请选择端口号!");
}
// 设置按钮显示字样,串口已经打开则显示“关闭”,否则显示“打开”
Btn_OpenPort.Text = _serial.IsOpen ? "关闭" : "打开";
// 当串口打开才能允许发送
Btn_Send.Enabled = _serial.IsOpen;
}
这里在关闭串口的过程中,首先将标志_closing置true,保证不再进行数据接收(后续函数中可以看到),同时判断_receiving标志,若为true则说明已经进入数据接收程序,那么应该等待此次接收完成再关闭串口;这两个标志位的用处是,在关闭串口和接收串口数据的过程中,可以避免两者相互冲突,造成程序出错(即卡死)。
双击信息显示区的清除按钮(Btn_ClearAll),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:
private void Btn_ClearAll_Click(object sender, EventArgs e)
{
txt_Display.Text = "";
_receiveCount = _sendCount = 0;
lbl_Received.Text = "接收:0";
lbl_Send.Text = "发送:0";
}
清除按钮用于清除数据显示区以及接收发送计数。
双击的发送按钮(Btn_Send),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:
private void Btn_Send_Click(object sender, EventArgs e)
{
// 调用_serial.Write函数将发送数据框里的字符串发送出去
_serial.Write(txt_Send.Text);
// 更新发送计数
_sendCount += txt_Send.Text.Length;
lbl_Send.Text = "发送:" + _sendCount.ToString();
}
这里直接调用了串口SerialPort的Write函数,直接向串口中写入字符串数据,即发送数据。
双击数据发送区的清除按钮(Btn_ClearSend),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:
private void Btn_ClearSend_Click(object sender, EventArgs e)
{
txt_Send.Text = "";
}
这个函数用于清除数据发送框。
_serial_DataReceived函数为本篇最重要的函数,作用是当串口接收到数据时,将其读取并展示在数据显示文本框内:
private void _serial_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// 如果正在关闭串口,则不再进行数据接收
if (_closing)
return;
try
{
// 将_receiving标志置true,表示正在进行数据接收
_receiving = true;
// 接收缓冲区内的数据字节数
int n = _serial.BytesToRead;
// buf变量用于存储从接收缓冲区内读取的字节数据
byte[] buf = new byte[n];
// 更新接收数据计数
_receiveCount += n;
// 将数据读取到buf中
_serial.Read(buf, 0, n);
// 字符串构建
StringBuilder sb = new StringBuilder();
// invoke委托,进行数据处理及更新
this.Invoke((EventHandler)(delegate
{
// 判断是否显示十六进制,若是则将buf中的每个字节数据转换成十六进制
// 否则直接转换成ASCII编码的字符串
if (ckb_DataHexView.Checked)
foreach (byte b in buf)
sb.Append(b.ToString("X2") + " ");
else
sb.Append(Encoding.ASCII.GetString(buf));
// 更新数据接收显示
txt_Display.AppendText(sb.ToString());
// 更新计数显示
lbl_Received.Text = "接收:" + _receiveCount.ToString();
}));
}
finally
{
// 数据接收结束后将_receiving标志置false
_receiving = false;
}
}
从这里可以看出_closing和_receiving标志的相互配合使用,防止关闭串口的操作和数据读取操作相冲突。当然也可以在使用Invoke时改为使用BeginInvoke,这样就不需要使用_closing和_receiving这两个标志位了;
在更新数据显示时,使用了invoke委托。
笔者理解如下:
至此,程序分析结束。
using System;
using System.Text;
using System.Windows.Forms;
using System.IO.Ports;
namespace _1_SimpleSerialTool
{
public partial class MainForm : Form
{
private SerialPort _serial;
private bool _receiving, _closing;
private int _receiveCount, _sendCount;
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
InitializeSettings();
_receiving = false; _closing = false;
_receiveCount = 0; _sendCount = 0;
Btn_Send.Enabled = false;
}
private void Btn_Detect_Click(object sender, EventArgs e)
{
InitializeSettings();
}
private void Btn_OpenPort_Click(object sender, EventArgs e)
{
if (_serial.IsOpen)
{
//_closing = true;
//while (_receiving)
// Application.DoEvents();
try
{
_serial.Close();
}
catch (Exception ex)
{
_serial = new SerialPort();
MessageBox.Show(ex.Message);
}
//_closing = false;
}
else
{
if (!string.IsNullOrEmpty(cb_PortNum.Text))
{
_serial.PortName = cb_PortNum.Text;
_serial.BaudRate = int.Parse(cb_Baudrate.Text);
try
{
_serial.Open();
}
catch (Exception ex)
{
_serial = new SerialPort();
MessageBox.Show(ex.Message);
}
}
else
MessageBox.Show("请选择端口号!");
}
Btn_OpenPort.Text = _serial.IsOpen ? "关闭" : "打开";
Btn_Send.Enabled = _serial.IsOpen;
}
private void Btn_ClearAll_Click(object sender, EventArgs e)
{
txt_Display.Text = "";
_receiveCount = _sendCount = 0;
lbl_Received.Text = "接收:0";
lbl_Send.Text = "发送:0";
}
private void Btn_Send_Click(object sender, EventArgs e)
{
_serial.Write(txt_Send.Text);
_sendCount += txt_Send.Text.Length;
lbl_Send.Text = "发送:" + _sendCount.ToString();
}
private void Btn_ClearSend_Click(object sender, EventArgs e)
{
txt_Send.Text = "";
}
private void _serial_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
//if (_closing)
// return;
try
{
_receiving = true;
int n = _serial.BytesToRead;
byte[] buf = new byte[n];
_receiveCount += n;
_serial.Read(buf, 0, n);
StringBuilder sb = new StringBuilder();
// this.Invoke((EventHandler)(delegate
this.BeginInvoke((EventHandler)(delegate
{
if (ckb_DataHexView.Checked)
foreach (byte b in buf)
sb.Append(b.ToString("X2") + " ");
else
sb.Append(Encoding.ASCII.GetString(buf));
txt_Display.AppendText(sb.ToString());
lbl_Received.Text = "接收:" + _receiveCount.ToString();
}));
}
finally
{
_receiving = false;
}
}
private void InitializeSettings()
{
string[] ports = SerialPort.GetPortNames();
Array.Sort(ports);
cb_PortNum.Items.Clear();
cb_PortNum.Items.AddRange(ports);
cb_PortNum.SelectedIndex = cb_PortNum.Items.Count > 0 ? 0 : -1;
string[] baudrate = { "2400", "4800", "9600", "19200", "38400", "57600", "115200" };
cb_Baudrate.Items.Clear();
cb_Baudrate.Items.AddRange(baudrate);
cb_Baudrate.SelectedIndex = cb_Baudrate.Items.IndexOf("115200");
_serial = new SerialPort
{
NewLine = "\n",
//RtsEnable = false,
//DtrEnable = false
};
_serial.DataReceived += _serial_DataReceived;
}
} // end of MainForm
}
将USB转串口设备的TX和RX用杜邦线短接,这样相当于发送给自己接收,用于测试串口上位机程序:
调试运行程序,结果如下: