CanOpenSlaveDevice.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209
  1. using CanTest;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace CCDCount.DLL.CanBus
  9. {
  10. /// <summary>
  11. /// CANopen从站设备 - 基于Nameless.eds文件实现
  12. /// 支持4个RPDO和4个TPDO,符合CiA DS301标准
  13. /// 直接基于创芯科技CAN卡底层库实现,不依赖CanOpenManager
  14. /// </summary>
  15. public class CanOpenSlaveDevice : IDisposable
  16. {
  17. #region 私有字段
  18. // CAN设备参数
  19. private UInt32 m_deviceType = 4; // USBCAN2
  20. private UInt32 m_deviceIndex = 0;
  21. private UInt32 m_canIndex = 0;
  22. private bool m_isOpened = false;
  23. private bool m_isStarted = false;
  24. private byte m_nodeId = 1; // 默认节点ID
  25. private NmtState m_currentState = NmtState.Initializing;
  26. // PDO配置 - 根据EDS文件定义
  27. private PdoConfig[] m_rpdoConfigs = new PdoConfig[4]; // 接收PDO
  28. private PdoConfig[] m_tpdoConfigs = new PdoConfig[4]; // 发送PDO
  29. // PDO映射对象
  30. private PdoMapping[,] m_rpdoMappings = new PdoMapping[4, 4]; // RPDO映射表
  31. private PdoMapping[,] m_tpdoMappings = new PdoMapping[4, 4]; // TPDO映射表
  32. // 应用数据缓冲区 (用于PDO数据交换)
  33. private Dictionary<ushort, object> m_objectDictionary = new Dictionary<ushort, object>();
  34. // 心跳参数
  35. private ushort m_producerHeartbeatTime = 0; // 心跳时间(ms),0表示禁用
  36. private Timer m_heartbeatTimer;
  37. // SYNC参数
  38. private bool m_syncEnabled = false;
  39. private byte m_syncCounter = 0;
  40. // 线程控制
  41. private CancellationTokenSource m_cancellationTokenSource;
  42. private Task m_receiveTask;
  43. private bool m_isRunning = false;
  44. #endregion
  45. #region 事件定义
  46. /// <summary>
  47. /// RPDO接收事件
  48. /// </summary>
  49. public event Action<byte, byte, byte[]> OnRpdoReceived;
  50. /// <summary>
  51. /// NMT状态改变事件
  52. /// </summary>
  53. public event Action<NmtState, NmtState> OnNmtStateChanged;
  54. /// <summary>
  55. /// SDO读取请求事件
  56. /// </summary>
  57. public event Action<byte, ushort, byte> OnSdoReadRequest;
  58. /// <summary>
  59. /// SDO写入请求事件
  60. /// </summary>
  61. public event Action<byte, ushort, byte, uint> OnSdoWriteRequest;
  62. #endregion
  63. #region 构造函数
  64. /// <summary>
  65. /// 构造函数
  66. /// </summary>
  67. /// <param name="nodeId">节点ID(1-127)</param>
  68. /// <param name="deviceType">设备类型</param>
  69. /// <param name="deviceIndex">设备索引</param>
  70. /// <param name="canIndex">CAN通道索引</param>
  71. public CanOpenSlaveDevice(byte nodeId = 1, UInt32 deviceType = 4, UInt32 deviceIndex = 0, UInt32 canIndex = 0)
  72. {
  73. if (nodeId < 1 || nodeId > 127)
  74. throw new ArgumentException("节点ID必须在1-127之间");
  75. m_nodeId = nodeId;
  76. m_deviceType = deviceType;
  77. m_deviceIndex = deviceIndex;
  78. m_canIndex = canIndex;
  79. InitializePdoConfigs();
  80. InitializeObjectDictionary();
  81. }
  82. #endregion
  83. #region 初始化方法
  84. /// <summary>
  85. /// 初始化PDO配置 - 根据EDS文件
  86. /// </summary>
  87. private void InitializePdoConfigs()
  88. {
  89. // 初始化RPDO配置 (1400-1403)
  90. for (int i = 0; i < 4; i++)
  91. {
  92. m_rpdoConfigs[i] = new PdoConfig
  93. {
  94. CobId = GetRpdoCobId((byte)(i + 1), m_nodeId),
  95. TransmissionType = 0xFF, // 事件驱动
  96. Enabled = true
  97. };
  98. // 初始化映射表为空
  99. for (int j = 0; j < 4; j++)
  100. {
  101. m_rpdoMappings[i, j] = new PdoMapping
  102. {
  103. Index = 0,
  104. SubIndex = 0,
  105. BitLength = 0
  106. };
  107. }
  108. }
  109. // 初始化TPDO配置 (1800-1803)
  110. for (int i = 0; i < 4; i++)
  111. {
  112. m_tpdoConfigs[i] = new PdoConfig
  113. {
  114. CobId = GetTpdoCobId((byte)(i + 1), m_nodeId),
  115. TransmissionType = 0xFF, // 事件驱动
  116. Enabled = true
  117. };
  118. // 初始化映射表为空
  119. for (int j = 0; j < 4; j++)
  120. {
  121. m_tpdoMappings[i, j] = new PdoMapping
  122. {
  123. Index = 0,
  124. SubIndex = 0,
  125. BitLength = 0
  126. };
  127. }
  128. }
  129. }
  130. /// <summary>
  131. /// 初始化对象字典 - 根据EDS文件定义
  132. /// </summary>
  133. private void InitializeObjectDictionary()
  134. {
  135. // 1000h - Device type
  136. m_objectDictionary[(ushort)0x1000] = (uint)0x00000000; // 默认值
  137. // 1001h - Error register
  138. m_objectDictionary[(ushort)0x1001] = (byte)0x00;
  139. // 1017h - Producer Heartbeat Time (心跳时间)
  140. // 根据CANopen标准,此对象用于配置心跳发送间隔
  141. m_objectDictionary[(ushort)0x1017] = (ushort)1000; // 默认1000ms
  142. // 1018h - Identity Object
  143. m_objectDictionary[(ushort)0x1018] = new IdentityObject
  144. {
  145. VendorId = 0x0000000D, // HanLin
  146. ProductCode = 0x00000001, // CCDCountCanOpenSlave
  147. RevisionNumber = 0x00010001
  148. };
  149. // 1800-1803h - TPDO communication parameters
  150. for (int i = 0; i < 4; i++)
  151. {
  152. m_objectDictionary[(ushort)(0x1800 + i)] = new TpdoCommParam
  153. {
  154. CobId = GetTpdoCobId((byte)(i + 1), m_nodeId),
  155. TransmissionType = 0xFF
  156. };
  157. }
  158. // 1A00-1A03h - TPDO mapping parameters
  159. for (int i = 0; i < 4; i++)
  160. {
  161. m_objectDictionary[(ushort)(0x1A00 + i)] = new PdoMappingParam
  162. {
  163. NumberOfEntries = 4,
  164. Mappings = new uint[4]
  165. };
  166. }
  167. // 1400-1403h - RPDO communication parameters
  168. for (int i = 0; i < 4; i++)
  169. {
  170. m_objectDictionary[(ushort)(0x1400 + i)] = new RpdoCommParam
  171. {
  172. CobId = GetRpdoCobId((byte)(i + 1), m_nodeId),
  173. TransmissionType = 0xFF
  174. };
  175. }
  176. // 1600-1603h - RPDO mapping parameters
  177. for (int i = 0; i < 4; i++)
  178. {
  179. m_objectDictionary[(ushort)(0x1600 + i)] = new PdoMappingParam
  180. {
  181. NumberOfEntries = 4,
  182. Mappings = new uint[4]
  183. };
  184. }
  185. }
  186. #endregion
  187. #region CAN底层操作
  188. /// <summary>
  189. /// 设置波特率参数
  190. /// </summary>
  191. private void SetBaudRate(ref VCI_INIT_CONFIG config, CanBaudRate baudRate)
  192. {
  193. switch (baudRate)
  194. {
  195. case CanBaudRate.BaudRate_1M:
  196. config.Timing0 = 0x00;
  197. config.Timing1 = 0x14;
  198. break;
  199. case CanBaudRate.BaudRate_500K:
  200. config.Timing0 = 0x00;
  201. config.Timing1 = 0x1C;
  202. break;
  203. case CanBaudRate.BaudRate_250K:
  204. config.Timing0 = 0x01;
  205. config.Timing1 = 0x1C;
  206. break;
  207. case CanBaudRate.BaudRate_125K:
  208. config.Timing0 = 0x03;
  209. config.Timing1 = 0x1C;
  210. break;
  211. default:
  212. config.Timing0 = 0x00;
  213. config.Timing1 = 0x1C;
  214. break;
  215. }
  216. }
  217. /// <summary>
  218. /// 发送原始CAN帧
  219. /// </summary>
  220. private unsafe bool SendCanFrame(uint cobId, byte[] data, bool isExtended = false, bool isRemote = false)
  221. {
  222. if (!m_isStarted)
  223. {
  224. Console.WriteLine("CAN通道未启动");
  225. return false;
  226. }
  227. if (data == null || data.Length > 8)
  228. {
  229. Console.WriteLine("SendCanFrame: 数据长度无效(必须0-8字节)");
  230. return false;
  231. }
  232. VCI_CAN_OBJ sendObj = new VCI_CAN_OBJ();
  233. sendObj.ID = cobId;
  234. sendObj.ExternFlag = isExtended ? (byte)1 : (byte)0;
  235. sendObj.RemoteFlag = isRemote ? (byte)1 : (byte)0;
  236. sendObj.SendType = 0;
  237. sendObj.TimeFlag = 0;
  238. sendObj.DataLen = (byte)data.Length;
  239. // 复制数据
  240. for (int i = 0; i < data.Length; i++)
  241. {
  242. sendObj.Data[i] = data[i];
  243. }
  244. // 填充剩余字节
  245. for (int i = data.Length; i < 8; i++)
  246. {
  247. sendObj.Data[i] = 0;
  248. }
  249. UInt32 result = CanLibraryClass.VCI_Transmit(m_deviceType, m_deviceIndex, m_canIndex, ref sendObj, 1);
  250. if (result == 0)
  251. {
  252. Console.WriteLine($"发送CAN帧失败 - COB-ID: 0x{cobId:X3}");
  253. return false;
  254. }
  255. if (cobId != 1793)
  256. {
  257. string logStr = $"发送CAN帧 - COB-ID: 0x{cobId:X3}, Len: {data.Length}, Data: ";
  258. for (int i = 0; i < data.Length; i++)
  259. {
  260. logStr += $"{data[i]:X2} ";
  261. }
  262. Console.WriteLine(logStr);
  263. }
  264. return true;
  265. }
  266. /// <summary>
  267. /// 接收CAN帧
  268. /// </summary>
  269. private unsafe List<CanOpenFrame> ReceiveCanFrames(int timeoutMs = 100)
  270. {
  271. if (!m_isStarted)
  272. {
  273. Console.WriteLine("CAN通道未启动");
  274. return new List<CanOpenFrame>();
  275. }
  276. List<CanOpenFrame> receivedFrames = new List<CanOpenFrame>();
  277. VCI_CAN_OBJ[] receiveBuffer = new VCI_CAN_OBJ[2500];
  278. UInt32 count = CanLibraryClass.VCI_Receive(
  279. m_deviceType,
  280. m_deviceIndex,
  281. m_canIndex,
  282. ref receiveBuffer[0],
  283. 2500,
  284. timeoutMs);
  285. if (count == 0xFFFFFFFF) count = 0;
  286. for (UInt32 i = 0; i < count; i++)
  287. {
  288. CanOpenFrame frame = new CanOpenFrame();
  289. frame.CobId = receiveBuffer[i].ID;
  290. frame.IsExtended = receiveBuffer[i].ExternFlag == 1;
  291. frame.IsRemote = receiveBuffer[i].RemoteFlag == 1;
  292. frame.DataLength = receiveBuffer[i].DataLen;
  293. frame.TimeStamp = receiveBuffer[i].TimeStamp;
  294. frame.Data = new byte[8];
  295. for (int j = 0; j < 8; j++)
  296. {
  297. frame.Data[j] = receiveBuffer[i].Data[j];
  298. }
  299. receivedFrames.Add(frame);
  300. // 输出每个CAN帧的详细信息,统一格式
  301. string dataHex = BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ");
  302. Console.WriteLine($"接收CAN帧 - COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {dataHex}");
  303. }
  304. return receivedFrames;
  305. }
  306. #endregion
  307. #region 启动和停止
  308. /// <summary>
  309. /// 启动从站设备
  310. /// </summary>
  311. /// <param name="baudRate">波特率</param>
  312. /// <returns>是否成功</returns>
  313. public bool Start(CanBaudRate baudRate = CanBaudRate.BaudRate_1M)
  314. {
  315. if (m_isRunning)
  316. {
  317. Console.WriteLine("从站设备已在运行");
  318. return true;
  319. }
  320. // 1. 打开设备
  321. UInt32 result = CanLibraryClass.VCI_OpenDevice(m_deviceType, m_deviceIndex, 0);
  322. if (result == 0)
  323. {
  324. Console.WriteLine("打开CAN设备失败");
  325. return false;
  326. }
  327. m_isOpened = true;
  328. Console.WriteLine("CAN设备打开成功");
  329. // 2. 初始化CAN通道
  330. VCI_INIT_CONFIG config = new VCI_INIT_CONFIG();
  331. config.AccCode = 0x00000000;
  332. config.AccMask = 0xFFFFFFFF;
  333. config.Filter = 1; // 接收所有帧
  334. config.Mode = 0; // 正常模式
  335. SetBaudRate(ref config, baudRate);
  336. result = CanLibraryClass.VCI_InitCAN(m_deviceType, m_deviceIndex, m_canIndex, ref config);
  337. if (result == 0)
  338. {
  339. Console.WriteLine("初始化CAN通道失败");
  340. Close();
  341. return false;
  342. }
  343. Console.WriteLine($"CAN通道初始化成功,波特率:{baudRate}");
  344. // 3. 启动CAN通道
  345. result = CanLibraryClass.VCI_StartCAN(m_deviceType, m_deviceIndex, m_canIndex);
  346. if (result == 0)
  347. {
  348. Console.WriteLine("启动CAN通道失败");
  349. Close();
  350. return false;
  351. }
  352. m_isStarted = true;
  353. Console.WriteLine("CAN通道启动成功");
  354. m_isRunning = true;
  355. m_currentState = NmtState.Initializing;
  356. // 默认配置心跳为1000ms
  357. ConfigureHeartbeat(1000);
  358. // 发送无错误紧急报文,表明设备正常
  359. SendNoErrorEmcy();
  360. // 等待主站发送NMT命令来控制状态转换
  361. Console.WriteLine($"NMT: 节点{m_nodeId}已初始化,等待主站命令...");
  362. // 启动接收线程
  363. m_cancellationTokenSource = new CancellationTokenSource();
  364. m_receiveTask = Task.Run(() => ReceiveLoop(m_cancellationTokenSource.Token));
  365. Console.WriteLine($"CANopen从站启动成功 - 节点ID: {m_nodeId}");
  366. return true;
  367. }
  368. /// <summary>
  369. /// 停止从站设备
  370. /// </summary>
  371. public void Stop()
  372. {
  373. if (!m_isRunning)
  374. return;
  375. m_isRunning = false;
  376. // 停止接收线程
  377. m_cancellationTokenSource?.Cancel();
  378. m_receiveTask?.Wait(1000);
  379. m_receiveTask?.Dispose();
  380. // 停止心跳定时器
  381. m_heartbeatTimer?.Dispose();
  382. // 关闭CAN
  383. Close();
  384. m_currentState = NmtState.Stopped;
  385. Console.WriteLine("CANopen从站已停止");
  386. }
  387. /// <summary>
  388. /// 关闭CAN设备
  389. /// </summary>
  390. private void Close()
  391. {
  392. if (m_isStarted)
  393. {
  394. CanLibraryClass.VCI_ResetCAN(m_deviceType, m_deviceIndex, m_canIndex);
  395. m_isStarted = false;
  396. }
  397. if (m_isOpened)
  398. {
  399. CanLibraryClass.VCI_CloseDevice(m_deviceType, m_deviceIndex);
  400. m_isOpened = false;
  401. Console.WriteLine("CAN设备已关闭");
  402. }
  403. }
  404. #endregion
  405. #region 接收处理循环
  406. /// <summary>
  407. /// CAN帧接收循环
  408. /// </summary>
  409. private void ReceiveLoop(CancellationToken cancellationToken)
  410. {
  411. while (!cancellationToken.IsCancellationRequested)
  412. {
  413. try
  414. {
  415. var frames = ReceiveCanFrames(50);
  416. foreach (var frame in frames)
  417. {
  418. ProcessCanFrame(frame);
  419. }
  420. }
  421. catch (Exception ex)
  422. {
  423. Console.WriteLine($"接收错误: {ex.Message}");
  424. }
  425. Thread.Sleep(1); // 避免CPU占用过高
  426. }
  427. }
  428. /// <summary>
  429. /// 处理接收到的CAN帧
  430. /// </summary>
  431. private void ProcessCanFrame(CanOpenFrame frame)
  432. {
  433. // 记录所有接收到的帧(用于调试)
  434. Console.WriteLine($"[接收详细] COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  435. // 检查是否是NMT命令 (COB-ID = 0x000)
  436. if (frame.CobId == (uint)0x000 && frame.DataLength >= 2)
  437. {
  438. Console.WriteLine($"[NMT命令] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  439. ProcessNmtCommand(frame.Data);
  440. return;
  441. }
  442. // 检查是否是SYNC (COB-ID = 0x080)
  443. if (frame.CobId == (uint)0x080)
  444. {
  445. Console.WriteLine($"[SYNC] COB-ID: 0x{frame.CobId:X3}");
  446. ProcessSync(frame.Data);
  447. return;
  448. }
  449. // 检查是否是SDO请求 (COB-ID = 0x600 + nodeId)
  450. uint sdoRequestCobId = (uint)0x600 + m_nodeId;
  451. if (frame.CobId == sdoRequestCobId)
  452. {
  453. Console.WriteLine($"[SDO请求] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  454. ProcessSdoRequest(frame.Data);
  455. return;
  456. }
  457. // 检查是否是RPDO
  458. for (int i = 0; i < 4; i++)
  459. {
  460. if (frame.CobId == m_rpdoConfigs[i].CobId && m_rpdoConfigs[i].Enabled)
  461. {
  462. Console.WriteLine($"[RPDO{i+1}] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  463. ProcessRpdo((byte)(i + 1), frame.Data, frame.DataLength);
  464. return;
  465. }
  466. }
  467. // 其他未知帧
  468. Console.WriteLine($"[未知帧] COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  469. }
  470. #endregion
  471. #region NMT处理
  472. /// <summary>
  473. /// 处理NMT命令
  474. /// </summary>
  475. private void ProcessNmtCommand(byte[] data)
  476. {
  477. byte commandSpecifier = data[0];
  478. byte nodeId = data[1];
  479. // 检查是否是广播命令或针对本节点的命令
  480. if (nodeId != 0 && nodeId != m_nodeId)
  481. return;
  482. NmtState oldState = m_currentState;
  483. Console.WriteLine($"[NMT详细] 收到命令: 0x{commandSpecifier:X2}, 目标节点: {nodeId}, 当前状态: {m_currentState}");
  484. switch (commandSpecifier)
  485. {
  486. case 0x01: // 启动节点
  487. Console.WriteLine($"[NMT详细] 执行启动节点命令");
  488. m_currentState = NmtState.Operational;
  489. Console.WriteLine($"NMT: 节点{m_nodeId}进入运行状态");
  490. break;
  491. case 0x02: // 停止节点
  492. Console.WriteLine($"[NMT详细] 执行停止节点命令");
  493. m_currentState = NmtState.Stopped;
  494. Console.WriteLine($"NMT: 节点{m_nodeId}进入停止状态");
  495. break;
  496. case 0x80: // 进入预操作状态
  497. Console.WriteLine($"[NMT详细] 执行进入预操作状态命令");
  498. m_currentState = NmtState.PreOperational;
  499. Console.WriteLine($"NMT: 节点{m_nodeId}进入预操作状态");
  500. break;
  501. case 0x81: // 重置节点
  502. Console.WriteLine($"[NMT详细] 执行重置节点命令 - 开始初始化流程");
  503. // 执行节点重置逻辑
  504. m_currentState = NmtState.Initializing;
  505. // 打印当前对象字典状态(用于调试)
  506. Console.WriteLine($"[NMT调试] 当前对象字典关键参数:");
  507. if (m_objectDictionary.ContainsKey((ushort)0x1000))
  508. Console.WriteLine($" - 设备类型(0x1000): {m_objectDictionary[(ushort)0x1000]}");
  509. if (m_objectDictionary.ContainsKey((ushort)0x1001))
  510. Console.WriteLine($" - 错误寄存器(0x1001): {m_objectDictionary[(ushort)0x1001]}");
  511. if (m_objectDictionary.ContainsKey((ushort)0x1017))
  512. Console.WriteLine($" - 心跳时间(0x1017): {m_objectDictionary[(ushort)0x1017]}ms");
  513. if (m_objectDictionary.ContainsKey((ushort)0x1018))
  514. {
  515. var identity = m_objectDictionary[(ushort)0x1018] as IdentityObject;
  516. if (identity != null)
  517. Console.WriteLine($" - 厂商ID(0x1018.1): 0x{identity.VendorId:X4}");
  518. }
  519. // 短暂延迟后自动进入预操作状态(模拟初始化过程)
  520. Task.Delay(50).ContinueWith(_ =>
  521. {
  522. // 立即发送一次心跳,确认状态转换
  523. SendHeartbeat();
  524. m_currentState = NmtState.PreOperational;
  525. Console.WriteLine($"NMT: 节点{m_nodeId}初始化完成,进入预操作状态");
  526. OnNmtStateChanged?.Invoke(oldState, m_currentState);
  527. });
  528. return; // 提前返回,等待异步完成
  529. case 0x82: // 重置通信
  530. Console.WriteLine($"[NMT详细] 执行重置通信命令");
  531. m_currentState = NmtState.PreOperational;
  532. Console.WriteLine($"NMT: 节点{m_nodeId}通信重置");
  533. // TODO: 执行通信重置逻辑
  534. break;
  535. default:
  536. Console.WriteLine($"[NMT详细] 未知命令: 0x{commandSpecifier:X2}");
  537. break;
  538. }
  539. // 触发状态改变事件
  540. if (oldState != m_currentState)
  541. {
  542. Console.WriteLine($"[NMT详细] 状态改变: {oldState} → {m_currentState}");
  543. OnNmtStateChanged?.Invoke(oldState, m_currentState);
  544. }
  545. else
  546. {
  547. Console.WriteLine($"[NMT详细] 状态未改变: {oldState}");
  548. }
  549. }
  550. #endregion
  551. #region SYNC处理
  552. /// <summary>
  553. /// 处理SYNC帧
  554. /// </summary>
  555. private void ProcessSync(byte[] data)
  556. {
  557. if (data.Length > 0)
  558. {
  559. m_syncCounter = data[0];
  560. }
  561. // 如果TPDO配置为同步触发,则发送
  562. for (int i = 0; i < 4; i++)
  563. {
  564. if (m_tpdoConfigs[i].Enabled &&
  565. m_tpdoConfigs[i].TransmissionType >= 1 &&
  566. m_tpdoConfigs[i].TransmissionType <= 240)
  567. {
  568. //SendTpdo((byte)(i + 1));
  569. }
  570. }
  571. }
  572. #endregion
  573. #region SDO处理
  574. /// <summary>
  575. /// 处理SDO请求
  576. /// </summary>
  577. private void ProcessSdoRequest(byte[] data)
  578. {
  579. if (data.Length < 8)
  580. return;
  581. byte cs = (byte)(data[0] >> 5); // 命令 specifier
  582. ushort index = (ushort)(data[1] | (data[2] << 8));
  583. byte subIndex = data[3];
  584. switch (cs)
  585. {
  586. case 2: // SDO上传请求 (读)
  587. HandleSdoUpload(index, subIndex);
  588. break;
  589. case 1: // SDO下载请求 (写)
  590. HandleSdoDownload(index, subIndex, data);
  591. break;
  592. default:
  593. Console.WriteLine($"未支持的SDO命令: CS={cs}");
  594. break;
  595. }
  596. }
  597. /// <summary>
  598. /// 处理SDO上传 (读对象字典)
  599. /// </summary>
  600. private void HandleSdoUpload(ushort index, byte subIndex)
  601. {
  602. byte[] response = new byte[8];
  603. Console.WriteLine($"[SDO详细] 收到上传请求: 索引0x{index:X4}, 子索引{subIndex}");
  604. // 触发自定义事件
  605. OnSdoReadRequest?.Invoke(m_nodeId, index, subIndex);
  606. // 根据索引返回数据
  607. if (m_objectDictionary.TryGetValue(index, out object value))
  608. {
  609. Console.WriteLine($"[SDO详细] 找到对象 0x{index:X4}");
  610. if (subIndex == 0)
  611. {
  612. // 返回子索引数量
  613. response[0] = 0x4F; // 快速上传响应
  614. response[1] = (byte)(index & 0xFF);
  615. response[2] = (byte)(index >> 8);
  616. response[3] = subIndex;
  617. if (value is IdentityObject identity)
  618. {
  619. response[4] = 4; // 4个子条目
  620. Console.WriteLine($"[SDO详细] 返回Identity对象,子条目数: 4");
  621. }
  622. else if (value is PdoMappingParam mappingParam)
  623. {
  624. response[4] = mappingParam.NumberOfEntries;
  625. Console.WriteLine($"[SDO详细] 返回PDO映射参数,子条目数: {mappingParam.NumberOfEntries}");
  626. }
  627. else
  628. {
  629. response[4] = 0;
  630. Console.WriteLine($"[SDO详细] 返回简单对象,子条目数: 0");
  631. }
  632. }
  633. else
  634. {
  635. // 返回具体子索引的值
  636. response[0] = 0x4B; // 4字节快速上传
  637. response[1] = (byte)(index & 0xFF);
  638. response[2] = (byte)(index >> 8);
  639. response[3] = subIndex;
  640. uint dataValue = 0;
  641. if (index == (ushort)0x1000 && value is uint deviceType)
  642. {
  643. dataValue = deviceType;
  644. Console.WriteLine($"[SDO详细] 返回设备类型: 0x{dataValue:X8}");
  645. }
  646. else if (index == (ushort)0x1001 && value is byte errorReg)
  647. {
  648. response[0] = 0x4F; // 1字节
  649. dataValue = errorReg;
  650. Console.WriteLine($"[SDO详细] 返回错误寄存器: 0x{dataValue:X2}");
  651. }
  652. else if (index == (ushort)0x1017 && value is ushort heartbeatTime)
  653. {
  654. response[0] = 0x4B; // 2字节
  655. dataValue = heartbeatTime;
  656. Console.WriteLine($"[SDO详细] 返回心跳时间: {heartbeatTime}ms");
  657. }
  658. else if (index == (ushort)0x1018 && value is IdentityObject identity)
  659. {
  660. response[0] = 0x4B;
  661. switch (subIndex)
  662. {
  663. case 1: dataValue = identity.VendorId; break;
  664. case 2: dataValue = identity.ProductCode; break;
  665. case 3: dataValue = identity.RevisionNumber; break;
  666. default: dataValue = 0; break;
  667. }
  668. Console.WriteLine($"[SDO详细] 返回Identity子索引{subIndex}: 0x{dataValue:X8}");
  669. }
  670. else if ((index >= (ushort)0x1800 && index <= (ushort)0x1803) && value is TpdoCommParam tpdoParam)
  671. {
  672. switch (subIndex)
  673. {
  674. case 1: dataValue = tpdoParam.CobId; break;
  675. case 2: dataValue = tpdoParam.TransmissionType; break;
  676. default: dataValue = 0; break;
  677. }
  678. Console.WriteLine($"[SDO详细] 返回TPDO{index - 0x1800 + 1}通信参数子索引{subIndex}: 0x{dataValue:X8}");
  679. }
  680. else if ((index >= (ushort)0x1400 && index <= (ushort)0x1403) && value is RpdoCommParam rpdoParam)
  681. {
  682. switch (subIndex)
  683. {
  684. case 1: dataValue = rpdoParam.CobId; break;
  685. case 2: dataValue = rpdoParam.TransmissionType; break;
  686. default: dataValue = 0; break;
  687. }
  688. Console.WriteLine($"[SDO详细] 返回RPDO{index - 0x1400 + 1}通信参数子索引{subIndex}: 0x{dataValue:X8}");
  689. }
  690. response[4] = (byte)(dataValue & 0xFF);
  691. response[5] = (byte)((dataValue >> 8) & 0xFF);
  692. response[6] = (byte)((dataValue >> 16) & 0xFF);
  693. response[7] = (byte)((dataValue >> 24) & 0xFF);
  694. }
  695. // 发送SDO响应
  696. uint responseCobId = (uint)0x580 + m_nodeId;
  697. Console.WriteLine($"[SDO响应] COB-ID: 0x{responseCobId:X3}, Index: 0x{index:X4}, SubIndex: {subIndex}, Data: {BitConverter.ToString(response).Replace("-", " ")}");
  698. SendCanFrame(responseCobId, response);
  699. }
  700. else
  701. {
  702. // 对象不存在,发送错误响应
  703. Console.WriteLine($"[SDO详细] 对象 0x{index:X4} 不存在,返回错误");
  704. response[0] = 0x80; // 错误响应
  705. response[1] = (byte)(index & 0xFF);
  706. response[2] = (byte)(index >> 8);
  707. response[3] = subIndex;
  708. response[4] = 0x06; // 错误代码: 对象不存在
  709. response[5] = 0x02;
  710. response[6] = 0x00;
  711. response[7] = 0x00;
  712. uint responseCobId = (uint)0x580 + m_nodeId;
  713. Console.WriteLine($"[SDO错误响应] COB-ID: 0x{responseCobId:X3}, Index: 0x{index:X4}, SubIndex: {subIndex}");
  714. SendCanFrame(responseCobId, response);
  715. }
  716. }
  717. /// <summary>
  718. /// 处理SDO下载 (写对象字典)
  719. /// </summary>
  720. private void HandleSdoDownload(ushort index, byte subIndex, byte[] data)
  721. {
  722. uint value = (uint)(data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24));
  723. // 触发自定义事件
  724. OnSdoWriteRequest?.Invoke(m_nodeId, index, subIndex, value);
  725. // 更新对象字典
  726. if (m_objectDictionary.ContainsKey(index))
  727. {
  728. // 特殊处理:心跳时间对象 (0x1017)
  729. if (index == (ushort)0x1017 && subIndex == 0)
  730. {
  731. ushort heartbeatTime = (ushort)value;
  732. m_objectDictionary[index] = heartbeatTime;
  733. ConfigureHeartbeat(heartbeatTime); // 重新配置心跳
  734. Console.WriteLine($"[SDO写入] 心跳时间已更新为: {heartbeatTime}ms");
  735. }
  736. else if (index >= (ushort)0x1800 && index <= (ushort)0x1803 && m_objectDictionary[index] is TpdoCommParam tpdoParam)
  737. {
  738. int pdoIndex = index - (ushort)0x1800;
  739. if (subIndex == 1)
  740. {
  741. tpdoParam.CobId = value;
  742. m_tpdoConfigs[pdoIndex].CobId = value;
  743. }
  744. else if (subIndex == 2)
  745. {
  746. tpdoParam.TransmissionType = (byte)value;
  747. m_tpdoConfigs[pdoIndex].TransmissionType = (byte)value;
  748. }
  749. }
  750. else if (index >= (ushort)0x1400 && index <= (ushort)0x1403 && m_objectDictionary[index] is RpdoCommParam rpdoParam)
  751. {
  752. int pdoIndex = index - (ushort)0x1400;
  753. if (subIndex == 1)
  754. {
  755. rpdoParam.CobId = value;
  756. m_rpdoConfigs[pdoIndex].CobId = value;
  757. }
  758. else if (subIndex == 2)
  759. {
  760. rpdoParam.TransmissionType = (byte)value;
  761. m_rpdoConfigs[pdoIndex].TransmissionType = (byte)value;
  762. }
  763. }
  764. }
  765. // 发送SDO确认响应
  766. byte[] response = new byte[8];
  767. response[0] = 0x60; // SDO下载确认
  768. response[1] = (byte)(index & 0xFF);
  769. response[2] = (byte)(index >> 8);
  770. response[3] = subIndex;
  771. uint responseCobId = (uint)0x580 + m_nodeId;
  772. SendCanFrame(responseCobId, response);
  773. }
  774. #endregion
  775. #region RPDO处理
  776. /// <summary>
  777. /// 处理接收到的RPDO
  778. /// </summary>
  779. private void ProcessRpdo(byte pdoNumber, byte[] data, byte length)
  780. {
  781. Console.WriteLine($"收到RPDO{pdoNumber}: 长度{length}字节");
  782. // 复制有效数据
  783. byte[] validData = new byte[length];
  784. Array.Copy(data, validData, length);
  785. // 触发事件
  786. OnRpdoReceived?.Invoke(m_nodeId, pdoNumber, validData);
  787. }
  788. #endregion
  789. #region TPDO发送
  790. /// <summary>
  791. /// 发送TPDO
  792. /// 长度不可以超过PLC中PDO映射的长度
  793. /// </summary>
  794. public void SendTpdo(byte pdoNumber, byte[] pdoData)
  795. {
  796. if (pdoNumber < 1 || pdoNumber > 4)
  797. throw new ArgumentException("PDO编号必须在1-4之间");
  798. if (!m_tpdoConfigs[pdoNumber - 1].Enabled)
  799. {
  800. Console.WriteLine($"TPDO{pdoNumber}未启用");
  801. return;
  802. }
  803. uint cobId = m_tpdoConfigs[pdoNumber - 1].CobId;
  804. SendCanFrame(cobId, pdoData);
  805. Console.WriteLine($"发送TPDO{pdoNumber}: COB-ID 0x{cobId:X3}");
  806. }
  807. #endregion
  808. #region 心跳机制
  809. /// <summary>
  810. /// 配置心跳
  811. /// </summary>
  812. /// <param name="heartbeatTimeMs">心跳时间(毫秒),0表示禁用</param>
  813. public void ConfigureHeartbeat(ushort heartbeatTimeMs)
  814. {
  815. m_producerHeartbeatTime = heartbeatTimeMs;
  816. // 同步更新对象字典中的 1017h 对象
  817. if (m_objectDictionary.ContainsKey((ushort)0x1017))
  818. {
  819. m_objectDictionary[(ushort)0x1017] = heartbeatTimeMs;
  820. }
  821. // 停止旧的定时器
  822. m_heartbeatTimer?.Dispose();
  823. if (heartbeatTimeMs > 0)
  824. {
  825. // 创建新的定时器
  826. m_heartbeatTimer = new Timer(state =>
  827. {
  828. // 只在PreOperational和Operational状态下发送心跳
  829. // Initializing和Stopped状态下不发送心跳
  830. if (m_currentState == NmtState.Operational ||
  831. m_currentState == NmtState.PreOperational)
  832. {
  833. SendHeartbeat();
  834. }
  835. else
  836. {
  837. // 在其他状态下跳过心跳发送
  838. //Console.WriteLine($"[心跳跳过] 当前状态: {m_currentState}, 不发送心跳");
  839. }
  840. }, null, heartbeatTimeMs, heartbeatTimeMs);
  841. Console.WriteLine($"心跳已配置: {heartbeatTimeMs}ms");
  842. }
  843. else
  844. {
  845. Console.WriteLine("心跳已禁用");
  846. }
  847. }
  848. /// <summary>
  849. /// 发送心跳
  850. /// </summary>
  851. private void SendHeartbeat()
  852. {
  853. byte[] heartbeatData = new byte[1];
  854. // 根据当前状态设置心跳字节
  855. switch (m_currentState)
  856. {
  857. case NmtState.Initializing:
  858. heartbeatData[0] = 0x00;
  859. break;
  860. case NmtState.PreOperational:
  861. heartbeatData[0] = 0x7F;
  862. break;
  863. case NmtState.Operational:
  864. heartbeatData[0] = 0x05;
  865. break;
  866. case NmtState.Stopped:
  867. heartbeatData[0] = 0x04;
  868. break;
  869. default:
  870. heartbeatData[0] = 0x00;
  871. break;
  872. }
  873. uint heartbeatCobId = (uint)0x700 + (uint)m_nodeId;
  874. //Console.WriteLine($"[心跳发送] COB-ID: 0x{heartbeatCobId:X3}, Data: {heartbeatData[0]:X2} (状态: {m_currentState})");
  875. SendCanFrame(heartbeatCobId, heartbeatData);
  876. }
  877. /// <summary>
  878. /// 发送无错误紧急报文 (EMCY)
  879. /// 用于在启动时表明设备无错误状态
  880. /// </summary>
  881. private void SendNoErrorEmcy()
  882. {
  883. byte[] emcyData = new byte[8];
  884. emcyData[0] = 0x00; // 错误代码低字节 - 无错误
  885. emcyData[1] = 0x00; // 错误代码高字节
  886. emcyData[2] = 0x00; // 错误寄存器
  887. uint emcyCobId = (uint)0x080 + (uint)m_nodeId;
  888. Console.WriteLine($"[EMCY] 发送无错误报文 - COB-ID: 0x{emcyCobId:X3}");
  889. SendCanFrame(emcyCobId, emcyData);
  890. }
  891. #endregion
  892. #region 辅助方法
  893. /// <summary>
  894. /// 获取RPDO的COB-ID
  895. /// </summary>
  896. private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
  897. {
  898. switch (pdoNumber)
  899. {
  900. case 1: return (uint)0x200 + nodeId;
  901. case 2: return (uint)0x300 + nodeId;
  902. case 3: return (uint)0x400 + nodeId;
  903. case 4: return (uint)0x500 + nodeId;
  904. default: throw new ArgumentException("无效的PDO编号");
  905. }
  906. }
  907. /// <summary>
  908. /// 获取TPDO的COB-ID
  909. /// </summary>
  910. private uint GetTpdoCobId(byte pdoNumber, byte nodeId)
  911. {
  912. switch (pdoNumber)
  913. {
  914. case 1: return (uint)0x180 + nodeId;
  915. case 2: return (uint)0x280 + nodeId;
  916. case 3: return (uint)0x380 + nodeId;
  917. case 4: return (uint)0x480 + nodeId;
  918. default: throw new ArgumentException("无效的PDO编号");
  919. }
  920. }
  921. #endregion
  922. #region 资源管理
  923. /// <summary>
  924. /// 释放资源
  925. /// </summary>
  926. public void Dispose()
  927. {
  928. Stop();
  929. m_cancellationTokenSource?.Dispose();
  930. m_heartbeatTimer?.Dispose();
  931. }
  932. #endregion
  933. #region 公共属性
  934. /// <summary>
  935. /// 获取当前节点ID
  936. /// </summary>
  937. public byte NodeId => m_nodeId;
  938. /// <summary>
  939. /// 获取当前NMT状态
  940. /// </summary>
  941. public NmtState CurrentState => m_currentState;
  942. /// <summary>
  943. /// 获取是否正在运行
  944. /// </summary>
  945. public bool IsRunning => m_isRunning;
  946. #endregion
  947. }
  948. #region 数据结构定义
  949. /// <summary>
  950. /// NMT状态枚举
  951. /// </summary>
  952. public enum NmtState
  953. {
  954. Initializing = 0x00,
  955. PreOperational = 0x7F,
  956. Operational = 0x05,
  957. Stopped = 0x04
  958. }
  959. /// <summary>
  960. /// PDO配置
  961. /// </summary>
  962. public class PdoConfig
  963. {
  964. public uint CobId { get; set; }
  965. public byte TransmissionType { get; set; } // 0xFF=事件驱动, 1-240=同步周期
  966. public bool Enabled { get; set; }
  967. }
  968. /// <summary>
  969. /// PDO映射
  970. /// </summary>
  971. public class PdoMapping
  972. {
  973. public ushort Index { get; set; }
  974. public byte SubIndex { get; set; }
  975. public byte BitLength { get; set; }
  976. }
  977. /// <summary>
  978. /// PDO映射参数
  979. /// </summary>
  980. public class PdoMappingParam
  981. {
  982. public byte NumberOfEntries { get; set; }
  983. public uint[] Mappings { get; set; }
  984. }
  985. /// <summary>
  986. /// 身份对象 (1018h)
  987. /// </summary>
  988. public class IdentityObject
  989. {
  990. public uint VendorId { get; set; }
  991. public uint ProductCode { get; set; }
  992. public uint RevisionNumber { get; set; }
  993. }
  994. /// <summary>
  995. /// TPDO通信参数 (1800-1803h)
  996. /// </summary>
  997. public class TpdoCommParam
  998. {
  999. public uint CobId { get; set; }
  1000. public byte TransmissionType { get; set; }
  1001. }
  1002. /// <summary>
  1003. /// RPDO通信参数 (1400-1403h)
  1004. /// </summary>
  1005. public class RpdoCommParam
  1006. {
  1007. public uint CobId { get; set; }
  1008. public byte TransmissionType { get; set; }
  1009. }
  1010. /// <summary>
  1011. /// CANopen帧数据结构
  1012. /// </summary>
  1013. public class CanOpenFrame
  1014. {
  1015. public uint CobId { get; set; }
  1016. public bool IsExtended { get; set; }
  1017. public bool IsRemote { get; set; }
  1018. public byte DataLength { get; set; }
  1019. public byte[] Data { get; set; }
  1020. public uint TimeStamp { get; set; }
  1021. public override string ToString()
  1022. {
  1023. string dataStr = "";
  1024. for (int i = 0; i < DataLength; i++)
  1025. {
  1026. dataStr += $"{Data[i]:X2} ";
  1027. }
  1028. return $"COB-ID: 0x{CobId:X3}, Len: {DataLength}, Data: [{dataStr}]";
  1029. }
  1030. }
  1031. #endregion
  1032. }