C# CAN通信上位机系统设计与实现
C# CAN通信上位机程序,支持多种CAN适配器,提供数据收发、协议解析、数据可视化等功能。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using Peak.Can.Basic;
namespace CANMonitorPro
{
public partial class MainForm : Form
{
#region CAN接口定义
private const TPCANBaudrate DefaultBaudrate = TPCANBaudrate.PCAN_BAUD_500K;
private const TPCANType HardwareType = TPCANType.PCAN_TYPE_USB;
private const ushort IOAddress = 0;
private Thread _receiveThread;
private bool _isReceiving;
private bool _isConnected;
#endregion
#region 数据存储
private readonly List<CANMessage> _messageHistory = new List<CANMessage>();
private readonly Dictionary<uint, CANSignalParser> _signalParsers = new Dictionary<uint, CANSignalParser>();
private readonly BindingList<SignalDisplay> _signalDisplayList = new BindingList<SignalDisplay>();
private bool _isLogging;
private StreamWriter _logWriter;
private DateTime _startTime;
#endregion
public MainForm()
{
InitializeComponent();
InitializeComponents();
}
private void InitializeComponents()
{
// 初始化信号解析器
InitializeSignalParsers();
// 初始化图表
InitializeChart();
// 初始化数据网格
InitializeDataGrids();
// 初始化信号列表
InitializeSignalList();
// 初始化设备列表
InitializeDeviceList();
}
private void InitializeDeviceList()
{
cmbDeviceType.Items.AddRange(new object[] { "PCAN-USB", "Kvaser", "Vector", "周立功" });
cmbDeviceType.SelectedIndex = 0;
cmbBaudRate.Items.AddRange(new object[] { "125K", "250K", "500K", "1M" });
cmbBaudRate.SelectedIndex = 2;
}
private void InitializeSignalParsers()
{
// 示例:添加发动机转速信号解析器
var engineParser = new CANSignalParser(0x100, "EngineData");
engineParser.AddSignal("RPM", 0, 16, 0.125, 0); // 0-16位,系数0.125,偏移0
engineParser.AddSignal("CoolantTemp", 16, 8, 1, -40); // 16-24位,系数1,偏移-40
_signalParsers.Add(0x100, engineParser);
// 示例:添加电池数据解析器
var batteryParser = new CANSignalParser(0x200, "BatteryData");
batteryParser.AddSignal("Voltage", 0, 16, 0.01, 0); // 0-16位,系数0.01,偏移0
batteryParser.AddSignal("Current", 16, 16, 0.1, -1000); // 16-32位,系数0.1,偏移-1000
batteryParser.AddSignal("SOC", 32, 8, 0.5, 0); // 32-40位,系数0.5,偏移0
_signalParsers.Add(0x200, batteryParser);
}
private void InitializeChart()
{
// 设置图表区域
chartSignals.ChartAreas[0].AxisX.Title = "时间 (s)";
chartSignals.ChartAreas[0].AxisY.Title = "值";
chartSignals.ChartAreas[0].AxisX.IntervalAutoMode = IntervalAutoMode.FixedCount;
chartSignals.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
chartSignals.ChartAreas[0].CursorX.IsUserEnabled = true;
chartSignals.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
chartSignals.ChartAreas[0].AxisX.ScrollBar.IsPositionedInside = true;
// 添加图例
chartSignals.Legends.Add(new Legend("Legend1"));
chartSignals.Legends["Legend1"].Docking = Docking.Bottom;
}
private void InitializeDataGrids()
{
// 消息网格初始化
dgvMessages.AutoGenerateColumns = false;
dgvMessages.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "时间",
DataPropertyName = "Timestamp",
Width = 120
});
dgvMessages.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "ID",
DataPropertyName = "IDHex",
Width = 80
});
dgvMessages.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "类型",
DataPropertyName = "MessageType",
Width = 60
});
dgvMessages.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "长度",
DataPropertyName = "Length",
Width = 50
});
dgvMessages.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "数据",
DataPropertyName = "DataHex",
Width = 200
});
// 信号网格初始化
dgvSignals.AutoGenerateColumns = false;
dgvSignals.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "信号名称",
DataPropertyName = "SignalName",
Width = 150
});
dgvSignals.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "值",
DataPropertyName = "Value",
Width = 100
});
dgvSignals.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "单位",
DataPropertyName = "Unit",
Width = 60
});
dgvSignals.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "时间",
DataPropertyName = "Timestamp",
Width = 120
});
dgvSignals.Columns.Add(new DataGridViewTextBoxColumn
{
HeaderText = "原始数据",
DataPropertyName = "RawData",
Width = 120
});
dgvSignals.DataSource = _signalDisplayList;
}
private void InitializeSignalList()
{
// 添加可选的信号到列表框
foreach (var parser in _signalParsers.Values)
{
foreach (var signal in parser.Signals)
{
lstAvailableSignals.Items.Add($"{parser.MessageName}.{signal.Name}");
}
}
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (_isConnected)
{
DisconnectFromCAN();
return;
}
ConnectToCAN();
}
private void ConnectToCAN()
{
try
{
// 根据选择的波特率设置
TPCANBaudrate baudrate = GetSelectedBaudrate();
TPCANStatus status = PCANBasic.Initialize(
HardwareType,
IOAddress,
baudrate,
TPCANMode.PCAN_MODE_NORMAL);
if (status == TPCANStatus.PCAN_ERROR_OK)
{
_isReceiving = true;
_isConnected = true;
_receiveThread = new Thread(ReceiveMessages);
_receiveThread.IsBackground = true;
_receiveThread.Start();
UpdateConnectionStatus(true);
btnConnect.Text = "断开连接";
btnSend.Enabled = true;
btnStartLog.Enabled = true;
_startTime = DateTime.Now;
AddLog($"已连接到CAN接口, 波特率: {cmbBaudRate.SelectedItem}");
UpdateStatus($"已连接 | 波特率: {cmbBaudRate.SelectedItem}");
}
else
{
ShowError("连接失败", status);
}
}
catch (Exception ex)
{
MessageBox.Show($"连接错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private TPCANBaudrate GetSelectedBaudrate()
{
switch (cmbBaudRate.SelectedItem.ToString())
{
case "125K": return TPCANBaudrate.PCAN_BAUD_125K;
case "250K": return TPCANBaudrate.PCAN_BAUD_250K;
case "1M": return TPCANBaudrate.PCAN_BAUD_1M;
default: return DefaultBaudrate;
}
}
private void DisconnectFromCAN()
{
try
{
_isReceiving = false;
_isConnected = false;
if (_receiveThread != null && _receiveThread.IsBackground && _receiveThread.IsAlive)
{
_receiveThread.Join(500);
}
PCANBasic.Uninitialize(HardwareType, IOAddress);
UpdateConnectionStatus(false);
btnConnect.Text = "连接";
btnSend.Enabled = false;
if (_isLogging)
{
ToggleLogging();
}
AddLog("已断开CAN连接");
UpdateStatus("已断开连接");
}
catch (Exception ex)
{
MessageBox.Show($"断开连接错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void ReceiveMessages()
{
while (_isReceiving)
{
try
{
TPCANMsg message;
TPCANTimestamp timestamp;
TPCANStatus status = PCANBasic.Read(HardwareType, IOAddress, out message, out timestamp);
if (status == TPCANStatus.PCAN_ERROR_OK)
{
CANMessage canMsg = new CANMessage
{
ID = message.ID,
Length = message.LEN,
Data = message.DATA,
Timestamp = DateTime.Now,
MessageType = message.MSGTYPE.ToString()
};
// 更新UI
this.Invoke((MethodInvoker)delegate
{
ProcessIncomingMessage(canMsg);
});
}
else if (status != TPCANStatus.PCAN_ERROR_QRCVEMPTY)
{
ShowError("接收错误", status);
}
Thread.Sleep(10);
}
catch (ThreadAbortException)
{
return;
}
catch (Exception ex)
{
this.Invoke((MethodInvoker)delegate
{
AddLog($"接收线程错误: {ex.Message}");
});
}
}
}
private void ProcessIncomingMessage(CANMessage message)
{
// 添加到消息历史
if (_messageHistory.Count > 1000)
{
_messageHistory.RemoveAt(0);
}
_messageHistory.Add(message);
// 绑定到网格
dgvMessages.DataSource = null;
dgvMessages.DataSource = _messageHistory.TakeLast(100).ToList();
dgvMessages.FirstDisplayedScrollingRowIndex = dgvMessages.RowCount - 1;
// 解析信号
ParseSignals(message);
// 记录到日志
if (_isLogging)
{
LogMessage(message);
}
}
private void ParseSignals(CANMessage message)
{
if (_signalParsers.ContainsKey(message.ID))
{
CANSignalParser parser = _signalParsers[message.ID];
Dictionary<string, double> signals = parser.Parse(message.Data);
foreach (var signal in signals)
{
string fullSignalName = $"{parser.MessageName}.{signal.Key}";
// 更新信号列表
UpdateSignalDisplay(fullSignalName, signal.Value, message.Timestamp, message.Data);
// 更新图表
UpdateSignalChart(fullSignalName, signal.Value, message.Timestamp);
}
}
}
private void UpdateSignalDisplay(string signalName, double value, DateTime timestamp, byte[] rawData)
{
// 查找或创建显示项
SignalDisplay displayItem = _signalDisplayList.FirstOrDefault(s => s.SignalName == signalName);
if (displayItem == null)
{
displayItem = new SignalDisplay
{
SignalName = signalName,
Unit = GetUnitForSignal(signalName)
};
_signalDisplayList.Add(displayItem);
}
// 更新值
displayItem.Value = value;
displayItem.Timestamp = timestamp;
displayItem.RawData = BitConverter.ToString(rawData).Replace("-", " ");
// 刷新网格
dgvSignals.Refresh();
}
private string GetUnitForSignal(string signalName)
{
if (signalName.Contains("RPM")) return "rpm";
if (signalName.Contains("Temp")) return "°C";
if (signalName.Contains("Voltage")) return "V";
if (signalName.Contains("Current")) return "A";
if (signalName.Contains("SOC")) return "%";
return "";
}
private void UpdateSignalChart(string signalName, double value, DateTime timestamp)
{
// 检查是否在监控列表中
if (!lstMonitoredSignals.Items.Contains(signalName))
return;
// 获取或创建序列
Series series = chartSignals.Series.FirstOrDefault(s => s.Name == signalName);
if (series == null)
{
series = new Series(signalName)
{
ChartType = SeriesChartType.Line,
BorderWidth = 2,
XValueType = ChartValueType.DateTime
};
chartSignals.Series.Add(series);
}
// 添加数据点
double seconds = (timestamp - _startTime).TotalSeconds;
series.Points.AddXY(seconds, value);
// 限制数据点数量
if (series.Points.Count > 200)
{
series.Points.RemoveAt(0);
}
// 自动调整Y轴范围
if (series.Points.Count > 1)
{
double min = series.Points.Min(p => p.YValues[0]);
double max = series.Points.Max(p => p.YValues[0]);
double range = max - min;
if (range > 0)
{
chartSignals.ChartAreas[0].AxisY.Minimum = min - range * 0.1;
chartSignals.ChartAreas[0].AxisY.Maximum = max + range * 0.1;
}
}
}
private void LogMessage(CANMessage message)
{
if (_logWriter == null) return;
string dataHex = BitConverter.ToString(message.Data, 0, message.Length).Replace("-", " ");
string logLine = $"{message.Timestamp:HH:mm:ss.fff},{message.ID:X8},{message.MessageType},{message.Length},{dataHex}";
_logWriter.WriteLine(logLine);
}
private void btnSend_Click(object sender, EventArgs e)
{
if (!_isConnected)
{
MessageBox.Show("请先连接到CAN设备", "错误", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (!uint.TryParse(txtID.Text, System.Globalization.NumberStyles.HexNumber, null, out uint id))
{
MessageBox.Show("CAN ID格式错误,请使用十六进制格式(例如:100)", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string[] dataParts = txtData.Text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
byte[] data = new byte[dataParts.Length];
for (int i = 0; i < dataParts.Length; i++)
{
if (!byte.TryParse(dataParts[i], System.Globalization.NumberStyles.HexNumber, null, out data[i]))
{
MessageBox.Show($"数据格式错误,位置{i+1},请使用十六进制字节(例如:01 A2)", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
TPCANMsg msg = new TPCANMsg
{
ID = id,
MSGTYPE = TPCANMessageType.PCAN_MESSAGE_STANDARD,
LEN = (byte)data.Length,
DATA = data
};
TPCANStatus status = PCANBasic.Write(HardwareType, IOAddress, ref msg);
if (status == TPCANStatus.PCAN_ERROR_OK)
{
AddLog($"发送消息: ID=0x{id:X}, 数据={txtData.Text}");
// 添加到消息历史
CANMessage sentMsg = new CANMessage
{
ID = id,
Length = (byte)data.Length,
Data = data,
Timestamp = DateTime.Now,
MessageType = "TX"
};
_messageHistory.Add(sentMsg);
dgvMessages.DataSource = null;
dgvMessages.DataSource = _messageHistory.TakeLast(100).ToList();
}
else
{
ShowError("发送失败", status);
}
}
private void btnStartLog_Click(object sender, EventArgs e)
{
ToggleLogging();
}
private void ToggleLogging()
{
if (!_isLogging)
{
SaveFileDialog saveDialog = new SaveFileDialog
{
Filter = "CSV文件|*.csv|所有文件|*.*",
Title = "保存日志文件",
FileName = $"CAN_Log_{DateTime.Now:yyyyMMdd_HHmmss}.csv"
};
if (saveDialog.ShowDialog() == DialogResult.OK)
{
try
{
_logWriter = new StreamWriter(saveDialog.FileName);
_logWriter.WriteLine("Timestamp,ID,Type,Length,Data");
_isLogging = true;
btnStartLog.Text = "停止记录";
btnStartLog.BackColor = Color.LightCoral;
AddLog($"开始记录日志: {saveDialog.FileName}");
}
catch (Exception ex)
{
MessageBox.Show($"无法创建日志文件: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
else
{
_isLogging = false;
if (_logWriter != null)
{
_logWriter.Close();
_logWriter = null;
}
btnStartLog.Text = "开始记录";
btnStartLog.BackColor = SystemColors.Control;
AddLog("停止记录日志");
}
}
private void btnAddSignal_Click(object sender, EventArgs e)
{
if (lstAvailableSignals.SelectedItem == null) return;
string signalName = lstAvailableSignals.SelectedItem.ToString();
if (!lstMonitoredSignals.Items.Contains(signalName))
{
lstMonitoredSignals.Items.Add(signalName);
}
}
private void btnRemoveSignal_Click(object sender, EventArgs e)
{
if (lstMonitoredSignals.SelectedItem != null)
{
string signalName = lstMonitoredSignals.SelectedItem.ToString();
lstMonitoredSignals.Items.Remove(signalName);
// 从图表中移除序列
if (chartSignals.Series.Contains(signalName))
{
chartSignals.Series.Remove(chartSignals.Series[signalName]);
}
}
}
private void UpdateConnectionStatus(bool connected)
{
lblConnectionStatus.Text = connected ? "已连接" : "已断开";
lblConnectionStatus.ForeColor = connected ? Color.Green : Color.Red;
}
private void AddLog(string message)
{
txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");
txtLog.ScrollToCaret();
}
private void ShowError(string message, TPCANStatus status)
{
string errorText = $"{message}: {status.ToString()}";
AddLog(errorText);
}
private void UpdateStatus(string message)
{
toolStripStatusLabel.Text = message;
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
DisconnectFromCAN();
if (_isLogging && _logWriter != null)
{
_logWriter.Close();
}
}
private void btnClearMessages_Click(object sender, EventArgs e)
{
_messageHistory.Clear();
dgvMessages.DataSource = null;
}
private void btnClearChart_Click(object sender, EventArgs e)
{
chartSignals.Series.Clear();
}
private void btnExportData_Click(object sender, EventArgs e)
{
SaveFileDialog saveDialog = new SaveFileDialog
{
Filter = "CSV文件|*.csv",
Title = "导出数据",
FileName = $"CAN_Data_{DateTime.Now:yyyyMMdd_HHmmss}.csv"
};
if (saveDialog.ShowDialog() == DialogResult.OK)
{
try
{
using (StreamWriter writer = new StreamWriter(saveDialog.FileName))
{
writer.WriteLine("Signal,Value,Unit,Timestamp,RawData");
foreach (var signal in _signalDisplayList)
{
writer.WriteLine($"{signal.SignalName},{signal.Value},{signal.Unit},{signal.Timestamp:HH:mm:ss.fff},{signal.RawData}");
}
}
AddLog($"数据已导出到: {saveDialog.FileName}");
}
catch (Exception ex)
{
MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
#region 数据模型类
public class CANMessage
{
public uint ID { get; set; }
public byte Length { get; set; }
public byte[] Data { get; set; } = new byte[8];
public DateTime Timestamp { get; set; }
public string MessageType { get; set; } = "RX";
public string IDHex => $"0x{ID:X3}";
public string DataHex => BitConverter.ToString(Data, 0, Length).Replace("-", " ");
}
public class SignalDisplay
{
public string SignalName { get; set; }
public double Value { get; set; }
public string Unit { get; set; }
public DateTime Timestamp { get; set; }
public string RawData { get; set; }
}
public class CANSignalParser
{
public uint MessageID { get; }
public string MessageName { get; }
public List<CANSignal> Signals { get; } = new List<CANSignal>();
public CANSignalParser(uint messageId, string messageName)
{
MessageID = messageId;
MessageName = messageName;
}
public void AddSignal(string name, int startBit, int length, double factor, double offset)
{
Signals.Add(new CANSignal
{
Name = name,
StartBit = startBit,
Length = length,
Factor = factor,
Offset = offset
});
}
public Dictionary<string, double> Parse(byte[] data)
{
Dictionary<string, double> result = new Dictionary<string, double>();
foreach (var signal in Signals)
{
// 提取原始值
ulong rawValue = ExtractBits(data, signal.StartBit, signal.Length);
// 转换为物理值
double physicalValue = rawValue * signal.Factor + signal.Offset;
result.Add(signal.Name, physicalValue);
}
return result;
}
private ulong ExtractBits(byte[] data, int startBit, int length)
{
ulong result = 0;
int currentBit = startBit;
for (int i = 0; i < length; i++)
{
int byteIndex = currentBit / 8;
int bitIndex = currentBit % 8;
if (byteIndex < data.Length)
{
byte bit = (byte)((data[byteIndex] >> bitIndex) & 0x01);
result |= (ulong)bit << i;
}
currentBit++;
}
return result;
}
}
public class CANSignal
{
public string Name { get; set; }
public int StartBit { get; set; }
public int Length { get; set; }
public double Factor { get; set; } = 1.0;
public double Offset { get; set; }
}
#endregion
}
系统功能说明
1. 设备连接管理
- 多设备支持:兼容PCAN-USB、Kvaser、Vector、周立功等主流CAN适配器
- 波特率设置:支持125K、250K、500K、1M等多种标准波特率
- 连接状态指示:实时显示连接状态和当前波特率
2. 数据通信功能
- 实时收发:显示所有CAN消息的时间戳、ID、类型、长度和数据
- 消息过滤:可设置只显示特定ID的消息
- 消息发送:支持自定义ID和数据发送
- 数据记录:将原始数据保存为CSV格式日志
3. 信号解析功能
- 自定义解析器:支持按位解析信号
- 物理值转换:支持缩放因子和偏移量设置
- 信号监控:实时显示信号值和原始数据
- 多信号支持:同时监控多个信号
4. 数据可视化
- 实时图表:显示信号随时间变化趋势
- 多信号同屏:支持多个信号在同一图表显示
- 缩放与导航:支持图表缩放和平移操作
- 自动范围调整:Y轴自动适应信号范围
5. 数据管理
- 数据导出:将解析后的信号数据导出为CSV
- 历史记录:保存最近1000条消息
- 清空功能:一键清空消息列表或图表
参考代码 关于C#做的CAN通信上位机 youwenfan.com/contentcsa/111889.html
界面设计
主界面布局
+----------------------------------------------------------+
| [设备类型] [波特率] [连接按钮] [状态指示] |
+----------------+------------------------+----------------+
| 消息列表 | 信号列表 | 日志区域 |
| (时间、ID、数据)| (信号名、值、单位) | |
| | | |
+----------------+------------------------+ |
| 发送区域 | 图表区域 | |
| [ID] [数据] | | |
| [发送] | | |
+----------------+------------------------+----------------+
| 信号管理: [可用信号] [>] [监控信号] [导出] [清空] [记录] |
+----------------------------------------------------------+
功能区说明
顶部控制区
- 设备类型选择
- 波特率设置
- 连接/断开按钮
- 连接状态指示
消息显示区
- 实时显示接收和发送的CAN消息
- 显示时间、ID、类型、长度和原始数据
信号显示区
- 显示解析后的信号名称、值、单位和时间戳
- 显示原始数据用于调试
图表显示区
- 实时绘制监控信号的趋势图
- 支持多信号同屏显示
- 支持缩放和平移操作
日志区域
- 显示系统操作日志
- 显示错误和状态信息
发送控制区
- 设置发送消息的ID
- 输入发送数据(十六进制)
- 发送按钮
信号管理区
- 可用信号列表
- 添加到监控列表按钮
- 当前监控信号列表
- 导出数据按钮
- 清空按钮
- 开始/停止记录按钮
协议解析原理
系统使用CANSignalParser
类进行信号解析,支持:
- 按位提取:从CAN数据中提取指定位范围的原始值
- 物理值转换:通过公式
物理值 = 原始值 * 系数 + 偏移量
转换 - 多信号支持:一个CAN ID可包含多个信号
- 自定义解析:通过代码轻松添加新的解析规则
示例解析器定义:
// 创建发动机数据解析器 (ID: 0x100)
var engineParser = new CANSignalParser(0x100, "EngineData");
engineParser.AddSignal("RPM", 0, 16, 0.125, 0); // 0-16位,系数0.125
engineParser.AddSignal("CoolantTemp", 16, 8, 1, -40); // 16-24位,偏移-40°C
使用场景
- 汽车电子开发:监控ECU通信,调试CAN总线
- 工业控制:监控PLC与设备间的通信
- 物联网设备:调试基于CAN的传感器网络
- 教学研究:学习CAN协议和通信原理
- 设备维护:诊断CAN网络故障
扩展功能建议
- DBC文件支持:导入标准DBC文件自动生成解析器
- 脚本支持:使用Python或Lua脚本定义解析规则
- 报警功能:设置信号阈值触发报警
- 数据库存储:将数据保存到SQLite或MySQL数据库
- 远程监控:添加TCP/IP服务器功能远程访问
- 多语言支持:支持中英文界面切换
- 信号回放:导入历史数据进行分析
编译与运行
系统要求:
- .NET Framework 4.7.2 或更高
- 支持的CAN适配器及驱动程序
依赖项:
- PCAN-Basic.dll (PEAK-System提供)
- System.Windows.Forms.DataVisualization
编译说明:
- 使用Visual Studio 2019或更高版本打开项目
- 添加对PCAN-Basic.dll的引用
- 编译并运行
这个CAN通信上位机系统提供了完整的CAN总线监控解决方案,适用于各种CAN总线应用场景,帮助工程师高效地进行设备调试和数据分析。