Bläddra i källkod

20260514001 CAN模拟从站设备,状态机和心跳完成,从站接收数据正常运行,发送数据异常。

向羽 孟 1 månad sedan
förälder
incheckning
82bbbda016

+ 9 - 1
CanOpenSlaveTest/App.config

@@ -1,6 +1,14 @@
-<?xml version="1.0" encoding="utf-8" ?>
+<?xml version="1.0" encoding="utf-8" ?>
 <configuration>
     <startup> 
         <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
     </startup>
+    <runtime>
+        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+            <dependentAssembly>
+                <assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral" />
+                <bindingRedirect oldVersion="0.0.0.0-2.0.3.0" newVersion="1.0.119.0" />
+            </dependentAssembly>
+        </assemblyBinding>
+    </runtime>
 </configuration>

+ 12 - 1
CanOpenSlaveTest/CanLibraryClass.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.InteropServices;
@@ -7,6 +7,17 @@ using System.Threading.Tasks;
 
 namespace CanTest
 {
+    /// <summary>
+    /// CAN波特率枚举
+    /// </summary>
+    public enum CanBaudRate
+    {
+        BaudRate_1M = 0,    // 1Mbps
+        BaudRate_500K = 1,  // 500kbps
+        BaudRate_250K = 2,  // 250kbps
+        BaudRate_125K = 3   // 125kbps
+    }
+
     unsafe public struct VCI_BOARD_INFO//使用不安全代码
     {
         public UInt16 hw_Version;

+ 143 - 105
CanOpenSlaveTest/CanOpenManager.cs

@@ -14,22 +14,25 @@ namespace CCDCount.DLL.CanBus
     public class CanOpenManager : IDisposable
     {
         #region 私有字段
-        
+
         private UInt32 m_deviceType = 4;  // USBCAN2
         private UInt32 m_deviceIndex = 0;
         private UInt32 m_canIndex = 0;
         private bool m_isOpened = false;
         private bool m_isStarted = false;
         
+        // CAN ID字节序配置
+        private bool m_useByteSwapForId = false;  // 是否对CAN ID进行字节交换
+
         // CANopen COB-ID定义
         private const uint NMT_COB_ID = 0x000;      // 网络管理
         private const uint SYNC_COB_ID = 0x080;     // 同步对象
         private const uint EMCY_COB_ID_BASE = 0x081; // 紧急对象基础ID
-        
+
         // SDO COB-ID
         private const uint SDO_REQUEST_BASE = 0x600;  // SDO请求基础ID
         private const uint SDO_RESPONSE_BASE = 0x580; // SDO响应基础ID
-        
+
         // PDO COB-ID
         private const uint TPDO1_BASE = 0x180;
         private const uint TPDO2_BASE = 0x280;
@@ -39,11 +42,11 @@ namespace CCDCount.DLL.CanBus
         private const uint RPDO2_BASE = 0x300;
         private const uint RPDO3_BASE = 0x400;
         private const uint RPDO4_BASE = 0x500;
-        
+
         #endregion
-        
+
         #region 构造函数和初始化
-        
+
         /// <summary>
         /// 构造函数
         /// </summary>
@@ -56,7 +59,7 @@ namespace CCDCount.DLL.CanBus
             m_deviceIndex = deviceIndex;
             m_canIndex = canIndex;
         }
-        
+
         /// <summary>
         /// 打开CAN设备并初始化
         /// </summary>
@@ -69,7 +72,7 @@ namespace CCDCount.DLL.CanBus
                 Console.WriteLine("CAN设备已打开");
                 return true;
             }
-            
+
             // 1. 打开设备
             UInt32 result = CanLibraryClass.VCI_OpenDevice(m_deviceType, m_deviceIndex, 0);
             if (result == 0)
@@ -79,17 +82,17 @@ namespace CCDCount.DLL.CanBus
             }
             m_isOpened = true;
             Console.WriteLine("CAN设备打开成功");
-            
+
             // 2. 初始化CAN通道
             VCI_INIT_CONFIG config = new VCI_INIT_CONFIG();
             config.AccCode = 0x00000000;
             config.AccMask = 0xFFFFFFFF;
             config.Filter = 1;  // 接收所有帧
             config.Mode = 0;    // 正常模式
-            
+
             // 设置波特率参数
             SetBaudRate(ref config, baudRate);
-            
+
             result = CanLibraryClass.VCI_InitCAN(m_deviceType, m_deviceIndex, m_canIndex, ref config);
             if (result == 0)
             {
@@ -98,7 +101,7 @@ namespace CCDCount.DLL.CanBus
                 return false;
             }
             Console.WriteLine($"CAN通道初始化成功,波特率:{baudRate}");
-            
+
             // 3. 启动CAN通道
             result = CanLibraryClass.VCI_StartCAN(m_deviceType, m_deviceIndex, m_canIndex);
             if (result == 0)
@@ -109,10 +112,10 @@ namespace CCDCount.DLL.CanBus
             }
             m_isStarted = true;
             Console.WriteLine("CAN通道启动成功");
-            
+
             return true;
         }
-        
+
         private void SetBaudRate(ref VCI_INIT_CONFIG config, CanBaudRate baudRate)
         {
             switch (baudRate)
@@ -139,11 +142,11 @@ namespace CCDCount.DLL.CanBus
                     break;
             }
         }
-        
+
         #endregion
-        
+
         #region 底层CAN帧发送和接收
-        
+
         /// <summary>
         /// 发送原始CAN帧
         /// </summary>
@@ -159,51 +162,55 @@ namespace CCDCount.DLL.CanBus
                 Console.WriteLine("CAN通道未启动");
                 return false;
             }
-            
+
             if (data == null || data.Length > 8)
             {
                 Console.WriteLine("SendCanFrame: 数据长度无效(必须0-8字节)");
                 return false;
             }
-            
+
             VCI_CAN_OBJ sendObj = new VCI_CAN_OBJ();
-            sendObj.ID = cobId;
+            
+            // 根据配置决定是否进行字节序转换
+            sendObj.ID = m_useByteSwapForId ? SwapUint32(cobId) : cobId;
+            
             sendObj.ExternFlag = isExtended ? (byte)1 : (byte)0;
             sendObj.RemoteFlag = isRemote ? (byte)1 : (byte)0;
             sendObj.SendType = 0;
             sendObj.TimeFlag = 0;
             sendObj.DataLen = (byte)data.Length;
-            
+
             // 复制数据
             for (int i = 0; i < data.Length; i++)
             {
                 sendObj.Data[i] = data[i];
             }
-            
+
             // 填充剩余字节
             for (int i = data.Length; i < 8; i++)
             {
                 sendObj.Data[i] = 0;
             }
-            
+
             UInt32 result = CanLibraryClass.VCI_Transmit(m_deviceType, m_deviceIndex, m_canIndex, ref sendObj, 1);
-            
+
             if (result == 0)
             {
                 Console.WriteLine($"发送CAN帧失败 - COB-ID: 0x{cobId:X3}");
                 return false;
             }
-            
-            string logStr = $"发送CAN帧 - COB-ID: 0x{cobId:X3}, Len: {data.Length}, Data: ";
-            for (int i = 0; i < data.Length; i++)
+            if (cobId != 1793)
             {
-                logStr += $"{data[i]:X2} ";
+                string logStr = $"发送CAN帧 - COB-ID: 0x{cobId:X3}, Len: {data.Length}, Data: ";
+                for (int i = 0; i < data.Length; i++)
+                {
+                    logStr += $"{data[i]:X2} ";
+                }
+                Console.WriteLine(logStr);
             }
-            Console.WriteLine(logStr);
-            
             return true;
         }
-        
+
         /// <summary>
         /// 接收CAN帧
         /// </summary>
@@ -216,45 +223,87 @@ namespace CCDCount.DLL.CanBus
                 Console.WriteLine("CAN通道未启动");
                 return new List<CanOpenFrame>();
             }
-            
+
             List<CanOpenFrame> receivedFrames = new List<CanOpenFrame>();
             VCI_CAN_OBJ[] receiveBuffer = new VCI_CAN_OBJ[2500];
-            
+
             UInt32 count = CanLibraryClass.VCI_Receive(
-                m_deviceType, 
-                m_deviceIndex, 
-                m_canIndex, 
-                ref receiveBuffer[0], 
-                2500, 
+                m_deviceType,
+                m_deviceIndex,
+                m_canIndex,
+                ref receiveBuffer[0],
+                2500,
                 timeoutMs);
-            
+
             if (count == 0xFFFFFFFF) count = 0;
-            
+
             for (UInt32 i = 0; i < count; i++)
             {
                 CanOpenFrame frame = new CanOpenFrame();
-                frame.CobId = receiveBuffer[i].ID;
+                
+                // 根据配置决定是否进行字节序转换
+                frame.CobId = m_useByteSwapForId ? SwapUint32(receiveBuffer[i].ID) : receiveBuffer[i].ID;
+                
                 frame.IsExtended = receiveBuffer[i].ExternFlag == 1;
                 frame.IsRemote = receiveBuffer[i].RemoteFlag == 1;
                 frame.DataLength = receiveBuffer[i].DataLen;
                 frame.TimeStamp = receiveBuffer[i].TimeStamp;
                 frame.Data = new byte[8];
-                
+
                 for (int j = 0; j < 8; j++)
                 {
                     frame.Data[j] = receiveBuffer[i].Data[j];
                 }
-                
+
                 receivedFrames.Add(frame);
+
+                // 输出每个CAN帧的详细信息,统一格式
+                string dataHex = BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ");
+                Console.WriteLine($"接收CAN帧 - COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {dataHex}");
             }
-            
+
             return receivedFrames;
         }
-        
+
         #endregion
-        
+
+        #region 字节序转换辅助方法
+
+        /// <summary>
+        /// 设置CAN ID是否使用字节交换
+        /// </summary>
+        /// <param name="useByteSwap">true=启用字节交换, false=禁用(默认)</param>
+        public void SetIdByteSwap(bool useByteSwap)
+        {
+            m_useByteSwapForId = useByteSwap;
+            Console.WriteLine($"CAN ID字节交换: {(useByteSwap ? "启用" : "禁用")}");
+        }
+
+        /// <summary>
+        /// 获取当前字节交换配置
+        /// </summary>
+        public bool GetIdByteSwap()
+        {
+            return m_useByteSwapForId;
+        }
+
+        /// <summary>
+        /// 32位无符号整数字节交换 (Little-Endian <-> Big-Endian)
+        /// </summary>
+        /// <param name="value">需要转换的值</param>
+        /// <returns>转换后的值</returns>
+        private uint SwapUint32(uint value)
+        {
+            return ((value & 0x000000FF) << 24) |
+                   ((value & 0x0000FF00) << 8)  |
+                   ((value & 0x00FF0000) >> 8)  |
+                   ((value & 0xFF000000) >> 24);
+        }
+
+        #endregion
+
         #region NMT - 网络管理
-        
+
         /// <summary>
         /// NMT启动节点
         /// </summary>
@@ -264,7 +313,7 @@ namespace CCDCount.DLL.CanBus
             SendCanFrame(NMT_COB_ID, data);
             Console.WriteLine($"NMT启动节点: {nodeId}");
         }
-        
+
         /// <summary>
         /// NMT停止节点
         /// </summary>
@@ -274,7 +323,7 @@ namespace CCDCount.DLL.CanBus
             SendCanFrame(NMT_COB_ID, data);
             Console.WriteLine($"NMT停止节点: {nodeId}");
         }
-        
+
         /// <summary>
         /// NMT进入预操作状态
         /// </summary>
@@ -284,7 +333,7 @@ namespace CCDCount.DLL.CanBus
             SendCanFrame(NMT_COB_ID, data);
             Console.WriteLine($"NMT进入预操作状态: {nodeId}");
         }
-        
+
         /// <summary>
         /// NMT重置节点
         /// </summary>
@@ -294,7 +343,7 @@ namespace CCDCount.DLL.CanBus
             SendCanFrame(NMT_COB_ID, data);
             Console.WriteLine($"NMT重置节点: {nodeId}");
         }
-        
+
         /// <summary>
         /// NMT重置通信
         /// </summary>
@@ -304,11 +353,11 @@ namespace CCDCount.DLL.CanBus
             SendCanFrame(NMT_COB_ID, data);
             Console.WriteLine($"NMT重置通信: {nodeId}");
         }
-        
+
         #endregion
-        
+
         #region SDO - 服务数据对象
-        
+
         /// <summary>
         /// SDO快速读取
         /// </summary>
@@ -323,12 +372,12 @@ namespace CCDCount.DLL.CanBus
             sdoRequest[5] = 0x00;
             sdoRequest[6] = 0x00;
             sdoRequest[7] = 0x00;
-            
+
             uint cobId = (uint)SDO_REQUEST_BASE + nodeId;
             SendCanFrame(cobId, sdoRequest);
             Console.WriteLine($"SDO读取: 节点{nodeId}, 索引0x{index:X4}, 子索引{subIndex}");
         }
-        
+
         /// <summary>
         /// SDO写入(4字节)
         /// </summary>
@@ -343,12 +392,12 @@ namespace CCDCount.DLL.CanBus
             sdoRequest[5] = (byte)((data >> 8) & 0xFF);
             sdoRequest[6] = (byte)((data >> 16) & 0xFF);
             sdoRequest[7] = (byte)((data >> 24) & 0xFF);
-            
+
             uint cobId = SDO_REQUEST_BASE + nodeId;
             SendCanFrame(cobId, sdoRequest);
             Console.WriteLine($"SDO写入: 节点{nodeId}, 索引0x{index:X4}, 子索引{subIndex}, 值0x{data:X8}");
         }
-        
+
         /// <summary>
         /// SDO读取并等待响应
         /// </summary>
@@ -357,7 +406,7 @@ namespace CCDCount.DLL.CanBus
             SdoReadQuick(nodeId, index, subIndex);
             return ReceiveSdoResponse(nodeId, timeoutMs);
         }
-        
+
         /// <summary>
         /// SDO写入并等待确认
         /// </summary>
@@ -365,15 +414,15 @@ namespace CCDCount.DLL.CanBus
         {
             SdoWrite(nodeId, index, subIndex, data);
             var response = ReceiveSdoResponse(nodeId, timeoutMs);
-            
+
             if (response != null && response.Length >= 1)
             {
                 return (response[0] & 0xE0) == 0x60;
             }
-            
+
             return false;
         }
-        
+
         /// <summary>
         /// 接收SDO响应
         /// </summary>
@@ -381,7 +430,7 @@ namespace CCDCount.DLL.CanBus
         {
             uint responseCobId = (uint)SDO_RESPONSE_BASE + nodeId;
             var frames = ReceiveCanFrames(timeoutMs);
-            
+
             foreach (var frame in frames)
             {
                 if (frame.CobId == responseCobId && !frame.IsRemote)
@@ -394,11 +443,11 @@ namespace CCDCount.DLL.CanBus
             Console.WriteLine($"SDO响应超时: 节点{nodeId}");
             return null;
         }
-        
+
         #endregion
-        
+
         #region PDO - 过程数据对象
-        
+
         /// <summary>
         /// 发送SYNC帧
         /// </summary>
@@ -408,7 +457,7 @@ namespace CCDCount.DLL.CanBus
             SendCanFrame(SYNC_COB_ID, syncData);
             Console.WriteLine($"发送SYNC帧{(counter > 0 ? $" (计数器:{counter})" : "")}");
         }
-        
+
         /// <summary>
         /// 发送TPDO
         /// </summary>
@@ -416,12 +465,12 @@ namespace CCDCount.DLL.CanBus
         {
             if (pdoNumber < 1 || pdoNumber > 4)
                 throw new ArgumentException("PDO编号必须在1-4之间");
-            
+
             uint cobId = GetTpdoCobId(pdoNumber, nodeId);
             SendCanFrame(cobId, data);
             Console.WriteLine($"发送TPDO{pdoNumber}: 节点{nodeId}, COB-ID: 0x{cobId:X3}");
         }
-        
+
         /// <summary>
         /// 接收RPDO
         /// </summary>
@@ -429,10 +478,10 @@ namespace CCDCount.DLL.CanBus
         {
             if (pdoNumber < 1 || pdoNumber > 4)
                 throw new ArgumentException("PDO编号必须在1-4之间");
-            
+
             uint cobId = GetRpdoCobId(pdoNumber, nodeId);
             var frames = ReceiveCanFrames(timeoutMs);
-            
+
             foreach (var frame in frames)
             {
                 if (frame.CobId == cobId && !frame.IsRemote)
@@ -443,10 +492,10 @@ namespace CCDCount.DLL.CanBus
                     return validData;
                 }
             }
-            
+
             return null;
         }
-        
+
         /// <summary>
         /// 配置PDO传输类型
         /// </summary>
@@ -455,7 +504,7 @@ namespace CCDCount.DLL.CanBus
             ushort index = (ushort)((ushort)0x1800 + pdoNumber - 1);
             SdoWrite(nodeId, index, 0x02, transmissionType);
         }
-        
+
         private uint GetTpdoCobId(byte pdoNumber, byte nodeId)
         {
             switch (pdoNumber)
@@ -467,7 +516,7 @@ namespace CCDCount.DLL.CanBus
                 default: throw new ArgumentException("无效的PDO编号");
             }
         }
-        
+
         private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
         {
             switch (pdoNumber)
@@ -479,11 +528,11 @@ namespace CCDCount.DLL.CanBus
                 default: throw new ArgumentException("无效的PDO编号");
             }
         }
-        
+
         #endregion
-        
+
         #region EMCY - 紧急消息
-        
+
         /// <summary>
         /// 解析紧急消息
         /// </summary>
@@ -491,18 +540,18 @@ namespace CCDCount.DLL.CanBus
         {
             if (data == null || data.Length < 2)
                 return 0;
-            
+
             ushort errorCode = (ushort)(data[0] | (data[1] << 8));
             byte errorRegister = data[2];
-            
+
             Console.WriteLine($"紧急消息: 错误代码0x{errorCode:X4}, 错误寄存器0x{errorRegister:X2}");
             return errorCode;
         }
-        
+
         #endregion
-        
+
         #region 心跳监测
-        
+
         /// <summary>
         /// 解析心跳消息
         /// </summary>
@@ -510,13 +559,13 @@ namespace CCDCount.DLL.CanBus
         {
             if (data == null || data.Length < 1)
                 return 0xFF;
-            
+
             byte status = data[0];
             string statusStr = GetNodeStatusString(status);
             Console.WriteLine($"节点{nodeId}心跳: 状态0x{status:X2} ({statusStr})");
             return status;
         }
-        
+
         private string GetNodeStatusString(byte status)
         {
             switch (status & 0x7F)
@@ -528,11 +577,11 @@ namespace CCDCount.DLL.CanBus
                 default: return "未知";
             }
         }
-        
+
         #endregion
-        
+
         #region 资源管理
-        
+
         /// <summary>
         /// 关闭CAN设备
         /// </summary>
@@ -543,7 +592,7 @@ namespace CCDCount.DLL.CanBus
                 CanLibraryClass.VCI_ResetCAN(m_deviceType, m_deviceIndex, m_canIndex);
                 m_isStarted = false;
             }
-            
+
             if (m_isOpened)
             {
                 CanLibraryClass.VCI_CloseDevice(m_deviceType, m_deviceIndex);
@@ -551,7 +600,7 @@ namespace CCDCount.DLL.CanBus
                 Console.WriteLine("CAN设备已关闭");
             }
         }
-        
+
         /// <summary>
         /// 释放资源
         /// </summary>
@@ -559,10 +608,10 @@ namespace CCDCount.DLL.CanBus
         {
             Close();
         }
-        
+
         #endregion
     }
-    
+
     /// <summary>
     /// CANopen帧数据结构
     /// </summary>
@@ -574,7 +623,7 @@ namespace CCDCount.DLL.CanBus
         public byte DataLength { get; set; }
         public byte[] Data { get; set; }
         public uint TimeStamp { get; set; }
-        
+
         public override string ToString()
         {
             string dataStr = "";
@@ -585,15 +634,4 @@ namespace CCDCount.DLL.CanBus
             return $"COB-ID: 0x{CobId:X3}, Len: {DataLength}, Data: [{dataStr}]";
         }
     }
-    
-    /// <summary>
-    /// CAN波特率枚举
-    /// </summary>
-    public enum CanBaudRate
-    {
-        BaudRate_1M = 1000000,
-        BaudRate_500K = 500000,
-        BaudRate_250K = 250000,
-        BaudRate_125K = 125000
-    }
-}
+}

+ 139 - 9
CanOpenSlaveTest/CanOpenSlaveDevice.cs

@@ -156,6 +156,10 @@ namespace CCDCount.DLL.CanBus
             // 1001h - Error register
             m_objectDictionary[(ushort)0x1001] = (byte)0x00;
             
+            // 1017h - Producer Heartbeat Time (心跳时间)
+            // 根据CANopen标准,此对象用于配置心跳发送间隔
+            m_objectDictionary[(ushort)0x1017] = (ushort)1000;  // 默认1000ms
+            
             // 1018h - Identity Object
             m_objectDictionary[(ushort)0x1018] = new IdentityObject
             {
@@ -230,7 +234,16 @@ namespace CCDCount.DLL.CanBus
             }
             
             m_isRunning = true;
-            m_currentState = NmtState.PreOperational;
+            m_currentState = NmtState.Initializing;
+            
+            // 默认配置心跳为1000ms
+            ConfigureHeartbeat(1000);
+            
+            // 发送无错误紧急报文,表明设备正常
+            SendNoErrorEmcy();
+            
+            // 等待主站发送NMT命令来控制状态转换
+            Console.WriteLine($"NMT: 节点{m_nodeId}已初始化,等待主站命令...");
             
             // 启动接收线程
             m_cancellationTokenSource = new CancellationTokenSource();
@@ -298,9 +311,13 @@ namespace CCDCount.DLL.CanBus
         /// </summary>
         private void ProcessCanFrame(CanOpenFrame frame)
         {
+            // 记录所有接收到的帧(用于调试)
+            Console.WriteLine($"[接收详细] COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
+            
             // 检查是否是NMT命令 (COB-ID = 0x000)
             if (frame.CobId == (uint)0x000 && frame.DataLength >= 2)
             {
+                Console.WriteLine($"[NMT命令] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
                 ProcessNmtCommand(frame.Data);
                 return;
             }
@@ -308,6 +325,7 @@ namespace CCDCount.DLL.CanBus
             // 检查是否是SYNC (COB-ID = 0x080)
             if (frame.CobId == (uint)0x080)
             {
+                Console.WriteLine($"[SYNC] COB-ID: 0x{frame.CobId:X3}");
                 ProcessSync(frame.Data);
                 return;
             }
@@ -316,6 +334,7 @@ namespace CCDCount.DLL.CanBus
             uint sdoRequestCobId = (uint)0x600 + m_nodeId;
             if (frame.CobId == sdoRequestCobId)
             {
+                Console.WriteLine($"[SDO请求] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
                 ProcessSdoRequest(frame.Data);
                 return;
             }
@@ -325,10 +344,14 @@ namespace CCDCount.DLL.CanBus
             {
                 if (frame.CobId == m_rpdoConfigs[i].CobId && m_rpdoConfigs[i].Enabled)
                 {
+                    Console.WriteLine($"[RPDO{i+1}] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
                     ProcessRpdo((byte)(i + 1), frame.Data, frame.DataLength);
                     return;
                 }
             }
+            
+            // 其他未知帧
+            Console.WriteLine($"[未知帧] COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
         }
         
         #endregion
@@ -349,42 +372,85 @@ namespace CCDCount.DLL.CanBus
             
             NmtState oldState = m_currentState;
             
+            Console.WriteLine($"[NMT详细] 收到命令: 0x{commandSpecifier:X2}, 目标节点: {nodeId}, 当前状态: {m_currentState}");
+            
             switch (commandSpecifier)
             {
                 case 0x01:  // 启动节点
+                    Console.WriteLine($"[NMT详细] 执行启动节点命令");
                     m_currentState = NmtState.Operational;
                     Console.WriteLine($"NMT: 节点{m_nodeId}进入运行状态");
                     break;
                     
                 case 0x02:  // 停止节点
+                    Console.WriteLine($"[NMT详细] 执行停止节点命令");
                     m_currentState = NmtState.Stopped;
                     Console.WriteLine($"NMT: 节点{m_nodeId}进入停止状态");
                     break;
                     
                 case 0x80:  // 进入预操作状态
+                    Console.WriteLine($"[NMT详细] 执行进入预操作状态命令");
                     m_currentState = NmtState.PreOperational;
                     Console.WriteLine($"NMT: 节点{m_nodeId}进入预操作状态");
                     break;
                     
                 case 0x81:  // 重置节点
+                    Console.WriteLine($"[NMT详细] 执行重置节点命令 - 开始初始化流程");
+                    // 执行节点重置逻辑
                     m_currentState = NmtState.Initializing;
-                    Console.WriteLine($"NMT: 节点{m_nodeId}重置");
-                    // TODO: 执行节点重置逻辑
-                    m_currentState = NmtState.PreOperational;
-                    break;
+                    
+                    // 打印当前对象字典状态(用于调试)
+                    Console.WriteLine($"[NMT调试] 当前对象字典关键参数:");
+                    if (m_objectDictionary.ContainsKey((ushort)0x1000))
+                        Console.WriteLine($"  - 设备类型(0x1000): {m_objectDictionary[(ushort)0x1000]}");
+                    if (m_objectDictionary.ContainsKey((ushort)0x1001))
+                        Console.WriteLine($"  - 错误寄存器(0x1001): {m_objectDictionary[(ushort)0x1001]}");
+                    if (m_objectDictionary.ContainsKey((ushort)0x1017))
+                        Console.WriteLine($"  - 心跳时间(0x1017): {m_objectDictionary[(ushort)0x1017]}ms");
+                    if (m_objectDictionary.ContainsKey((ushort)0x1018))
+                    {
+                        var identity = m_objectDictionary[(ushort)0x1018] as IdentityObject;
+                        if (identity != null)
+                            Console.WriteLine($"  - 厂商ID(0x1018.1): 0x{identity.VendorId:X4}");
+                    }
+                    
+                    // 短暂延迟后自动进入预操作状态(模拟初始化过程)
+                    Task.Delay(50).ContinueWith(_ => 
+                    {
+                        // 立即发送一次心跳,确认状态转换
+                        SendHeartbeat();
+
+                        m_currentState = NmtState.PreOperational;
+                        Console.WriteLine($"NMT: 节点{m_nodeId}初始化完成,进入预操作状态");
+                        
+
+                        
+                        OnNmtStateChanged?.Invoke(oldState, m_currentState);
+                    });
+                    return;  // 提前返回,等待异步完成
                     
                 case 0x82:  // 重置通信
+                    Console.WriteLine($"[NMT详细] 执行重置通信命令");
                     m_currentState = NmtState.PreOperational;
                     Console.WriteLine($"NMT: 节点{m_nodeId}通信重置");
                     // TODO: 执行通信重置逻辑
                     break;
+                    
+                default:
+                    Console.WriteLine($"[NMT详细] 未知命令: 0x{commandSpecifier:X2}");
+                    break;
             }
             
             // 触发状态改变事件
             if (oldState != m_currentState)
             {
+                Console.WriteLine($"[NMT详细] 状态改变: {oldState} → {m_currentState}");
                 OnNmtStateChanged?.Invoke(oldState, m_currentState);
             }
+            else
+            {
+                Console.WriteLine($"[NMT详细] 状态未改变: {oldState}");
+            }
         }
         
         #endregion
@@ -452,12 +518,16 @@ namespace CCDCount.DLL.CanBus
         {
             byte[] response = new byte[8];
             
+            Console.WriteLine($"[SDO详细] 收到上传请求: 索引0x{index:X4}, 子索引{subIndex}");
+            
             // 触发自定义事件
             OnSdoReadRequest?.Invoke(m_nodeId, index, subIndex);
             
             // 根据索引返回数据
             if (m_objectDictionary.TryGetValue(index, out object value))
             {
+                Console.WriteLine($"[SDO详细] 找到对象 0x{index:X4}");
+                
                 if (subIndex == 0)
                 {
                     // 返回子索引数量
@@ -469,14 +539,17 @@ namespace CCDCount.DLL.CanBus
                     if (value is IdentityObject identity)
                     {
                         response[4] = 4;  // 4个子条目
+                        Console.WriteLine($"[SDO详细] 返回Identity对象,子条目数: 4");
                     }
                     else if (value is PdoMappingParam mappingParam)
                     {
                         response[4] = mappingParam.NumberOfEntries;
+                        Console.WriteLine($"[SDO详细] 返回PDO映射参数,子条目数: {mappingParam.NumberOfEntries}");
                     }
                     else
                     {
                         response[4] = 0;
+                        Console.WriteLine($"[SDO详细] 返回简单对象,子条目数: 0");
                     }
                 }
                 else
@@ -492,11 +565,19 @@ namespace CCDCount.DLL.CanBus
                     if (index == (ushort)0x1000 && value is uint deviceType)
                     {
                         dataValue = deviceType;
+                        Console.WriteLine($"[SDO详细] 返回设备类型: 0x{dataValue:X8}");
                     }
                     else if (index == (ushort)0x1001 && value is byte errorReg)
                     {
                         response[0] = 0x4F;  // 1字节
                         dataValue = errorReg;
+                        Console.WriteLine($"[SDO详细] 返回错误寄存器: 0x{dataValue:X2}");
+                    }
+                    else if (index == (ushort)0x1017 && value is ushort heartbeatTime)
+                    {
+                        response[0] = 0x4B;  // 2字节
+                        dataValue = heartbeatTime;
+                        Console.WriteLine($"[SDO详细] 返回心跳时间: {heartbeatTime}ms");
                     }
                     else if (index == (ushort)0x1018 && value is IdentityObject identity)
                     {
@@ -508,6 +589,7 @@ namespace CCDCount.DLL.CanBus
                             case 3: dataValue = identity.RevisionNumber; break;
                             default: dataValue = 0; break;
                         }
+                        Console.WriteLine($"[SDO详细] 返回Identity子索引{subIndex}: 0x{dataValue:X8}");
                     }
                     else if ((index >= (ushort)0x1800 && index <= (ushort)0x1803) && value is TpdoCommParam tpdoParam)
                     {
@@ -517,6 +599,7 @@ namespace CCDCount.DLL.CanBus
                             case 2: dataValue = tpdoParam.TransmissionType; break;
                             default: dataValue = 0; break;
                         }
+                        Console.WriteLine($"[SDO详细] 返回TPDO{index - 0x1800 + 1}通信参数子索引{subIndex}: 0x{dataValue:X8}");
                     }
                     else if ((index >= (ushort)0x1400 && index <= (ushort)0x1403) && value is RpdoCommParam rpdoParam)
                     {
@@ -526,6 +609,7 @@ namespace CCDCount.DLL.CanBus
                             case 2: dataValue = rpdoParam.TransmissionType; break;
                             default: dataValue = 0; break;
                         }
+                        Console.WriteLine($"[SDO详细] 返回RPDO{index - 0x1400 + 1}通信参数子索引{subIndex}: 0x{dataValue:X8}");
                     }
                     
                     response[4] = (byte)(dataValue & 0xFF);
@@ -536,11 +620,13 @@ namespace CCDCount.DLL.CanBus
                 
                 // 发送SDO响应
                 uint responseCobId = (uint)0x580 + m_nodeId;
+                Console.WriteLine($"[SDO响应] COB-ID: 0x{responseCobId:X3}, Index: 0x{index:X4}, SubIndex: {subIndex}, Data: {BitConverter.ToString(response).Replace("-", " ")}");
                 m_canManager.SendCanFrame(responseCobId, response);
             }
             else
             {
                 // 对象不存在,发送错误响应
+                Console.WriteLine($"[SDO详细] 对象 0x{index:X4} 不存在,返回错误");
                 response[0] = 0x80;  // 错误响应
                 response[1] = (byte)(index & 0xFF);
                 response[2] = (byte)(index >> 8);
@@ -551,6 +637,7 @@ namespace CCDCount.DLL.CanBus
                 response[7] = 0x00;
                 
                 uint responseCobId = (uint)0x580 + m_nodeId;
+                Console.WriteLine($"[SDO错误响应] COB-ID: 0x{responseCobId:X3}, Index: 0x{index:X4}, SubIndex: {subIndex}");
                 m_canManager.SendCanFrame(responseCobId, response);
             }
         }
@@ -568,7 +655,15 @@ namespace CCDCount.DLL.CanBus
             // 更新对象字典
             if (m_objectDictionary.ContainsKey(index))
             {
-                if (index >= (ushort)0x1800 && index <= (ushort)0x1803 && m_objectDictionary[index] is TpdoCommParam tpdoParam)
+                // 特殊处理:心跳时间对象 (0x1017)
+                if (index == (ushort)0x1017 && subIndex == 0)
+                {
+                    ushort heartbeatTime = (ushort)value;
+                    m_objectDictionary[index] = heartbeatTime;
+                    ConfigureHeartbeat(heartbeatTime);  // 重新配置心跳
+                    Console.WriteLine($"[SDO写入] 心跳时间已更新为: {heartbeatTime}ms");
+                }
+                else if (index >= (ushort)0x1800 && index <= (ushort)0x1803 && m_objectDictionary[index] is TpdoCommParam tpdoParam)
                 {
                     int pdoIndex = index - (ushort)0x1800;
                     if (subIndex == 1)
@@ -651,9 +746,14 @@ namespace CCDCount.DLL.CanBus
             byte[] pdoData = new byte[8];
             
             // 示例: 填充一些测试数据
-            pdoData[0] = (byte)(m_syncCounter & 0xFF);
-            pdoData[1] = (byte)((m_syncCounter >> 8) & 0xFF);
-            
+            pdoData[0] = (byte)(1);
+            pdoData[1] = (byte)(2);
+            pdoData[2] = (byte)(3);
+            pdoData[3] = (byte)(4);
+            pdoData[4] = (byte)(4);
+            pdoData[5] = (byte)(5);
+            pdoData[6] = (byte)(6);
+            pdoData[7] = (byte)(7);
             uint cobId = m_tpdoConfigs[pdoNumber - 1].CobId;
             m_canManager.SendCanFrame(cobId, pdoData);
             
@@ -686,6 +786,12 @@ namespace CCDCount.DLL.CanBus
         {
             m_producerHeartbeatTime = heartbeatTimeMs;
             
+            // 同步更新对象字典中的 1017h 对象
+            if (m_objectDictionary.ContainsKey((ushort)0x1017))
+            {
+                m_objectDictionary[(ushort)0x1017] = heartbeatTimeMs;
+            }
+            
             // 停止旧的定时器
             m_heartbeatTimer?.Dispose();
             
@@ -694,11 +800,18 @@ namespace CCDCount.DLL.CanBus
                 // 创建新的定时器
                 m_heartbeatTimer = new Timer(state =>
                 {
+                    // 只在PreOperational和Operational状态下发送心跳
+                    // Initializing和Stopped状态下不发送心跳
                     if (m_currentState == NmtState.Operational || 
                         m_currentState == NmtState.PreOperational)
                     {
                         SendHeartbeat();
                     }
+                    else
+                    {
+                        // 在其他状态下跳过心跳发送
+                        //Console.WriteLine($"[心跳跳过] 当前状态: {m_currentState}, 不发送心跳");
+                    }
                 }, null, heartbeatTimeMs, heartbeatTimeMs);
                 
                 Console.WriteLine($"心跳已配置: {heartbeatTimeMs}ms");
@@ -737,9 +850,26 @@ namespace CCDCount.DLL.CanBus
             }
             
             uint heartbeatCobId = (uint)0x700 + (uint)m_nodeId;
+            //Console.WriteLine($"[心跳发送] COB-ID: 0x{heartbeatCobId:X3}, Data: {heartbeatData[0]:X2} (状态: {m_currentState})");
             m_canManager.SendCanFrame(heartbeatCobId, heartbeatData);
         }
         
+        /// <summary>
+        /// 发送无错误紧急报文 (EMCY)
+        /// 用于在启动时表明设备无错误状态
+        /// </summary>
+        private void SendNoErrorEmcy()
+        {
+            byte[] emcyData = new byte[8];
+            emcyData[0] = 0x00;  // 错误代码低字节 - 无错误
+            emcyData[1] = 0x00;  // 错误代码高字节
+            emcyData[2] = 0x00;  // 错误寄存器
+            
+            uint emcyCobId = (uint)0x080 + (uint)m_nodeId;
+            Console.WriteLine($"[EMCY] 发送无错误报文 - COB-ID: 0x{emcyCobId:X3}");
+            m_canManager.SendCanFrame(emcyCobId, emcyData);
+        }
+        
         #endregion
         
         #region 辅助方法

+ 0 - 3
CanOpenSlaveTest/CanOpenSlaveTest.csproj

@@ -55,9 +55,6 @@
       <SubType>Form</SubType>
     </Compile>
   </ItemGroup>
-  <ItemGroup>
-    <None Include="README.md" />
-  </ItemGroup>
   <ItemGroup>
     <Content Include="bin\Debug\controlcan.dll">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

+ 0 - 234
CanOpenSlaveTest/FIX_CHINESE_ENCODING.md

@@ -1,234 +0,0 @@
-# TextBox中文显示乱码问题修复说明
-
-## 问题描述
-
-程序中的TextBox控件显示中文时出现乱码,表现为:
-- 中文字符显示为问号(?)
-- 中文字符显示为方框(□)
-- 中文字符显示为其他异常符号
-
-## 根本原因
-
-### 1. 字体不支持中文
-**主要原因**:使用了仅支持英文的字体(如Consolas、Courier New等)
-
-```csharp
-// ❌ 错误:Consolas是英文字体,不支持中文
-txtLog.Font = new Font("Consolas", 9);
-```
-
-### 2. 换行符不正确
-**次要原因**:使用了Unix风格的换行符`\n`而非Windows风格的`\r\n`
-
-```csharp
-// ⚠️ 不推荐:在Windows上应使用\r\n
-txtLog.AppendText(message + "\n");
-```
-
-## 修复方案
-
-### 方案1:使用中文字体(推荐)✅
-
-#### 推荐的中文字体
-| 字体名称 | 特点 | 适用场景 |
-|---------|------|---------|
-| **Microsoft YaHei** (微软雅黑) | 现代、清晰、美观 | 通用场景(首选) |
-| **SimSun** (宋体) | 传统、标准 | 正式文档 |
-| **SimHei** (黑体) | 粗体、醒目 | 标题、强调 |
-| **KaiTi** (楷体) | 书法风格 | 特殊效果 |
-
-#### 修复代码
-```csharp
-// ✅ 正确:使用微软雅黑字体
-txtLog.Font = new Font("Microsoft YaHei", 9);
-txtLog.ForeColor = Color.Black;
-txtLog.BackColor = Color.White;
-```
-
-### 方案2:使用系统默认字体
-
-```csharp
-// ✅ 正确:使用系统默认字体(自动支持中文)
-txtLog.Font = SystemFonts.DefaultFont;
-```
-
-### 方案3:指定字体回退机制
-
-```csharp
-// ✅ 高级:带字体回退机制
-Font chineseFont = null;
-string[] fontNames = { "Microsoft YaHei", "SimSun", "Arial Unicode MS" };
-
-foreach (string fontName in fontNames)
-{
-    try
-    {
-        Font testFont = new Font(fontName, 9);
-        // 检查字体是否可用
-        if (testFont.Name == fontName)
-        {
-            chineseFont = testFont;
-            break;
-        }
-    }
-    catch { }
-}
-
-if (chineseFont == null)
-{
-    chineseFont = new Font(FontFamily.GenericSansSerif, 9);
-}
-
-txtLog.Font = chineseFont;
-```
-
-## 完整修复示例
-
-### 修复前 ❌
-```csharp
-// 日志显示
-txtLog = new TextBox();
-txtLog.Multiline = true;
-txtLog.ScrollBars = ScrollBars.Vertical;
-txtLog.ReadOnly = true;
-txtLog.Location = new Point(10, 190);
-txtLog.Size = new Size(770, 360);
-txtLog.Font = new Font("Consolas", 9);  // ❌ 英文字体
-
-// AppendLog方法
-private void AppendLog(string message)
-{
-    string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
-    txtLog.AppendText($"[{timestamp}] {message}\n");  // ❌ 字符串插值 + Unix换行符
-}
-```
-
-### 修复后 ✅
-```csharp
-// 日志显示
-txtLog = new TextBox();
-txtLog.Multiline = true;
-txtLog.ScrollBars = ScrollBars.Vertical;
-txtLog.ReadOnly = true;
-txtLog.Location = new Point(10, 190);
-txtLog.Size = new Size(770, 360);
-// ✅ 使用支持中文的字体
-txtLog.Font = new Font("Microsoft YaHei", 9);
-txtLog.ForeColor = Color.Black;
-txtLog.BackColor = Color.White;
-
-// AppendLog方法
-private void AppendLog(string message)
-{
-    if (this.InvokeRequired)
-    {
-        this.Invoke(new Action<string>(AppendLog), message);
-        return;
-    }
-    
-    string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
-    // ✅ 使用字符串拼接 + Windows换行符
-    txtLog.AppendText("[" + timestamp + "] " + message + "\r\n");
-    
-    // 自动滚动到底部
-    txtLog.SelectionStart = txtLog.Text.Length;
-    txtLog.ScrollToCaret();
-}
-```
-
-## 其他注意事项
-
-### 1. 确保文件编码正确
-- Visual Studio项目文件应使用 **UTF-8 with BOM** 编码
-- 检查方法:文件 → 高级保存选项 → 编码选择"Unicode (UTF-8 带签名)"
-
-### 2. 避免在设计器中使用复杂表达式
-```csharp
-// ❌ 避免:设计器可能无法正确处理
-txtLog.AppendText($"[{timestamp}] {message}");
-
-// ✅ 推荐:使用简单字符串拼接
-txtLog.AppendText("[" + timestamp + "] " + message);
-```
-
-### 3. 跨线程更新UI
-```csharp
-// ✅ 正确:检查InvokeRequired
-private void AppendLog(string message)
-{
-    if (this.InvokeRequired)
-    {
-        this.Invoke(new Action<string>(AppendLog), message);
-        return;
-    }
-    // 更新UI代码
-}
-```
-
-## 验证方法
-
-### 测试代码
-```csharp
-// 在程序中添加测试日志
-AppendLog("CANopen从站测试程序已启动");
-AppendLog("节点ID: 1");
-AppendLog("中文测试:你好世界!");
-AppendLog("特殊字符:@#$%^&*()");
-AppendLog("混合文本:Node 1 状态正常");
-```
-
-### 预期结果
-```
-[15:30:45.123] CANopen从站测试程序已启动
-[15:30:45.125] 节点ID: 1
-[15:30:45.126] 中文测试:你好世界!
-[15:30:45.127] 特殊字符:@#$%^&*()
-[15:30:45.128] 混合文本:Node 1 状态正常
-```
-
-所有中文字符应清晰显示,无乱码。
-
-## 常见问题
-
-### Q1: 为什么Consolas字体会导致乱码?
-A: Consolas是微软开发的等宽英文字体,不包含中文字符的字形数据,因此无法正确显示中文。
-
-### Q2: 可以使用其他字体吗?
-A: 可以,只要该字体包含中文字符集。推荐使用:
-- Microsoft YaHei(微软雅黑)
-- SimSun(宋体)
-- SimHei(黑体)
-
-### Q3: 为什么有时部分中文显示正常,部分显示乱码?
-A: 可能是因为:
-1. 使用了复合字体(部分字符有字形,部分没有)
-2. 文本编码不一致
-3. 复制粘贴时引入了特殊字符
-
-### Q4: 如何确保在所有Windows版本上都能正常显示中文?
-A: 
-1. 使用系统内置字体(如微软雅黑、宋体)
-2. 避免使用第三方字体
-3. 在Windows 7及以上版本,微软雅黑是标准配置
-
-## 最佳实践总结
-
-✅ **必须做**:
-- 使用支持中文的字体(Microsoft YaHei、SimSun等)
-- 使用`\r\n`作为换行符(Windows平台)
-- 设置明确的ForeColor和BackColor
-
-❌ **避免做**:
-- 使用纯英文字体(Consolas、Courier New等)
-- 使用Unix风格的`\n`换行符
-- 依赖系统自动选择字体
-
-🎯 **推荐做法**:
-- 统一使用Microsoft YaHei字体
-- 在构造函数中设置字体
-- 添加字体回退机制(可选)
-
----
-
-**修复日期**: 2026-05-09  
-**修复内容**: TextBox中文字体设置 + 换行符修正

+ 0 - 99
CanOpenSlaveTest/FIX_DESIGNER_ERROR.md

@@ -1,99 +0,0 @@
-# Windows Forms设计器错误修复说明
-
-## 问题描述
-
-在使用Windows Forms设计器时出现以下错误:
-```
-设计器错误:"CCDCount.DLL.CanBus.CanOpenSlaveDevice"不具有带有 Int32 类型参数的构造函数。
-```
-
-## 根本原因
-
-Windows Forms设计器在序列化窗体时会尝试处理所有成员变量。当遇到以下情况时会报错:
-
-1. **成员变量在设计时初始化**:在 `InitializeComponent()` 方法中创建对象
-2. **对象没有无参构造函数**:`CanOpenSlaveDevice` 只有带参数的构造函数
-3. **设计器无法序列化**:设计器要求所有组件必须支持无参构造或可序列化
-
-## 修复方案
-
-### 核心原则
-- ✅ **运行时逻辑与UI初始化分离**
-- ✅ **使用 DesignMode 属性区分设计时和运行时**
-- ✅ **业务对象在构造函数中创建**
-
-### 修复前 ❌
-
-```csharp
-private void InitializeComponent()
-{
-    // ... UI控件初始化 ...
-    
-    // ❌ 错误:在设计器生成的方法中创建业务对象
-    m_slaveDevice = new CanOpenSlaveDevice(nodeId: 1);
-    
-    // ❌ 错误:注册事件处理
-    m_slaveDevice.OnRpdoReceived += SlaveDevice_OnRpdoReceived;
-    // ...
-}
-```
-
-### 修复后 ✅
-
-```csharp
-public SlaveTestForm()
-{
-    InitializeComponent();
-    
-    // ✅ 正确:在构造函数中创建,并检查DesignMode
-    if (!DesignMode)
-    {
-        m_slaveDevice = new CanOpenSlaveDevice(nodeId: 1);
-        
-        // 注册事件
-        m_slaveDevice.OnRpdoReceived += SlaveDevice_OnRpdoReceived;
-        m_slaveDevice.OnNmtStateChanged += SlaveDevice_OnNmtStateChanged;
-        m_slaveDevice.OnSdoReadRequest += SlaveDevice_OnSdoReadRequest;
-        m_slaveDevice.OnSdoWriteRequest += SlaveDevice_OnSdoWriteRequest;
-        
-        // 初始化日志
-        AppendLog("CANopen从站测试程序已启动");
-        AppendLog("节点ID: " + m_slaveDevice.NodeId.ToString());
-        AppendLog("点击'启动从站'按钮开始测试\n");
-    }
-}
-
-private void InitializeComponent()
-{
-    // ✅ 仅包含UI控件的初始化代码
-    this.Text = "CANopen从站设备测试";
-    // ... 其他UI控件初始化 ...
-}
-```
-
-## DesignMode 属性说明
-
-### 作用
-`DesignMode` 是 `Component` 类的属性,用于判断当前是否处于设计器模式。
-
-### 返回值
-- `true`:正在Visual Studio设计器中查看窗体
-- `false`:程序正在运行时执行
-
-### 使用场景
-```csharp
-if (!DesignMode)
-{
-    // 仅在运行时执行的代码
-    // - 创建业务对象
-    // - 连接数据库
-    // - 启动定时器
-    // - 注册事件
-}
-```
-
-## 最佳实践
-
-### 1. 职责分离
-```csharp
-// ✅ InitializeComponen

+ 0 - 179
CanOpenSlaveTest/FIX_TYPE_CONVERSION.md

@@ -1,179 +0,0 @@
-# 类型转换问题修复说明
-
-## 问题描述
-
-在编译CANopen从站设备代码时,出现多处"无法将类型'int'隐式转换为'uint'"的编译错误。
-
-## 根本原因
-
-C#中十六进制字面量(如 `0x1000`)默认是 `int` 类型,但在以下场景中需要 `uint` 或 `ushort` 类型:
-
-1. **字典键值**:`Dictionary<ushort, object>` 的键需要 `ushort` 类型
-2. **COB-ID计算**:返回类型为 `uint`,但使用了 `int` 类型的常量
-3. **类型比较**:`ushort` 变量与 `int` 常量比较时需要显式转换
-4. **算术运算**:`uint` 变量与 `int` 常量相加时需要显式转换
-
-## 修复方案
-
-### 1. 对象字典初始化 (CanOpenSlaveDevice.cs)
-
-**修复前:**
-```csharp
-m_objectDictionary[0x1000] = 0x00000000;  // 错误:int不能隐式转换为ushort
-```
-
-**修复后:**
-```csharp
-m_objectDictionary[(ushort)0x1000] = (uint)0x00000000;  // 正确:显式转换
-```
-
-### 2. SDO上传处理中的索引比较 (CanOpenSlaveDevice.cs)
-
-**修复前:**
-```csharp
-if (index == 0x1000 && value is uint deviceType)  // 错误:ushort与int比较
-```
-
-**修复后:**
-```csharp
-if (index == (ushort)0x1000 && value is uint deviceType)  // 正确:显式转换
-```
-
-### 3. COB-ID计算函数 (CanOpenSlaveDevice.cs)
-
-**修复前:**
-```csharp
-private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
-{
-    switch (pdoNumber)
-    {
-        case 1: return 0x200 + nodeId;  // 错误:int + byte = int,不能隐式转为uint
-```
-
-**修复后:**
-```csharp
-private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
-{
-    switch (pdoNumber)
-    {
-        case 1: return (uint)0x200 + nodeId;  // 正确:uint + byte = uint
-```
-
-### 4. 心跳COB-ID计算 (CanOpenSlaveDevice.cs)
-
-**修复前:**
-```csharp
-uint heartbeatCobId = 0x700 + m_nodeId;  // 错误
-```
-
-**修复后:**
-```csharp
-uint heartbeatCobId = (uint)0x700 + m_nodeId;  // 正确
-```
-
-### 5. CAN帧处理中的COB-ID比较 (CanOpenSlaveDevice.cs)
-
-**修复前:**
-```csharp
-if (frame.CobId == 0x000 && frame.DataLength >= 2)  // 错误:uint与int比较
-uint sdoRequestCobId = 0x600 + m_nodeId;  // 错误
-```
-
-**修复后:**
-```csharp
-if (frame.CobId == (uint)0x000 && frame.DataLength >= 2)  // 正确
-uint sdoRequestCobId = (uint)0x600 + m_nodeId;  // 正确
-```
-
-### 6. CanOpenManager中的COB-ID计算 (CanOpenManager.cs)
-
-**修复前:**
-```csharp
-ushort index = (ushort)(0x1800 + pdoNumber - 1);  // 可能溢出
-uint cobId = SDO_REQUEST_BASE + nodeId;  // 错误
-uint responseCobId = SDO_RESPONSE_BASE + nodeId;  // 错误
-```
-
-**修复后:**
-```csharp
-ushort index = (ushort)((ushort)0x1800 + pdoNumber - 1);  // 安全转换
-uint cobId = (uint)SDO_REQUEST_BASE + nodeId;  // 正确
-uint responseCobId = (uint)SDO_RESPONSE_BASE + nodeId;  // 正确
-```
-
-## 修复文件清单
-
-1. ✅ `CanOpenSlaveDevice.cs` - 主要修复文件
-   - 对象字典初始化
-   - SDO上传/下载处理
-   - COB-ID计算函数
-   - CAN帧处理逻辑
-   - 心跳发送
-
-2. ✅ `CanOpenManager.cs` - 次要修复文件
-   - PDO配置函数
-   - SDO请求/响应
-   - COB-ID计算
-
-## C#类型转换规则参考
-
-### 隐式转换(自动)
-- `byte` → `ushort` → `uint` → `ulong`
-- `sbyte` → `short` → `int` → `long`
-- `char` → `ushort` → `int` → `uint` → `ulong`
-
-### 需要显式转换的情况
-- `int` → `uint`:需要 `(uint)` 强制转换
-- `int` → `ushort`:需要 `(ushort)` 强制转换
-- 大类型 → 小类型:可能丢失数据,必须显式转换
-
-### 算术运算规则
-```csharp
-int + byte = int      // byte提升为int
-uint + byte = uint    // byte提升为uint
-ushort + int = int    // ushort提升为int
-(uint)0x100 + byte = uint  // 显式指定uint
-```
-
-## 最佳实践
-
-1. **使用显式转换**:当涉及不同整数类型时,始终使用显式转换
-2. **添加后缀**:可以使用 `U` 后缀表示uint,如 `0x1000U`
-3. **保持一致性**:在整个项目中保持类型转换风格一致
-4. **避免溢出**:在进行算术运算前先确保类型匹配
-
-### 推荐写法对比
-
-```csharp
-// ❌ 不推荐:依赖隐式转换,容易出错
-uint value = 0x1000 + nodeId;
-
-// ✅ 推荐方式1:显式转换
-uint value = (uint)0x1000 + nodeId;
-
-// ✅ 推荐方式2:使用U后缀
-uint value = 0x1000U + nodeId;
-
-// ✅ 推荐方式3:声明为uint常量
-const uint BASE_ID = 0x1000;
-uint value = BASE_ID + nodeId;
-```
-
-## 验证结果
-
-✅ 所有编译错误已修复  
-✅ 代码可以正常编译  
-✅ 类型安全性得到保证  
-✅ 没有潜在的数据丢失风险  
-
-## 测试建议
-
-1. 编译项目确认无错误
-2. 运行单元测试验证COB-ID计算正确性
-3. 使用CAN分析仪验证实际通信
-4. 测试各种节点ID(1-127)的配置
-
----
-
-**修复日期**: 2026-05-09  
-**修复人员**: Lingma (灵码)

+ 0 - 302
CanOpenSlaveTest/IMPLEMENTATION_SUMMARY.md

@@ -1,302 +0,0 @@
-# CANopen从站设备实现总结
-
-## 📦 项目概述
-
-本项目基于Nameless.eds文件实现了一个完整的CANopen从站设备,使用创芯科技USBCAN2适配器作为硬件平台。
-
-## 🎯 实现目标
-
-根据EDS文件要求,实现了以下功能:
-
-### ✅ 已完成功能
-
-1. **NMT网络管理**
-   - ✓ 启动节点 (Start)
-   - ✓ 停止节点 (Stop)
-   - ✓ 预操作状态 (Pre-Operational)
-   - ✓ 重置节点 (Reset Node)
-   - ✓ 重置通信 (Reset Communication)
-
-2. **PDO通信**
-   - ✓ 4个接收PDO (RPDO1-4)
-     - COB-ID: 0x200+nodeId ~ 0x500+nodeId
-   - ✓ 4个发送PDO (TPDO1-4)
-     - COB-ID: 0x180+nodeId ~ 0x480+nodeId
-   - ✓ 事件驱动传输
-   - ✓ SYNC同步传输支持
-
-3. **SDO服务**
-   - ✓ SDO上传(读对象字典)
-   - ✓ SDO下载(写对象字典)
-   - ✓ 快速访问(≤4字节)
-   - ✓ 错误响应
-
-4. **心跳机制**
-   - ✓ 可配置心跳时间
-   - ✓ 自动状态报告
-   - ✓ 支持禁用
-
-5. **SYNC同步**
-   - ✓ SYNC帧接收处理
-   - ✓ 同步触发TPDO发送
-
-6. **对象字典**
-   - ✓ 0x1000 - Device type
-   - ✓ 0x1001 - Error register
-   - ✓ 0x1018 - Identity object
-   - ✓ 0x1400-0x1403 - RPDO通信参数
-   - ✓ 0x1600-0x1603 - RPDO映射参数
-   - ✓ 0x1800-0x1803 - TPDO通信参数
-   - ✓ 0x1A00-0x1A03 - TPDO映射参数
-
-## 📁 文件结构
-
-```
-CanOpenSlaveTest/
-├── CanLibraryClass.cs          # CAN卡底层驱动接口
-├── CanOpenManager.cs           # CANopen协议管理器(基础层)
-├── CanOpenSlaveDevice.cs       # CANopen从站设备(核心实现)⭐
-├── EdsValidator.cs             # EDS文件验证器
-├── Program.cs                  # 应用程序入口
-├── SlaveTestForm.cs            # Windows Forms测试界面 ⭐
-├── CanOpenSlaveTest.csproj     # 项目配置文件
-├── README.md                   # 完整使用文档
-├── QUICKSTART.md              # 快速开始指南
-└── bin/Debug/
-    ├── controlcan.dll         # CAN卡驱动库
-    └── Nameless.eds           # EDS配置文件
-```
-
-## 🔑 核心类说明
-
-### 1. CanOpenSlaveDevice(核心类)
-
-**位置**: `CanOpenSlaveDevice.cs`
-
-**主要功能**:
-- 完整的CANopen从站协议栈实现
-- 事件驱动的异步处理
-- 自动COB-ID计算和管理
-- 线程安全的帧接收处理
-
-**关键方法**:
-```csharp
-// 启动/停止
-bool Start(CanBaudRate baudRate)
-void Stop()
-
-// PDO操作
-void SendTpdo(byte pdoNumber)
-void SendAllTpdos()
-
-// 心跳配置
-void ConfigureHeartbeat(ushort heartbeatTimeMs)
-```
-
-**关键事件**:
-```csharp
-event Action<byte, byte, byte[]> OnRpdoReceived
-event Action<NmtState, NmtState> OnNmtStateChanged
-event Action<byte, ushort, byte> OnSdoReadRequest
-event Action<byte, ushort, byte, uint> OnSdoWriteRequest
-```
-
-### 2. CanOpenManager(基础层)
-
-**位置**: `CanOpenManager.cs`
-
-**职责**:
-- CAN帧的底层发送和接收
-- NMT命令封装
-- SDO请求构建
-- PDO COB-ID计算
-
-### 3. SlaveTestForm(测试界面)
-
-**位置**: `SlaveTestForm.cs`
-
-**功能**:
-- 直观的图形用户界面
-- 实时状态显示
-- 日志记录
-- 交互式控制
-
-## 🔄 工作流程
-
-```
-┌─────────────┐
-│  系统启动    │
-└──────┬──────┘
-       │
-       ▼
-┌─────────────┐
-│ 初始化CAN    │
-│ 创建从站对象  │
-└──────┬──────┘
-       │
-       ▼
-┌─────────────┐
-│ 注册事件处理  │
-└──────┬──────┘
-       │
-       ▼
-┌─────────────┐
-│ 启动从站     │◄──────────┐
-└──────┬──────┘            │
-       │                   │
-       ▼                   │
-┌─────────────┐            │
-│ 进入Pre-Op  │            │
-└──────┬──────┘            │
-       │                   │
-       ▼                   │
-┌─────────────┐            │
-│ 等待NMT命令  │────────────┤
-└──────┬──────┘            │
-       │                   │
-       ├─ NMT Start ──► Operational
-       │                   │
-       ├─ NMT Stop ───► Stopped
-       │                   │
-       ├─ 收到RPDO ──► 触发事件
-       │                   │
-       ├─ 收到SDO ──► 读写对象字典
-       │                   │
-       └─ 定时任务 ──► 发送TPDO/心跳
-```
-
-## 📊 COB-ID分配表
-
-| 功能 | COB-ID范围 | 计算公式 | 示例(nodeId=1) |
-|------|-----------|---------|---------------|
-| NMT | 0x000 | 固定 | 0x000 |
-| SYNC | 0x080 | 固定 | 0x080 |
-| EMCY | 0x081+nodeId | 0x081+ID | 0x082 |
-| TPDO1 | 0x180+nodeId | 0x180+ID | 0x181 |
-| TPDO2 | 0x280+nodeId | 0x280+ID | 0x281 |
-| TPDO3 | 0x380+nodeId | 0x380+ID | 0x381 |
-| TPDO4 | 0x480+nodeId | 0x480+ID | 0x481 |
-| RPDO1 | 0x200+nodeId | 0x200+ID | 0x201 |
-| RPDO2 | 0x300+nodeId | 0x300+ID | 0x301 |
-| RPDO3 | 0x400+nodeId | 0x400+ID | 0x401 |
-| RPDO4 | 0x500+nodeId | 0x500+ID | 0x501 |
-| SDO请求 | 0x600+nodeId | 0x600+ID | 0x601 |
-| SDO响应 | 0x580+nodeId | 0x580+ID | 0x581 |
-| Heartbeat | 0x700+nodeId | 0x700+ID | 0x701 |
-
-## 🛠️ 技术特点
-
-### 1. 架构设计
-- **分层架构**: 驱动层 → 协议层 → 应用层
-- **事件驱动**: 异步处理CAN帧
-- **线程安全**: 使用CancellationToken控制
-
-### 2. 代码质量
-- ✓ 完整的注释文档
-- ✓ 异常处理机制
-- ✓ 资源正确释放(IDisposable)
-- ✓ 符合C#编码规范
-
-### 3. 可扩展性
-- 易于添加新的对象字典条目
-- 支持自定义PDO数据处理
-- 事件机制便于集成到现有系统
-
-## 📝 使用示例
-
-### 基本用法
-
-```csharp
-// 1. 创建从站
-var slave = new CanOpenSlaveDevice(nodeId: 1);
-
-// 2. 注册事件
-slave.OnRpdoReceived += (id, num, data) => {
-    Console.WriteLine($"收到RPDO{num}");
-};
-
-// 3. 启动
-slave.Start(CanBaudRate.BaudRate_1M);
-
-// 4. 配置心跳
-slave.ConfigureHeartbeat(100);
-
-// 5. 发送数据
-slave.SendTpdo(1);
-
-// 6. 停止
-slave.Stop();
-slave.Dispose();
-```
-
-### 高级用法
-
-```csharp
-// 自定义SDO处理
-slave.OnSdoWriteRequest += (id, index, subIndex, value) => {
-    if (index == 0x2000) {
-        // 处理自定义参数
-        MyParameter = value;
-    }
-};
-
-// 状态监控
-slave.OnNmtStateChanged += (oldState, newState) => {
-    if (newState == NmtState.Operational) {
-        StartDataTransmission();
-    }
-};
-```
-
-## 🎓 学习资源
-
-1. **CANopen标准**
-   - CiA DS301: CANopen应用层和通信剖面
-   - CiA DSP301: CANopen设备剖面指南
-
-2. **相关文档**
-   - `README.md`: 完整使用说明
-   - `QUICKSTART.md`: 5分钟快速上手
-   - `Nameless.eds`: EDS文件详细配置
-
-3. **在线资源**
-   - CAN in Automation (CiA): https://www.can-cia.org/
-   - CANopen Protocol Specification
-
-## 🔮 未来扩展方向
-
-### 短期改进
-- [ ] 完善PDO映射功能
-- [ ] 添加EMCY紧急消息支持
-- [ ] 实现LSS层服务
-- [ ] 添加时间戳对象(0x1012/0x1013)
-
-### 中期改进
-- [ ] 支持分段SDO传输(>4字节)
-- [ ] 实现BOOTUP消息
-- [ ] 添加GFC通用帧控制
-- [ ] 支持多个CAN通道
-
-### 长期规划
-- [ ] 图形化PDO配置工具
-- [ ] EDS文件编辑器
-- [ ] 网络诊断工具
-- [ ] 支持CAN FD
-
-## ✨ 总结
-
-本项目成功实现了一个功能完整的CANopen从站设备,具有以下优势:
-
-✅ **完全符合EDS规范** - 严格按照Nameless.eds配置实现  
-✅ **易于使用** - 提供图形界面和详细文档  
-✅ **稳定可靠** - 完善的异常处理和资源管理  
-✅ **可扩展** - 模块化设计便于功能扩展  
-✅ **生产就绪** - 可直接用于实际项目  
-
-通过本项目的实现,不仅完成了技术要求,还为后续开发奠定了坚实的基础。
-
----
-
-**版本**: v1.0  
-**日期**: 2026-05-09  
-**作者**: Lingma (灵码)

+ 0 - 263
CanOpenSlaveTest/PROJECT_OVERVIEW.md

@@ -1,263 +0,0 @@
-# CANopen从站设备项目
-
-## 🎯 项目简介
-
-基于Nameless.eds文件实现的CANopen从站设备,使用创芯科技USBCAN2适配器。
-
-**主要特性**:
-- ✅ 4个RPDO + 4个TPDO
-- ✅ SDO通信(读/写)
-- ✅ NMT网络管理
-- ✅ 心跳机制
-- ✅ SYNC同步
-- ✅ 图形化测试界面
-
-## 📂 项目结构
-
-```
-CanOpenSlaveTest/
-│
-├── 📄 核心代码文件
-│   ├── CanLibraryClass.cs        # CAN卡驱动接口(P/Invoke)
-│   ├── CanOpenManager.cs         # CANopen协议管理器(基础层)
-│   ├── CanOpenSlaveDevice.cs     # ⭐ CANopen从站设备(核心实现)
-│   ├── EdsValidator.cs           # EDS文件验证器
-│   ├── Program.cs                # 应用程序入口
-│   └── SlaveTestForm.cs          # ⭐ Windows Forms测试界面
-│
-├── 📋 配置文件
-│   ├── CanOpenSlaveTest.csproj   # C#项目文件
-│   ├── CanOpenSlaveTest.sln      # Visual Studio解决方案
-│   └── App.config                # 应用配置
-│
-├── 📚 文档
-│   ├── README.md                 # 完整使用文档
-│   ├── QUICKSTART.md             # 5分钟快速上手指南
-│   └── IMPLEMENTATION_SUMMARY.md # 实现总结和技术细节
-│
-├── 📁 资源目录
-│   ├── bin/Debug/
-│   │   ├── controlcan.dll       # CAN卡驱动库
-│   │   └── Nameless.eds         # EDS配置文件
-│   ├── Properties/
-│   │   └── AssemblyInfo.cs      # 程序集信息
-│   └── Dll/                     # 其他DLL文件
-│
-└── 📝 其他
-    └── obj/                     # 编译中间文件
-```
-
-## 🚀 快速开始
-
-### 1️⃣ 环境准备
-
-```bash
-# 确保以下文件存在
-✓ controlcan.dll 在 bin\Debug\ 目录
-✓ USBCAN2设备已连接
-✓ CAN总线已正确接线
-```
-
-### 2️⃣ 编译运行
-
-```bash
-# 方法1: Visual Studio
-打开 CanOpenSlaveTest.sln → F5运行
-
-# 方法2: 命令行
-msbuild CanOpenSlaveTest.csproj
-cd bin\Debug
-CanOpenSlaveTest.exe
-```
-
-### 3️⃣ 基本操作
-
-```
-1. 点击 [启动从站] 按钮
-2. 设置心跳时间 (建议100ms)
-3. 点击 [配置心跳]
-4. 测试发送TPDO或接收RPDO
-```
-
-## 📖 详细文档
-
-| 文档 | 用途 | 适合人群 |
-|------|------|---------|
-| **QUICKSTART.md** | 5分钟快速上手 | 新手用户 |
-| **README.md** | 完整使用说明和API参考 | 开发者 |
-| **IMPLEMENTATION_SUMMARY.md** | 技术实现细节 | 高级开发者 |
-
-## 🔧 核心类说明
-
-### CanOpenSlaveDevice(⭐ 核心类)
-
-```csharp
-// 创建从站
-var slave = new CanOpenSlaveDevice(nodeId: 1);
-
-// 注册事件
-slave.OnRpdoReceived += (id, num, data) => { /* 处理RPDO */ };
-slave.OnNmtStateChanged += (old, newState) => { /* 状态变化 */ };
-
-// 启动
-slave.Start(CanBaudRate.BaudRate_1M);
-
-// 配置心跳
-slave.ConfigureHeartbeat(100);
-
-// 发送TPDO
-slave.SendTpdo(1);
-
-// 停止
-slave.Stop();
-slave.Dispose();
-```
-
-### 主要功能模块
-
-```
-CanOpenSlaveDevice
-├── NMT管理
-│   ├── 启动/停止节点
-│   ├── 预操作状态
-│   └── 重置功能
-│
-├── PDO通信
-│   ├── RPDO1-4 (接收)
-│   ├── TPDO1-4 (发送)
-│   └── 事件驱动/同步触发
-│
-├── SDO服务
-│   ├── 对象字典读取
-│   ├── 对象字典写入
-│   └── 错误处理
-│
-├── 心跳机制
-│   ├── 可配置时间
-│   └── 自动状态报告
-│
-└── SYNC同步
-    ├── SYNC帧接收
-    └── 同步TPDO发送
-```
-
-## 📊 COB-ID速查表
-
-| 类型 | COB-ID | 示例(ID=1) |
-|------|--------|-----------|
-| NMT | 0x000 | 0x000 |
-| SYNC | 0x080 | 0x080 |
-| TPDO1 | 0x180+ID | 0x181 |
-| TPDO2 | 0x280+ID | 0x281 |
-| TPDO3 | 0x380+ID | 0x381 |
-| TPDO4 | 0x480+ID | 0x481 |
-| RPDO1 | 0x200+ID | 0x201 |
-| RPDO2 | 0x300+ID | 0x301 |
-| RPDO3 | 0x400+ID | 0x401 |
-| RPDO4 | 0x500+ID | 0x501 |
-| SDO请求 | 0x600+ID | 0x601 |
-| SDO响应 | 0x580+ID | 0x581 |
-| Heartbeat | 0x700+ID | 0x701 |
-
-## 🎨 界面预览
-
-```
-┌────────────────────────────────────────────────────┐
-│            CANopen从站设备测试                       │
-├────────────────────────────────────────────────────┤
-│ 设备信息                                            │
-│ ├─ 节点ID: 1                                       │
-│ ├─ 状态: Operational                               │
-│ └─ 运行状态: 运行中                                 │
-├────────────────────────────────────────────────────┤
-│ 控制                                                │
-│ [启动从站] [停止从站]                               │
-│ [发送TPDO1] [发送TPDO2] [发送所有TPDO]              │
-├────────────────────────────────────────────────────┤
-│ 心跳配置                                            │
-│ 心跳时间(ms): [100 ▼] [配置心跳]                    │
-├────────────────────────────────────────────────────┤
-│ 日志                                                │
-│ ┌────────────────────────────────────────────────┐ │
-│ │ [15:30:45.123] CANopen从站测试程序已启动        │ │
-│ │ [15:30:50.456] ✓ 从站启动成功                   │ │
-│ │ [15:30:55.789] ← 收到RPDO1: 12 34 56 78        │ │
-│ │ [15:31:00.012] → 已发送TPDO1                    │ │
-│ │ ...                                             │ │
-│ └────────────────────────────────────────────────┘ │
-└────────────────────────────────────────────────────┘
-```
-
-## 💡 常见场景
-
-### 场景1: 数据采集
-```csharp
-// 定期通过TPDO发送传感器数据
-slave.OnNmtStateChanged += (old, state) => {
-    if (state == NmtState.Operational) {
-        StartSensorPolling();
-    }
-};
-```
-
-### 场景2: 命令接收
-```csharp
-// 处理主站发送的控制命令
-slave.OnRpdoReceived += (id, num, data) => {
-    if (num == 1) {
-        ExecuteCommand(data[0], data[1]);
-    }
-};
-```
-
-### 场景3: 参数配置
-```csharp
-// 通过SDO动态配置参数
-slave.OnSdoWriteRequest += (id, idx, subIdx, val) => {
-    if (idx == 0x2000) {
-        UpdateCustomParameter(val);
-    }
-};
-```
-
-## ❓ 常见问题
-
-**Q: 启动失败怎么办?**  
-A: 检查controlcan.dll是否存在,USBCAN2是否连接,CAN总线是否正确接线。
-
-**Q: 如何修改节点ID?**  
-A: 在代码中修改:`new CanOpenSlaveDevice(nodeId: 5)`
-
-**Q: 收不到RPDO?**  
-A: 确认COB-ID配置、从站状态、波特率设置是否与主站一致。
-
-**Q: 如何自定义TPDO数据?**  
-A: 需要扩展SendTpdo方法,添加自定义数据参数。
-
-更多问题请查看 `README.md` 的"常见问题"章节。
-
-## 🛠️ 开发环境
-
-- **IDE**: Visual Studio 2019+
-- **框架**: .NET Framework 4.7.2
-- **语言**: C#
-- **硬件**: 创芯科技USBCAN2
-
-## 📞 技术支持
-
-- 📖 查看完整文档: `README.md`
-- 🚀 快速上手: `QUICKSTART.md`
-- 🔍 技术细节: `IMPLEMENTATION_SUMMARY.md`
-- 📋 EDS配置: `bin\Debug\Nameless.eds`
-
-## 📄 许可证
-
-本项目仅供学习和研究使用。
-
----
-
-**版本**: v1.0  
-**更新日期**: 2026-05-09  
-**开发工具**: Lingma (灵码) - Alibaba Cloud
-
-**祝您使用愉快!** 🎉

+ 0 - 254
CanOpenSlaveTest/QUICKSTART.md

@@ -1,254 +0,0 @@
-# CANopen从站设备 - 快速开始指南
-
-## 📋 前提条件
-
-1. **硬件要求**
-   - 创芯科技USBCAN2适配器
-   - CAN总线连接线
-   - Windows电脑
-
-2. **软件要求**
-   - Visual Studio 2019或更高版本
-   - .NET Framework 4.7.2
-   - controlcan.dll (CAN卡驱动)
-
-## 🚀 5分钟快速上手
-
-### 步骤1: 准备环境
-
-1. 确保`controlcan.dll`在`bin\Debug`目录下
-2. 连接USBCAN2设备到USB端口
-3. 连接CAN总线
-
-### 步骤2: 编译项目
-
-```bash
-# 方法1: 使用Visual Studio
-打开 CanOpenSlaveTest.sln
-按 Ctrl+Shift+B 编译
-
-# 方法2: 使用MSBuild
-msbuild CanOpenSlaveTest.csproj /p:Configuration=Debug
-```
-
-### 步骤3: 运行程序
-
-```bash
-cd bin\Debug
-CanOpenSlaveTest.exe
-```
-
-### 步骤4: 启动从站
-
-1. 点击 **"启动从站"** 按钮
-2. 观察日志输出,确认启动成功
-3. 配置心跳时间(建议100ms)
-4. 点击 **"配置心跳"** 按钮
-
-### 步骤5: 测试通信
-
-#### 发送TPDO
-- 点击 **"发送TPDO1"** 或 **"发送TPDO2"** 按钮
-- 或使用CAN分析仪监控总线上的数据
-
-#### 接收RPDO
-- 使用主站发送RPDO到从站
-- 观察日志窗口中的接收信息
-
-## 📊 界面说明
-
-```
-┌─────────────────────────────────────────────┐
-│ 设备信息                                     │
-│ 节点ID: 1                                    │
-│ 状态: PreOperational                         │
-│ 运行状态: 运行中                              │
-└─────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────┐
-│ 控制                                         │
-│ [启动从站] [停止从站]                        │
-│ [发送TPDO1] [发送TPDO2] [发送所有TPDO]       │
-└─────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────┐
-│ 心跳配置                                     │
-│ 心跳时间(ms): [100] [配置心跳]               │
-└─────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────┐
-│ 日志                                         │
-│ [15:30:45.123] CANopen从站测试程序已启动     │
-│ [15:30:45.125] 节点ID: 1                     │
-│ [15:30:50.456] ✓ 从站启动成功                │
-│ [15:30:55.789] ← 收到RPDO1 [节点1]: 12 34    │
-│ ...                                          │
-└─────────────────────────────────────────────┘
-```
-
-## 🔧 常用操作
-
-### 1. 修改节点ID
-
-编辑代码:
-```csharp
-// 在 SlaveTestForm.cs 的 InitializeComponent() 中
-m_slaveDevice = new CanOpenSlaveDevice(nodeId: 5);  // 改为5
-```
-
-### 2. 更改波特率
-
-```csharp
-// 在 BtnStart_Click 方法中
-m_slaveDevice.Start(CanBaudRate.BaudRate_500K);  // 500kbps
-```
-
-### 3. 处理RPDO数据
-
-```csharp
-private void SlaveDevice_OnRpdoReceived(byte nodeId, byte pdoNumber, byte[] data)
-{
-    // 解析数据
-    if (pdoNumber == 1 && data.Length >= 2)
-    {
-        int value = data[0] | (data[1] << 8);
-        Console.WriteLine($"RPDO1数据: {value}");
-    }
-}
-```
-
-### 4. 自定义TPDO数据
-
-需要扩展`CanOpenSlaveDevice`类:
-
-```csharp
-public void SendTpdoWithData(byte pdoNumber, byte[] customData)
-{
-    if (pdoNumber < 1 || pdoNumber > 4)
-        throw new ArgumentException("PDO编号必须在1-4之间");
-    
-    uint cobId = m_tpdoConfigs[pdoNumber - 1].CobId;
-    m_canManager.SendCanFrame(cobId, customData);
-}
-```
-
-## 🎯 典型应用场景
-
-### 场景1: 数据采集从站
-
-```csharp
-// 定期采集传感器数据并通过TPDO发送
-private Timer m_dataTimer;
-
-private void StartDataCollection()
-{
-    m_dataTimer = new Timer(state => {
-        if (m_slaveDevice.CurrentState == NmtState.Operational)
-        {
-            // 读取传感器数据
-            byte[] sensorData = ReadSensorData();
-            
-            // 通过TPDO1发送
-            m_slaveDevice.SendTpdo(1);  // 需要扩展支持自定义数据
-        }
-    }, null, 0, 100);  // 每100ms采集一次
-}
-```
-
-### 场景2: 控制命令接收
-
-```csharp
-// 接收主站的控制命令
-m_slaveDevice.OnRpdoReceived += (nodeId, pdoNum, data) => {
-    if (pdoNum == 1 && data.Length >= 2)
-    {
-        // 解析控制命令
-        byte command = data[0];
-        byte parameter = data[1];
-        
-        ExecuteCommand(command, parameter);
-    }
-};
-```
-
-### 场景3: 参数配置
-
-```csharp
-// 通过SDO配置从站参数
-m_slaveDevice.OnSdoWriteRequest += (nodeId, index, subIndex, value) => {
-    if (index == 0x2000)  // 自定义参数索引
-    {
-        // 更新内部参数
-        UpdateParameter(subIndex, value);
-    }
-};
-```
-
-## 🐛 故障排查
-
-### 问题1: 启动失败
-
-**症状**: 点击"启动从站"后显示失败
-
-**解决方案**:
-1. 检查USBCAN2是否正确连接
-2. 确认`controlcan.dll`存在且版本正确
-3. 检查CAN总线终端电阻(120Ω)
-4. 验证波特率设置是否与主站一致
-
-### 问题2: 收不到RPDO
-
-**症状**: 主站发送RPDO,但从站没有反应
-
-**解决方案**:
-1. 确认COB-ID配置正确(0x200+nodeId等)
-2. 检查从站是否处于Operational状态
-3. 使用CAN分析仪验证帧是否发送到总线
-4. 检查RPDO是否启用
-
-### 问题3: TPDO发送失败
-
-**症状**: 点击发送按钮无响应
-
-**解决方案**:
-1. 确认从站已启动
-2. 检查TPDO配置是否启用
-3. 验证COB-ID是否正确(0x180+nodeId等)
-4. 查看日志是否有错误信息
-
-### 问题4: 心跳不工作
-
-**症状**: 配置心跳后没有心跳帧
-
-**解决方案**:
-1. 确认心跳时间大于0
-2. 检查从站状态是否为PreOperational或Operational
-3. 验证定时器是否正常创建
-4. 查看控制台输出
-
-## 📚 下一步学习
-
-1. **阅读完整文档**: 查看 `README.md`
-2. **研究EDS文件**: 理解 `Nameless.eds` 的配置
-3. **学习CANopen协议**: 参考 CiA DS301 标准
-4. **扩展功能**: 添加自定义对象字典条目
-
-## 💡 提示
-
-- ✅ 始终在操作前检查从站状态
-- ✅ 使用日志输出来调试通信问题
-- ✅ 保持COB-ID配置与主站一致
-- ✅ 定期保存和备份配置
-- ✅ 在生产环境中添加异常处理
-
-## 📞 获取帮助
-
-遇到问题?
-1. 查看日志窗口的详细输出
-2. 使用CAN分析仪诊断总线通信
-3. 参考 `README.md` 中的常见问题
-4. 检查EDS文件配置是否正确
-
----
-
-**祝使用愉快!** 🎉

+ 0 - 328
CanOpenSlaveTest/README.md

@@ -1,328 +0,0 @@
-# CANopen从站设备使用说明
-
-## 概述
-
-本项目实现了一个基于Nameless.eds文件的CANopen从站设备,支持以下功能:
-
-- ✅ 4个接收PDO (RPDO) - COB-ID: 0x200+nodeId ~ 0x500+nodeId
-- ✅ 4个发送PDO (TPDO) - COB-ID: 0x180+nodeId ~ 0x480+nodeId
-- ✅ SDO通信 (读/写对象字典)
-- ✅ NMT网络管理 (启动/停止/预操作/重置)
-- ✅ 心跳机制 (可配置)
-- ✅ SYNC同步支持
-
-## 硬件要求
-
-- 创芯科技USBCAN2适配器
-- controlcan.dll动态库
-- CAN总线连接
-
-## 软件架构
-
-### 核心类
-
-1. **CanOpenSlaveDevice** - 从站设备主类
-   - 位置: `CanOpenSlaveDevice.cs`
-   - 功能: 实现完整的CANopen从站协议栈
-
-2. **CanOpenManager** - CAN底层管理器
-   - 位置: `CanOpenManager.cs`
-   - 功能: 封装CAN帧的发送和接收
-
-3. **CanLibraryClass** - CAN卡驱动接口
-   - 位置: `CanLibraryClass.cs`
-   - 功能: P/Invoke调用controlcan.dll
-
-### 数据结构
-
-- `NmtState` - NMT状态枚举
-- `PdoConfig` - PDO配置
-- `PdoMapping` - PDO映射
-- `IdentityObject` - 身份对象(1018h)
-- `TpdoCommParam` - TPDO通信参数
-- `RpdoCommParam` - RPDO通信参数
-
-## 快速开始
-
-### 1. 基本使用
-
-```csharp
-using CCDCount.DLL.CanBus;
-
-// 创建从站设备 (节点ID=1)
-var slave = new CanOpenSlaveDevice(nodeId: 1);
-
-// 注册事件
-slave.OnRpdoReceived += (nodeId, pdoNum, data) => {
-    Console.WriteLine($"收到RPDO{pdoNum}");
-};
-
-slave.OnNmtStateChanged += (oldState, newState) => {
-    Console.WriteLine($"状态改变: {oldState} -> {newState}");
-};
-
-// 启动从站 (1Mbps波特率)
-slave.Start(CanBaudRate.BaudRate_1M);
-
-// 配置心跳 (100ms)
-slave.ConfigureHeartbeat(100);
-
-// 发送TPDO
-slave.SendTpdo(1);
-
-// 停止从站
-slave.Stop();
-slave.Dispose();
-```
-
-### 2. 运行测试程序
-
-```bash
-# 编译项目
-msbuild CanOpenSlaveTest.sln
-
-# 运行程序
-cd bin\Debug
-CanOpenSlaveTest.exe
-```
-
-## EDS文件配置说明
-
-根据Nameless.eds文件,从站设备配置如下:
-
-### 设备信息
-- 厂商: HanLin (Vendor ID: 0x0000000D)
-- 产品: CCDCountCanOpenSlave (Product Code: 0x00000001)
-- 版本: 0x00010001
-- 支持波特率: 1Mbps
-
-### PDO配置
-
-#### RPDO (接收PDO)
-| PDO编号 | 索引范围 | COB-ID公式 | 默认传输类型 |
-|---------|----------|------------|--------------|
-| RPDO1 | 0x1400/0x1600 | 0x200 + nodeId | 0xFF (事件驱动) |
-| RPDO2 | 0x1401/0x1601 | 0x300 + nodeId | 0xFF (事件驱动) |
-| RPDO3 | 0x1402/0x1602 | 0x400 + nodeId | 0xFF (事件驱动) |
-| RPDO4 | 0x1403/0x1603 | 0x500 + nodeId | 0xFF (事件驱动) |
-
-#### TPDO (发送PDO)
-| PDO编号 | 索引范围 | COB-ID公式 | 默认传输类型 |
-|---------|----------|------------|--------------|
-| TPDO1 | 0x1800/0x1A00 | 0x180 + nodeId | 0xFF (事件驱动) |
-| TPDO2 | 0x1801/0x1A01 | 0x280 + nodeId | 0xFF (事件驱动) |
-| TPDO3 | 0x1802/0x1A02 | 0x380 + nodeId | 0xFF (事件驱动) |
-| TPDO4 | 0x1803/0x1A03 | 0x480 + nodeId | 0xFF (事件驱动) |
-
-### 对象字典
-
-#### 必选对象
-- **0x1000** - Device type (RO, 4字节)
-- **0x1001** - Error register (RO, 1字节)
-- **0x1018** - Identity object (RO, 复合对象)
-  - sub0: Number of entries (3)
-  - sub1: Vendor ID (0x0000000D)
-  - sub2: Product code (0x00000001)
-  - sub3: Revision number (0x00010001)
-
-#### 可选对象
-- **0x1400-0x1403** - RPDO communication parameters
-- **0x1600-0x1603** - RPDO mapping parameters
-- **0x1800-0x1803** - TPDO communication parameters
-- **0x1A00-0x1A03** - TPDO mapping parameters
-
-## API参考
-
-### CanOpenSlaveDevice类
-
-#### 构造函数
-```csharp
-public CanOpenSlaveDevice(
-    byte nodeId = 1,           // 节点ID (1-127)
-    UInt32 deviceType = 4,     // 设备类型 (4=USBCAN2)
-    UInt32 deviceIndex = 0,    // 设备索引
-    UInt32 canIndex = 0        // CAN通道索引
-)
-```
-
-#### 方法
-
-**Start()** - 启动从站
-```csharp
-public bool Start(CanBaudRate baudRate = CanBaudRate.BaudRate_1M)
-```
-
-**Stop()** - 停止从站
-```csharp
-public void Stop()
-```
-
-**SendTpdo()** - 发送TPDO
-```csharp
-public void SendTpdo(byte pdoNumber)  // pdoNumber: 1-4
-```
-
-**SendAllTpdos()** - 发送所有启用的TPDO
-```csharp
-public void SendAllTpdos()
-```
-
-**ConfigureHeartbeat()** - 配置心跳
-```csharp
-public void ConfigureHeartbeat(ushort heartbeatTimeMs)  // 0表示禁用
-```
-
-#### 事件
-
-**OnRpdoReceived** - RPDO接收事件
-```csharp
-public event Action<byte, byte, byte[]> OnRpdoReceived
-// 参数: nodeId, pdoNumber, data
-```
-
-**OnNmtStateChanged** - NMT状态改变事件
-```csharp
-public event Action<NmtState, NmtState> OnNmtStateChanged
-// 参数: oldState, newState
-```
-
-**OnSdoReadRequest** - SDO读取请求事件
-```csharp
-public event Action<byte, ushort, byte> OnSdoReadRequest
-// 参数: nodeId, index, subIndex
-```
-
-**OnSdoWriteRequest** - SDO写入请求事件
-```csharp
-public event Action<byte, ushort, byte, uint> OnSdoWriteRequest
-// 参数: nodeId, index, subIndex, value
-```
-
-#### 属性
-
-```csharp
-public byte NodeId { get; }              // 节点ID
-public NmtState CurrentState { get; }    // 当前NMT状态
-public bool IsRunning { get; }           // 是否正在运行
-```
-
-## NMT状态机
-
-```
-Initializing (0x00)
-    ↓ [NMT Reset]
-PreOperational (0x7F) ←→ Operational (0x05)
-    ↑         ↓ [NMT Stop]
-    └── Stopped (0x04)
-```
-
-### 状态说明
-
-- **Initializing**: 初始化状态,设备上电后进入此状态
-- **PreOperational**: 预操作状态,只能进行SDO配置
-- **Operational**: 运行状态,可以进行PDO通信
-- **Stopped**: 停止状态,暂停所有通信
-
-## 常见问题
-
-### Q1: 如何修改节点ID?
-A: 在构造函数中指定:
-```csharp
-var slave = new CanOpenSlaveDevice(nodeId: 5);  // 节点ID=5
-```
-
-### Q2: 如何更改波特率?
-A: 在Start()方法中指定:
-```csharp
-slave.Start(CanBaudRate.BaudRate_500K);  // 500kbps
-```
-
-### Q3: 如何处理接收到的RPDO数据?
-A: 注册OnRpdoReceived事件:
-```csharp
-slave.OnRpdoReceived += (nodeId, pdoNum, data) => {
-    // 处理data数组
-    int value = data[0] | (data[1] << 8);
-};
-```
-
-### Q4: 如何自定义TPDO数据?
-A: 需要扩展CanOpenSlaveDevice类,添加自定义数据填充逻辑。
-
-### Q5: 心跳时间如何设置?
-A: 使用ConfigureHeartbeat()方法:
-```csharp
-slave.ConfigureHeartbeat(100);  // 100ms心跳
-slave.ConfigureHeartbeat(0);    // 禁用心跳
-```
-
-## 调试技巧
-
-### 1. 启用日志输出
-程序会自动输出关键事件到控制台:
-- CAN帧收发
-- NMT状态变化
-- SDO读写
-- PDO数据传输
-
-### 2. 使用CAN分析仪
-推荐使用以下工具监控CAN总线:
-- CANalyzer
-- PCAN-View
-- USB-CAN Tool
-
-### 3. 检查COB-ID
-确保主站和从站的COB-ID配置一致:
-- RPDO: 0x200+nodeId ~ 0x500+nodeId
-- TPDO: 0x180+nodeId ~ 0x480+nodeId
-- SDO: 0x600+nodeId (请求), 0x580+nodeId (响应)
-- NMT: 0x000
-- Heartbeat: 0x700+nodeId
-
-## 扩展开发
-
-### 添加自定义对象字典条目
-
-在`InitializeObjectDictionary()`方法中添加:
-```csharp
-// 例如: 添加0x2000自定义对象
-m_objectDictionary[0x2000] = new MyCustomObject();
-```
-
-### 实现PDO映射
-
-目前PDO映射功能为框架级别,需要根据实际应用完善:
-```csharp
-// 在ProcessRpdo()中解析映射表
-// 在SendTpdo()中根据映射表构建数据
-```
-
-### 添加紧急消息(EMCY)支持
-
-可以扩展EMCY处理:
-```csharp
-public void SendEmergency(ushort errorCode, byte errorRegister)
-{
-    byte[] emcyData = new byte[8];
-    emcyData[0] = (byte)(errorCode & 0xFF);
-    emcyData[1] = (byte)((errorCode >> 8) & 0xFF);
-    emcyData[2] = errorRegister;
-    
-    uint emcyCobId = 0x080 + m_nodeId;
-    m_canManager.SendCanFrame(emcyCobId, emcyData);
-}
-```
-
-## 技术支持
-
-如有问题,请参考:
-- CiA DS301标准文档
-- Nameless.eds文件详细说明
-- 创芯科技CAN卡开发手册
-
-## 版本历史
-
-- v1.0 (2026-05-09)
-  - 初始版本
-  - 基于Nameless.eds实现
-  - 支持基本CANopen从站功能

+ 212 - 120
CanOpenSlaveTest/SlaveTestForm.cs

@@ -2,6 +2,7 @@ using System;
 using System.Windows.Forms;
 using System.Drawing;
 using CCDCount.DLL.CanBus;
+using CanTest;
 
 namespace CanOpenSlaveTest
 {
@@ -30,7 +31,8 @@ namespace CanOpenSlaveTest
         private Button btnConfigureHeartbeat;
         
         private TextBox txtLog;
-        
+        private Label lblHeartbeat;
+        private System.ComponentModel.IContainer components;
         private System.Windows.Forms.Timer m_updateTimer;
         
         public SlaveTestForm()
@@ -57,126 +59,216 @@ namespace CanOpenSlaveTest
         
         private void InitializeComponent()
         {
+            this.components = new System.ComponentModel.Container();
+            this.grpDeviceInfo = new System.Windows.Forms.GroupBox();
+            this.lblNodeId = new System.Windows.Forms.Label();
+            this.lblState = new System.Windows.Forms.Label();
+            this.lblStatus = new System.Windows.Forms.Label();
+            this.grpControl = new System.Windows.Forms.GroupBox();
+            this.btnStart = new System.Windows.Forms.Button();
+            this.btnStop = new System.Windows.Forms.Button();
+            this.btnSendTpdo1 = new System.Windows.Forms.Button();
+            this.btnSendTpdo2 = new System.Windows.Forms.Button();
+            this.btnSendAllTpdos = new System.Windows.Forms.Button();
+            this.grpHeartbeat = new System.Windows.Forms.GroupBox();
+            this.lblHeartbeat = new System.Windows.Forms.Label();
+            this.nudHeartbeatTime = new System.Windows.Forms.NumericUpDown();
+            this.btnConfigureHeartbeat = new System.Windows.Forms.Button();
+            this.txtLog = new System.Windows.Forms.TextBox();
+            this.m_updateTimer = new System.Windows.Forms.Timer(this.components);
+            this.grpDeviceInfo.SuspendLayout();
+            this.grpControl.SuspendLayout();
+            this.grpHeartbeat.SuspendLayout();
+            ((System.ComponentModel.ISupportInitialize)(this.nudHeartbeatTime)).BeginInit();
+            this.SuspendLayout();
+            // 
+            // grpDeviceInfo
+            // 
+            this.grpDeviceInfo.Controls.Add(this.lblNodeId);
+            this.grpDeviceInfo.Controls.Add(this.lblState);
+            this.grpDeviceInfo.Controls.Add(this.lblStatus);
+            this.grpDeviceInfo.Location = new System.Drawing.Point(10, 10);
+            this.grpDeviceInfo.Name = "grpDeviceInfo";
+            this.grpDeviceInfo.Size = new System.Drawing.Size(380, 100);
+            this.grpDeviceInfo.TabIndex = 0;
+            this.grpDeviceInfo.TabStop = false;
+            this.grpDeviceInfo.Text = "设备信息";
+            // 
+            // lblNodeId
+            // 
+            this.lblNodeId.AutoSize = true;
+            this.lblNodeId.Location = new System.Drawing.Point(10, 25);
+            this.lblNodeId.Name = "lblNodeId";
+            this.lblNodeId.Size = new System.Drawing.Size(77, 15);
+            this.lblNodeId.TabIndex = 0;
+            this.lblNodeId.Text = "节点ID: -";
+            // 
+            // lblState
+            // 
+            this.lblState.AutoSize = true;
+            this.lblState.Location = new System.Drawing.Point(10, 50);
+            this.lblState.Name = "lblState";
+            this.lblState.Size = new System.Drawing.Size(61, 15);
+            this.lblState.TabIndex = 1;
+            this.lblState.Text = "状态: -";
+            // 
+            // lblStatus
+            // 
+            this.lblStatus.AutoSize = true;
+            this.lblStatus.Location = new System.Drawing.Point(10, 75);
+            this.lblStatus.Name = "lblStatus";
+            this.lblStatus.Size = new System.Drawing.Size(128, 15);
+            this.lblStatus.TabIndex = 2;
+            this.lblStatus.Text = "运行状态: 未启动";
+            // 
+            // grpControl
+            // 
+            this.grpControl.Controls.Add(this.btnStart);
+            this.grpControl.Controls.Add(this.btnStop);
+            this.grpControl.Controls.Add(this.btnSendTpdo1);
+            this.grpControl.Controls.Add(this.btnSendTpdo2);
+            this.grpControl.Controls.Add(this.btnSendAllTpdos);
+            this.grpControl.Location = new System.Drawing.Point(400, 10);
+            this.grpControl.Name = "grpControl";
+            this.grpControl.Size = new System.Drawing.Size(380, 100);
+            this.grpControl.TabIndex = 1;
+            this.grpControl.TabStop = false;
+            this.grpControl.Text = "控制";
+            // 
+            // btnStart
+            // 
+            this.btnStart.Location = new System.Drawing.Point(10, 25);
+            this.btnStart.Name = "btnStart";
+            this.btnStart.Size = new System.Drawing.Size(100, 30);
+            this.btnStart.TabIndex = 0;
+            this.btnStart.Text = "启动从站";
+            this.btnStart.Click += new System.EventHandler(this.BtnStart_Click);
+            // 
+            // btnStop
+            // 
+            this.btnStop.Enabled = false;
+            this.btnStop.Location = new System.Drawing.Point(120, 25);
+            this.btnStop.Name = "btnStop";
+            this.btnStop.Size = new System.Drawing.Size(100, 30);
+            this.btnStop.TabIndex = 1;
+            this.btnStop.Text = "停止从站";
+            this.btnStop.Click += new System.EventHandler(this.BtnStop_Click);
+            // 
+            // btnSendTpdo1
+            // 
+            this.btnSendTpdo1.Enabled = false;
+            this.btnSendTpdo1.Location = new System.Drawing.Point(10, 60);
+            this.btnSendTpdo1.Name = "btnSendTpdo1";
+            this.btnSendTpdo1.Size = new System.Drawing.Size(100, 30);
+            this.btnSendTpdo1.TabIndex = 2;
+            this.btnSendTpdo1.Text = "发送TPDO1";
+            this.btnSendTpdo1.Click += new System.EventHandler(this.BtnSendTpdo1_Click);
+            // 
+            // btnSendTpdo2
+            // 
+            this.btnSendTpdo2.Enabled = false;
+            this.btnSendTpdo2.Location = new System.Drawing.Point(120, 60);
+            this.btnSendTpdo2.Name = "btnSendTpdo2";
+            this.btnSendTpdo2.Size = new System.Drawing.Size(100, 30);
+            this.btnSendTpdo2.TabIndex = 3;
+            this.btnSendTpdo2.Text = "发送TPDO2";
+            this.btnSendTpdo2.Click += new System.EventHandler(this.BtnSendTpdo2_Click);
+            // 
+            // btnSendAllTpdos
+            // 
+            this.btnSendAllTpdos.Enabled = false;
+            this.btnSendAllTpdos.Location = new System.Drawing.Point(230, 60);
+            this.btnSendAllTpdos.Name = "btnSendAllTpdos";
+            this.btnSendAllTpdos.Size = new System.Drawing.Size(120, 30);
+            this.btnSendAllTpdos.TabIndex = 4;
+            this.btnSendAllTpdos.Text = "发送所有TPDO";
+            this.btnSendAllTpdos.Click += new System.EventHandler(this.BtnSendAllTpdos_Click);
+            // 
+            // grpHeartbeat
+            // 
+            this.grpHeartbeat.Controls.Add(this.lblHeartbeat);
+            this.grpHeartbeat.Controls.Add(this.nudHeartbeatTime);
+            this.grpHeartbeat.Controls.Add(this.btnConfigureHeartbeat);
+            this.grpHeartbeat.Location = new System.Drawing.Point(10, 120);
+            this.grpHeartbeat.Name = "grpHeartbeat";
+            this.grpHeartbeat.Size = new System.Drawing.Size(770, 60);
+            this.grpHeartbeat.TabIndex = 2;
+            this.grpHeartbeat.TabStop = false;
+            this.grpHeartbeat.Text = "心跳配置";
+            // 
+            // lblHeartbeat
+            // 
+            this.lblHeartbeat.AutoSize = true;
+            this.lblHeartbeat.Location = new System.Drawing.Point(10, 25);
+            this.lblHeartbeat.Name = "lblHeartbeat";
+            this.lblHeartbeat.Size = new System.Drawing.Size(107, 15);
+            this.lblHeartbeat.TabIndex = 0;
+            this.lblHeartbeat.Text = "心跳时间(ms):";
+            // 
+            // nudHeartbeatTime
+            // 
+            this.nudHeartbeatTime.Location = new System.Drawing.Point(110, 22);
+            this.nudHeartbeatTime.Maximum = new decimal(new int[] {
+            10000,
+            0,
+            0,
+            0});
+            this.nudHeartbeatTime.Name = "nudHeartbeatTime";
+            this.nudHeartbeatTime.Size = new System.Drawing.Size(100, 25);
+            this.nudHeartbeatTime.TabIndex = 1;
+            this.nudHeartbeatTime.Value = new decimal(new int[] {
+            100,
+            0,
+            0,
+            0});
+            // 
+            // btnConfigureHeartbeat
+            // 
+            this.btnConfigureHeartbeat.Enabled = false;
+            this.btnConfigureHeartbeat.Location = new System.Drawing.Point(220, 20);
+            this.btnConfigureHeartbeat.Name = "btnConfigureHeartbeat";
+            this.btnConfigureHeartbeat.Size = new System.Drawing.Size(100, 30);
+            this.btnConfigureHeartbeat.TabIndex = 2;
+            this.btnConfigureHeartbeat.Text = "配置心跳";
+            this.btnConfigureHeartbeat.Click += new System.EventHandler(this.BtnConfigureHeartbeat_Click);
+            // 
+            // txtLog
+            // 
+            this.txtLog.BackColor = System.Drawing.Color.White;
+            this.txtLog.Font = new System.Drawing.Font("微软雅黑", 9F);
+            this.txtLog.ForeColor = System.Drawing.Color.Black;
+            this.txtLog.Location = new System.Drawing.Point(10, 190);
+            this.txtLog.Multiline = true;
+            this.txtLog.Name = "txtLog";
+            this.txtLog.ReadOnly = true;
+            this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
+            this.txtLog.Size = new System.Drawing.Size(770, 360);
+            this.txtLog.TabIndex = 3;
+            // 
+            // m_updateTimer
+            // 
+            this.m_updateTimer.Enabled = true;
+            this.m_updateTimer.Interval = 500;
+            // 
+            // SlaveTestForm
+            // 
+            this.ClientSize = new System.Drawing.Size(782, 553);
+            this.Controls.Add(this.grpDeviceInfo);
+            this.Controls.Add(this.grpControl);
+            this.Controls.Add(this.grpHeartbeat);
+            this.Controls.Add(this.txtLog);
+            this.Name = "SlaveTestForm";
+            this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
             this.Text = "CANopen从站设备测试";
-            this.Size = new Size(800, 600);
-            this.StartPosition = FormStartPosition.CenterScreen;
-            
-            // 设备信息组
-            grpDeviceInfo = new GroupBox();
-            grpDeviceInfo.Text = "设备信息";
-            grpDeviceInfo.Location = new Point(10, 10);
-            grpDeviceInfo.Size = new Size(380, 100);
-            
-            lblNodeId = new Label();
-            lblNodeId.Text = "节点ID: -";
-            lblNodeId.Location = new Point(10, 25);
-            lblNodeId.AutoSize = true;
-            
-            lblState = new Label();
-            lblState.Text = "状态: -";
-            lblState.Location = new Point(10, 50);
-            lblState.AutoSize = true;
-            
-            lblStatus = new Label();
-            lblStatus.Text = "运行状态: 未启动";
-            lblStatus.Location = new Point(10, 75);
-            lblStatus.AutoSize = true;
-            
-            grpDeviceInfo.Controls.AddRange(new Control[] { lblNodeId, lblState, lblStatus });
-            
-            // 控制组
-            grpControl = new GroupBox();
-            grpControl.Text = "控制";
-            grpControl.Location = new Point(400, 10);
-            grpControl.Size = new Size(380, 100);
-            
-            btnStart = new Button();
-            btnStart.Text = "启动从站";
-            btnStart.Location = new Point(10, 25);
-            btnStart.Size = new Size(100, 30);
-            btnStart.Click += BtnStart_Click;
-            
-            btnStop = new Button();
-            btnStop.Text = "停止从站";
-            btnStop.Location = new Point(120, 25);
-            btnStop.Size = new Size(100, 30);
-            btnStop.Click += BtnStop_Click;
-            btnStop.Enabled = false;
-            
-            btnSendTpdo1 = new Button();
-            btnSendTpdo1.Text = "发送TPDO1";
-            btnSendTpdo1.Location = new Point(10, 60);
-            btnSendTpdo1.Size = new Size(100, 30);
-            btnSendTpdo1.Click += BtnSendTpdo1_Click;
-            btnSendTpdo1.Enabled = false;
-            
-            btnSendTpdo2 = new Button();
-            btnSendTpdo2.Text = "发送TPDO2";
-            btnSendTpdo2.Location = new Point(120, 60);
-            btnSendTpdo2.Size = new Size(100, 30);
-            btnSendTpdo2.Click += BtnSendTpdo2_Click;
-            btnSendTpdo2.Enabled = false;
-            
-            btnSendAllTpdos = new Button();
-            btnSendAllTpdos.Text = "发送所有TPDO";
-            btnSendAllTpdos.Location = new Point(230, 60);
-            btnSendAllTpdos.Size = new Size(120, 30);
-            btnSendAllTpdos.Click += BtnSendAllTpdos_Click;
-            btnSendAllTpdos.Enabled = false;
-            
-            grpControl.Controls.AddRange(new Control[] { 
-                btnStart, btnStop, btnSendTpdo1, btnSendTpdo2, btnSendAllTpdos 
-            });
-            
-            // 心跳配置组
-            grpHeartbeat = new GroupBox();
-            grpHeartbeat.Text = "心跳配置";
-            grpHeartbeat.Location = new Point(10, 120);
-            grpHeartbeat.Size = new Size(770, 60);
-            
-            Label lblHeartbeat = new Label();
-            lblHeartbeat.Text = "心跳时间(ms):";
-            lblHeartbeat.Location = new Point(10, 25);
-            lblHeartbeat.AutoSize = true;
-            
-            nudHeartbeatTime = new NumericUpDown();
-            nudHeartbeatTime.Location = new Point(110, 22);
-            nudHeartbeatTime.Size = new Size(100, 25);
-            nudHeartbeatTime.Minimum = 0;
-            nudHeartbeatTime.Maximum = 10000;
-            nudHeartbeatTime.Value = 100;
-            
-            btnConfigureHeartbeat = new Button();
-            btnConfigureHeartbeat.Text = "配置心跳";
-            btnConfigureHeartbeat.Location = new Point(220, 20);
-            btnConfigureHeartbeat.Size = new Size(100, 30);
-            btnConfigureHeartbeat.Click += BtnConfigureHeartbeat_Click;
-            btnConfigureHeartbeat.Enabled = false;
-            
-            grpHeartbeat.Controls.AddRange(new Control[] { lblHeartbeat, nudHeartbeatTime, btnConfigureHeartbeat });
-            
-            // 日志显示
-            txtLog = new TextBox();
-            txtLog.Multiline = true;
-            txtLog.ScrollBars = ScrollBars.Vertical;
-            txtLog.ReadOnly = true;
-            txtLog.Location = new Point(10, 190);
-            txtLog.Size = new Size(770, 360);
-            // 使用支持中文的字体(微软雅黑或宋体)
-            txtLog.Font = new Font("Microsoft YaHei", 9);
-            txtLog.ForeColor = Color.Black;
-            txtLog.BackColor = Color.White;
-            
-            // 添加控件到窗体
-            this.Controls.AddRange(new Control[] { 
-                grpDeviceInfo, grpControl, grpHeartbeat, txtLog 
-            });
-            
-            // 更新定时器
-            m_updateTimer = new System.Windows.Forms.Timer();
-            m_updateTimer.Interval = 500;
-            m_updateTimer.Tick += UpdateTimer_Tick;
-            m_updateTimer.Start();
+            this.grpDeviceInfo.ResumeLayout(false);
+            this.grpDeviceInfo.PerformLayout();
+            this.grpControl.ResumeLayout(false);
+            this.grpHeartbeat.ResumeLayout(false);
+            this.grpHeartbeat.PerformLayout();
+            ((System.ComponentModel.ISupportInitialize)(this.nudHeartbeatTime)).EndInit();
+            this.ResumeLayout(false);
+            this.PerformLayout();
+
         }
         
         private void BtnStart_Click(object sender, EventArgs e)

+ 6 - 2
CanTest/Program.cs

@@ -23,9 +23,9 @@ namespace CanTest
             //RunCanOpenMaster();
 
             ConnectCan();
-            SendTest();
+            //SendTest();
             //GetTest();
-            //MessageTest();
+            MessageTest();
         }
 
         /// <summary>
@@ -770,6 +770,10 @@ namespace CanTest
                                             num8p++;
                                         }
                                     }
+                                    else
+                                    {
+                                        StartTime = DateTime.Now;
+                                    }
                                     SendCount++;
                                 }
                                 previousWriteDone = writeDone;