
CANopenNode 遵循非阻塞、多线程的设计。所有协议代码均运行于三种执行上下文之一每种上下文各司其职。下图展示了这些线程如何通过共享的 CANopen 对象CO_t进行交互CAN 接收线程以最高优先级运行——通常由硬件中断驱动——并且只执行最少的处理它识别 CAN 报文将有效载荷复制到相应的对象字典条目中并可选地触发回调以唤醒低优先级的线程。定时器周期线程以固定的 1ms 频率运行负责处理时间敏感且与网络同步的操作SYNC 的生成/消费、RPDO 输入复制以及 TPDO 输出发送。主线程则负责处理诸如 SDO 服务器处理、紧急报文处理、NMT 状态机和 LED 更新等非紧急任务。对于微控制器而言推荐采用这种三线程模型架构。在 Linux 或单线程系统中所有的处理可以合并到一个线程中——可以在同一个循环中依次调用 CO_process() 以及 CO_process_SYNC()、CO_process_RPDO() 和 CO_process_TPDO()。第 1 步构建示例example/ 目录中的示例特意设计为与硬件无关。它可以通过一个简单的 Makefile 在任何拥有 GCC 的系统上编译。这是你的第一个检查点——如果能编译通过你就拥有了一个有效的基线。cd example/ makeMakefile 编译了以下源文件它们构成了一个功能完备的 CANopen 从站设备的最小集合源文件 作用CO_driver_blank.c CAN 驱动桩代码由你替换实现CO_storageBlank.c 非易失性存储桩代码301/CO_ODinterface.c 对象字典访问层301/CO_NMT_Heartbeat.c NMT 状态机 心跳生产者301/CO_HBconsumer.c 心跳消费者301/CO_Emergency.c 紧急报文生产者/消费者301/CO_SDOserver.c SDO 服务器OD 读写访问301/CO_SYNC.c SYNC 生产者/消费者301/CO_PDO.c 过程数据对象301/CO_TIME.c 时间戳协议303/CO_LEDs.c LED 指示灯状态机305/CO_LSSslave.c LSS 从站Node-ID/比特率配置storage/CO_storage.c 存储基对象CANopen.c 中央初始化与处理OD.c 生成的对象字典数据main_blank.c 应用程序入口模板Makefile 还展示了两个可选的编译时标志你可以根据需要取消注释-DCO_USE_GLOBALS 用静态全局变量替代堆内存分配适用于资源受限的微控制器-DCO_MULTIPLE_OD 则在单个应用程序中启用对多个对象字典的支持。第 2 步理解初始化序列main_blank.c 模板编码了完整的 CANopenNode 生命周期。以下是拆解后的必经初始化序列每个阶段都有其特定的作用与失败模式。以下是详细说明阶段 1 — 内存分配CO_new()CO_new() 函数会在单个堆内存块中分配所有的 CANopen 对象NMT、Emergency、SDO 服务器、PDO、SYNC、LSS、LEDs 等。它会从 CO_config_t 结构体当定义了 CO_MULTIPLE_OD 时或直接从自动生成的 OD.h 文件中读取对象数量。该函数返回一个指向中央 CO_t 结构体的指针该结构体将作为后续所有 API 调用的句柄。CO_t* CO NULL; CO CO_new(config_ptr, heapMemoryUsed); if (CO NULL) { log_printf(Error: Cant allocate memory\n); return 0; }在内部CO_new() 会遍历每一种 CANopen 对象类型通过 CO_alloc_break_on_fail默认使用 calloc分配内存并计算所需的 CAN 接收和发送报文缓冲区总数。CAN 报文缓冲区索引按 CAN 标识符的优先级排序——NMT 从站报文获得最低的 RX 索引而 LSS 主站获得最高的 RX 索引。阶段 2 — CAN 与 LSS 初始化内存分配完成后必须配置 CAN 硬件模块。CO_CANinit() 会调用你目标特定的驱动程序CO_driver_blank.c 中的 CO_CANmodule_init()以指定的比特率设置 CAN 控制器初始化 RX/TX 缓冲区数组并配置硬件过滤器。随后CO_LSSinit() 会初始化 LSS 从站这使得节点的 ID 和比特率能够通过 CAN 网络重新配置——这在不宜使用 DIP 开关的生产部署中非常有用。err CO_CANinit(CO, CANptr, pendingBitRate); // ... CO_LSS_address_t lssAddress { .identity { .vendorID OD_PERSIST_COMM.x1018_identity.vendor_ID, .productCode OD_PERSIST_COMM.x1018_identity.productCode, .revisionNumber OD_PERSIST_COMM.x1018_identity.revisionNumber, .serialNumber OD_PERSIST_COMM.x1018_identity.serialNumber } }; err CO_LSSinit(CO, lssAddress, pendingNodeId, pendingBitRate);LSS 地址取自 OD 对象 0x1018Identity Object其中包含供应商 ID、产品代码、版本号和序列号——这些信息能在网络上唯一标识你的设备。阶段 3 — CANopen 对象初始化CO_CANopenInit()这是核心的初始化调用它将所有 CANopen 通信对象与对象字典关联起来。它基于 CO_config.h 中的配置宏和 OD.h 中的 OD 条目数量初始化 NMT包含启动行为、错误处理和心跳生产者定时、SDO 服务器带可配置超时、Emergency 生产者/消费者、心跳消费者、SYNC、TIME、LSS 从站以及 LEDs。参数 用途 示例值NMT_CONTROL 启动行为 错误响应标志 CO_NMT_STARTUP_TO_OPERATIONAL | CO_NMT_ERR_ON_ERR_REGFIRST_HB_TIME 启动后首次心跳间隔 (ms) 500SDO_SRV_TIMEOUT_TIME SDO 服务器超时 (ms) 1000SDO_CLI_TIMEOUT_TIME SDO 客户端超时 (ms) 500activeNodeId CANopen Node-ID (1–127) 10随后通过单独调用 CO_CANopenInitPDO()从对象字典中读取通信参数0x1400–0x1403, 0x1800–0x1803和映射参数0x1600–0x1603, 0x1A00–0x1A03以配置所有 RPDO 和 TPDO 对象。CO_CANopenInit() 和 CO_CANopenInitPDO() 均可能返回 CO_ERROR_NODE_ID_UNCONFIGURED_LSS——这并非致命错误。它仅仅表示 Node-ID 尚未设置LSS 处于挂起状态。协议栈仍会初始化但在 LSS 分配有效的 Node-ID 之前将保持等待状态。第 3 步理解运行时循环初始化完成后应用程序会进入一个嵌套的循环结构。外层循环处理通信复位由 NMT 复位命令触发内层循环则处理常规的周期性处理。主线程 — CO_process()主线程周期性地调用 CO_process()。该函数按固定的优先级顺序处理所有非实时的 CANopen 对象while (reset CO_RESET_NOT) { uint32_t timeDifference_us 500; reset CO_process(CO, false, timeDifference_us, NULL); LED_red CO_LED_RED(CO-LEDs, CO_LED_CANopen); LED_green CO_LED_GREEN(CO-LEDs, CO_LED_CANopen); /* 非阻塞的应用程序代码可以放在这里。 */ }在 CO_process() 内部会按顺序处理以下对象顺序 对象 调用的函数 条件1 CAN 模块 CO_CANmodule_process() 始终执行2 LSS 从站 CO_LSSslave_process() 若定义 CO_CONFIG_LSS_SLAVE3 LEDs CO_LEDs_process() 若定义 CO_CONFIG_LEDS_ENABLE4 Emergency CO_EM_process() 若 NMT 处于预操作或操作状态5 NMT/心跳 CO_NMT_process() 始终执行返回复位命令6 SDO 服务器 CO_SDOserver_process() 若 NMT 处于预操作或操作状态7 HB 消费者 CO_HBconsumer_process() 若 NMT 处于预操作或操作状态8 TIME CO_TIME_process() 若定义 CO_CONFIG_TIME_ENABLE9 网关 CO_GTWA_process() 若定义 CO_CONFIG_GTW_ASCII如果 CO_process() 返回 CO_RESET_COMM内层循环将退出外层循环会重新初始化通信栈重新执行初始化序列中的阶段 3 至 8。如果返回 CO_RESET_APP则整个应用程序将关闭。定时器周期线程 — 实时处理定时器线程以固定的 1ms 间隔运行负责处理时间关键型且与 SYNC 同步的操作。在处理之前它必须锁定对象字典以防止与主线程发生并发访问void tmrTask_thread(void) { for (;;) { CO_LOCK_OD(CO-CANmodule); if (!CO-nodeIdUnconfigured CO-CANmodule-CANnormal) { bool_t syncWas false; uint32_t timeDifference_us 1000; syncWas CO_process_SYNC(CO, timeDifference_us, NULL); CO_process_RPDO(CO, syncWas, timeDifference_us, NULL); CO_process_TPDO(CO, syncWas, timeDifference_us, NULL); } CO_UNLOCK_OD(CO-CANmodule); } }这种处理顺序是刻意设计的首先评估 SYNC可能会生成或消费一条 SYNC 报文然后 RPDO 将接收到的过程数据复制到对象字典中最后 TPDO 将对象字典中的过程数据发送到 CAN 总线上。syncWas 布尔值确保了 PDO 仅在 SYNC 周期边界进行传输。CAN 接收中断处理程序CAN 接收线程通常是一个中断服务程序。在空白模板中它被留作一个桩代码void CO_CAN1InterruptHandler(void) { /* 清除中断标志 */}在真实的实现中该处理程序必须调用 CAN 驱动程序以读取接收到的报文将其与已注册的 RX 缓冲区过滤器进行匹配通过 CO_CANrx_t 条目并调用相应的 CANrx_callback 函数。每个 CANopen 对象都会在初始化期间注册自己的回调——例如SDO 服务器会注册一个与其 COB-ID 匹配的回调而 RPDO 对象则会注册一个将数据复制到 OD 中的回调。第 4 步移植到你的硬件将 CANopenNode 移植到新的微控制器需要实现三个构成硬件抽象层的文件文件 1CO_driver_target.h — 类型定义此文件定义了协议栈所依赖的基础类型和宏。你必须提供定义 用途CO_LITTLE_ENDIAN / 字节交换宏 平台的字节序bool_t 布尔类型通常为 uint_fast8_tCO_CANrx_t 接收报文缓冲区结构体标识符、掩码、回调CO_CANtx_t 发送报文缓冲区结构体标识符、DLC、数据、标志CO_CANmodule_t CAN 模块对象包含 RX/TX 数组、错误状态、状态标志CO_LOCK_OD() / CO_UNLOCK_OD() 用于 OD 访问的临界区宏CO_FLAG_SET() / CO_FLAG_CLEAR() / CO_FLAG_READ() 线程间同步标志文件 2CO_driver_blank.c → 你的驱动实现此文件实现了实际的 CAN 硬件接口。你必须填充的关键函数包括函数 职责CO_CANsetConfigurationMode() 将 CAN 控制器切换至配置模式CO_CANsetNormalMode() 将 CAN 控制器切换至正常活动模式CO_CANmodule_init() 初始化 CAN 模块、缓冲区、过滤器、中断CO_CANrxBufferInit() 注册带有回调的 CAN RX 过滤器CO_CANtxBufferInit() 配置 CAN TX 缓冲区CO_CANsend() 发送 CAN 报文或将其排入中断服务程序队列CO_CANclearPendingSyncPDOs() 终止挂起的同步 TPDOCO_CANmodule_disable() 关闭 CAN 模块CO_CANmodule_process() 基于轮询的 CAN 错误状态处理文件 3OD.h / OD.c — 对象字典这些文件是由 CANopenEditor 工具根据 .xpd 项目文件自动生成的。切勿手动编辑它们。OD.h 文件包含对象计数宏OD_CNT_NMT、OD_CNT_SDO_SRV、OD_CNT_RPDO 等——由 CANopen.c 读取以确定内部结构体大小数据类型定义OD_PERSIST_COMM_t、OD_RAM_t——映射 OD 布局的 C 结构体条目快捷方式OD_ENTRY_H1001、OD_ENTRY_H1018 等——初始化函数使用的指针配置助手OD_INIT_CONFIG()——仅在启用 CO_MULTIPLE_OD 时使用在 CANopenEditor 中将导出器设置为 “CANopenNode_V4” 即可生成兼容的输出。配置宏控制协议栈规模301/CO_config.h 文件为每个 CANopen 对象定义了启用特性的宏。每个宏都是一个位掩码——特性之间可以通过按位或OR组合。默认值在各自的头文件中设定并可在 CO_driver_target.h 或 CO_driver_custom.h 中覆盖。宏 关键标志 启用后的效果CO_CONFIG_NMT CO_CONFIG_NMT_MASTER 添加 NMT 主站功能CO_CONFIG_HB_CONS CO_CONFIG_HB_CONS_ENABLE 启用心跳消费者CO_CONFIG_EM CO_CONFIG_EM_PRODUCER, CO_CONFIG_EM_CONSUMER 紧急报文的生产/消费CO_CONFIG_SYNC CO_CONFIG_SYNC_ENABLE, CO_CONFIG_SYNC_PRODUCER SYNC 协议生产者或消费者CO_CONFIG_PDO CO_CONFIG_RPDO_ENABLE, CO_CONFIG_TPDO_ENABLE RPDO/TPDO 处理CO_CONFIG_SDO_CLI CO_CONFIG_SDO_CLI_ENABLE SDO 客户端主站功能CO_CONFIG_LSS CO_CONFIG_LSS_SLAVE, CO_CONFIG_LSS_MASTER LSS 从站/主站CO_CONFIG_GTW CO_CONFIG_GTW_ASCII CiA 309 ASCII 网关CO_CONFIG_SRDO CO_CONFIG_SRDO_ENABLE 安全相关数据对象CO_CONFIG_FLAG_CALLBACK_PRE — 在 CAN RX 预处理后启用异步回调CO_CONFIG_FLAG_TIMERNEXT — 计算下一个定时器事件以优化操作系统休眠清理与关闭当应用程序退出时通常是在收到 NMT 应用复位命令后清理序列与初始化过程正好相反。CAN 模块会被切回配置模式随后 CO_delete() 将按照与分配相反的顺序释放所有已分配的内存CO_CANsetConfigurationMode((void*)CANptr); CO_delete(CO);CO_delete() 会遍历每一个对象——从网关和跟踪记录开始依次经过 LSS、SRDO、GFC、LEDs、PDO、SYNC、TIME、SDO、Emergency、HB 消费者、NMT最后是 CAN 模块和 CO_t 结构体本身——对每一个对象调用 CO_free()。如果定义了 CO_USE_GLOBALS则完全不会编译 CO_delete()因为所有对象都是静态分配的。