睿擎派以瑞芯微 RK3506 為主控芯片,底層搭載 RT-Thread 操作系統,基于專為工業場景打造的睿擎工業平臺進行開發。該平臺是全棧自主可控的軟硬件一體化解決方案,整合了數據采集、通信、控制、工業協議、AI、顯示六大核心功能,精準適配工業應用需求。
官方僅提供了基于 CANOpen 協議(即 DS402 設備規范)操作伺服電機的示例代碼,暫無 IO 模塊相關的操作參考文檔與實踐案例。經過數日的深入鉆研與反復調試,最終成功實現雷賽 EM32DX-C4 模塊的 IO 信號采集與輸出控制功能。
下面將簡要分享這段時間積累的 CANOpen 相關技術要點,以及代碼編寫與調試的具體實踐過程。
一、CANOpen背景知識介紹
CAN 總線于 1986 年 2 月正式發布,CANOpen 協議則在 1994 年 11 月推出。作為基于 CAN 總線的工業級通信協議,CANOpen 遵循 EN 50325-4 標準,是工業自動化領域主流的現場總線解決方案之一。
其核心優勢體現在標準化設計上 —— 通過統一的對象字典保障設備間互操作性,支持 PDO(過程數據對象)、SDO(服務數據對象)等靈活通信機制,兼顧實時性與數據完整性。協議內置 DS401(IO 模塊)、DS402(運動控制)等專用行規,可適配伺服電機、IO 模塊、傳感器等各類工業設備。
我們自行生產的網關產品很早就集成了 CAN 總線功能,但僅用于與自有 IO 模塊的實時通信,較少對接第三方模塊。目前國內主流智能模塊仍以 RS485 通信為主,我雖早已知曉 CANOpen 協議,但實際應用機會不多。隨著計劃基于睿賽德睿擎工業平臺開發新一代網關產品,各類現場工業總線均需深入研究。
CANOpen 協議相對復雜,核心原因是它要求設備遵循嚴格的狀態機模式,主要包含四大核心狀態:
(1) 初始化狀態(Initialization)
設備剛上電,正在進行硬件自檢和協議棧初始化
不參與網絡通信,不接收任何命令 (除基本復位)
完成后自動進入 Pre-operational 狀態,并發送一個啟動心跳信號
(2) 預操作狀態(Pre-operationa)
設備已初始化完成,等待配置
可接收 SDO (服務數據對象),進行參數配置和診斷
PDO (過程數據對象) 通信被禁用,無法進行正常數據交換
是唯一可修改對象字典的狀態,適合設備參數配置
(3) 操作狀態(Operational)
設備正常工作狀態,所有功能激活
PDO 和 SDO 通信全部啟用,可收發過程數據
設備自主執行任務,響應網絡請求
由 NMT 主機發送 “Start Node” 命令觸發進入
(4) 停止狀態(Stopped)
設備的安全狀態,功能受到限制。
PDO 通信被完全禁用,僅允許 NMT 命令和心跳
設備保持配置,但不執行控制任務
由 NMT 主機發送 “Stop Node” 命令觸發,常用于安全暫停

注:對伺服運動設備(DS402),還額外定義了電源相關狀態,比如電源禁用區,電源啟用區,故障區等相關定義。
CANOpen 協議的四大核心狀態中,嵌套了三種核心通信模型,具體如下:
(1) 主 / 從站模型:與 Modbus 協議邏輯類似,核心差異是支持多主機架構,最多可接入 127 個從站,主要用于網絡診斷和設備狀態管理。
(2) 客戶端 / 服務器模型:借鑒 TCP/IP 協議的交互模式,專門適配對象字典(OD)的讀寫操作。從設備作為服務器提供參數訪問服務,主設備作為客戶端發起讀寫請求。
(3) 生產者 / 消費者模型:與 MQTT 協議的通信邏輯一致,從設備擔任數據生產者,主設備擔任數據消費者。生產者可按主設備的明確請求(拉模型),或主動無請求推送(推模型),傳輸預設的目標數據。
了解了以上知識后,還需要了解CANOpen協議的如下相關概念
(1) 對象字典
對象字典的功能類似 Modbus 協議中的寄存器,是定義 CANOpen 節點所有行為、參數及通信規則的核心。與 Modbus 寄存器不同,它通過 “16 位索引 + 8 位子索引” 的雙標識方式,唯一確定每個條目。
對象字典分為公共對象字典和私有對象字典。其中 DS301 協議作為 CANOpen 的基礎通用協議,明確了所有 CANOpen 設備必須遵循的公共對象字典規范。

針對我們對接的EM32DX-C4查看設備手冊,DS301對應的數據字典的通用參數如下:

設備參數如下:

DS401屬于子協議,專門定義數字量 / 模擬量 IO 的采集、控制、診斷等特有功能,索引范圍集中在 0x6000-0x77FF,核心條目按 “數字量 IO”“模擬量 IO”“診斷” 分類

查EM32DX-C4設備手冊 DS401協議對應的對象字典參數如下:

(2) COB-ID
COB-ID其實就是11位CAN ID,它分兩部分組成,高4位為功能碼,低7位為從設備地址碼,所以最多支持127個從設備。

功能碼和具體的通信服務相關(如下圖所示):

(3) 網絡管理(NMT)
NMT服務用于通過NMT命令(如:啟動、停止、復位)來控制CANopen設備的狀態(如:預運行、運行、停止)。
為了改變狀態,NMT主機發送一個帶有 CAN ID 的2字節消息(即功能代碼和節點ID )。所有從站節點都處理這個報文。節點ID 0表示廣播命令。

功能代碼如下:

(4) 服務數據對象(SDO)
SDO(Server Data Object,服務數據對象)的核心功能是訪問或修改 CANopen 設備對象字典內的參數值。例如,當應用主站需調整 CANopen 設備的特定配置參數時,可借助 SDO 服務完成參數的讀寫操作,實現設備配置的靈活變更。
(5) 過程數據對象(PDO)
PDO(Process Data Object,過程數據對象) 是專為設備間實時、高速數據傳輸設計的核心通信服務,是工業場景中過程數據交互的關鍵通道。
PDO數據上傳有四種方式觸發:
定時發送,同步傳輸(同步信號觸發),遠程請求和事件觸發。
(6) 心跳信號(Heartbeat)
CANopen 的心跳服務具有雙重核心目的:一是向網絡發送 “設備在線” 的活動消息,二是確認 NMT 命令的執行狀態。NMT 從設備會按預設周期(例如 200 毫秒)發送心跳消息,消息 CAN ID 遵循固定規則(如節點 2 的 CAN ID 為 0x702),其第一個數據字節攜帶節點當前的 NMT 狀態碼(如下圖)。若心跳消息的接收方(如NMT主站)在設定時限內未收到該消息,將觸發預設的離線響應機制。

(7) 同步(SYNC)
CANopen 的 SYNC 報文核心作用是同步多個從設備的輸入采集與輸出響應,通常由應用主站發起。主站向 CANopen 網絡發送 SYNC 消息(COB-ID 為 0x080),支持帶或不帶 SYNC 計數器兩種傳輸形式。多個從節點可預先配置為響應 SYNC 信號,要么同步捕獲輸入數據并傳輸,要么與其他參與同步的節點協同設置輸出,確保動作一致性。
SYNC 計數器的存在可靈活劃分同步組,實現多組設備的獨立同步操作,適配不同場景下的協同需求。
(8) 緊急情況(EMCY)
CANopen 的緊急服務(EMCY)專為設備發生致命錯誤(如傳感器故障)設計,用于向網絡其他節點及時上報故障狀態。受影響的節點會以高優先級、單次觸發式向網絡發送 EMCY 消息(例如節點 2 的消息 COB-ID 為 0x082)。消息的數據字節攜帶具體錯誤碼及相關輔助信息,通過查詢設備手冊或協議規范,可獲取對應的故障詳情。
(9) 時間戳(Timestamp)
該消息由主站發起,對應的 CAN ID 為 0x100。使用 6 字節 (48 位)表示。前 4 個字節 (32 位): 表示從午夜開始的毫秒數(范圍: 0-4294967295 毫秒,約 1193 小時)。后 2 個字節 (16 位): 表示自 1984 年 1 月 1 日 0 時起的天數(范圍: 0-65535 天,約 179.4 年) 。
二、CANOpen DS401協議實現
官方示例(06_bus_canopen_master_motor)在免費開源的CanFestival(LGPLv2 許可證)的基礎上實現的。該開源代碼實現了CANOpen協議如下功能:
(1)NMT(網絡管理):節點狀態控制(初始化、預操作、操作、停止)和心跳監測
(2)PDO(過程數據對象):高速實時數據傳輸,支持循環和事件觸發模式,優化工業控制場景響應速度
(3)SDO(服務數據對象):對象字典參數訪問,支持快速下載和分段下載,用于設備配置和參數調整
(4)SYNC(同步對象):網絡時鐘同步和周期性數據傳輸協調
(5)EMCY(緊急對象):錯誤報告和故障通知機制

我是在官方示例06_bus_canopen_master_motor的基礎上進行大幅度修改而完成。除了canopen_callback.*相關內容沒有多少變化外,其他文件改動比較大。
講解代碼之前,先簡單說一下硬件接線。查EM32DX-C4手冊,CANOpen接口采用了以太網的接口,管腳定義如下:

根據這個定義,自己做了一個CAN網絡連接線,主要用到1,2兩根線,對應的網線是1-白橙和2-橙色。白橙也就是CAN_P接入睿擎派的CAN_H接口,橙色接入睿擎派的CAN_L接口。

由于我對 CANOpen 協議的了解不夠深入,且是初次接觸塞雷 EM32DX-C4 硬件模塊,初期的調試工作遇到了不少阻礙。好在手頭恰好有 PCAN-USB 模塊,將其接入 CAN 總線后,我通過 PCAN-View 工具實時監聽 CAN 幀數據,這一操作直接顯著提升了開發與調試的效率(如下圖)。

master402_od.c改名為master401_od.c
主要是DS301和DS401對象字典定義的地方。對原有的數據字典進行了大幅度的刪減。
原有的對象字典定義:
const indextable master402_objdict[] ={ { (subindex*)master402_Index1000,sizeof(master402_Index1000)/sizeof(master402_Index1000[0]), 0x1000}, { (subindex*)master402_Index1001,sizeof(master402_Index1001)/sizeof(master402_Index1001[0]), 0x1001}, { (subindex*)master402_Index1005,sizeof(master402_Index1005)/sizeof(master402_Index1005[0]), 0x1005}, { (subindex*)master402_Index1006,sizeof(master402_Index1006)/sizeof(master402_Index1006[0]), 0x1006}, { (subindex*)master402_Index1014,sizeof(master402_Index1014)/sizeof(master402_Index1014[0]), 0x1014}, { (subindex*)master402_Index1016,sizeof(master402_Index1016)/sizeof(master402_Index1016[0]), 0x1016}, { (subindex*)master402_Index1017,sizeof(master402_Index1017)/sizeof(master402_Index1017[0]), 0x1017}, { (subindex*)master402_Index1018,sizeof(master402_Index1018)/sizeof(master402_Index1018[0]), 0x1018}, { (subindex*)master402_Index1200,sizeof(master402_Index1200)/sizeof(master402_Index1200[0]), 0x1200}, { (subindex*)master402_Index1280,sizeof(master402_Index1280)/sizeof(master402_Index1280[0]), 0x1280}, { (subindex*)master402_Index1281,sizeof(master402_Index1281)/sizeof(master402_Index1281[0]), 0x1281}, { (subindex*)master402_Index1400,sizeof(master402_Index1400)/sizeof(master402_Index1400[0]), 0x1400}, { (subindex*)master402_Index1401,sizeof(master402_Index1401)/sizeof(master402_Index1401[0]), 0x1401}, { (subindex*)master402_Index1402,sizeof(master402_Index1402)/sizeof(master402_Index1402[0]), 0x1402}, { (subindex*)master402_Index1403,sizeof(master402_Index1403)/sizeof(master402_Index1403[0]), 0x1403}, { (subindex*)master402_Index1600,sizeof(master402_Index1600)/sizeof(master402_Index1600[0]), 0x1600}, { (subindex*)master402_Index1601,sizeof(master402_Index1601)/sizeof(master402_Index1601[0]), 0x1601}, { (subindex*)master402_Index1602,sizeof(master402_Index1602)/sizeof(master402_Index1602[0]), 0x1602}, { (subindex*)master402_Index1603,sizeof(master402_Index1603)/sizeof(master402_Index1603[0]), 0x1603}, { (subindex*)master402_Index1800,sizeof(master402_Index1800)/sizeof(master402_Index1800[0]), 0x1800}, { (subindex*)master402_Index1801,sizeof(master402_Index1801)/sizeof(master402_Index1801[0]), 0x1801}, { (subindex*)master402_Index1802,sizeof(master402_Index1802)/sizeof(master402_Index1802[0]), 0x1802}, { (subindex*)master402_Index1803,sizeof(master402_Index1803)/sizeof(master402_Index1803[0]), 0x1803}, { (subindex*)master402_Index1A00,sizeof(master402_Index1A00)/sizeof(master402_Index1A00[0]), 0x1A00}, { (subindex*)master402_Index1A01,sizeof(master402_Index1A01)/sizeof(master402_Index1A01[0]), 0x1A01}, { (subindex*)master402_Index1A02,sizeof(master402_Index1A02)/sizeof(master402_Index1A02[0]), 0x1A02}, { (subindex*)master402_Index1A03,sizeof(master402_Index1A03)/sizeof(master402_Index1A03[0]), 0x1A03}, { (subindex*)master402_Index2001,sizeof(master402_Index2001)/sizeof(master402_Index2001[0]), 0x2001}, { (subindex*)master402_Index2002,sizeof(master402_Index2002)/sizeof(master402_Index2002[0]), 0x2002}, { (subindex*)master402_Index2003,sizeof(master402_Index2003)/sizeof(master402_Index2003[0]), 0x2003}, { (subindex*)master402_Index2004,sizeof(master402_Index2004)/sizeof(master402_Index2004[0]), 0x2004}, { (subindex*)master402_Index2005,sizeof(master402_Index2005)/sizeof(master402_Index2005[0]), 0x2005}, { (subindex*)master402_Index2006,sizeof(master402_Index2006)/sizeof(master402_Index2006[0]), 0x2006}, { (subindex*)master402_Index2007,sizeof(master402_Index2007)/sizeof(master402_Index2007[0]), 0x2007}, { (subindex*)master402_Index2124,sizeof(master402_Index2124)/sizeof(master402_Index2124[0]), 0x2124}, { (subindex*)master402_Index2F00,sizeof(master402_Index2F00)/sizeof(master402_Index2F00[0]), 0x2F00}, { (subindex*)master402_Index2F01,sizeof(master402_Index2F01)/sizeof(master402_Index2F01[0]), 0x2F01}, { (subindex*)master402_Index6040,sizeof(master402_Index6040)/sizeof(master402_Index6040[0]), 0x6040}, { (subindex*)master402_Index6041,sizeof(master402_Index6041)/sizeof(master402_Index6041[0]), 0x6041}, { (subindex*)master402_Index6060,sizeof(master402_Index6060)/sizeof(master402_Index6060[0]), 0x6060}, { (subindex*)master402_Index6064,sizeof(master402_Index6064)/sizeof(master402_Index6064[0]), 0x6064}, { (subindex*)master402_Index606C,sizeof(master402_Index606C)/sizeof(master402_Index606C[0]), 0x606C}, { (subindex*)master402_Index607A,sizeof(master402_Index607A)/sizeof(master402_Index607A[0]), 0x607A}, { (subindex*)master402_Index607C,sizeof(master402_Index607C)/sizeof(master402_Index607C[0]), 0x607C}, { (subindex*)master402_Index6081,sizeof(master402_Index6081)/sizeof(master402_Index6081[0]), 0x6081}, { (subindex*)master402_Index6098,sizeof(master402_Index6098)/sizeof(master402_Index6098[0]), 0x6098}, { (subindex*)master402_Index6099,sizeof(master402_Index6099)/sizeof(master402_Index6099[0]), 0x6099}, { (subindex*)master402_Index60C1,sizeof(master402_Index60C1)/sizeof(master402_Index60C1[0]), 0x60C1}, { (subindex*)master402_Index60C2,sizeof(master402_Index60C2)/sizeof(master402_Index60C2[0]), 0x60C2}, { (subindex*)master402_Index60FF,sizeof(master402_Index60FF)/sizeof(master402_Index60FF[0]), 0x60FF},};
刪減后的對象字典定義:
const indextable master401_objdict[] ={ { (subindex*)master401_Index1000,sizeof(master401_Index1000)/sizeof(master401_Index1000[0]), 0x1000}, { (subindex*)master401_Index1001,sizeof(master401_Index1001)/sizeof(master401_Index1001[0]), 0x1001}, { (subindex*)master401_Index1005,sizeof(master401_Index1005)/sizeof(master401_Index1005[0]), 0x1005}, { (subindex*)master401_Index1006,sizeof(master401_Index1006)/sizeof(master401_Index1006[0]), 0x1006}, { (subindex*)master401_Index1014,sizeof(master401_Index1014)/sizeof(master401_Index1014[0]), 0x1014}, { (subindex*)master401_Index1016,sizeof(master401_Index1016)/sizeof(master401_Index1016[0]), 0x1016}, { (subindex*)master401_Index1017,sizeof(master401_Index1017)/sizeof(master401_Index1017[0]), 0x1017}, { (subindex*)master401_Index1018,sizeof(master401_Index1018)/sizeof(master401_Index1018[0]), 0x1018}, { (subindex*)master401_Index1200,sizeof(master401_Index1200)/sizeof(master401_Index1200[0]), 0x1200}, { (subindex*)master401_Index1280,sizeof(master401_Index1280)/sizeof(master401_Index1280[0]), 0x1280}, { (subindex*)master401_Index1400,sizeof(master401_Index1400)/sizeof(master401_Index1400[0]), 0x1400}, { (subindex*)master401_Index1600,sizeof(master401_Index1600)/sizeof(master401_Index1600[0]), 0x1600}, { (subindex*)master401_Index1800,sizeof(master401_Index1800)/sizeof(master401_Index1800[0]), 0x1800}, { (subindex*)master401_Index1A00,sizeof(master401_Index1A00)/sizeof(master401_Index1A00[0]), 0x1A00}, { (subindex*)master401_Index2000,sizeof(master401_Index2000)/sizeof(master401_Index2000[0]), 0x2000}, { (subindex*)master401_Index2001,sizeof(master401_Index2001)/sizeof(master401_Index2001[0]), 0x2001},};
相比原有代碼,增加了DO和DI相關的對象字典的定義:
/* -------------------------- 0x2000 本地DO輸出緩存 -------------------------- */// 子索引0:最高子索引編號(=1,因為有2個子索引:0和1)// 子索引1:實際DO數據存儲(uint16,RW)UNS8 master401_highestSubIndex_obj2000 =1; /* 最高子索引編號 = 子索引數量-1 */uint16_tmaster401_obj2000_do_val =0x0000; /* DO數據存儲變量(關聯g_em32dx_do)*/subindex master401_Index2000[] ={ // 子索引0:聲明最高子索引編號(RO,不可寫) { RO, uint8,sizeof(UNS8), (void*)&master401_highestSubIndex_obj2000,NULL}, // 子索引1:實際DO數據(RW,uint16) { RW, uint16,sizeof(uint16_t), (void*)&master401_obj2000_do_val,NULL}};/* -------------------------- 0x2001 本地DI輸入緩存 -------------------------- */// 子索引0:最高子索引編號(=1)// 子索引1:實際DI數據存儲(uint16,RO)UNS8 master401_highestSubIndex_obj2001 =1; /* 最高子索引編號 = 子索引數量-1 */uint16_tmaster401_obj2001_di_val =0x0000; /* DI數據存儲變量(關聯g_em32dx_di)*/subindex master401_Index2001[] ={ // 子索引0:聲明最高子索引編號(RO,不可寫) { RO, uint8,sizeof(UNS8), (void*)&master401_highestSubIndex_obj2001,NULL}, // 子索引1:實際DI數據(RO,uint16,協議棧自動更新) { RO, uint16,sizeof(uint16_t), (void*)&master401_obj2001_di_val,NULL}};
需要特別注意的是,master401_od.c中定義的對象字典僅適用于主設備 —— 這是我初期的核心困惑點,曾誤以為主設備無需額外定義對象字典。且主設備對象字典中0x1400、0x1800 索引的含義,與從設備對應索引的描述恰好相反:具體來說,主設備的 TPDO1(發送過程數據對象 1)對應從設備的 RPDO1(接收過程數據對象 1),而主設備的 RPDO1 則對應從設備的 TPDO1。
文件調整方面:已移除motor_control.c與motor_control.h文件,并將原文件中的相關 IO 操作整合至master401_canopen.c中;同時將原master402_canopen.c文件重命名為master401_canopen.c,且對文件內大部分核心代碼進行了適配性修改。
從設備 IO 模塊的對象字典配置,均在該文件中完成實現,具體代碼如下:
/************************** 核心修改:IO模塊PDO映射配置 **************************/// 說明:// - 從站(EM32DX-C4)接收DO輸出:RPDO1(0x1400)映射DO0-DO15(2字節)// - 從站(EM32DX-C4)發送DI輸入:TPDO1(0x1800)映射DI0-DI15(2字節)// - 復用原PDO通道,刪除伺服相關映射/* TPDO1配置(從站→主站:DI輸入)*/staticUNS8IO_DIS_SLAVE_TPDO1(uint8_tnodeId){ rt_kprintf("config...0!\n"); UNS32 TPDO_COBId =PDO_DISANBLE(0x00000180, nodeId); // COB-ID: 0x182(IO_NODEID=2) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1800,1,4, uint32, &TPDO_COBId, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_TPDO1_Type(uint8_tnodeId){ rt_kprintf("config...1!\n"); UNS8 trans_type = PDO_TRANSMISSION_TYPE; // 同步傳輸 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1800,2,1, uint8, &trans_type, config_node_param_cb,0);}staticUNS8IO_Clear_SLAVE_TPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...2!\n"); UNS8 pdo_map_cnt =0; // 清除原有映射 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1A00,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_TPDO1_Map(uint8_tnodeId){ rt_kprintf("config...3!\n"); // TPDO1映射:DI0-DI15(模塊DI對應索引0x6100,子索引0x01,2字節) UNS32 pdo_map_val =0x61000110; // 索引0x6100 + 子索引0x01 + 16位長度(0x10) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1A00,1,4, uint32, &pdo_map_val, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_TPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...4!\n"); UNS8 pdo_map_cnt =1; // 1個映射項(2字節) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1A00,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_EN_SLAVE_TPDO1(uint8_tnodeId){ rt_kprintf("config...5!\n"); UNS32 TPDO_COBId =PDO_ENANBLE(0x00000180, nodeId); returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1800,1,4, uint32, &TPDO_COBId, config_node_param_cb,0);}//-----------------------------------------------------------///* RPDO1配置(主站→從站:DO輸出)*/staticUNS8IO_DIS_SLAVE_RPDO1(uint8_tnodeId){ rt_kprintf("config...6!\n"); UNS32 RPDO_COBId =PDO_DISANBLE(0x00000200, nodeId); // COB-ID: 0x202(IO_NODEID=2) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1400,1,4, uint32, &RPDO_COBId, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_RPDO1_Type(uint8_tnodeId){ rt_kprintf("config...7!\n"); UNS8 trans_type = PDO_TRANSMISSION_TYPE; // 同步傳輸 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1400,2,1, uint8, &trans_type, config_node_param_cb,0);}staticUNS8IO_Clear_SLAVE_RPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...8!\n"); UNS8 pdo_map_cnt =0; // 清除原有映射 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1600,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_RPDO1_Map(uint8_tnodeId){ rt_kprintf("config...9!\n"); // RPDO1映射:DO0-DO15(模塊DO對應索引0x6300,子索引0x01,2字節) UNS32 pdo_map_val =0x63000110; // 索引0x6300 + 子索引0x01 + 16位長度(0x10) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1600,1,4, uint32, &pdo_map_val, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_RPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...10!\n"); UNS8 pdo_map_cnt =1; // 1個映射項(2字節) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1600,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_EN_SLAVE_RPDO1(uint8_tnodeId){ rt_kprintf("config...11!\n"); UNS32 RPDO_COBId =PDO_ENANBLE(0x00000200, nodeId); returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1400,1,4, uint32, &RPDO_COBId, config_node_param_cb,0);}//-----------------------------------------------------------///* 心跳配置(IO模塊生產者心跳)*/staticUNS8IO_Write_SLAVE_Heartbeat(uint8_tnodeId){ rt_kprintf("config...12!\n"); UNS16 producer_heartbeat_time = PRODUCER_HEARTBEAT_TIME; returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1017,0,2, uint16, &producer_heartbeat_time, config_node_param_cb,0);}/* 配置完成回調 */staticUNS8IO_Config_Done(uint8_tnodeId){ rt_kprintf("config...13!\n"); node_config_state *conf = &slave_conf; rt_sem_release(&(conf->finish_sem)); return0;}// IO模塊配置函數指針數組(按順序執行)staticUNS8(*IOCFG_Operation[])(uint8_tnodeId)= { // TPDO1(DI輸入)配置(6步) IO_DIS_SLAVE_TPDO1, // 步驟0:禁用TPDO1 IO_Write_SLAVE_TPDO1_Type,// 步驟1:寫TPDO1傳輸類型 IO_Clear_SLAVE_TPDO1_Cnt, // 步驟2:清除TPDO1映射數 IO_Write_SLAVE_TPDO1_Map, // 步驟3:寫TPDO1映射 IO_Write_SLAVE_TPDO1_Cnt, // 步驟4:設置TPDO1映射數 IO_EN_SLAVE_TPDO1, // 步驟5:啟用TPDO1 // RPDO1(DO輸出)配置(6步) IO_DIS_SLAVE_RPDO1, // 步驟6:禁用RPDO1 IO_Write_SLAVE_RPDO1_Type,// 步驟7:寫RPDO1傳輸類型 IO_Clear_SLAVE_RPDO1_Cnt, // 步驟8:清除RPDO1映射數 IO_Write_SLAVE_RPDO1_Map, // 步驟9:寫RPDO1映射 IO_Write_SLAVE_RPDO1_Cnt, // 步驟10:設置RPDO1映射數 IO_EN_SLAVE_RPDO1, // 步驟11:啟用RPDO1 // 心跳配置(1步) IO_Write_SLAVE_Heartbeat, // 步驟12:寫從站心跳 // 配置完成(1步) IO_Config_Done, // 步驟13:釋放信號量};
原先DS301一些邏輯我們進行了保留。
并且新增了一些 IO操作接口函數,代碼如下:
/************************** 新增IO操作API(上層調用) **************************//***@brief設置EM32DX-C4的DO輸出*@paramdo_val: 16位DO值(bit0=DO0, bit15=DO15,1=導通,0=斷開)*@retvalRT_EOK: 成功,-RT_ERROR: 失敗*/rt_err_tem32dx_set_do(uint16_t do_val){ if(*can_node[1].nmt_state != Operational) { rt_kprintf("EM32DX-C4 not in Operational state!\n"); return-RT_ERROR; } // 更新全局緩存 g_em32dx_do = do_val; // 通過RPDO1發送DO值 UNS32size=2; UNS32errorCode=writeLocalDict(OD_Data,0x2000,1, &do_val, &size,0); if(errorCode != OD_SUCCESSFUL) { rt_kprintf("Write DO failed! Error code: 0x%08X\n", errorCode); return-RT_ERROR; } returnRT_EOK;}/***@brief讀取EM32DX-C4的DI輸入*@paramdi_val: 輸出參數,存儲16位DI值(bit0=DI0, bit15=DI15,1=導通,0=斷開)*@retvalRT_EOK: 成功,-RT_ERROR: 失敗*/rt_err_tem32dx_get_di(){ if(*can_node[1].nmt_state != Operational) { rt_kprintf("EM32DX-C4 not ready!\n"); return-RT_ERROR; } // 從本地字典讀取TPDO1接收的DI值 uint16_tdi_val=0; UNS32size=2; UNS8 data_type; UNS32errorCode=readLocalDict(OD_Data,0x2001,1, &di_val, &size, &data_type,0); if(errorCode != OD_SUCCESSFUL) { rt_kprintf("Read DI failed! Error code: 0x%08X\n", errorCode); return-RT_ERROR; } rt_kprintf("Read DI: 0x%04X\n", di_val); // 更新全局緩存 g_em32dx_di = di_val; returnRT_EOK;}MSH_CMD_EXPORT(em32dx_get_di, Get EM32DX-C4 DI input);/***@brief單獨控制某一路DO*@paramchannel: DO通道(0-15)*@paramstate: 0=斷開,1=導通*@retvalRT_EOK: 成功,-RT_ERROR: 失敗*/rt_err_tem32dx_set_do_channel(uint8_t argc,char**argv){ if(argc 2) {? ? ? ? ? rt_kprintf("em32dx_set_do_channel 1 1\n");? ? ? ? ? return?-RT_ERROR;? ? }? ? uint8_t?channel?=?atoi(argv[1]);? ? uint8_t?state?=?atoi(argv[2]);? ? rt_kprintf("channel=%d state=%d\n",channel,state);? ? if?(channel >=16) { rt_kprintf("DO channel out of range (0-15)!\n"); return-RT_ERROR; } if(state) { g_em32dx_do |= (1<< channel);? ? }?else?{? ? ? ? g_em32dx_do &= ~(1?<< channel);? ? }? ? return?em32dx_set_do(g_em32dx_do);}MSH_CMD_EXPORT(em32dx_set_do_channel, Set single DO?channel?(channel?0-15, state?0/1));
代碼編譯完成后,我們將其部署至睿擎派,具體操作步驟如下:
(1)執行 canopen_start 指令,完成 CANOpen 服務的初始化與啟動;
(2)執行 em32dx_get_di 指令,獲取 16 路開關量的當前狀態;
(3)執行 em32dx_set_do_channel 1 1 指令,配置 16 路 DO 通道的輸出狀態。
其中第一個參數為通道索引(取值范圍:0–15),第二個參數為輸出狀態(0 = 關閉,1 = 打開)。


上述指令執行完成后,我們可以觀察到對應的 DO 通道狀態指示燈,會同步呈現出預期的狀態變化(與指令配置的輸出狀態一致)。

源代碼下載鏈接:
鏈接:https://pan.baidu.com/s/1aZDxzb3NNhn3WRBA4OeN4w?pwd=w8au
提取碼: w8au
附錄:
(1)CANOpen DS301、DS302、DS401、DS402等全套協議下載:
https://link.gitcode.com/i/614ed2a5064e1990bff8ffcde2328ada?uuid_tt_dd=10_19283516180-1733805088376-790686&isLogin=1&from_id=142936482
(2)DS301協議中文版
https://files.cnblogs.com/files/winshton/301_v04020005_cn_v02_ro.pdf
https://winshton.gitbooks.io/canopen-ds301-cn/content/
————————————————
版權聲明:本文為RT-Thread論壇用戶「yefanqiu」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:
https://club.rt-thread.org/ask/article/bb8a52de0882d43b.html
-
CAN
+關注
關注
59文章
3066瀏覽量
472731 -
總線
+關注
關注
10文章
3040瀏覽量
91656 -
RT-Thread
+關注
關注
32文章
1613瀏覽量
44818
發布評論請先 登錄
基于睿擎派輕松玩轉CANopen電機控制
睿擎派3562快速上手體驗
【睿擎派】CANOpen總線之IO模塊讀寫(DS401協議)
評論