一、EtherCAT 基础
1.1 EtherCAT定义
EtherCAT(Ethernet for Control Automation Technology)是由德国倍福(Beckhoff)公司2003年推出的工业以太网协议,核心定位是高性能、低成本、易扩展的实时工业总线。它并非取代以太网,而是在标准以太网帧的基础上优化了数据处理方式,解决了传统以太网(CSMA/CD)实时性差的问题,成为工业机器人、运动控制、自动化产线的主流总线方案。
对比传统工业总线(Profinet、Modbus RTU、CANopen),EtherCAT的核心优势:
- 实时性:通信周期可达μs级,抖动<1μs;
- 带宽利用率:高达99%(传统以太网仅30%左右);
- 拓扑灵活:支持线性、星型、树型、环形冗余拓扑;
- 兼容性:基于标准以太网物理层(RJ45、光纤),可复用普通以太网硬件;
- 成本:从站无需独立CPU/以太网控制器,仅需专用ESC(EtherCAT Slave Controller)芯片(如ET1100)。
1.2 核心设计理念
EtherCAT打破了"以太网帧需完整接收后再处理"的传统逻辑,采用On-the-Fly(飞读飞写) 机制:主站发送的单个以太网帧遍历所有从站,每个从站在帧传输过程中"实时截取"属于自己的数据(读),或"实时写入"自身状态(写),帧无需在从站缓存,最终由最后一个从站将帧回传给主站(或主站自身接收)。这是其高实时性、高带宽利用率的核心。
二、EtherCAT 核心技术原理
2.1 通信架构与拓扑
(1)主从架构
- 主站(Master):唯一主动发起通信的节点,通常由工控机/嵌入式主板(如x86、ARM)实现,负责总线管理、数据收发、时钟同步;
- 从站(Slave):被动响应的节点,如驱动器、传感器、IO模块,内置ESC芯片,无需独立操作系统,仅处理自身数据。
(2)拓扑类型
- 线性拓扑:最常用,从站依次串联,布线简单;
- 星型拓扑:通过交换机扩展,适合分散布局的设备;
- 环形拓扑:冗余设计,某段链路故障时自动切换,提升可靠性;
- 混合拓扑:结合线性+星型,适配复杂产线。
物理层规范:
- 传输速率:100Mbps(全双工),暂不支持1Gbps(工业场景无需);
- 电缆:标准CAT5e/CAT6以太网电缆,最大单段长度100m;
- 接头:RJ45(常规)或M12(工业级,抗震动)。
2.2 实时性核心:分布式时钟(DC)
EtherCAT的μs级同步依赖Distributed Clock(DC)分布式时钟 机制,解决多从站的时间同步问题:
- 主站选择一个从站作为"参考时钟源"(通常是第一个带DC功能的从站);
- 主站通过"广播校准帧"测量主站与参考从站、参考从站与其他从站的链路延迟;
- 各从站根据校准结果调整本地时钟,同步精度可达±1ns;
- 所有从站的动作(如电机触发、传感器采样)基于同步时钟执行,避免时序错乱。
对于ROS2开发者,DC同步可直接对接ROS2的ros2/time模块,实现总线时钟与ROS2系统时钟的对齐。
2.3 数据交互核心:PDO与SDO
EtherCAT的所有数据交互基于对象字典(OD,Object Dictionary) ------可理解为从站的"寄存器映射表",每个数据项(如电机位置、IO状态)对应唯一的索引(Index)和子索引(SubIndex),用16进制表示(如0x6040代表电机控制字)。
基于OD衍生出两种核心数据交互方式:
(1)过程数据对象(PDO,Process Data Object)
- 定位:实时周期数据传输(核心),用于传感器/执行器的高频数据交互(如1ms周期);
- 分类:
- TxPDO:从站→主站(如编码器位置、传感器值);
- RxPDO:主站→从站(如电机速度指令、IO输出控制);
- 特点:无协议开销、固定长度、映射灵活------可将多个OD项"打包"到一个PDO中,减少通信次数。
(2)服务数据对象(SDO,Service Data Object)
- 定位:非周期配置数据传输,用于设备参数配置(如电机加速度、从站地址);
- 通信方式:客户端-服务器模式(主站为客户端,从站为服务器),基于请求-响应机制;
- 特点:支持任意OD项读写、有错误校验,但实时性差,仅在初始化/参数修改时使用。
2.4 从站状态机
EtherCAT从站必须按固定状态切换才能正常工作,核心状态(按启动顺序):
| 状态 | 缩写 | 作用 |
|---|---|---|
| INIT | 初始化 | 从站上电,ESC芯片自检,主站识别从站ID和型号 |
| PRE-OP | 预运行 | 主站配置从站OD、PDO映射、DC参数,未允许过程数据交互 |
| SAFE-OP | 安全运行 | 允许PDO通信,但从站输出被禁用(如电机不上电),用于安全检查 |
| OP | 运行 | 从站正常工作,PDO实时交互,执行器/传感器正常输出/输入 |
状态切换需主站发送特定指令(如写0x0001到从站控制寄存器),C++开发中需严格检查状态切换结果,避免设备异常。
三、EtherCAT 协议栈与C++开发
3.1 主流开源协议栈对比
| 协议栈 | 语言 | 特点 | 适配场景 |
|---|---|---|---|
| SOEM | C | 轻量、无内核依赖、跨平台(Linux/Windows/嵌入式)、代码量少 | 嵌入式设备、小型机器人 |
| IgH EtherCAT | C | 功能完善、支持Linux内核态/用户态、冗余主站、DC同步优化 | ROS2工控机、工业产线 |
两者均为C语言编写,但可通过extern "C"无缝集成到C++项目中,以下以SOEM为例讲解C++封装,以IgH为例讲解ROS2集成。
3.2 SOEM的C++封装示例
SOEM是轻量协议栈,适合快速验证EtherCAT通信,以下是核心封装类(简化版):
cpp
#include <soem/ethercat.h>
#include <soem/ethercatdc.h>
#include <string>
#include <vector>
#include <stdexcept>
// EtherCAT主站类
class EtherCATMaster {
public:
// 构造函数:初始化主站,指定网卡(如"eth0")
EtherCATMaster(const std::string& ifname) : ifname_(ifname), is_running_(false) {
// 初始化SOEM
if (ec_init(ifname_.c_str()) == 0) {
throw std::runtime_error("EtherCAT主站初始化失败:网卡" + ifname_ + "不存在");
}
// 扫描从站
ec_config_init(FALSE); // FALSE=不自动配置PDO,手动配置更灵活
slave_count_ = ec_slavecount;
if (slave_count_ == 0) {
throw std::runtime_error("未检测到EtherCAT从站");
}
printf("检测到%d个EtherCAT从站\n", slave_count_);
}
// 配置从站状态机到OP状态
void configSlaveToOP() {
// 1. 配置DC同步(可选,需从站支持)
ec_configdc();
// 2. 切换到PRE-OP
ec_writestate(EC_STATE_PRE_OP);
// 3. 切换到SAFE-OP
ec_writestate(EC_STATE_SAFE_OP);
// 4. 切换到OP
ec_writestate(EC_STATE_OP);
// 检查状态
for (int i = 1; i <= slave_count_; i++) {
if (ec_slave[i].state != EC_STATE_OP) {
throw std::runtime_error("从站" + std::to_string(i) + "切换到OP状态失败");
}
}
is_running_ = true;
printf("所有从站已进入OP状态\n");
}
// 读取从站TxPDO数据(示例:读取从站1的编码器位置,索引0x6064)
int32_t readSlaveTPDO(int slave_id, uint16_t index, uint8_t subindex) {
if (!is_running_) {
throw std::runtime_error("主站未运行,无法读取PDO");
}
// 查找PDO映射的内存地址(SOEM已将PDO映射到本地数组)
//指定 EtherCAT 从站的 PDO 数据区(RxPDO)的首地址,存到指针 pdo_addr 里
uint8_t* pdo_addr = ec_slave[slave_id].outputs; // TxPDO对应outputs(SOEM命名习惯)
//.inputs:从站的输入数据区起始地址
// 从OD中读取数据(简化示例,实际需先配置PDO映射)
return EC_READ_U32(&pdo_addr[ec_findmap(slave_id, index, subindex)]);
}
// 写入从站RxPDO数据(示例:写入从站1的速度指令,索引0x60FF)
void writeSlaveRPDO(int slave_id, uint16_t index, uint8_t subindex, int32_t value) {
if (!is_running_) {
throw std::runtime_error("主站未运行,无法写入PDO");
}
uint8_t* pdo_addr = ec_slave[slave_id].inputs; // RxPDO对应inputs
EC_WRITE_U32(&pdo_addr[ec_findmap(slave_id, index, subindex)], value);
}
// 周期更新总线数据(必须在循环中调用,实现飞读飞写)
void updateBus() {
if (!is_running_) return;
ec_send_processdata(); // 发送PDO数据
ec_receive_processdata(EC_TIMEOUTRET); // 接收PDO数据,超时时间EC_TIMEOUTRET
}
// 析构函数:释放资源
~EtherCATMaster() {
if (is_running_) {
ec_writestate(EC_STATE_INIT); // 切换从站到初始状态
}
ec_close(); // 关闭主站
}
private:
std::string ifname_; // 网卡名称
int slave_count_; // 从站数量
bool is_running_; // 主站运行状态
};
// 测试代码
int main() {
try {
// 初始化主站(网卡eth0)
EtherCATMaster master("eth0");
// 配置从站到OP状态
master.configSlaveToOP();
// 周期读写数据(1ms周期)
while (true) {
// 读取从站1的编码器位置
int32_t pos = master.readSlaveTPDO(1, 0x6064, 0x00);
// 写入从站1的速度指令(1000rpm)
master.writeSlaveRPDO(1, 0x60FF, 0x00, 1000);
// 更新总线
master.updateBus();
// 1ms延迟(实际需用高精度定时器)
usleep(1000);
printf("编码器位置:%d\n", pos);
}
} catch (const std::exception& e) {
printf("错误:%s\n", e.what());
return -1;
}
return 0;
}
代码说明:
ec_init():初始化SOEM主站,绑定指定网卡;ec_config_init():扫描从站并初始化配置;ec_writestate():切换从站状态机;ec_send/receive_processdata():核心的飞读飞写接口,必须在周期循环中调用;ec_findmap():根据OD索引查找PDO在本地内存的映射地址,是C++封装的关键。
3.3 编译与依赖
编译上述代码需先安装SOEM:
bash
# 安装依赖
sudo apt install libpcap-dev
# 克隆SOEM源码
git clone https://github.com/OpenEtherCATsociety/SOEM.git
cd SOEM && mkdir build && cd build
cmake .. && make && sudo make install
# 编译C++代码(假设文件名为ethercat_master.cpp)
g++ ethercat_master.cpp -o ethercat_master -lsoem -lpcap -lpthread
四、EtherCAT 与ROS2的集成
ROS2的核心优势是分布式通信和实时性优化(如Cyclone DDS、PREEMPT_RT内核),与EtherCAT结合可实现"ROS2上层控制 + EtherCAT底层执行"的工业机器人/自动化系统。
4.1 集成架构
发布指令/订阅状态
ROS2控制节点
发布状态话题
回传数据
反馈状态数据
EtherCAT从站(驱动器/传感器)
4.2 基于IgH + ROS2的实战示例
IgH是ROS2环境下最常用的EtherCAT协议栈(支持内核态实时),以下是ROS2节点开发步骤:
步骤1:安装IgH EtherCAT Master
bash
# 1. 安装依赖
sudo apt install build-essential linux-headers-$(uname -r) git
# 2. 克隆IgH源码
git clone https://github.com/etherlab.org/ethercat.git
cd ethercat
# 3. 配置(指定网卡驱动,如e1000e)
./configure --prefix=/usr/local --sysconfdir=/etc --enable-generic --enable-8139too=no --enable-e1000e=yes
# 4. 编译安装
make && sudo make install
# 5. 加载内核模块
sudo modprobe ec_master
# 6. 配置主站(绑定网卡eth0)
sudo ethercatctl attach eth0
# 7. 检查从站
sudo ethercatctl slaves # 列出已连接的从站
步骤2:编写ROS2 EtherCAT节点(C++)
创建ROS2包(假设包名为ethercat_ros2),核心代码(src/ethercat_node.cpp):
cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
#include "std_srvs/srv/set_bool.hpp"
#include <ethercatcpp/ethercat.h> // IgH的C++封装(需安装ethercatcpp库)
using namespace std::chrono_literals;
class EtherCATROS2Node : public rclcpp::Node {
public:
EtherCATROS2Node() : Node("ethercat_ros2_node"), is_ethercat_running_(false) {
// 1. 初始化参数
this->declare_parameter("ethercat_ifname", "eth0");
this->get_parameter("ethercat_ifname", ifname_);
// 2. 创建ROS2话题发布者(发布编码器位置)
pos_pub_ = this->create_publisher<std_msgs::msg::Int32>("encoder_pos", 10);
// 创建ROS2服务(启停EtherCAT)
start_stop_srv_ = this->create_service<std_srvs::srv::SetBool>(
"start_stop_ethercat",
std::bind(&EtherCATROS2Node::startStopCallback, this, std::placeholders::_1, std::placeholders::_2)
);
// 3. 创建周期定时器(1ms,对应EtherCAT通信周期)
timer_ = this->create_wall_timer(
1ms,
std::bind(&EtherCATROS2Node::timerCallback, this)
);
RCLCPP_INFO(this->get_logger(), "EtherCAT ROS2节点已启动");
}
// 启停服务回调
void startStopCallback(
const std::shared_ptr<std_srvs::srv::SetBool::Request> req,
std::shared_ptr<std_srvs::srv::SetBool::Response> res
) {
if (req->data) {
// 启动EtherCAT
try {
ethercat_master_.attach(ifname_); // 绑定网卡
ethercat_master_.configSlaves(); // 配置从站
ethercat_master_.setState(EC_STATE_OP); // 切换到OP状态
is_ethercat_running_ = true;
res->success = true;
res->message = "EtherCAT主站已启动,从站数量:" + std::to_string(ethercat_master_.getSlaveCount());
} catch (const std::exception& e) {
res->success = false;
res->message = "启动失败:" + std::string(e.what());
}
} else {
// 停止EtherCAT
ethercat_master_.setState(EC_STATE_INIT);
ethercat_master_.detach();
is_ethercat_running_ = false;
res->success = true;
res->message = "EtherCAT主站已停止";
}
RCLCPP_INFO(this->get_logger(), "%s", res->message.c_str());
}
// 周期回调(1ms)
void timerCallback() {
if (!is_ethercat_running_) return;
// 1. 更新EtherCAT总线数据
ethercat_master_.updateProcessData();
// 2. 读取从站1的编码器位置(OD索引0x6064)
int32_t pos = ethercat_master_.readPDO(1, 0x6064, 0x00);
// 3. 发布到ROS2话题
auto msg = std_msgs::msg::Int32();
msg.data = pos;
pos_pub_->publish(msg);
// 4. (可选)订阅ROS2指令话题并写入PDO
// int32_t vel_cmd = ...; // 从话题获取速度指令
// ethercat_master_.writePDO(1, 0x60FF, 0x00, vel_cmd);
}
private:
std::string ifname_; // 网卡名称
bool is_ethercat_running_; // EtherCAT运行状态
ethercatcpp::Master ethercat_master_; // IgH的C++封装类
rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr pos_pub_; // 位置发布者
rclcpp::Service<std_srvs::srv::SetBool>::SharedPtr start_stop_srv_; // 启停服务
rclcpp::TimerBase::SharedPtr timer_; // 周期定时器
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
// 启用ROS2实时执行器(优化实时性)
rclcpp::executors::SingleThreadedExecutor executor(rclcpp::ExecutorOptions(), rclcpp::Clock::make_shared());
auto node = std::make_shared<EtherCATROS2Node>();
executor.add_node(node);
executor.spin();
rclcpp::shutdown();
return 0;
}
步骤3:配置ROS2编译文件(CMakeLists.txt)
cmake
cmake_minimum_required(VERSION 3.8)
project(ethercat_ros2)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# 查找ROS2依赖
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(std_srvs REQUIRED)
# 查找IgH EtherCAT依赖
find_package(ethercatcpp REQUIRED)
# 编译节点
add_executable(ethercat_node src/ethercat_node.cpp)
ament_target_dependencies(ethercat_node rclcpp std_msgs std_srvs)
target_link_libraries(ethercat_node ethercatcpp::ethercat)
# 安装可执行文件
install(TARGETS
ethercat_node
DESTINATION lib/${PROJECT_NAME}
)
ament_package()
4.3 ROS2 + EtherCAT 实时性优化
ROS2默认非实时,需结合以下优化确保EtherCAT的μs级实时性:
-
安装Linux PREEMPT_RT实时内核:
bashsudo apt install linux-image-rt-amd64 linux-headers-rt-amd64 -
配置ROS2实时执行器:使用
SingleThreadedExecutor或MultiThreadedExecutor的实时模式; -
进程优先级:将EtherCAT ROS2节点设置为高优先级(如SCHED_FIFO,优先级99);
-
内存锁定:通过
mlockall()锁定内存,避免换页导致的延迟; -
CPU亲和性:将节点绑定到独立CPU核心,避免其他进程干扰。
五、EtherCAT 高级特性
5.1 冗余机制
(1)网络冗余(环形拓扑)
从站需支持双端口(ESC芯片带两个以太网口),主站发送的帧在环形拓扑中双向传输,某段链路故障时,帧自动从另一方向传输,切换时间<1ms。
(2)主站冗余
支持热备主站:主主站故障时,备用主站无缝接管总线,基于EtherCAT的AL Status Code实现主站切换,无数据丢失。
5.2 功能安全:FSoE
FSoE(Fail-Safe over EtherCAT)是EtherCAT的安全通信协议,满足IEC 61508 SIL3安全等级,用于紧急停止、安全门监控等场景:
- 基于加密的安全通道传输安全数据;
- 支持安全状态的快速同步(<10ms);
- 与普通PDO/SDO并行传输,不影响实时性。
5.3 诊断与调试
- 工具:
ethercatctl:IgH自带工具,可查看从站状态、OD、PDO映射;- Wireshark:抓包分析EtherCAT帧(需安装EtherCAT插件);
- SOEM自带的
ethercatdbg:调试从站通信。
- 诊断数据:从站的AL Status Code(错误码)、DC同步误差、链路质量,可通过ROS2话题发布,便于远程监控。
六、工程实践与常见问题
6.1 常见问题与排查
| 问题现象 | 排查方向 |
|---|---|
| 主站无法识别从站 | 1. 网卡未绑定;2. 从站未上电;3. 电缆故障;4. 从站地址冲突 |
| PDO读写失败 | 1. PDO映射配置错误;2. 从站未进入OP状态;3. OD索引/子索引错误 |
| 同步精度差 | 1. DC未配置;2. 参考时钟源选择错误;3. 链路延迟未校准 |
| 通信抖动大 | 1. 未启用RT内核;2. 进程优先级低;3. 网卡驱动未优化 |
6.2 工程选型建议
- 从站芯片:ET1100(入门)、ET9300(高性能,支持FSoE);
- 从站模块:倍福(EL系列)、汇川(AM系列)、雷赛(CL系列);
- 协议栈:嵌入式场景选SOEM,ROS2工控机场景选IgH;
- 开发工具:倍福TwinCAT(商用,可视化配置,可导出C++代码)。
总结
- 核心原理:EtherCAT的高实时性源于On-the-Fly飞读飞写机制和DC分布式时钟,数据交互核心是PDO(周期实时)和SDO(非周期配置);
- C++开发:可通过封装SOEM/IgH的C接口实现面向对象的EtherCAT主站,核心是状态机管理、PDO/SDO读写、周期数据更新;
- ROS2集成:通过ROS2节点封装EtherCAT主站,利用话题/服务实现上层控制与底层总线的交互,需结合RT内核优化实时性;
- 工程落地:重点关注从站状态切换、PDO映射配置、实时性优化,冗余机制和FSoE可提升系统可靠性。
补充1.
c
uint8_t* pdo_addr = ec_slave[slave_id].inputs;
// RxPDO对应inputs
1. 变量含义
uint8_t*:8 位无符号字符指针 ,本质就是一段连续内存的起始地址(字节级指针)。ec_slave[]:全局 EtherCAT 从站数组,每个元素代表一个 EtherCAT 从站设备。slave_id:从站编号(0、1、2...),用来定位具体设备。.inputs:从站的输入数据区起始地址(注意注释:RxPDO 对应 inputs)。
2. 核心含义
c
uint8_t* pdo_addr = ec_slave[slave_id].inputs;
意思是:
拿到指定 EtherCAT 从站的 PDO 数据区(RxPDO)的首地址,存到指针 pdo_addr 里。
3. 为什么是 uint8_t* ?
因为 EtherCAT PDO 数据是按字节排布 的,不管你是 bool、int16、int32、float,底层都是字节流。
用 uint8_t* 可以精确按字节偏移寻址,这是第二行代码能工作的基础。
补充2.
c
EC_WRITE_U32(&pdo_addr[ec_findmap(slave_id, index, subindex)], value);
这一行是嵌套结构 ,从内到外一层层拆开
第一步:拆解最内层函数 ec_findmap()
c
ec_findmap(slave_id, index, subindex)
作用
在 EtherCAT 从站的 PDO 映射表中,查找某个对象的【字节偏移量】。
参数
slave_id:从站编号index:CANopen / EtherCAT 对象字典索引(如 0x6000)subindex:子索引(如 0x01)
返回值
返回一个 整数偏移量(字节数) ,例如:
返回 4 → 代表这个 PDO 数据在从站数据区的 第 4 个字节开始。
第二步:数组寻址 pdo_addr[偏移量]
c
pdo_addr[ec_findmap(...)]
pdo_addr 是 uint8_t* 类型(字节指针)。
所以:
c
pdo_addr[n]
== *(pdo_addr + n)
== 数据区起始地址 + n 字节
这一步得到的是:
目标 PDO 变量所在的那个字节的【值】
第三步:取地址 &
c
&pdo_addr[偏移量]
& 是取地址符。
所以这整段:
c
&pdo_addr[ec_findmap(slave_id, index, subindex)]
最终得到:
目标 PDO 变量在内存中的【起始地址】
类型是:uint8_t*
第四步:宏 EC_WRITE_U32
c
EC_WRITE_U32(地址, 值);
这是 EtherCAT 库提供的写 32 位数据宏 ,作用:
往指定地址,写入 4 字节(32bit)无符号整数。
等价于:
c
*(uint32_t*)地址 = 值;
代码完整语义
c
EC_WRITE_U32(
&pdo_addr[ ec_findmap(slave_id, index, subindex) ],
value
);
- 找到从站
slave_id里,对象字典index:subindex对应的 PDO 字节偏移 - 从
pdo_addr(PDO 数据区首地址)偏移这么多字节 - 取这个位置的内存地址
- 把
value(32 位数值)以 4 字节形式写入这个地址
内存结构图解
假设:
ec_slave[0].inputs地址 =0x1000ec_findmap()返回偏移 =4
那么内存布局是:
0x1000 pdo_addr[0] 字节0
0x1001 pdo_addr[1] 字节1
0x1002 pdo_addr[2] 字节2
0x1003 pdo_addr[3] 字节3
0x1004 pdo_addr[4] ← 目标地址(&pdo_addr[4] = 0x1004)
0x1005 pdo_addr[5]
0x1006 pdo_addr[6]
0x1007 pdo_addr[7]
EC_WRITE_U32(0x1004, value)
就是把 4 字节写入:
0x1004 ~ 0x1007
参数传递完整流程
c
uint8_t* pdo_addr = ec_slave[slave_id].inputs;
EC_WRITE_U32(
&pdo_addr[ ec_findmap(slave_id, index, subindex) ],
value
);
参数传递路径:
slave_id→ 传给ec_slave[]和ec_findmap()index + subindex→ 传给ec_findmap()查偏移- 偏移 → 传给
pdo_addr[]做字节偏移 &pdo_addr[]→ 传给EC_WRITE_U32作为目标地址value→ 传给EC_WRITE_U32作为要写入的 32 位数据
底层字节数组 + 指针偏移 + 宏写入
典型的 硬件驱动 / 协议栈 写法:
- 把一段连续内存当作 PDO 数据区
- 用字节指针精确控制每个变量的位置
- 用映射表(findmap)动态查找地址
- 用类型强转宏写入指定宽度数据
结构总结:
ec_slave[slave_id].inputs
→ uint8_t* 字节数组首地址
↓ 加偏移
目标地址
↓
EC_WRITE_U32 写入 4 字节