C# WinForms上位机程序,实现电机转速显示、实时波形绘制、串口通信、数据保存等功能。
一、项目结构
MotorMonitor/
├── MotorMonitor.sln
├── MotorMonitor.csproj
├── FormMain.cs # 主窗口
├── FormMain.Designer.cs
├── SerialPortManager.cs # 串口管理
├── DataProcessor.cs # 数据处理
├── WaveformChart.cs # 波形图表
├── MotorData.cs # 数据结构
├── Program.cs
└── 资源文件/
二、数据结构定义
MotorData.cs
csharp
using System;
using System.Collections.Generic;
namespace MotorMonitor
{
/// <summary>
/// 电机数据结构
/// </summary>
public class MotorData
{
public DateTime TimeStamp { get; set; }
public double RPM { get; set; } // 转速(转/分钟)
public double Current { get; set; } // 电流(A)
public double Voltage { get; set; } // 电压(V)
public double Temperature { get; set; } // 温度(℃)
public double Torque { get; set; } // 扭矩(N·m)
public MotorData()
{
TimeStamp = DateTime.Now;
}
public MotorData(double rpm, double current, double voltage, double temp, double torque)
{
TimeStamp = DateTime.Now;
RPM = rpm;
Current = current;
Voltage = voltage;
Temperature = temp;
Torque = torque;
}
}
/// <summary>
/// 数据包协议
/// </summary>
public class DataProtocol
{
// 示例协议格式:$RPM,12.5,220,45,2.5# (转速,电流,电压,温度,扭矩)
public const byte START_BYTE = 0x24; // '$'
public const byte END_BYTE = 0x23; // '#'
public const byte SEPARATOR = 0x2C; // ','
public static bool TryParseData(byte[] buffer, out MotorData data)
{
data = null;
try
{
string dataStr = System.Text.Encoding.ASCII.GetString(buffer);
if (dataStr.StartsWith("$") && dataStr.EndsWith("#"))
{
string content = dataStr.Trim('$', '#');
string[] parts = content.Split(',');
if (parts.Length >= 5)
{
data = new MotorData
{
RPM = double.Parse(parts[0]),
Current = double.Parse(parts[1]),
Voltage = double.Parse(parts[2]),
Temperature = double.Parse(parts[3]),
Torque = double.Parse(parts[4])
};
return true;
}
}
}
catch
{
return false;
}
return false;
}
public static byte[] CreateDataPacket(MotorData data)
{
string packet = string.Format("${0:F1},{1:F2},{2:F1},{3:F1},{4:F2}#",
data.RPM, data.Current, data.Voltage, data.Temperature, data.Torque);
return System.Text.Encoding.ASCII.GetBytes(packet);
}
}
}
三、串口通信管理
SerialPortManager.cs
csharp
using System;
using System.IO.Ports;
using System.Threading;
namespace MotorMonitor
{
/// <summary>
/// 串口通信管理器
/// </summary>
public class SerialPortManager : IDisposable
{
private SerialPort _serialPort;
private Thread _receiveThread;
private bool _isReceiving;
private readonly object _lockObject = new object();
// 事件定义
public event EventHandler<string> LogMessage;
public event EventHandler<MotorData> DataReceived;
public event EventHandler<bool> ConnectionChanged;
public bool IsConnected => _serialPort?.IsOpen ?? false;
public SerialPortManager()
{
_serialPort = new SerialPort
{
ReadTimeout = 500,
WriteTimeout = 500
};
}
/// <summary>
/// 获取可用串口列表
/// </summary>
public string[] GetAvailablePorts()
{
return SerialPort.GetPortNames();
}
/// <summary>
/// 连接串口
/// </summary>
public bool Connect(string portName, int baudRate = 115200,
Parity parity = Parity.None, int dataBits = 8,
StopBits stopBits = StopBits.One)
{
lock (_lockObject)
{
if (IsConnected)
{
Disconnect();
}
try
{
_serialPort.PortName = portName;
_serialPort.BaudRate = baudRate;
_serialPort.Parity = parity;
_serialPort.DataBits = dataBits;
_serialPort.StopBits = stopBits;
_serialPort.Handshake = Handshake.None;
_serialPort.NewLine = "\r\n";
_serialPort.Open();
OnLogMessage($"串口 {portName} 已连接,波特率 {baudRate}");
// 启动接收线程
_isReceiving = true;
_receiveThread = new Thread(ReceiveDataThread)
{
IsBackground = true,
Priority = ThreadPriority.AboveNormal
};
_receiveThread.Start();
ConnectionChanged?.Invoke(this, true);
return true;
}
catch (Exception ex)
{
OnLogMessage($"连接失败: {ex.Message}");
return false;
}
}
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
lock (_lockObject)
{
_isReceiving = false;
if (_receiveThread != null && _receiveThread.IsAlive)
{
_receiveThread.Join(1000);
}
if (_serialPort.IsOpen)
{
_serialPort.Close();
OnLogMessage("串口已断开");
}
ConnectionChanged?.Invoke(this, false);
}
}
/// <summary>
/// 发送数据
/// </summary>
public bool SendData(byte[] data)
{
lock (_lockObject)
{
if (!IsConnected) return false;
try
{
_serialPort.Write(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
OnLogMessage($"发送失败: {ex.Message}");
return false;
}
}
}
public bool SendCommand(string command)
{
byte[] data = System.Text.Encoding.ASCII.GetBytes(command + "\r\n");
return SendData(data);
}
/// <summary>
/// 接收数据线程
/// </summary>
private void ReceiveDataThread()
{
System.Text.StringBuilder buffer = new System.Text.StringBuilder();
while (_isReceiving && _serialPort.IsOpen)
{
try
{
// 读取可用数据
if (_serialPort.BytesToRead > 0)
{
string received = _serialPort.ReadExisting();
buffer.Append(received);
// 处理完整数据包
ProcessBuffer(buffer);
}
else
{
Thread.Sleep(10);
}
}
catch (TimeoutException)
{
// 正常超时,继续
}
catch (Exception ex)
{
OnLogMessage($"接收错误: {ex.Message}");
Thread.Sleep(100);
}
}
}
/// <summary>
/// 处理接收缓冲区
/// </summary>
private void ProcessBuffer(System.Text.StringBuilder buffer)
{
string dataStr = buffer.ToString();
// 查找完整的数据包
int startIndex = dataStr.IndexOf('$');
int endIndex = dataStr.IndexOf('#');
while (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex)
{
string packet = dataStr.Substring(startIndex, endIndex - startIndex + 1);
// 解析数据包
if (DataProtocol.TryParseData(System.Text.Encoding.ASCII.GetBytes(packet),
out MotorData data))
{
DataReceived?.Invoke(this, data);
}
// 移除已处理的数据
buffer.Remove(startIndex, endIndex - startIndex + 1);
dataStr = buffer.ToString();
// 查找下一个数据包
startIndex = dataStr.IndexOf('$');
endIndex = dataStr.IndexOf('#');
}
// 如果缓冲区太大,清理
if (buffer.Length > 1024)
{
buffer.Remove(0, buffer.Length - 512);
}
}
private void OnLogMessage(string message)
{
LogMessage?.Invoke(this, message);
}
public void Dispose()
{
Disconnect();
_serialPort?.Dispose();
}
}
}
四、波形图表控件
WaveformChart.cs
csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace MotorMonitor
{
/// <summary>
/// 实时波形图表控件
/// </summary>
public partial class WaveformChart : UserControl
{
private Chart chart;
private Series seriesRPM;
private Series seriesCurrent;
private Series seriesVoltage;
private Series seriesTemperature;
private Queue<DataPoint> dataPointsRPM = new Queue<DataPoint>();
private Queue<DataPoint> dataPointsCurrent = new Queue<DataPoint>();
private Queue<DataPoint> dataPointsVoltage = new Queue<DataPoint>();
private Queue<DataPoint> dataPointsTemperature = new Queue<DataPoint>();
private int maxPoints = 1000; // 最多显示点数
private DateTime startTime = DateTime.Now;
public WaveformChart()
{
InitializeChart();
}
private void InitializeChart()
{
chart = new Chart
{
Dock = DockStyle.Fill,
BackColor = Color.FromArgb(240, 240, 240),
BorderlineColor = Color.Gray,
BorderlineDashStyle = ChartDashStyle.Solid,
BorderlineWidth = 1
};
// 图表区域
ChartArea chartArea = new ChartArea
{
Name = "ChartArea1",
BackColor = Color.White,
BorderColor = Color.Gray,
BorderWidth = 1,
BorderDashStyle = ChartDashStyle.Solid
};
// 坐标轴设置
chartArea.AxisX.Title = "时间 (s)";
chartArea.AxisX.TitleFont = new Font("Microsoft YaHei", 9, FontStyle.Regular);
chartArea.AxisX.LabelStyle.Format = "F1";
chartArea.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount;
chartArea.AxisX.MajorGrid.LineColor = Color.LightGray;
chartArea.AxisX.MinorGrid.Enabled = false;
chartArea.AxisY.Title = "数值";
chartArea.AxisY.TitleFont = new Font("Microsoft YaHei", 9, FontStyle.Regular);
chartArea.AxisY.MajorGrid.LineColor = Color.LightGray;
chartArea.AxisY.MinorGrid.Enabled = false;
chartArea.AxisY2.Title = "温度 (℃) / 扭矩 (N·m)";
chartArea.AxisY2.TitleFont = new Font("Microsoft YaHei", 9, FontStyle.Regular);
chartArea.AxisY2.MajorGrid.Enabled = false;
chart.ChartAreas.Add(chartArea);
// 图例
Legend legend = new Legend
{
Name = "Legend1",
Docking = Docking.Top,
Alignment = StringAlignment.Center,
BackColor = Color.Transparent,
Font = new Font("Microsoft YaHei", 9),
IsTextAutoFit = false
};
chart.Legends.Add(legend);
// 创建数据系列
seriesRPM = new Series
{
Name = "转速 (RPM)",
ChartType = SeriesChartType.Line,
Color = Color.Red,
BorderWidth = 2,
XValueType = ChartValueType.DateTime,
YValueType = ChartValueType.Double
};
seriesCurrent = new Series
{
Name = "电流 (A)",
ChartType = SeriesChartType.Line,
Color = Color.Blue,
BorderWidth = 2,
XValueType = ChartValueType.DateTime,
YValueType = ChartValueType.Double
};
seriesVoltage = new Series
{
Name = "电压 (V)",
ChartType = SeriesChartType.Line,
Color = Color.Green,
BorderWidth = 2,
XValueType = ChartValueType.DateTime,
YValueType = ChartValueType.Double
};
seriesTemperature = new Series
{
Name = "温度 (℃)",
ChartType = SeriesChartType.Line,
Color = Color.Orange,
BorderWidth = 2,
XValueType = ChartValueType.DateTime,
YValueType = ChartValueType.Double,
YAxisType = AxisType.Secondary
};
chart.Series.Add(seriesRPM);
chart.Series.Add(seriesCurrent);
chart.Series.Add(seriesVoltage);
chart.Series.Add(seriesTemperature);
// 添加到控件
this.Controls.Add(chart);
}
/// <summary>
/// 添加数据点
/// </summary>
public void AddDataPoint(MotorData data)
{
if (chart.InvokeRequired)
{
chart.Invoke(new Action<MotorData>(AddDataPoint), data);
return;
}
try
{
double timeElapsed = (data.TimeStamp - startTime).TotalSeconds;
// 添加数据点
DataPoint pointRPM = new DataPoint(timeElapsed, data.RPM);
DataPoint pointCurrent = new DataPoint(timeElapsed, data.Current);
DataPoint pointVoltage = new DataPoint(timeElapsed, data.Voltage);
DataPoint pointTemp = new DataPoint(timeElapsed, data.Temperature);
dataPointsRPM.Enqueue(pointRPM);
dataPointsCurrent.Enqueue(pointCurrent);
dataPointsVoltage.Enqueue(pointVoltage);
dataPointsTemperature.Enqueue(pointTemp);
// 限制数据点数
while (dataPointsRPM.Count > maxPoints)
{
dataPointsRPM.Dequeue();
dataPointsCurrent.Dequeue();
dataPointsVoltage.Dequeue();
dataPointsTemperature.Dequeue();
}
// 更新图表
UpdateChart();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"添加数据点错误: {ex.Message}");
}
}
/// <summary>
/// 更新图表显示
/// </summary>
private void UpdateChart()
{
try
{
// 清空系列
seriesRPM.Points.Clear();
seriesCurrent.Points.Clear();
seriesVoltage.Points.Clear();
seriesTemperature.Points.Clear();
// 重新添加所有点
foreach (var point in dataPointsRPM)
{
seriesRPM.Points.Add(point);
}
foreach (var point in dataPointsCurrent)
{
seriesCurrent.Points.Add(point);
}
foreach (var point in dataPointsVoltage)
{
seriesVoltage.Points.Add(point);
}
foreach (var point in dataPointsTemperature)
{
seriesTemperature.Points.Add(point);
}
// 自动调整Y轴范围
if (seriesRPM.Points.Count > 0)
{
double minRPM = double.MaxValue;
double maxRPM = double.MinValue;
foreach (DataPoint point in seriesRPM.Points)
{
double y = point.YValues[0];
if (y < minRPM) minRPM = y;
if (y > maxRPM) maxRPM = y;
}
chart.ChartAreas[0].AxisY.Minimum = Math.Floor(minRPM * 0.9);
chart.ChartAreas[0].AxisY.Maximum = Math.Ceiling(maxRPM * 1.1);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新图表错误: {ex.Message}");
}
}
/// <summary>
/// 清空图表
/// </summary>
public void Clear()
{
if (chart.InvokeRequired)
{
chart.Invoke(new Action(Clear));
return;
}
dataPointsRPM.Clear();
dataPointsCurrent.Clear();
dataPointsVoltage.Clear();
dataPointsTemperature.Clear();
seriesRPM.Points.Clear();
seriesCurrent.Points.Clear();
seriesVoltage.Points.Clear();
seriesTemperature.Points.Clear();
startTime = DateTime.Now;
}
/// <summary>
/// 保存图表为图片
/// </summary>
public void SaveImage(string filePath)
{
try
{
chart.SaveImage(filePath, ChartImageFormat.Png);
}
catch (Exception ex)
{
throw new Exception($"保存图片失败: {ex.Message}", ex);
}
}
/// <summary>
/// 设置最大显示点数
/// </summary>
public int MaxPoints
{
get { return maxPoints; }
set
{
maxPoints = Math.Max(100, value);
// 立即应用
while (dataPointsRPM.Count > maxPoints)
{
dataPointsRPM.Dequeue();
dataPointsCurrent.Dequeue();
dataPointsVoltage.Dequeue();
dataPointsTemperature.Dequeue();
}
UpdateChart();
}
}
}
}
五、主窗口界面
FormMain.cs
csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Globalization;
namespace MotorMonitor
{
public partial class FormMain : Form
{
private SerialPortManager serialManager;
private List<MotorData> dataHistory = new List<MotorData>();
private Timer updateTimer;
private bool isRecording = false;
private DateTime startTime;
// 状态颜色
private readonly Color COLOR_CONNECTED = Color.Green;
private readonly Color COLOR_DISCONNECTED = Color.Red;
private readonly Color COLOR_NORMAL = Color.Black;
private readonly Color COLOR_WARNING = Color.Orange;
private readonly Color COLOR_ALARM = Color.Red;
// 报警阈值
private double alarmRPM = 3000;
private double alarmCurrent = 10.0;
private double alarmTemperature = 80.0;
public FormMain()
{
InitializeComponent();
InitializeComponents();
}
private void InitializeComponents()
{
// 初始化串口管理器
serialManager = new SerialPortManager();
serialManager.LogMessage += SerialManager_LogMessage;
serialManager.DataReceived += SerialManager_DataReceived;
serialManager.ConnectionChanged += SerialManager_ConnectionChanged;
// 初始化更新定时器
updateTimer = new Timer
{
Interval = 100, // 100ms更新一次
Enabled = false
};
updateTimer.Tick += UpdateTimer_Tick;
// 初始化波形图表
waveformChart1.MaxPoints = 500;
// 初始化下拉框
InitializeComboBoxes();
// 状态初始化
UpdateConnectionStatus(false);
}
private void InitializeComboBoxes()
{
// 串口号
cmbPort.Items.Clear();
cmbPort.Items.AddRange(serialManager.GetAvailablePorts());
if (cmbPort.Items.Count > 0)
cmbPort.SelectedIndex = 0;
// 波特率
cmbBaudRate.Items.AddRange(new object[] { 9600, 19200, 38400, 57600, 115200, 230400 });
cmbBaudRate.SelectedItem = 115200;
// 数据位
cmbDataBits.Items.AddRange(new object[] { 7, 8 });
cmbDataBits.SelectedItem = 8;
// 停止位
cmbStopBits.Items.AddRange(new object[] { 1, 1.5, 2 });
cmbStopBits.SelectedItem = 1;
// 校验位
cmbParity.Items.AddRange(Enum.GetNames(typeof(Parity)));
cmbParity.SelectedItem = "None";
}
#region 事件处理
private void SerialManager_LogMessage(object sender, string message)
{
if (txtLog.InvokeRequired)
{
txtLog.Invoke(new Action<string>(AddLogMessage), message);
}
else
{
AddLogMessage(message);
}
}
private void SerialManager_DataReceived(object sender, MotorData data)
{
if (this.InvokeRequired)
{
this.Invoke(new Action<MotorData>(ProcessReceivedData), data);
}
else
{
ProcessReceivedData(data);
}
}
private void SerialManager_ConnectionChanged(object sender, bool isConnected)
{
if (btnConnect.InvokeRequired)
{
btnConnect.Invoke(new Action<bool>(UpdateConnectionStatus), isConnected);
}
else
{
UpdateConnectionStatus(isConnected);
}
}
private void ProcessReceivedData(MotorData data)
{
// 添加到历史数据
dataHistory.Add(data);
// 更新波形
waveformChart1.AddDataPoint(data);
// 更新数字显示
UpdateDigitalDisplays(data);
// 检查报警
CheckAlarms(data);
// 如果正在记录,保存到文件
if (isRecording)
{
AppendDataToFile(data);
}
}
private void UpdateDigitalDisplays(MotorData data)
{
lblRPM.Text = $"{data.RPM:F0}";
lblCurrent.Text = $"{data.Current:F2} A";
lblVoltage.Text = $"{data.Voltage:F1} V";
lblTemperature.Text = $"{data.Temperature:F1} ℃";
lblTorque.Text = $"{data.Torque:F2} N·m";
lblPower.Text = $"{data.Torque * data.RPM / 9549:F2} kW"; // 功率 = 扭矩 * 转速 / 9549
}
private void CheckAlarms(MotorData data)
{
// 检查转速报警
if (data.RPM > alarmRPM)
{
lblRPM.ForeColor = COLOR_ALARM;
AddLogMessage($"警告: 转速过高 {data.RPM:F0} RPM");
}
else if (data.RPM > alarmRPM * 0.9)
{
lblRPM.ForeColor = COLOR_WARNING;
}
else
{
lblRPM.ForeColor = COLOR_NORMAL;
}
// 检查电流报警
if (data.Current > alarmCurrent)
{
lblCurrent.ForeColor = COLOR_ALARM;
AddLogMessage($"警告: 电流过大 {data.Current:F2} A");
}
else
{
lblCurrent.ForeColor = COLOR_NORMAL;
}
// 检查温度报警
if (data.Temperature > alarmTemperature)
{
lblTemperature.ForeColor = COLOR_ALARM;
AddLogMessage($"警告: 温度过高 {data.Temperature:F1} ℃");
}
else if (data.Temperature > alarmTemperature * 0.9)
{
lblTemperature.ForeColor = COLOR_WARNING;
}
else
{
lblTemperature.ForeColor = COLOR_NORMAL;
}
}
private void UpdateConnectionStatus(bool isConnected)
{
btnConnect.Text = isConnected ? "断开连接" : "连接";
btnConnect.BackColor = isConnected ? COLOR_CONNECTED : SystemColors.Control;
lblStatus.Text = isConnected ? "已连接" : "未连接";
lblStatus.ForeColor = isConnected ? COLOR_CONNECTED : COLOR_DISCONNECTED;
// 更新按钮状态
cmbPort.Enabled = !isConnected;
cmbBaudRate.Enabled = !isConnected;
cmbDataBits.Enabled = !isConnected;
cmbStopBits.Enabled = !isConnected;
cmbParity.Enabled = !isConnected;
// 启动/停止定时器
updateTimer.Enabled = isConnected;
if (!isConnected)
{
// 清空数据显示
lblRPM.Text = "---";
lblCurrent.Text = "---";
lblVoltage.Text = "---";
lblTemperature.Text = "---";
lblTorque.Text = "---";
lblPower.Text = "---";
}
}
private void UpdateTimer_Tick(object sender, EventArgs e)
{
// 更新运行时间显示
if (serialManager.IsConnected)
{
TimeSpan runTime = DateTime.Now - startTime;
lblRunTime.Text = $"运行时间: {runTime:hh\\:mm\\:ss}";
}
}
private void AddLogMessage(string message)
{
if (txtLog.TextLength > 10000)
{
txtLog.Clear();
}
txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");
txtLog.ScrollToCaret();
}
#endregion
#region 控件事件
private void btnConnect_Click(object sender, EventArgs e)
{
if (serialManager.IsConnected)
{
serialManager.Disconnect();
}
else
{
if (cmbPort.SelectedItem == null)
{
MessageBox.Show("请选择串口号", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
string portName = cmbPort.SelectedItem.ToString();
int baudRate = (int)cmbBaudRate.SelectedItem;
int dataBits = (int)cmbDataBits.SelectedItem;
float stopBits = (float)cmbStopBits.SelectedItem;
Parity parity = (Parity)Enum.Parse(typeof(Parity), cmbParity.SelectedItem.ToString());
bool success = serialManager.Connect(portName, baudRate, parity, dataBits,
stopBits == 1.5f ? StopBits.OnePointFive : (stopBits == 2 ? StopBits.Two : StopBits.One));
if (success)
{
startTime = DateTime.Now;
}
}
catch (Exception ex)
{
MessageBox.Show($"连接失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void btnRefreshPorts_Click(object sender, EventArgs e)
{
string currentPort = cmbPort.SelectedItem?.ToString();
cmbPort.Items.Clear();
cmbPort.Items.AddRange(serialManager.GetAvailablePorts());
if (!string.IsNullOrEmpty(currentPort) && cmbPort.Items.Contains(currentPort))
{
cmbPort.SelectedItem = currentPort;
}
else if (cmbPort.Items.Count > 0)
{
cmbPort.SelectedIndex = 0;
}
}
private void btnStartRecord_Click(object sender, EventArgs e)
{
if (isRecording)
{
// 停止记录
isRecording = false;
btnStartRecord.Text = "开始记录";
btnStartRecord.BackColor = SystemColors.Control;
AddLogMessage("数据记录已停止");
}
else
{
// 开始记录
SaveFileDialog saveDialog = new SaveFileDialog
{
Filter = "CSV文件|*.csv|文本文件|*.txt|所有文件|*.*",
FileName = $"MotorData_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
Title = "保存数据文件"
};
if (saveDialog.ShowDialog() == DialogResult.OK)
{
try
{
// 创建CSV文件头
using (StreamWriter writer = new StreamWriter(saveDialog.FileName, false, System.Text.Encoding.UTF8))
{
writer.WriteLine("时间,转速(RPM),电流(A),电压(V),温度(℃),扭矩(N·m),功率(kW)");
}
isRecording = true;
btnStartRecord.Text = "停止记录";
btnStartRecord.BackColor = Color.LightGreen;
AddLogMessage($"开始记录数据到: {saveDialog.FileName}");
}
catch (Exception ex)
{
MessageBox.Show($"创建文件失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
private void AppendDataToFile(MotorData data)
{
try
{
// 查找最新的文件
string[] csvFiles = Directory.GetFiles(Application.StartupPath, "MotorData_*.csv");
if (csvFiles.Length == 0) return;
string latestFile = csvFiles[0];
foreach (string file in csvFiles)
{
if (File.GetLastWriteTime(file) > File.GetLastWriteTime(latestFile))
{
latestFile = file;
}
}
using (StreamWriter writer = new StreamWriter(latestFile, true, System.Text.Encoding.UTF8))
{
double power = data.Torque * data.RPM / 9549;
writer.WriteLine($"{data.TimeStamp:HH:mm:ss.fff},{data.RPM:F1},{data.Current:F2},{data.Voltage:F1},{data.Temperature:F1},{data.Torque:F2},{power:F2}");
}
}
catch (Exception ex)
{
AddLogMessage($"保存数据错误: {ex.Message}");
}
}
private void btnClearChart_Click(object sender, EventArgs e)
{
waveformChart1.Clear();
dataHistory.Clear();
AddLogMessage("图表已清空");
}
private void btnSaveImage_Click(object sender, EventArgs e)
{
SaveFileDialog saveDialog = new SaveFileDialog
{
Filter = "PNG图片|*.png|JPEG图片|*.jpg|BMP图片|*.bmp",
FileName = $"Waveform_{DateTime.Now:yyyyMMdd_HHmmss}.png",
Title = "保存波形图片"
};
if (saveDialog.ShowDialog() == DialogResult.OK)
{
try
{
waveformChart1.SaveImage(saveDialog.FileName);
AddLogMessage($"波形图已保存: {saveDialog.FileName}");
}
catch (Exception ex)
{
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void btnSendCommand_Click(object sender, EventArgs e)
{
if (!serialManager.IsConnected)
{
MessageBox.Show("请先连接串口", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string command = txtCommand.Text.Trim();
if (!string.IsNullOrEmpty(command))
{
bool success = serialManager.SendCommand(command);
if (success)
{
AddLogMessage($"发送命令: {command}");
txtCommand.Clear();
}
}
}
private void btnSetAlarm_Click(object sender, EventArgs e)
{
using (FormAlarmSetting form = new FormAlarmSetting(alarmRPM, alarmCurrent, alarmTemperature))
{
if (form.ShowDialog() == DialogResult.OK)
{
alarmRPM = form.AlarmRPM;
alarmCurrent = form.AlarmCurrent;
alarmTemperature = form.AlarmTemperature;
AddLogMessage($"报警阈值已更新: 转速>{alarmRPM}RPM, 电流>{alarmCurrent}A, 温度>{alarmTemperature}℃");
}
}
}
private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
{
serialManager?.Dispose();
}
#endregion
}
}
六、报警设置窗口
FormAlarmSetting.cs
csharp
using System;
using System.Windows.Forms;
namespace MotorMonitor
{
public partial class FormAlarmSetting : Form
{
public double AlarmRPM { get; private set; }
public double AlarmCurrent { get; private set; }
public double AlarmTemperature { get; private set; }
public FormAlarmSetting(double rpm, double current, double temp)
{
InitializeComponent();
// 初始化控件
numRPM.Value = (decimal)rpm;
numCurrent.Value = (decimal)current;
numTemperature.Value = (decimal)temp;
}
private void btnOK_Click(object sender, EventArgs e)
{
AlarmRPM = (double)numRPM.Value;
AlarmCurrent = (double)numCurrent.Value;
AlarmTemperature = (double)numTemperature.Value;
DialogResult = DialogResult.OK;
Close();
}
private void btnCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}
七、界面设计(FormMain.Designer.cs 部分)
csharp
partial class FormMain
{
private System.ComponentModel.IContainer components = null;
// 控件定义
private Panel panelTop;
private GroupBox gbConnection;
private ComboBox cmbPort;
private ComboBox cmbBaudRate;
private ComboBox cmbDataBits;
private ComboBox cmbStopBits;
private ComboBox cmbParity;
private Button btnRefreshPorts;
private Button btnConnect;
private Label lblStatus;
private GroupBox gbDataDisplay;
private Label lblRPM;
private Label lblCurrent;
private Label lblVoltage;
private Label lblTemperature;
private Label lblTorque;
private Label lblPower;
private Label lblRunTime;
private Panel panelMiddle;
private WaveformChart waveformChart1;
private Panel panelBottom;
private GroupBox gbControl;
private Button btnStartRecord;
private Button btnClearChart;
private Button btnSaveImage;
private Button btnSetAlarm;
private GroupBox gbCommand;
private TextBox txtCommand;
private Button btnSendCommand;
private GroupBox gbLog;
private TextBox txtLog;
private Label label1, label2, label3, label4, label5, label6, label7, label8, label9, label10;
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FormMain));
// 主窗口设置
this.Text = "电机监控系统";
this.Size = new System.Drawing.Size(1200, 800);
this.StartPosition = FormStartPosition.CenterScreen;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
// 顶部面板
panelTop = new Panel
{
Dock = DockStyle.Top,
Height = 120,
BorderStyle = BorderStyle.FixedSingle
};
// 连接设置组
gbConnection = new GroupBox
{
Text = "串口设置",
Location = new Point(10, 10),
Size = new Size(400, 100)
};
// 串口控件
label1 = new Label { Text = "端口:", Location = new Point(10, 25), Size = new Size(40, 20) };
cmbPort = new ComboBox { Location = new Point(50, 22), Size = new Size(80, 21) };
label2 = new Label { Text = "波特率:", Location = new Point(140, 25), Size = new Size(50, 20) };
cmbBaudRate = new ComboBox { Location = new Point(190, 22), Size = new Size(80, 21) };
label3 = new Label { Text = "数据位:", Location = new Point(280, 25), Size = new Size(50, 20) };
cmbDataBits = new ComboBox { Location = new Point(330, 22), Size = new Size(50, 21) };
label4 = new Label { Text = "停止位:", Location = new Point(10, 55), Size = new Size(50, 20) };
cmbStopBits = new ComboBox { Location = new Point(60, 52), Size = new Size(50, 21) };
label5 = new Label { Text = "校验位:", Location = new Point(120, 55), Size = new Size(50, 20) };
cmbParity = new ComboBox { Location = new Point(170, 52), Size = new Size(80, 21) };
btnRefreshPorts = new Button { Text = "刷新", Location = new Point(260, 50), Size = new Size(60, 25) };
btnConnect = new Button { Text = "连接", Location = new Point(330, 50), Size = new Size(60, 25) };
gbConnection.Controls.AddRange(new Control[] { label1, cmbPort, label2, cmbBaudRate, label3, cmbDataBits,
label4, cmbStopBits, label5, cmbParity, btnRefreshPorts, btnConnect });
// 数据显示组
gbDataDisplay = new GroupBox
{
Text = "电机数据",
Location = new Point(420, 10),
Size = new Size(350, 100)
};
label6 = new Label { Text = "转速:", Location = new Point(10, 25), Size = new Size(40, 20) };
lblRPM = new Label { Text = "---", Location = new Point(50, 25), Size = new Size(60, 20),
Font = new Font("Microsoft YaHei", 10, FontStyle.Bold), ForeColor = Color.Red };
label7 = new Label { Text = "电流:", Location = new Point(120, 25), Size = new Size(40, 20) };
lblCurrent = new Label { Text = "---", Location = new Point(160, 25), Size = new Size(60, 20),
Font = new Font("Microsoft YaHei", 9, FontStyle.Bold) };
label8 = new Label { Text = "电压:", Location = new Point(230, 25), Size = new Size(40, 20) };
lblVoltage = new Label { Text = "---", Location = new Point(270, 25), Size = new Size(60, 20),
Font = new Font("Microsoft YaHei", 9, FontStyle.Bold) };
label9 = new Label { Text = "温度:", Location = new Point(10, 55), Size = new Size(40, 20) };
lblTemperature = new Label { Text = "---", Location = new Point(50, 55), Size = new Size(60, 20),
Font = new Font("Microsoft YaHei", 9, FontStyle.Bold) };
label10 = new Label { Text = "扭矩:", Location = new Point(120, 55), Size = new Size(40, 20) };
lblTorque = new Label { Text = "---", Location = new Point(160, 55), Size = new Size(60, 20),
Font = new Font("Microsoft YaHei", 9, FontStyle.Bold) };
Label label11 = new Label { Text = "功率:", Location = new Point(230, 55), Size = new Size(40, 20) };
lblPower = new Label { Text = "---", Location = new Point(270, 55), Size = new Size(60, 20),
Font = new Font("Microsoft YaHei", 9, FontStyle.Bold) };
gbDataDisplay.Controls.AddRange(new Control[] { label6, lblRPM, label7, lblCurrent, label8, lblVoltage,
label9, lblTemperature, label10, lblTorque, label11, lblPower });
// 状态显示
lblStatus = new Label
{
Text = "未连接",
ForeColor = Color.Red,
Font = new Font("Microsoft YaHei", 10, FontStyle.Bold),
Location = new Point(780, 20),
Size = new Size(100, 30)
};
lblRunTime = new Label
{
Text = "运行时间: 00:00:00",
Location = new Point(780, 60),
Size = new Size(150, 20)
};
panelTop.Controls.AddRange(new Control[] { gbConnection, gbDataDisplay, lblStatus, lblRunTime });
// 中间面板 - 波形图
panelMiddle = new Panel
{
Dock = DockStyle.Fill,
BorderStyle = BorderStyle.FixedSingle
};
waveformChart1 = new WaveformChart
{
Dock = DockStyle.Fill
};
panelMiddle.Controls.Add(waveformChart1);
// 底部面板
panelBottom = new Panel
{
Dock = DockStyle.Bottom,
Height = 180,
BorderStyle = BorderStyle.FixedSingle
};
// 控制组
gbControl = new GroupBox
{
Text = "控制",
Location = new Point(10, 10),
Size = new Size(200, 150)
};
btnStartRecord = new Button { Text = "开始记录", Location = new Point(20, 25), Size = new Size(160, 30) };
btnClearChart = new Button { Text = "清空图表", Location = new Point(20, 65), Size = new Size(160, 30) };
btnSaveImage = new Button { Text = "保存图片", Location = new Point(20, 105), Size = new Size(160, 30) };
btnSetAlarm = new Button { Text = "报警设置", Location = new Point(20, 145), Size = new Size(160, 30) };
gbControl.Controls.AddRange(new Control[] { btnStartRecord, btnClearChart, btnSaveImage, btnSetAlarm });
// 命令组
gbCommand = new GroupBox
{
Text = "命令发送",
Location = new Point(220, 10),
Size = new Size(300, 150)
};
txtCommand = new TextBox
{
Location = new Point(10, 25),
Size = new Size(280, 80),
Multiline = true,
ScrollBars = ScrollBars.Vertical
};
btnSendCommand = new Button
{
Text = "发送",
Location = new Point(220, 110),
Size = new Size(70, 30)
};
gbCommand.Controls.AddRange(new Control[] { txtCommand, btnSendCommand });
// 日志组
gbLog = new GroupBox
{
Text = "系统日志",
Location = new Point(530, 10),
Size = new Size(640, 150)
};
txtLog = new TextBox
{
Dock = DockStyle.Fill,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
BackColor = Color.Black,
ForeColor = Color.Lime,
Font = new Font("Consolas", 9)
};
gbLog.Controls.Add(txtLog);
panelBottom.Controls.AddRange(new Control[] { gbControl, gbCommand, gbLog });
// 添加所有面板到主窗口
this.Controls.AddRange(new Control[] { panelTop, panelMiddle, panelBottom });
// 绑定事件
btnConnect.Click += btnConnect_Click;
btnRefreshPorts.Click += btnRefreshPorts_Click;
btnStartRecord.Click += btnStartRecord_Click;
btnClearChart.Click += btnClearChart_Click;
btnSaveImage.Click += btnSaveImage_Click;
btnSendCommand.Click += btnSendCommand_Click;
btnSetAlarm.Click += btnSetAlarm_Click;
this.FormClosing += FormMain_FormClosing;
}
}
参考代码 上位机显示电机转速和波形 www.youwenfan.com/contentcsv/111859.html
八、安装和使用说明
1. NuGet包依赖
在项目中添加以下NuGet包:
xml
<!-- 在.csproj文件中 -->
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
<PackageReference Include="System.Windows.Forms.DataVisualization" Version="1.0.0" />
或通过NuGet管理器安装:
- System.IO.Ports
- System.Windows.Forms.DataVisualization
2. 运行要求
- .NET Framework 4.7.2 或更高版本
- Windows 7/8/10/11
- 串口通信支持
3. 使用步骤
-
连接设备
- 选择正确的串口号
- 设置波特率(通常115200)
- 点击"连接"按钮
-
数据显示
- 实时显示转速、电流、电压、温度、扭矩
- 波形图显示历史数据
- 数字会根据阈值变化颜色(正常→黄色→红色)
-
数据记录
- 点击"开始记录"保存数据到CSV文件
- 点击"保存图片"保存波形图
- 点击"清空图表"清除历史数据
-
命令发送
- 在下方的文本框中输入命令
- 点击"发送"按钮发送到下位机
4. 下位机数据格式
下位机应发送以下格式的数据包:
$转速,电流,电压,温度,扭矩#
例如:$1500,5.2,24.5,45.2,3.8#
九、扩展功能建议
1. 添加网络通信支持
csharp
public class NetworkManager
{
// 支持TCP/UDP通信
public bool ConnectTCP(string ip, int port) { ... }
public bool ConnectUDP(int localPort) { ... }
}
2. 添加数据回放功能
csharp
public class DataReplayer
{
public void LoadDataFromFile(string filePath) { ... }
public void Play() { ... }
public void Pause() { ... }
public void Seek(TimeSpan time) { ... }
}
3. 添加FFT频谱分析
csharp
public class SpectrumAnalyzer
{
public double[] ComputeFFT(double[] timeDomain) { ... }
public void DisplaySpectrum(Chart chart) { ... }
public double FindDominantFrequency() { ... }
}
4. 添加数据库支持
csharp
public class DatabaseManager
{
public void SaveToSQLite(MotorData data) { ... }
public List<MotorData> LoadHistory(DateTime start, DateTime end) { ... }
public void ExportToExcel(string filePath) { ... }
}
十、故障排除
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 无法连接串口 | 端口被占用 | 关闭其他占用端口的程序 |
| 无数据显示 | 波特率不匹配 | 检查下位机波特率设置 |
| 波形图卡顿 | 数据点太多 | 减少最大显示点数 |
| 数据乱码 | 协议格式错误 | 检查下位机数据格式 |
| 保存失败 | 文件被占用 | 关闭已打开的数据文件 |