C#进行CAN【控制器局域网】通讯

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);
                }
            }
        }
    }
}

运行如图:

相关推荐
张人玉3 小时前
C#WPF——MVVM框架编写管理系统所遇到的问题
开发语言·c#·wpf·mvvm框架
马达加斯加D3 小时前
C# --- 如何写UT
前端·c#·log4j
Charles_go3 小时前
C#中级39、什么是依赖注入设计模式
java·设计模式·c#
eggcode4 小时前
C#开源库ACadSharp将Dwg转Dxf
c#·dxf·dwg
拼好饭和她皆失5 小时前
C#学习入门
开发语言·学习·c#
小小编程能手7 小时前
大小端字节序
c#
冒泡P9 小时前
【Unity】TextMeshPro富文本中使用精灵图集
ui·unity·c#·游戏引擎
世洋Blog9 小时前
开发思想-组合模式和接口多态的一点思考
c#·组合模式
梵克之泪11 小时前
【号码分离】从Excel表格、文本、word文档混乱文字中提取分离11位手机号出来,基于WPF的实现方案
开发语言·ui·c#