CAN通讯【控制器局域网】
控制器局域网总线(CAN,Controller Area Network)是一种用于实时应用的串行通讯协议总线,它可以使用双绞线来传输信号,是世界上应用最广泛的现场总线之一。
CAN协议用于汽车中各种不同元件之间的通信,以此取代昂贵而笨重的配电线束。该协议的健壮性使其用途延伸到其他自动化和工业应用。
CAN协议的特性包括完整性的串行数据通讯、提供实时支持、传输速率高达1Mb/s、同时具有11位的寻址以及检错能力。
CAN通讯原理说明:
https://baike.baidu.com/item/CAN通信系统/18884364?fr=aladdin
控制器局域网CAN( Controller Area Network)属于现场总线的范畴,是一种有效支持分布式控制系统的串行通信网络。
是由德国博世公司在20世纪80年代专门为汽车行业开发的一种串行通信总线。
由于其高性能、高可靠性以及独特的设计而越来越受到人们的重视,被广泛应用于诸多领域。而且能够检测出产生的任何错误。
当信号传输距离达到10km时,CAN仍可提供高达50kbit/s的数据传输速率。
由于CAN总线具有很高的实时性能和应用范围,从位速率最高可达1Mbps的高速网络到低成本多线路的50Kbps网络都可以任意搭配。
因此,CAN己经在汽车业、航空业、工业控制、安全防护等领域中得到了广泛应用。
CAN总线的工作原理
CAN总线使用串行数据传输方式,可以1Mb/s的速率在40m的双绞线上运行,也可以使用光缆连接,而且在这种总线上总线协议支持多主控制器。
CAN与I2C总线的许多细节很类似,但也有一些明显的区别。
当CAN总线上的一个节点(站)发送数据时,它以报文形式广播给网络中所有节点。
对每个节点来说,无论数据是否是发给自己的,都对其进行接收。
每组报文开头的11位字符为标识符,定义了报文的优先级,这种报文格式称为面向内容的编址方案。
在同一系统中标识符是唯一的,不可能有两个站发送具有相同标识符的报文。当几个站同时竞争总线读取时,这种配置十分重要。
当一个站要向其它站发送数据时,该站的CPU将要发送的数据和自己的标识符传送给本站的CAN芯片,并处于准备状态;当它收到总线分配时,转为发送报文状态。
CAN芯片将数据根据协议组织成一定的报文格式发出,这时网上的其它站处于接收状态。
每个处于接收状态的站对接收到的报文进行检测,判断这些报文是否是发给自己的,以确定是否接收它。
由于CAN总线是一种面向内容的编址方案,因此很容易建立高水准的控制系统并灵活地进行配置。
我们可以很容易地在CAN总线中加进一些新站而无需在硬件或软件上进行修改。
当所提供的新站是纯数据接收设备时,数据传输协议不要求独立的部分有物理目的地址。
它允许分布过程同步化,即总线上控制器需要测量数据时,可由网上获得,而无须每个控制器都有自己独立的传感器。
参考程序:https://www.cnblogs.com/yjh3524/p/19101108
新建窗体应用程序CANCommunicationFromCSharp,.net framework 4.7.2.将默认的Form1重命名为FormDemoCAN
Nuget中获取 Peak.PCANBasic.NET,安装完成后

添加图表框架的引用 System.Windows.Forms.DataVisualization
关键的框架引用如图:

新建四个辅助类CANMessage、CANSignal、CANSignalParser、SignalDisplay:
文件CANMessage.cs
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CANCommunicationFromCSharp
{
/// <summary>
/// 封装CAN的消息体
/// </summary>
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("-", " ");
}
}
文件CANSignal.cs
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CANCommunicationFromCSharp
{
/// <summary>
/// CAN信号
/// </summary>
public class CANSignal
{
public string Name { get; set; }
public int StartBit { get; set; }
public int Length { get; set; }
/// <summary>
/// 因子,比例。线性方程y=kx+b的k
/// </summary>
public double Factor { get; set; } = 1.0;
/// <summary>
/// 偏移量。线性方程y=kx+b的b
/// </summary>
public double Offset { get; set; }
}
}
文件CANSignalParser.cs
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CANCommunicationFromCSharp
{
/// <summary>
/// CAN数据解析
/// </summary>
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
});
}
/// <summary>
/// 解析数据,返回 键值对字典
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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);
// 转换为物理值y=factor*x+offset 即线性方程y=kx+b
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;
}
}
}
文件SignalDisplay.cs
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CANCommunicationFromCSharp
{
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; }
}
}
窗体设计器FormDemoCAN如图:

窗体FormDemoCAN.Designer.cs设计器程序
文件FormDemoCAN.Designer.cs
cs
namespace CANCommunicationFromCSharp
{
partial class FormDemoCAN
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea6 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
System.Windows.Forms.DataVisualization.Charting.Legend legend6 = new System.Windows.Forms.DataVisualization.Charting.Legend();
System.Windows.Forms.DataVisualization.Charting.Series series6 = new System.Windows.Forms.DataVisualization.Charting.Series();
this.chartSignals = new System.Windows.Forms.DataVisualization.Charting.Chart();
this.dgvMessages = new System.Windows.Forms.DataGridView();
this.dgvSignals = new System.Windows.Forms.DataGridView();
this.lstAvailableSignals = new System.Windows.Forms.ListBox();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.cmbDeviceType = new System.Windows.Forms.ComboBox();
this.cmbBaudRate = new System.Windows.Forms.ComboBox();
this.label4 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.btnConnect = new System.Windows.Forms.Button();
this.btnSend = new System.Windows.Forms.Button();
this.btnStartLog = new System.Windows.Forms.Button();
this.btnAddSignal = new System.Windows.Forms.Button();
this.btnRemoveSignal = new System.Windows.Forms.Button();
this.label6 = new System.Windows.Forms.Label();
this.lstMonitoredSignals = new System.Windows.Forms.ListBox();
this.lblConnectionStatus = new System.Windows.Forms.Label();
this.txtLog = new System.Windows.Forms.RichTextBox();
this.txtID = new System.Windows.Forms.TextBox();
this.txtData = new System.Windows.Forms.TextBox();
this.label7 = new System.Windows.Forms.Label();
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripStatusLabel = new System.Windows.Forms.ToolStripLabel();
this.btnClearMessage = new System.Windows.Forms.Button();
this.btnClearChart = new System.Windows.Forms.Button();
this.btnExportData = new System.Windows.Forms.Button();
this.label8 = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.chartSignals)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.dgvMessages)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.dgvSignals)).BeginInit();
this.toolStrip1.SuspendLayout();
this.SuspendLayout();
//
// chartSignals
//
chartArea6.Name = "ChartArea1";
this.chartSignals.ChartAreas.Add(chartArea6);
legend6.Name = "Legend1";
this.chartSignals.Legends.Add(legend6);
this.chartSignals.Location = new System.Drawing.Point(853, 180);
this.chartSignals.Name = "chartSignals";
series6.ChartArea = "ChartArea1";
series6.Legend = "Legend1";
series6.Name = "Series1";
this.chartSignals.Series.Add(series6);
this.chartSignals.Size = new System.Drawing.Size(300, 345);
this.chartSignals.TabIndex = 0;
this.chartSignals.Text = "chart1";
//
// dgvMessages
//
this.dgvMessages.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvMessages.Location = new System.Drawing.Point(12, 49);
this.dgvMessages.Name = "dgvMessages";
this.dgvMessages.RowTemplate.Height = 23;
this.dgvMessages.Size = new System.Drawing.Size(530, 150);
this.dgvMessages.TabIndex = 1;
//
// dgvSignals
//
this.dgvSignals.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvSignals.Location = new System.Drawing.Point(12, 237);
this.dgvSignals.Name = "dgvSignals";
this.dgvSignals.RowTemplate.Height = 23;
this.dgvSignals.Size = new System.Drawing.Size(400, 157);
this.dgvSignals.TabIndex = 2;
//
// lstAvailableSignals
//
this.lstAvailableSignals.FormattingEnabled = true;
this.lstAvailableSignals.ItemHeight = 12;
this.lstAvailableSignals.Location = new System.Drawing.Point(424, 227);
this.lstAvailableSignals.Name = "lstAvailableSignals";
this.lstAvailableSignals.Size = new System.Drawing.Size(147, 148);
this.lstAvailableSignals.TabIndex = 3;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(442, 204);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(65, 12);
this.label1.TabIndex = 4;
this.label1.Text = "可选信号:";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(10, 26);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(65, 12);
this.label2.TabIndex = 5;
this.label2.Text = "消息网格:";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(12, 211);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(65, 12);
this.label3.TabIndex = 6;
this.label3.Text = "信号网格:";
//
// cmbDeviceType
//
this.cmbDeviceType.FormattingEnabled = true;
this.cmbDeviceType.Location = new System.Drawing.Point(658, 18);
this.cmbDeviceType.Name = "cmbDeviceType";
this.cmbDeviceType.Size = new System.Drawing.Size(184, 20);
this.cmbDeviceType.TabIndex = 7;
//
// cmbBaudRate
//
this.cmbBaudRate.FormattingEnabled = true;
this.cmbBaudRate.Location = new System.Drawing.Point(658, 47);
this.cmbBaudRate.Name = "cmbBaudRate";
this.cmbBaudRate.Size = new System.Drawing.Size(184, 20);
this.cmbBaudRate.TabIndex = 8;
//
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(587, 26);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(65, 12);
this.label4.TabIndex = 9;
this.label4.Text = "设备类型:";
//
// label5
//
this.label5.AutoSize = true;
this.label5.Location = new System.Drawing.Point(587, 55);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(53, 12);
this.label5.TabIndex = 10;
this.label5.Text = "波特率:";
//
// btnConnect
//
this.btnConnect.Location = new System.Drawing.Point(925, 14);
this.btnConnect.Name = "btnConnect";
this.btnConnect.Size = new System.Drawing.Size(75, 23);
this.btnConnect.TabIndex = 11;
this.btnConnect.Text = "连接CAN";
this.btnConnect.UseVisualStyleBackColor = true;
this.btnConnect.Click += new System.EventHandler(this.btnConnect_Click);
//
// btnSend
//
this.btnSend.Location = new System.Drawing.Point(938, 108);
this.btnSend.Name = "btnSend";
this.btnSend.Size = new System.Drawing.Size(75, 23);
this.btnSend.TabIndex = 12;
this.btnSend.Text = "发送";
this.btnSend.UseVisualStyleBackColor = true;
this.btnSend.Click += new System.EventHandler(this.btnSend_Click);
//
// btnStartLog
//
this.btnStartLog.Location = new System.Drawing.Point(925, 61);
this.btnStartLog.Name = "btnStartLog";
this.btnStartLog.Size = new System.Drawing.Size(75, 23);
this.btnStartLog.TabIndex = 13;
this.btnStartLog.Text = "开始记录";
this.btnStartLog.UseVisualStyleBackColor = true;
this.btnStartLog.Click += new System.EventHandler(this.btnStartLog_Click);
//
// btnAddSignal
//
this.btnAddSignal.Location = new System.Drawing.Point(767, 251);
this.btnAddSignal.Name = "btnAddSignal";
this.btnAddSignal.Size = new System.Drawing.Size(75, 23);
this.btnAddSignal.TabIndex = 14;
this.btnAddSignal.Text = "添加信号";
this.btnAddSignal.UseVisualStyleBackColor = true;
this.btnAddSignal.Click += new System.EventHandler(this.btnAddSignal_Click);
//
// btnRemoveSignal
//
this.btnRemoveSignal.Location = new System.Drawing.Point(767, 309);
this.btnRemoveSignal.Name = "btnRemoveSignal";
this.btnRemoveSignal.Size = new System.Drawing.Size(75, 23);
this.btnRemoveSignal.TabIndex = 15;
this.btnRemoveSignal.Text = "移除信号";
this.btnRemoveSignal.UseVisualStyleBackColor = true;
this.btnRemoveSignal.Click += new System.EventHandler(this.btnRemoveSignal_Click);
//
// label6
//
this.label6.AutoSize = true;
this.label6.Location = new System.Drawing.Point(632, 204);
this.label6.Name = "label6";
this.label6.Size = new System.Drawing.Size(65, 12);
this.label6.TabIndex = 17;
this.label6.Text = "监控信号:";
//
// lstMonitoredSignals
//
this.lstMonitoredSignals.FormattingEnabled = true;
this.lstMonitoredSignals.ItemHeight = 12;
this.lstMonitoredSignals.Location = new System.Drawing.Point(614, 227);
this.lstMonitoredSignals.Name = "lstMonitoredSignals";
this.lstMonitoredSignals.Size = new System.Drawing.Size(147, 148);
this.lstMonitoredSignals.TabIndex = 16;
//
// lblConnectionStatus
//
this.lblConnectionStatus.AutoSize = true;
this.lblConnectionStatus.Location = new System.Drawing.Point(1014, 21);
this.lblConnectionStatus.Name = "lblConnectionStatus";
this.lblConnectionStatus.Size = new System.Drawing.Size(53, 12);
this.lblConnectionStatus.TabIndex = 18;
this.lblConnectionStatus.Text = "连接状态";
//
// txtLog
//
this.txtLog.Location = new System.Drawing.Point(12, 413);
this.txtLog.Name = "txtLog";
this.txtLog.Size = new System.Drawing.Size(803, 164);
this.txtLog.TabIndex = 19;
this.txtLog.Text = "";
//
// txtID
//
this.txtID.Location = new System.Drawing.Point(728, 104);
this.txtID.Name = "txtID";
this.txtID.Size = new System.Drawing.Size(186, 21);
this.txtID.TabIndex = 20;
//
// txtData
//
this.txtData.Location = new System.Drawing.Point(627, 140);
this.txtData.Name = "txtData";
this.txtData.Size = new System.Drawing.Size(287, 21);
this.txtData.TabIndex = 21;
//
// label7
//
this.label7.AutoSize = true;
this.label7.Location = new System.Drawing.Point(675, 113);
this.label7.Name = "label7";
this.label7.Size = new System.Drawing.Size(47, 12);
this.label7.TabIndex = 22;
this.label7.Text = "CAN ID:";
//
// toolStrip1
//
this.toolStrip1.Dock = System.Windows.Forms.DockStyle.Bottom;
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripStatusLabel});
this.toolStrip1.Location = new System.Drawing.Point(0, 580);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(1170, 25);
this.toolStrip1.TabIndex = 23;
this.toolStrip1.Text = "toolStrip1";
//
// toolStripStatusLabel
//
this.toolStripStatusLabel.Name = "toolStripStatusLabel";
this.toolStripStatusLabel.Size = new System.Drawing.Size(96, 22);
this.toolStripStatusLabel.Text = "toolStripLabel1";
//
// btnClearMessage
//
this.btnClearMessage.Location = new System.Drawing.Point(111, 14);
this.btnClearMessage.Name = "btnClearMessage";
this.btnClearMessage.Size = new System.Drawing.Size(75, 23);
this.btnClearMessage.TabIndex = 24;
this.btnClearMessage.Text = "清除消息";
this.btnClearMessage.UseVisualStyleBackColor = true;
this.btnClearMessage.Click += new System.EventHandler(this.btnClearMessage_Click);
//
// btnClearChart
//
this.btnClearChart.Location = new System.Drawing.Point(978, 541);
this.btnClearChart.Name = "btnClearChart";
this.btnClearChart.Size = new System.Drawing.Size(75, 23);
this.btnClearChart.TabIndex = 25;
this.btnClearChart.Text = "清除图表";
this.btnClearChart.UseVisualStyleBackColor = true;
this.btnClearChart.Click += new System.EventHandler(this.btnClearChart_Click);
//
// btnExportData
//
this.btnExportData.Location = new System.Drawing.Point(1029, 61);
this.btnExportData.Name = "btnExportData";
this.btnExportData.Size = new System.Drawing.Size(75, 23);
this.btnExportData.TabIndex = 26;
this.btnExportData.Text = "导出数据";
this.btnExportData.UseVisualStyleBackColor = true;
this.btnExportData.Click += new System.EventHandler(this.btnExportData_Click);
//
// label8
//
this.label8.AutoSize = true;
this.label8.Location = new System.Drawing.Point(558, 144);
this.label8.Name = "label8";
this.label8.Size = new System.Drawing.Size(65, 12);
this.label8.TabIndex = 27;
this.label8.Text = "Data(Hex):";
//
// FormDemoCAN
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1170, 605);
this.Controls.Add(this.label8);
this.Controls.Add(this.btnExportData);
this.Controls.Add(this.btnClearChart);
this.Controls.Add(this.btnClearMessage);
this.Controls.Add(this.toolStrip1);
this.Controls.Add(this.label7);
this.Controls.Add(this.txtData);
this.Controls.Add(this.txtID);
this.Controls.Add(this.txtLog);
this.Controls.Add(this.lblConnectionStatus);
this.Controls.Add(this.label6);
this.Controls.Add(this.lstMonitoredSignals);
this.Controls.Add(this.btnRemoveSignal);
this.Controls.Add(this.btnAddSignal);
this.Controls.Add(this.btnStartLog);
this.Controls.Add(this.btnSend);
this.Controls.Add(this.btnConnect);
this.Controls.Add(this.label5);
this.Controls.Add(this.label4);
this.Controls.Add(this.cmbBaudRate);
this.Controls.Add(this.cmbDeviceType);
this.Controls.Add(this.label3);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.Controls.Add(this.lstAvailableSignals);
this.Controls.Add(this.dgvSignals);
this.Controls.Add(this.dgvMessages);
this.Controls.Add(this.chartSignals);
this.Name = "FormDemoCAN";
this.Text = "CAN通讯【Controller Area Network:控制器局域网:广播式通讯】-Peak.PCANBasic.NET-斯内科";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormDemoCAN_FormClosing);
((System.ComponentModel.ISupportInitialize)(this.chartSignals)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.dgvMessages)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.dgvSignals)).EndInit();
this.toolStrip1.ResumeLayout(false);
this.toolStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.DataVisualization.Charting.Chart chartSignals;
private System.Windows.Forms.DataGridView dgvMessages;
private System.Windows.Forms.DataGridView dgvSignals;
private System.Windows.Forms.ListBox lstAvailableSignals;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.ComboBox cmbDeviceType;
private System.Windows.Forms.ComboBox cmbBaudRate;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.Button btnConnect;
private System.Windows.Forms.Button btnSend;
private System.Windows.Forms.Button btnStartLog;
private System.Windows.Forms.Button btnAddSignal;
private System.Windows.Forms.Button btnRemoveSignal;
private System.Windows.Forms.Label label6;
private System.Windows.Forms.ListBox lstMonitoredSignals;
private System.Windows.Forms.Label lblConnectionStatus;
private System.Windows.Forms.RichTextBox txtLog;
private System.Windows.Forms.TextBox txtID;
private System.Windows.Forms.TextBox txtData;
private System.Windows.Forms.Label label7;
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripLabel toolStripStatusLabel;
private System.Windows.Forms.Button btnClearMessage;
private System.Windows.Forms.Button btnClearChart;
private System.Windows.Forms.Button btnExportData;
private System.Windows.Forms.Label label8;
}
}
FormDemoCAN相关CAN测试程序
文件FormDemoCAN.cs
cs
using Peak.Can.Basic.BackwardCompatibility;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace CANCommunicationFromCSharp
{
public partial class FormDemoCAN : Form
{
#region CAN接口定义
private const TPCANBaudrate DefaultBaudrate = TPCANBaudrate.PCAN_BAUD_500K;
private const TPCANType HardwareType = TPCANType.PCAN_TYPE_ISA;
private const ushort IOAddress = 0;
ushort Channel = 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 FormDemoCAN()
{
InitializeComponent();
//初始化信号解析器、图表、数据网格、信号列表、设备列表
InitializeComponents();
}
/// <summary>
/// 初始化CAN相关组件以及图表等
/// 初始化信号解析器、图表、数据网格、信号列表、设备列表
/// </summary>
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.Clear();
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(Channel, baudrate, HardwareType, IOAddress, 0);
//TPCANStatus status = PCANBasic.Initialize(
//HardwareType,
//IOAddress,
//baudrate,
//TPCANMode.PCAN_MODE_STANDARD);
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(Channel);// (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);
TPCANStatus status = PCANBasic.Read(Channel, 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;
if (_messageHistory.Count <= 100)
{
dgvMessages.DataSource = _messageHistory;
}
else //超过100条消息是,只考虑最新的100条
{
dgvMessages.DataSource = _messageHistory.Skip(_messageHistory.Count - 100).Take(100).ToList();//_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);
TPCANStatus status = PCANBasic.Write(Channel, 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;
if (_messageHistory.Count <= 100)
{
dgvMessages.DataSource = _messageHistory;
}
else //超过100条消息是,只考虑最新的100条
{
dgvMessages.DataSource = _messageHistory.Skip(_messageHistory.Count - 100).Take(100).ToList();//_messageHistory.TakeLast(100).ToList();
}
//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.FindByName(signalName) != null)
{
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 FormDemoCAN_FormClosing(object sender, FormClosingEventArgs e)
{
DisconnectFromCAN();
if (_isLogging && _logWriter != null)
{
_logWriter.Close();
}
}
private void btnClearMessage_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);
}
}
}
}
}
运行如图:
