CanOpenSlave.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  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从站 - 基于创芯科技CAN卡底层库实现
  12. /// 支持SDO、PDO、NMT、心跳等标准CANopen从站功能
  13. /// </summary>
  14. public class CanOpenSlave : IDisposable
  15. {
  16. #region 私有字段
  17. private CanOpenManager m_canManager;
  18. private ObjectDictionary m_objectDict;
  19. private byte m_nodeId;
  20. private NmtState m_currentState = NmtState.Initializing;
  21. private bool m_isRunning = false;
  22. private Thread m_receiveThread;
  23. private CancellationTokenSource m_cancellationTokenSource;
  24. // PDO配置
  25. private PdoConfig[] m_tpdoConfigs = new PdoConfig[4];
  26. private PdoConfig[] m_rpdoConfigs = new PdoConfig[4];
  27. // 心跳配置
  28. private ushort m_heartbeatTime = 1000; // 毫秒
  29. private DateTime m_lastHeartbeatTime = DateTime.MinValue; // 初始化为最小值,确保首次立即发送
  30. #endregion
  31. #region 构造函数和初始化
  32. /// <summary>
  33. /// 构造函数
  34. /// </summary>
  35. /// <param name="nodeId">节点ID(1-127)</param>
  36. /// <param name="deviceType">设备类型</param>
  37. /// <param name="deviceIndex">设备索引</param>
  38. /// <param name="canIndex">CAN通道索引</param>
  39. public CanOpenSlave(byte nodeId, UInt32 deviceType = 4, UInt32 deviceIndex = 0, UInt32 canIndex = 0)
  40. {
  41. if (nodeId < 1 || nodeId > 127)
  42. throw new ArgumentException("节点ID必须在1-127之间");
  43. m_nodeId = nodeId;
  44. m_canManager = new CanOpenManager(deviceType, deviceIndex, canIndex);
  45. m_objectDict = new ObjectDictionary(nodeId);
  46. InitializePdoConfigs();
  47. }
  48. private void InitializePdoConfigs()
  49. {
  50. for (int i = 0; i < 4; i++)
  51. {
  52. m_tpdoConfigs[i] = new PdoConfig { Enabled = false, TransmissionType = 255 };
  53. m_rpdoConfigs[i] = new PdoConfig { Enabled = false };
  54. }
  55. }
  56. /// <summary>
  57. /// 初始化从站
  58. /// </summary>
  59. /// <param name="baudRate">波特率</param>
  60. /// <returns>是否成功</returns>
  61. public bool Initialize(CanBaudRate baudRate = CanBaudRate.BaudRate_500K)
  62. {
  63. if (!m_canManager.Initialize(baudRate))
  64. return false;
  65. // 设置初始状态为预操作
  66. m_currentState = NmtState.PreOperational;
  67. // 立即发送第一个心跳,让PLC能够快速发现设备
  68. SendHeartbeatImmediately();
  69. Console.WriteLine($"CANopen从站初始化完成 - 节点ID: {m_nodeId}");
  70. return true;
  71. }
  72. /// <summary>
  73. /// 立即发送心跳报文(用于设备发现)
  74. /// </summary>
  75. private void SendHeartbeatImmediately()
  76. {
  77. byte statusByte = GetNodeStatusByte();
  78. byte[] heartbeatData = new byte[] { statusByte };
  79. uint heartbeatCobId = (uint)(0x700 + m_nodeId);
  80. m_canManager.SendCanFrame(heartbeatCobId, heartbeatData);
  81. m_lastHeartbeatTime = DateTime.Now;
  82. }
  83. /// <summary>
  84. /// 启动从站
  85. /// </summary>
  86. public void Start()
  87. {
  88. if (m_isRunning)
  89. return;
  90. m_isRunning = true;
  91. m_cancellationTokenSource = new CancellationTokenSource();
  92. m_receiveThread = new Thread(ReceiveLoop);
  93. m_receiveThread.IsBackground = true;
  94. m_receiveThread.Start();
  95. Console.WriteLine("CANopen从站已启动");
  96. }
  97. /// <summary>
  98. /// 停止从站
  99. /// </summary>
  100. public void Stop()
  101. {
  102. m_isRunning = false;
  103. m_cancellationTokenSource?.Cancel();
  104. m_receiveThread?.Join(1000);
  105. Console.WriteLine("CANopen从站已停止");
  106. }
  107. #endregion
  108. #region 接收处理线程
  109. private void ReceiveLoop()
  110. {
  111. while (m_isRunning && !m_cancellationTokenSource.Token.IsCancellationRequested)
  112. {
  113. try
  114. {
  115. var frames = m_canManager.ReceiveCanFrames(50);
  116. foreach (var frame in frames)
  117. {
  118. ProcessCanFrame(frame);
  119. }
  120. // 发送心跳
  121. SendHeartbeat();
  122. Thread.Sleep(1);
  123. }
  124. catch (Exception ex)
  125. {
  126. Console.WriteLine($"接收线程异常: {ex.Message}");
  127. }
  128. }
  129. }
  130. private void ProcessCanFrame(CanOpenFrame frame)
  131. {
  132. uint cobId = frame.CobId;
  133. // NMT消息处理(COB-ID = 0x000)
  134. if (cobId == 0x000)
  135. {
  136. ProcessNmtMessage(frame);
  137. return;
  138. }
  139. // SDO请求处理
  140. if ((cobId & 0x780) == 0x600)
  141. {
  142. byte remoteNodeId = (byte)(cobId & 0x7F);
  143. if (remoteNodeId == m_nodeId)
  144. {
  145. ProcessSdoRequest(frame);
  146. }
  147. return;
  148. }
  149. // RPDO处理
  150. if ((cobId & 0x780) >= 0x200 && (cobId & 0x780) <= 0x500)
  151. {
  152. ProcessRpdo(frame);
  153. return;
  154. }
  155. }
  156. #endregion
  157. #region NMT处理
  158. private void ProcessNmtMessage(CanOpenFrame frame)
  159. {
  160. if (frame.DataLength < 2)
  161. return;
  162. byte command = frame.Data[0];
  163. byte nodeId = frame.Data[1];
  164. // 0表示广播到所有节点
  165. if (nodeId != 0 && nodeId != m_nodeId)
  166. return;
  167. switch (command)
  168. {
  169. case 0x01: // 启动节点
  170. if (m_currentState == NmtState.PreOperational || m_currentState == NmtState.Stopped)
  171. {
  172. m_currentState = NmtState.Operational;
  173. Console.WriteLine($"NMT: 节点{m_nodeId}进入运行状态");
  174. }
  175. break;
  176. case 0x02: // 停止节点
  177. if (m_currentState == NmtState.Operational)
  178. {
  179. m_currentState = NmtState.Stopped;
  180. Console.WriteLine($"NMT: 节点{m_nodeId}进入停止状态");
  181. }
  182. break;
  183. case 0x80: // 进入预操作
  184. m_currentState = NmtState.PreOperational;
  185. Console.WriteLine($"NMT: 节点{m_nodeId}进入预操作状态");
  186. break;
  187. case 0x81: // 重置节点
  188. ResetNode();
  189. break;
  190. case 0x82: // 重置通信
  191. ResetCommunication();
  192. break;
  193. }
  194. }
  195. private void ResetNode()
  196. {
  197. Console.WriteLine($"NMT: 节点{m_nodeId}重置");
  198. m_currentState = NmtState.Initializing;
  199. Thread.Sleep(100);
  200. m_currentState = NmtState.PreOperational;
  201. }
  202. private void ResetCommunication()
  203. {
  204. Console.WriteLine($"NMT: 节点{m_nodeId}重置通信");
  205. // 重置PDO配置等通信参数
  206. InitializePdoConfigs();
  207. m_currentState = NmtState.PreOperational;
  208. }
  209. #endregion
  210. #region SDO处理
  211. private void ProcessSdoRequest(CanOpenFrame frame)
  212. {
  213. if (frame.DataLength < 8)
  214. return;
  215. byte sdoCommand = frame.Data[0];
  216. ushort index = (ushort)(frame.Data[1] | (frame.Data[2] << 8));
  217. byte subIndex = frame.Data[3];
  218. // 判断是读取还是写入
  219. if ((sdoCommand & 0xE0) == 0x40) // SDO上传请求(读)
  220. {
  221. HandleSdoRead(index, subIndex);
  222. }
  223. else if ((sdoCommand & 0xE0) == 0x20) // SDO下载请求(写)
  224. {
  225. HandleSdoWrite(sdoCommand, index, subIndex, frame.Data);
  226. }
  227. }
  228. private void HandleSdoRead(ushort index, byte subIndex)
  229. {
  230. byte[] responseData = new byte[8];
  231. // 从对象字典读取数据
  232. object value = m_objectDict.Read(index, subIndex);
  233. if (value == null)
  234. {
  235. // 对象不存在,返回错误
  236. responseData[0] = 0x80; // abort transfer
  237. responseData[1] = (byte)(index & 0xFF);
  238. responseData[2] = (byte)(index >> 8);
  239. responseData[3] = subIndex;
  240. responseData[4] = 0x06; // 对象不存在
  241. responseData[5] = 0x09;
  242. responseData[6] = 0x00;
  243. responseData[7] = 0x00;
  244. SendSdoResponse(responseData);
  245. Console.WriteLine($"SDO读失败: 0x{index:X4}sub{subIndex} 不存在");
  246. return;
  247. }
  248. // 特殊处理字符串类型(设备名称、版本等)
  249. if (value is string strVal)
  250. {
  251. // 对于可见字符串,需要分段传输或使用快速上传
  252. byte[] strBytes = Encoding.ASCII.GetBytes(strVal);
  253. int maxLen = Math.Min(strBytes.Length, 4); // 最多4字节用于快速上传
  254. responseData[0] = (byte)(0x4F | ((4 - maxLen) << 2)); // 快速上传,指定有效字节数
  255. responseData[1] = (byte)(index & 0xFF);
  256. responseData[2] = (byte)(index >> 8);
  257. responseData[3] = subIndex;
  258. for (int i = 0; i < maxLen; i++)
  259. {
  260. responseData[4 + i] = strBytes[i];
  261. }
  262. SendSdoResponse(responseData);
  263. Console.WriteLine($"SDO读字符串: 0x{index:X4}sub{subIndex} = {strVal.Substring(0, maxLen)}...");
  264. return;
  265. }
  266. // 根据数据类型构造响应
  267. if (value is byte byteVal)
  268. {
  269. responseData[0] = 0x4F; // 快速上传,1字节
  270. responseData[1] = (byte)(index & 0xFF);
  271. responseData[2] = (byte)(index >> 8);
  272. responseData[3] = subIndex;
  273. responseData[4] = byteVal;
  274. }
  275. else if (value is ushort ushortVal)
  276. {
  277. responseData[0] = 0x4B; // 快速上传,2字节
  278. responseData[1] = (byte)(index & 0xFF);
  279. responseData[2] = (byte)(index >> 8);
  280. responseData[3] = subIndex;
  281. responseData[4] = (byte)(ushortVal & 0xFF);
  282. responseData[5] = (byte)(ushortVal >> 8);
  283. }
  284. else if (value is uint uintVal)
  285. {
  286. responseData[0] = 0x43; // 快速上传,4字节
  287. responseData[1] = (byte)(index & 0xFF);
  288. responseData[2] = (byte)(index >> 8);
  289. responseData[3] = subIndex;
  290. responseData[4] = (byte)(uintVal & 0xFF);
  291. responseData[5] = (byte)((uintVal >> 8) & 0xFF);
  292. responseData[6] = (byte)((uintVal >> 16) & 0xFF);
  293. responseData[7] = (byte)((uintVal >> 24) & 0xFF);
  294. }
  295. else
  296. {
  297. // 不支持的类型
  298. responseData[0] = 0x80;
  299. responseData[4] = 0x06; // 对象不存在
  300. Console.WriteLine($"SDO读失败: 0x{index:X4}sub{subIndex} 类型不支持");
  301. }
  302. SendSdoResponse(responseData);
  303. }
  304. private void HandleSdoWrite(byte command, ushort index, byte subIndex, byte[] data)
  305. {
  306. byte[] responseData = new byte[8];
  307. bool success = false;
  308. // 解析写入的值
  309. uint writeValue = 0;
  310. if ((command & 0x03) == 0x01) // 1字节
  311. {
  312. writeValue = data[4];
  313. }
  314. else if ((command & 0x03) == 0x02) // 2字节
  315. {
  316. writeValue = (uint)(data[4] | (data[5] << 8));
  317. }
  318. else if ((command & 0x03) == 0x00) // 4字节
  319. {
  320. writeValue = (uint)(data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24));
  321. }
  322. // 写入对象字典
  323. success = m_objectDict.Write(index, subIndex, writeValue);
  324. if (success)
  325. {
  326. // 发送确认响应
  327. responseData[0] = 0x60;
  328. responseData[1] = (byte)(index & 0xFF);
  329. responseData[2] = (byte)(index >> 8);
  330. responseData[3] = subIndex;
  331. SendSdoResponse(responseData);
  332. }
  333. else
  334. {
  335. // 发送错误响应
  336. responseData[0] = 0x80;
  337. responseData[1] = (byte)(index & 0xFF);
  338. responseData[2] = (byte)(index >> 8);
  339. responseData[3] = subIndex;
  340. responseData[4] = 0x06; // 对象不存在或只读
  341. responseData[5] = 0x09;
  342. responseData[6] = 0x00;
  343. responseData[7] = 0x00;
  344. SendSdoResponse(responseData);
  345. }
  346. }
  347. private void SendSdoResponse(byte[] data)
  348. {
  349. uint responseCobId = (uint)(0x580 + m_nodeId);
  350. m_canManager.SendCanFrame(responseCobId, data);
  351. }
  352. #endregion
  353. #region PDO处理
  354. private void ProcessRpdo(CanOpenFrame frame)
  355. {
  356. if (m_currentState != NmtState.Operational)
  357. return;
  358. // 解析COB-ID确定是哪个RPDO
  359. uint baseId = frame.CobId & 0x780;
  360. byte pdoNumber = 0;
  361. if (baseId == 0x200) pdoNumber = 1;
  362. else if (baseId == 0x300) pdoNumber = 2;
  363. else if (baseId == 0x400) pdoNumber = 3;
  364. else if (baseId == 0x500) pdoNumber = 4;
  365. if (pdoNumber == 0)
  366. return;
  367. int configIndex = pdoNumber - 1;
  368. if (!m_rpdoConfigs[configIndex].Enabled)
  369. return;
  370. // 将RPDO数据映射到对象字典
  371. MapRpdoToObjectDict(pdoNumber, frame.Data, frame.DataLength);
  372. Console.WriteLine($"收到RPDO{pdoNumber}: 节点{m_nodeId}, 长度{frame.DataLength}");
  373. }
  374. private void MapRpdoToObjectDict(byte pdoNumber, byte[] data, byte length)
  375. {
  376. // 根据配置的映射关系将数据写入对象字典
  377. // 这里简化处理,实际应用中需要根据具体映射配置
  378. ushort mappingIndex = (ushort)(0x1600 + pdoNumber - 1);
  379. // 示例:假设第一个映射项是0x6000:00
  380. object mappingEntry = m_objectDict.Read(mappingIndex, 0x01);
  381. if (mappingEntry != null && mappingEntry is uint mapping)
  382. {
  383. ushort objIndex = (ushort)(mapping >> 16);
  384. byte objSubIndex = (byte)(mapping & 0xFF);
  385. if (length >= 1)
  386. {
  387. m_objectDict.Write(objIndex, objSubIndex, data[0]);
  388. }
  389. }
  390. }
  391. /// <summary>
  392. /// 发送TPDO
  393. /// </summary>
  394. public void SendTpdo(byte pdoNumber)
  395. {
  396. if (m_currentState != NmtState.Operational)
  397. return;
  398. if (pdoNumber < 1 || pdoNumber > 4)
  399. return;
  400. int configIndex = pdoNumber - 1;
  401. if (!m_tpdoConfigs[configIndex].Enabled)
  402. return;
  403. // 从对象字典读取数据并打包
  404. byte[] tpdoData = PackTpdoData(pdoNumber);
  405. if (tpdoData != null && tpdoData.Length > 0)
  406. {
  407. m_canManager.SendTpdo(m_nodeId, pdoNumber, tpdoData);
  408. }
  409. }
  410. private byte[] PackTpdoData(byte pdoNumber)
  411. {
  412. List<byte> data = new List<byte>();
  413. ushort mappingIndex = (ushort)(0x1A00 + pdoNumber - 1);
  414. // 读取映射条目数量
  415. object entryCountObj = m_objectDict.Read(mappingIndex, 0x00);
  416. if (entryCountObj == null)
  417. return null;
  418. byte entryCount = Convert.ToByte(entryCountObj);
  419. // 遍历所有映射项
  420. for (byte i = 1; i <= entryCount; i++)
  421. {
  422. object mappingObj = m_objectDict.Read(mappingIndex, i);
  423. if (mappingObj == null)
  424. continue;
  425. uint mapping = Convert.ToUInt32(mappingObj);
  426. ushort objIndex = (ushort)(mapping >> 16);
  427. byte objSubIndex = (byte)((mapping >> 8) & 0xFF);
  428. byte dataSize = (byte)(mapping & 0xFF);
  429. object value = m_objectDict.Read(objIndex, objSubIndex);
  430. if (value == null)
  431. continue;
  432. // 根据数据大小添加字节
  433. if (dataSize == 8 && value is byte byteVal)
  434. {
  435. data.Add(byteVal);
  436. }
  437. else if (dataSize == 16 && value is ushort ushortVal)
  438. {
  439. data.Add((byte)(ushortVal & 0xFF));
  440. data.Add((byte)(ushortVal >> 8));
  441. }
  442. else if (dataSize == 32 && value is uint uintVal)
  443. {
  444. data.Add((byte)(uintVal & 0xFF));
  445. data.Add((byte)((uintVal >> 8) & 0xFF));
  446. data.Add((byte)((uintVal >> 16) & 0xFF));
  447. data.Add((byte)((uintVal >> 24) & 0xFF));
  448. }
  449. }
  450. return data.ToArray();
  451. }
  452. #endregion
  453. #region 心跳机制
  454. private void SendHeartbeat()
  455. {
  456. if (m_heartbeatTime == 0)
  457. return;
  458. TimeSpan elapsed = DateTime.Now - m_lastHeartbeatTime;
  459. if (elapsed.TotalMilliseconds >= m_heartbeatTime)
  460. {
  461. byte statusByte = GetNodeStatusByte();
  462. byte[] heartbeatData = new byte[] { statusByte };
  463. uint heartbeatCobId = (uint)(0x700 + m_nodeId);
  464. m_canManager.SendCanFrame(heartbeatCobId, heartbeatData);
  465. m_lastHeartbeatTime = DateTime.Now;
  466. }
  467. }
  468. private byte GetNodeStatusByte()
  469. {
  470. switch (m_currentState)
  471. {
  472. case NmtState.Initializing:
  473. return 0x00;
  474. case NmtState.PreOperational:
  475. return 0x7F;
  476. case NmtState.Operational:
  477. return 0x05;
  478. case NmtState.Stopped:
  479. return 0x04;
  480. default:
  481. return 0x7F;
  482. }
  483. }
  484. /// <summary>
  485. /// 设置心跳时间
  486. /// </summary>
  487. public void SetHeartbeatTime(ushort milliseconds)
  488. {
  489. m_heartbeatTime = milliseconds;
  490. m_objectDict.Write(0x1017, 0x00, milliseconds);
  491. }
  492. #endregion
  493. #region 公共API
  494. /// <summary>
  495. /// 设置对象字典值
  496. /// </summary>
  497. public bool SetObjectValue(ushort index, byte subIndex, object value)
  498. {
  499. return m_objectDict.Write(index, subIndex, value);
  500. }
  501. /// <summary>
  502. /// 获取对象字典值
  503. /// </summary>
  504. public object GetObjectValue(ushort index, byte subIndex)
  505. {
  506. return m_objectDict.Read(index, subIndex);
  507. }
  508. /// <summary>
  509. /// 配置TPDO
  510. /// </summary>
  511. public void ConfigureTpdo(byte pdoNumber, bool enabled, byte transmissionType = 255)
  512. {
  513. if (pdoNumber < 1 || pdoNumber > 4)
  514. return;
  515. int index = pdoNumber - 1;
  516. m_tpdoConfigs[index].Enabled = enabled;
  517. m_tpdoConfigs[index].TransmissionType = transmissionType;
  518. // 更新对象字典
  519. ushort commIndex = (ushort)(0x1800 + index);
  520. m_objectDict.Write(commIndex, 0x01, enabled ? (uint)(0x180 + m_nodeId + index * 0x100) : (uint)(0x180 + m_nodeId + index * 0x100 | 0x80000000));
  521. m_objectDict.Write(commIndex, 0x02, transmissionType);
  522. }
  523. /// <summary>
  524. /// 配置RPDO
  525. /// </summary>
  526. public void ConfigureRpdo(byte pdoNumber, bool enabled)
  527. {
  528. if (pdoNumber < 1 || pdoNumber > 4)
  529. return;
  530. int index = pdoNumber - 1;
  531. m_rpdoConfigs[index].Enabled = enabled;
  532. // 更新对象字典
  533. ushort commIndex = (ushort)(0x1400 + index);
  534. m_objectDict.Write(commIndex, 0x01, enabled ? (uint)(0x200 + m_nodeId + index * 0x100) : (uint)(0x200 + m_nodeId + index * 0x100 | 0x80000000));
  535. }
  536. /// <summary>
  537. /// 获取当前NMT状态
  538. /// </summary>
  539. public NmtState GetCurrentState()
  540. {
  541. return m_currentState;
  542. }
  543. /// <summary>
  544. /// 生成EDS文件
  545. /// </summary>
  546. public string GenerateEdsFile(string filePath = null)
  547. {
  548. var edsGenerator = new EdsFileGenerator(m_nodeId);
  549. return edsGenerator.Generate(m_objectDict, filePath);
  550. }
  551. #endregion
  552. #region 资源管理
  553. public void Dispose()
  554. {
  555. Stop();
  556. m_canManager?.Dispose();
  557. }
  558. #endregion
  559. }
  560. #region 辅助类
  561. /// <summary>
  562. /// NMT状态枚举
  563. /// </summary>
  564. public enum NmtState
  565. {
  566. Initializing,
  567. PreOperational,
  568. Operational,
  569. Stopped
  570. }
  571. /// <summary>
  572. /// PDO配置
  573. /// </summary>
  574. public class PdoConfig
  575. {
  576. public bool Enabled { get; set; }
  577. public byte TransmissionType { get; set; }
  578. public uint CobId { get; set; }
  579. }
  580. #endregion
  581. }