1. 项目概述
最近完成了一个工业电源监控系统的开发,采用C#作为上位机开发语言,搭配STM32作为下位机控制器。这个项目完美结合了工业控制领域常见的几种数据展示方式:实时曲线、仪表盘和数码管显示,实现了对电源参数的实时监控功能。
核心功能包括:
- 自动检测本机所有可用串口
- 双坐标轴动态显示设定电压、AD采样电压、设定电流和AD采样电流
- 电压电流数据更新频率10Hz,温度数据更新频率0.5Hz
- 采用工业级控件实现专业可视化效果
这个项目特别适合想要进入工业控制软件开发领域的朋友参考,涵盖了从硬件通讯到软件显示的全流程实现。下面我将详细拆解整个系统的设计思路和关键技术点。
2. 系统架构设计
2.1 整体架构
系统采用典型的上位机-下位机架构:
code复制[STM32下位机] --串口通讯--> [C#上位机]
下位机负责:
- 电源参数的AD采样
- 数据打包和串口发送
上位机负责:
- 串口通讯管理
- 数据解析和处理
- 可视化展示
2.2 通讯协议设计
通讯采用自定义二进制协议,帧格式如下:
| 字段 | 帧头 | 数据类型 | 数据长度 | 数据内容 | 校验和 |
|---|---|---|---|---|---|
| 字节数 | 1 | 1 | 1 | N | 1 |
| 值 | 0xAA | 0x01-0x04 | 4/8 | float*2 | XOR |
数据类型定义:
- 0x01: 电压数据
- 0x02: 电流数据
- 0x03: 温度数据
- 0x04: 设定参数
3. 上位机关键技术实现
3.1 串口通讯模块
串口通讯是整个系统的基础,采用System.IO.Ports.SerialPort组件实现。关键点在于:
- 自动获取可用串口:
csharp复制string[] ports = SerialPort.GetPortNames();
cmbPorts.BeginUpdate();
cmbPorts.Items.Clear();
foreach (string port in ports) {
cmbPorts.Items.Add(port);
}
cmbPorts.EndUpdate();
- 串口参数配置:
csharp复制serialPort.PortName = "COM3";
serialPort.BaudRate = 115200;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
注意:工业应用中建议使用更高的波特率(如921600)以保证数据传输实时性,同时要确保硬件支持。
3.2 数据接收与解析
采用生产者-消费者模型处理串口数据:
- 数据接收线程(生产者):
csharp复制private ConcurrentQueue<byte> _rawDataQueue = new ConcurrentQueue<byte>();
void sp_DataReceived(object sender, SerialDataReceivedEventArgs e) {
byte[] buffer = new byte[sp.BytesToRead];
sp.Read(buffer, 0, buffer.Length);
foreach (byte b in buffer) {
_rawDataQueue.Enqueue(b);
}
}
- 数据解析线程(消费者):
csharp复制private void DataParseThread() {
while (!_isDisposed) {
if (_rawDataQueue.TryDequeue(out byte data)) {
AnalyzePackage(data);
}
Thread.Sleep(1);
}
}
- 协议解析关键代码:
csharp复制private void AnalyzePackage(byte[] data) {
// 检查帧头
if (data[0] != 0xAA) return;
// 校验和验证
byte checksum = CalculateXOR(data, 0, data.Length - 1);
if (checksum != data[data.Length - 1]) return;
// 解析数据
int dataType = data[1];
int dataLength = data[2];
switch (dataType) {
case 0x01: // 电压
float voltage = BitConverter.ToSingle(new byte[] { data[6], data[5], data[4], data[3] }, 0);
UpdateVoltageDisplay(voltage);
break;
// 其他数据类型处理...
}
}
经验分享:使用ConcurrentQueue代替Queue+lock可以提高多线程性能,特别是在高频率数据接收场景下。
4. 数据可视化实现
4.1 实时曲线绘制
采用ZedGraph控件实现专业级曲线展示:
- 初始化双Y轴图表:
csharp复制GraphPane pane = zedGraphControl1.GraphPane;
pane.Title.Text = "电源参数实时监控";
pane.XAxis.Title.Text = "时间";
// 添加电压轴
YAxis voltageAxis = pane.YAxisList.Add("电压(V)");
voltageAxis.Scale.Min = 0;
voltageAxis.Scale.Max = 30;
voltageAxis.Color = Color.Red;
// 添加电流轴
YAxis currentAxis = pane.YAxisList.Add("电流(A)");
currentAxis.Scale.Min = 0;
currentAxis.Scale.Max = 5;
currentAxis.Color = Color.Blue;
- 动态更新曲线:
csharp复制LineItem voltageCurve = pane.CurveList[0] as LineItem;
PointPairList voltagePoints = voltageCurve.Points as PointPairList;
// 添加新数据点
voltagePoints.Add(XDate.DateTimeToXLDateTime(DateTime.Now), voltage);
// 限制数据点数量
if (voltagePoints.Count > 100) {
voltagePoints.RemoveAt(0);
}
// 刷新显示
zedGraphControl1.AxisChange();
zedGraphControl1.Invalidate();
4.2 仪表盘实现
使用LBIndustrialCtrls控件库实现工业级仪表盘:
- 数码管显示:
csharp复制lbLedDisplayVoltage.DigitCount = 4;
lbLedDisplayVoltage.DecimalPosition = 2;
lbLedDisplayVoltage.Value = voltage;
lbLedDisplayCurrent.DigitCount = 3;
lbLedDisplayCurrent.DecimalPosition = 1;
lbLedDisplayCurrent.Value = current;
- 表盘控件:
xml复制<lbi:AnalogMeterControl x:Name="analogMeterVoltage"
Minimum="0" Maximum="30"
MajorTickCount="6" MinorTickCount="5"
Value="{Binding Voltage}"/>
技巧:仪表盘控件通常比较耗资源,建议在不需要高精度动画时降低刷新频率。
5. 性能优化与稳定性保障
5.1 线程管理
为避免界面卡顿,采用多线程架构:
- UI线程:只负责界面更新
- 串口接收线程:负责原始数据接收
- 数据处理线程:负责协议解析
- 显示更新线程:负责控件刷新
使用Invoke进行跨线程UI更新:
csharp复制this.Invoke((MethodInvoker)delegate {
lbLedDisplayVoltage.Value = voltage;
zedGraphControl1.Invalidate();
});
5.2 数据缓冲处理
针对可能的数据拥堵情况,实现以下机制:
- 数据队列最大长度限制
- 异常数据自动丢弃
- 断线自动重连
- 数据接收超时检测
csharp复制// 队列长度监控
if (_rawDataQueue.Count > MAX_QUEUE_SIZE) {
_rawDataQueue.Clear();
LogWarning("数据队列溢出,已清空");
}
5.3 错误处理与日志
完善的错误处理机制:
csharp复制try {
serialPort.Open();
} catch (UnauthorizedAccessException ex) {
MessageBox.Show($"串口被占用:{ex.Message}");
} catch (IOException ex) {
MessageBox.Show($"串口不存在:{ex.Message}");
}
日志记录采用NLog框架:
xml复制<nlog>
<targets>
<target name="file" xsi:type="File"
fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate}|${level}|${message}" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="file" />
</rules>
</nlog>
6. 下位机实现要点
虽然本项目重点在上位机,但下位机实现也值得简要说明:
6.1 STM32硬件配置
- ADC配置:
c复制ADC_HandleTypeDef hadc1;
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
HAL_ADC_Init(&hadc1);
- 串口配置:
c复制UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
HAL_UART_Init(&huart1);
6.2 数据采集与发送
数据采集流程:
c复制void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
float voltage = HAL_ADC_GetValue(&hadc1) * 3.3f / 4095.0f * 10.0f;
SendDataPacket(0x01, &voltage, sizeof(voltage));
}
数据打包函数:
c复制void SendDataPacket(uint8_t type, void* data, uint8_t len) {
uint8_t buffer[32];
buffer[0] = 0xAA; // 帧头
buffer[1] = type; // 数据类型
buffer[2] = len; // 数据长度
memcpy(&buffer[3], data, len);
// 计算校验和
buffer[3+len] = 0;
for(int i=0; i<3+len; i++) {
buffer[3+len] ^= buffer[i];
}
HAL_UART_Transmit(&huart1, buffer, 4+len, 100);
}
7. 项目扩展与改进方向
在实际使用过程中,我发现这个系统还有以下可以改进的地方:
-
通讯协议增强:
- 增加心跳包机制检测连接状态
- 添加命令重发机制提高可靠性
- 支持大数据块传输
-
显示功能扩展:
- 增加历史数据回放功能
- 实现多窗口分屏显示
- 添加报警阈值设置和触发功能
-
数据分析功能:
- 实时计算统计参数(平均值、最大值等)
- 支持数据导出为Excel/CSV
- 添加简单的FFT频谱分析
-
硬件兼容性:
- 支持更多型号的电源设备
- 添加USB-CAN适配器支持
- 实现以太网通讯接口
这个项目最让我满意的是它展示了一个完整的工业监控系统开发流程,从硬件通讯到软件显示,每个环节都经过实际验证。特别是ZedGraph控件的使用,让数据显示效果达到了专业水准。