去年接了个设备改造项目,客户要求用RK3588工业开发板直接跑EtherCAT主站,控制四台伺服电机做同步凸轮运动。当时项目组里有人觉得RK3588干这活有点大材小用,也有人担心Linux系统做运动控制实时性不够。实际做下来,RK3588配合IgH EtherCAT主站控制伺服电机完全没有问题,但这中间的坑是真的不少。
这篇文章把我从内核编译、主站配置、从站调试到最终稳定跑通的全过程写出来,代码部分是能直接拿过去用的C语言框架。写这篇不是为了堆概念,是给准备在RK3588这类ARM工控板上做EtherCAT主站的人一份能少走弯路的参考。
先说清楚一件事:IgH是开源的EtherCAT主站实现,跑在Linux用户态+内核模块上,配合PREEMPT_RT实时补丁之后,完全能承担伺服电机的位置/速度控制任务。RK3588算力对EtherCAT主站来说绰绰有余,毕竟主站本质上是周期性收发以太网帧,真正吃CPU的是你跑在旁边的视觉算法、HMI逻辑这些上层应用。
1. 为什么选RK3588跑EtherCAT主站:硬件选型与网卡坑
1.1 算力冗余不是浪费,是给整个控制系统留余量
用过传统IPC方案的人应该知道,以前做多轴运动控制基本是“工控机+PCI EtherCAT卡”的组合,一张卡几千块,还得装专用驱动,部署和售后都比较麻烦。RK3588的出现把这件事简化了一大截:8核ARM,4个A76大核跑实时任务绰绰有余,4个A55小核处理中断和杂事,GPU和NPU还能顺带跑机器视觉。
关键点在于RK3588的算力让“控制+视觉+IO逻辑”集成到了一台板子上,省掉了控制器的网线通信环节,延迟直接少了几个数量级。我实际测试过,在A76大核上跑1ms周期的EtherCAT任务,同时开一路MIPI摄像头做图像识别,CPU总占用不到40%,周期抖动控制在几十微秒以内。这个余量在产线上很值钱,因为现场设备往往还要挂触摸屏、OPC UA、数据库记录这些乱七八糟的负载。
不过要提醒一句:算力强不等于主站稳定,EtherCAT主站的实时性核心在于网络这一层,而不在CPU本身。
1.2 网卡选型决定主站稳定性的一半
这是整个项目里踩得最深的坑,我必须放最前面说。
RK3588处理器自带千兆GMAC控制器,开发板上通常会引出1到2个千兆网口。一开始我想当然地用了板载网口做EtherCAT,结果发现两个问题:第一,IgH对板载GMAC的支持要看具体内核版本和驱动(不同开发板底层的PHY芯片和时钟配置都不一样);第二,板载网口的DMA中断和实时任务抢CPU的问题比较难隔离,周期抖动明显偏大,1ms周期还能勉强跑,换成0.5ms就开始出现掉帧。
后来换了PCIe转出来的独立网卡,情况完全不一样。IgH官方支持列表里最稳的几类网卡驱动是e1000e、igb、r8169,对应的就是Intel I210/I211以及Realtek RTL8111/RTL8168系列。RK3588的PCIe接口很好用,一块I211网卡模块几十块钱,却能换来回事、稳定的主站通信。
网卡选型的核心结论是:不要用板载GMAC跑EtherCAT主站,直接上PCIe独立网卡,优先选Intel I210/I211,其次Realtek 8111/8168。Intel网卡的igb驱动在IgH生态里维护得最久,稳定性和文档完善程度远超其他选择。Realtek也不是不能用,我另一台设备上RTL8168跑了几个月也没出问题,但配置时记得关掉网卡的节能模式。
1.3 电源、散热与整机布局
RK3588工业板跑EtherCAT主站,功耗比想象中高。四台伺服电机通过EtherCAT总线连接时,主站网卡需要持续发送帧,网络控制器的功耗会增加,加上RK3588本身在高负载下发热不小,工业现场环境温度普遍偏高,所以散热不能省。建议用带风扇的被动散热外壳,或者至少把A76大核的调频策略固定在高性能档位,避免温度导致降频影响实时性。
电源方面,伺服驱动器的EtherCAT从站接口不需要主站供电,但RK3588开发板本身需要稳定电源。如果现场有伺服电机启停的大电流波动,建议在开发板电源入口加一个隔离DCDC或工业电源模块,否则电机急停瞬间的电压跌落足以让主站网卡离线。
2. 实时性地基:PREEMPT_RT内核编译和CPU隔离
2.1 内核版本与补丁选择
IgH EtherCAT主站依赖Linux内核的网络协议栈和中断处理,默认内核下任务调度延迟不稳定,必须打上PREEMPT_RT实时补丁。以6.6系列当前最新的长期稳定内核为例,下载对应的rt补丁,两者解压后直接打进去:
# 下载内核源码和对应rt补丁 tar xf linux-6.6.tar.xz cd linux-6.6 xz -dk ../patch-6.6-rt*.patch.xz patch -p1 < ../patch-6.6-rt*.patchIgH对内核版本不算挑剔,官方维护重点还是在x86平台,ARM平台在高版本内核上需要一点运气。我建议直接使用支持新内核的IgH 2.0分支,而不是还在用的1.5.2老版本。2.0的API与1.5.2基本一致,但底层适配了较新的内核网络接口,省去自己打补丁的功夫。
2.2 编译内核的步骤与参数
内核配置至少要保证这几个选项,缺一个实时性都上不来:
CONFIG_PREEMPT_RT=y CONFIG_HZ_1000=y CONFIG_CPU_ISOLATION=y CONFIG_NO_HZ_FULL=y CONFIG_RCU_NOCB_CPU=yRK3588的设备树在编译内核时需要单独指定,不同开发板的设备树文件名不一样。如果你用的是常见RK3588开发板,一般在arch/arm64/boot/dts/rockchip/目录下都能找到对应dts文件。编译命令按标准流程来:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip_linux_defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig # 在这里配置上面的PREEMPT_RT等选项 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)编译完成后把内核镜像和设备树放到开发板的启动分区,根文件系统不变,直接重启进入新内核,用uname -a确认PREEMPT_RT生效。这一步每个开发板略有差异,但原理一样。
2.3 CPU隔离、中断亲和性与网卡队列
光有实时内核还不够,Linux默认的任务调度和中断分发会把实时任务和网卡中断打到同一个核上,导致周期性抖动。我的做法是把A76大核隔离出来给控制任务用,A55小核处理网卡中断和系统杂务。
在bootargs里加上:
isolcpus=4,5,6,7 rcu_nocbs=4,5,6,7 nohz_full=4,5,6,7这样4个A76大核就不参与普通任务的负载均衡,控制线程通过CPU亲和性绑到其中一个核上。网卡中断则用smp_affinity_list固定到A55小核:
# 找到网卡中断号 cat /proc/interrupts | grep eth # 假设中断号是78,绑定到0-3核 echo 0-3 > /proc/irq/78/smp_affinity_list如果PCIe网卡是多队列的,用ethtool把队列数降到1,避免中断风暴:
ethtool -L eth0 combined 1这套组合实测下来,1ms周期任务的最大抖动从裸内核的500微秒以上降到了40微秒左右,完全满足伺服电机的位置控制需求。
3. IgH主站编译安装:从configure参数到总线扫描
3.1 编译前必须想清楚的几个参数
IgH主站的编译参数直接影响运行时的功能,网上教程大多直接照抄configure命令,其实这些参数要根据项目情况选。我用的组合是:
./bootstrap ./configure \ --prefix=/opt/etherlab \ --enable-cycles \ --enable-rttm \ --disable-eoe \ --enable-debug-if--enable-cycles是开启周期时间测量,配合rtdm或打印可以精确评估每次通信周期的实际耗时;--enable-rttm是实时任务模型支持;--disable-eoe后面详细讲;--enable-debug-if保留网卡调试接口,真出问题时还能用debugfs看状态。
编译安装流程很简单:
make sudo make install sudo mkdir -p /opt/etherlab/etc sudo cp script/ethercatctl /opt/etherlab/etc/装完后工具链在/opt/etherlab/sbin/下,建议把sbin目录加进PATH,后面用ethercat命令方便。
3.2 加载模块和绑定网卡
IgH运行时需要把EtherCAT主站模块和对应网卡驱动模块加载到内核。很多人卡在这一步:加载ec_master模块后,网卡还是普通网卡,EtherCAT总线根本扫不到从站。关键是加载顺序和参数。
先确认网卡的PCI地址:
lspci | grep -i ethernet假设结果是02:00.0,那么加载模块时这样指定:
sudo modprobe ec_master main_devices=02:00.0注意主站模块加载时,IgH会自动把该PCI设备从原驱动中解绑并接管。加载完成后原来的eth0网口消失,这正常。然后启动主站:
sudo /opt/etherlab/etc/ethercatctl start查看主站和总线设备:
ethercat master ethercat slaves -v能列出从站说明主站已经跑起来了。如果ethercat slaves显示总线为空,优先检查网卡驱动是否被正确接管、PCI地址是否写对。
3.3 主站调试级别和日志
IgH的好处是调试手段丰富。遇到总线异常时,先打开内核模块调试输出:
echo 0xffff > /sys/module/ec_master/parameters/debug dmesg | tail -50调试级别设为0xffff会把所有主站的通信状态、状态机跳转、错误码全部打出来,虽然日志量大,但定位问题非常有用。排查完记得设回0。
4. 伺服从站配置:PDO映射和DC同步这关必须过
4.1 从站信息的获取与PDO映射
每个伺服驱动器的从站信息都在它的ESI文件里,IgH加载后会从从站的EEPROM读取。用ethercat slaves -v可以查看每个从站的厂商ID、产品码和当前PDO信息。这是一个很关键的环节:很多伺服出厂PDO映射只包含最基础的几个对象,够用但不一定满足你的运动控制需求。
PDO映射的定义就是主站和从站之间周期性交换的数据布局。常用的伺服电机控制至少需要这些对象:
| 方向 | 索引 | 内容 |
|---|---|---|
| 主站→从站 | 0x6040 | 控制字 |
| 主站→从站 | 0x6060 | 运行模式 |
| 主站→从站 | 0x607A | 目标位置 |
| 主站→从站 | 0x6080 | 最大速度 |
| 主站→从站 | 0x6081 | 加速度 |
| 主站→从站 | 0x6083 | 减速度 |
| 从站→主站 | 0x6041 | 状态字 |
| 从站→主站 | 0x6061 | 模式显示 |
| 从站→主站 | 0x6064 | 实际位置 |
| 从站→主站 | 0x606C | 实际速度 |
如果伺服出厂映射里缺了对象,就不能直接改从站EEPROM(生产环境不建议改),而应该在主站应用代码里通过SDO在线写映射表。映射操作要在Pre-Op状态下进行,先清空映射表再逐项填入,最后激活。
4.2 DC分布时钟:同步的命根子
EtherCAT主站和多个伺服从站之间要保持严格同步,靠的是分布时钟机制。主站通过SYNC0信号让所有从站在同一时刻采样输入、更新输出,这样多轴之间就没有累积误差。
配置DC的核心是周期时间,我常用1ms。代码里这样配置从站DC参数:
ecrt_slave_config_dc(sc, 0x0700, 1000000, 0, 0, 0);第二个参数0x0700是DC控制位,第三个参数1000000是SYNC0周期,单位纳秒,即1ms。0x0700的含义是启用SYNC0和SYNC1同步输出。不同伺服从站对DC的支持程度不同,有的型号还需要设置SYNC1的周期和偏移,基本原则是:要么都用SYNC0周期输出,要么让SYNC1跟随SYNC0但偏移半个周期,具体看驱动器的操作手册。
如果不配置DC,从站会在FreeRun模式下运行,主站发一帧从站响应一次,帧到达时间有微秒级差异,单个轴也许不影响,多轴联动的轨迹精度会出问题。四台伺服做同步凸轮运动时,不配DC根本没法看。
4.3 为什么我编译时直接禁用EoE
IgH把EtherCAT的EoE(以太网邮箱)协议也做了实现,理论上可以通过EtherCAT总线传输以太网数据,比如给伺服驱动器维护口提供远程配置通道。但EoE在主站侧需要在实时任务里处理以太网桥接,这会显著增加周期任务负载和抖动。
产线上的伺服电机做运动控制,根本不需要通过EtherCAT总线传以太网数据。维护口的网络一般单独拉一根跳线直连电脑。所以编译IgH时直接加上--disable-eoe,既精简了内核模块,又避开了网络协议栈对实时任务的干扰。这也是很多IgH长期使用者的共同选择。
5. 完整控制代码:状态机切换与周期任务实现
5.1 定义PDO数据结构和映射表
下面的代码是我在项目里实际跑通的框架,按CIA402协议和伺服驱动器的标准对象字典实现。PDO结构体必须用packed属性,否则编译器对齐填充会让数据偏移错位,这是新手最容易忽略的细节:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <signal.h> #include <sched.h> #include <time.h> #include <sys/mman.h> #include <errno.h> #include "ecrt.h" #define CYCLE_NS 1000000 // 1ms周期 /* 主站向从站发送的数据结构 */ typedef struct __attribute__((packed)) { uint16_t control_word; // 0x6040 uint8_t mode_of_op; // 0x6060 int32_t target_pos; // 0x607A uint32_t max_velocity; // 0x6080 uint32_t accel; // 0x6081 uint32_t decel; // 0x6083 } rxpdo_data_t; /* 从站向主站反馈的数据结构 */ typedef struct __attribute__((packed)) { uint16_t status_word; // 0x6041 uint8_t mode_display; // 0x6061 int32_t actual_pos; // 0x6064 int32_t actual_velocity;// 0x606C uint32_t digital_input; // 0x60FD } txpdo_data_t; /* PDO映射表 */ static ec_pdo_entry_info_t rxpdo_entries[] = { {0x6040, 0x00, 16}, {0x6060, 0x00, 8}, {0x607A, 0x00, 32}, {0x6080, 0x00, 32}, {0x6081, 0x00, 32}, {0x6083, 0x00, 32}, }; static ec_pdo_entry_info_t txpdo_entries[] = { {0x6041, 0x00, 16}, {0x6061, 0x00, 8}, {0x6064, 0x00, 32}, {0x606C, 0x00, 32}, {0x60FD, 0x00, 32}, }; static ec_pdo_info_t rxpdo[] = { {0x1600, sizeof(rxpdo_entries)/sizeof(rxpdo_entries[0]), rxpdo_entries}, }; static ec_pdo_info_t txpdo[] = { {0x1A00, sizeof(txpdo_entries)/sizeof(txpdo_entries[0]), txpdo_entries}, }; static ec_sync_info_t syncs[] = { {0, EC_DIR_OUTPUT, 1, rxpdo, EC_WD_ENABLE}, {1, EC_DIR_INPUT, 1, txpdo, EC_WD_ENABLE}, {0xff} };这里把RXPDO放在SM0、TXPDO放在SM1,方向分别是输出和输入,同时开启了看门狗。如果从站的SM方向配置反了,就会出现能进OP但数据完全是错乱的现象,后面排查章节会细说。
5.2 主站初始化与从站使能
主站初始化逻辑固定,按顺序调用即可。需要注意两个细节:从站的vendor_id和product_code必须和ethercat slaves -v输出一致,写0/0也能匹配但风险很大,一旦总线上有多台不同类型从站就可能配置错对象。域注册的PDO列表要等所有从站配置完成后统一注册。
ec_master_t *master = NULL; ec_domain_t *domain = NULL; ec_slave_config_t *sc = NULL; rxpdo_data_t *rx_data = NULL; txpdo_data_t *tx_data = NULL; static int run = 1; void signal_handler(int sig) { run = 0; } int main(int argc, char *argv[]) { signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); /* 锁定内存防止页面调度 */ mlockall(MCL_CURRENT | MCL_FUTURE); master = ecrt_request_master(0); if (!master) { perror("ecrt_request_master"); return -1; } domain = ecrt_master_create_domain(master); if (!domain) { perror("ecrt_master_create_domain"); return -1; } /* 第0个从站的配置,vendor_id和product_code按实际从站填 */ sc = ecrt_master_slave_config(master, 0, 0, 0x00000101, 0x00010000); if (!sc) { perror("ecrt_master_slave_config"); return -1; } if (ecrt_slave_config_pdos(sc, 1, rxpdo, 1, txpdo)) { fprintf(stderr, "PDO配置失败,检查映射表\n"); return -1; } /* 配置DC同步,1ms周期 */ ecrt_slave_config_dc(sc, 0x0700, CYCLE_NS, 0, 0, 0); /* 注册域PDO条目 */ ec_pdo_entry_reg_t domain_regs[] = { {0, 0, 0x00000101, 0x00010000, 0x6040, 0x00, &rx_data->control_word}, {0, 0, 0x00000101, 0x00010000, 0x6060, 0x00, &rx_data->mode_of_op}, {0, 0, 0x00000101, 0x00010000, 0x607A, 0x00, &rx_data->target_pos}, {0, 0, 0x00000101, 0x00010000, 0x6080, 0x00, &rx_data->max_velocity}, {0, 0, 0x00000101, 0x00010000, 0x6081, 0x00, &rx_data->accel}, {0, 0, 0x00000101, 0x00010000, 0x6083, 0x00, &rx_data->decel}, {0, 0, 0x00000101, 0x00010000, 0x6041, 0x00, &tx_data->status_word}, {0, 0, 0x00000101, 0x00010000, 0x6061, 0x00, &tx_data->mode_display}, {0, 0, 0x00000101, 0x00010000, 0x6064, 0x00, &tx_data->actual_pos}, {0, 0, 0x00000101, 0x00010000, 0x606C, 0x00, &tx_data->actual_velocity}, {0, 0, 0x00000101, 0x00010000, 0x60FD, 0x00, &tx_data->digital_input}, {} }; if (ecrt_domain_reg_pdo_entry_list(domain, domain_regs)) { fprintf(stderr, "域PDO条目注册失败\n"); return -1; } /* 激活主站 */ if (ecrt_master_activate(master)) { fprintf(stderr, "主站激活失败\n"); return -1; } /* 激活后获取域数据指针,这个动作只能在激活后做一次 */ unsigned int data_offset; unsigned char *pdo_data = ecrt_domain_data(domain, &data_offset); rx_data = (rxpdo_data_t *)(pdo_data + 0); tx_data = (txpdo_data_t *)(pdo_data + rxpdo_size_offset); printf("主站激活成功,开始运行\n"); /* 初始化参数 */ rx_data->control_word = 0x00; rx_data->mode_of_op = 8; // CSP模式 rx_data->max_velocity = 500000; // 500000计数/秒 rx_data->accel = 2000000; // 2倍速度加速度 rx_data->decel = 2000000; rx_data->target_pos = 0; /* 开始周期任务 */ cycle_task(); /* 停止时先失能 */ rx_data->control_word = 0x00; ecrt_domain_queue(domain); ecrt_master_send(master); ecrt_master_deactivate(master); ecrt_release_master(master); return 0; }这里有个需要特别注意的细节:ecrt_domain_data返回的指针必须在主站激活后、周期循环开始前获取一次,主循环里直接使用这个指针读写数据。如果放在循环里反复调用,不仅性能差,而且某些IgH版本下会拿到不稳定的地址,导致数据错乱。这个坑我见不少人踩过。
5.3 周期任务与状态机切换
周期任务的定时用clock_nanosleep的绝对时间模式,比usleep准得多。在1ms周期下,这个定时的误差能控制在50微秒以内,前提是任务绑核并且优先级够高。伺服使能的过程遵循CIA402状态机,控制字和状态字的对应关系是标准化的:
| 状态字(0x6041) | 含义 | 下一步控制字 |
|---|---|---|
| 0x40 | Switch on disabled | 0x06 (Shutdown) |
| 0x21 | Ready to switch on | 0x07 (Switch on) |
| 0x23 | Switched on | 0x0F (Enable op) |
| 0x27 | Operation enabled | 0x0F (保持) |
static void cycle_task(void) { struct sched_param param = {.sched_priority = 90}; sched_setscheduler(0, SCHED_FIFO, ¶m); cpu_set_t set; CPU_ZERO(&set); CPU_SET(4, &set); // 绑定到A76大核 sched_setaffinity(0, sizeof(set), &set); struct timespec next; clock_gettime(CLOCK_MONOTONIC, &next); next.tv_nsec += CYCLE_NS; int op_enabled = 0; int32_t target_pos_global = 0; while (run) { /* 绝对时间休眠,保证周期稳定 */ clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL); /* 接收从站数据 */ ecrt_master_receive(master); ecrt_domain_process(domain); /* 伺服上电使能状态机 */ if (!op_enabled) { switch (tx_data->status_word & 0x6F) { case 0x40: // switch on disabled rx_data->control_word = 0x06; break; case 0x21: // ready to switch on rx_data->control_word = 0x07; break; case 0x23: // switched on rx_data->control_word = 0x0F; break; case 0x27: // operation enabled op_enabled = 1; printf("伺服已使能,当前位置 %d\n", tx_data->actual_pos); break; default: rx_data->control_word = 0x00; break; } } else { /* 使能后每周期下发目标位置 */ rx_data->control_word = 0x0F; rx_data->target_pos = target_pos_global; } /* 发送数据到从站 */ ecrt_domain_queue(domain); ecrt_master_send(master); /* 更新绝对时间 */ next.tv_nsec += CYCLE_NS; if (next.tv_nsec >= 1000000000L) { next.tv_sec++; next.tv_nsec -= 1000000000L; } } }这段代码里伺服使能不是瞬间完成的,要按照状态字逐步推进。曾经遇到有人直接把0x0F一次性写入,结果从站没有反应,就是因为没等状态字到达0x23就跳过了中间步骤。CIA402状态机是标准化的,每个状态必须确认到位后再切下一个状态。
5.4 位置斜坡限制:防止目标位置突变冲击机械
直接修改target_pos_global然后整个写进去,在CSP模式下伺服会自动按内部参数规划位置轨迹。但如果生产逻辑里需要瞬间切换目标位置(比如从当前位置直接跳到一个远处坐标),冲击电流会很大,机械结构会损伤。
我的做法是在主站侧加一层斜坡限制,每周期只允许目标位置朝着最终目标移动一个最大步进:
static int32_t move_towards(int32_t current, int32_t target, int32_t max_step) { int32_t diff = target - current; if (diff > max_step) return current + max_step; if (diff < -max_step) return current - max_step; return target; }然后在周期循环里:
int32_t max_step_per_cycle = 50000; // 1ms周期,最大每秒50000计数 target_pos_global = move_towards(target_pos_global, user_target_pos, max_step_per_cycle); rx_data->target_pos = target_pos_global;这样即使外部命令突然给出一个很远的坐标值,实际位置指令也是平滑爬过去的。伺服内部的加减速规划和主站侧的斜坡限制可以同时使用,主站管粗粒度,伺服管细粒度,效果最好。
6. 进OP后读不到数据和位置不动的实战排查
6.1 现象三连:能进OP,但数据全是零
这是EtherCAT调试中最高频的问题,我一度被折磨了一整天。
现象是:ethercat slaves能看到从站,状态也能切到OP,但程序里读到的实际位置永远是0,写进去的目标位置从站也没反应。用ethercat data查看PDO数据,输出要么全零,要么明显不对。
第一个排查点是PDO映射是否真正生效。在命令行执行:
ethercat pdos如果SM1通道显示“无映射条目”或者映射对象和预期不符,问题就出在映射表上。常见原因是之前某次实验把从站的0x1600/0x1A00映射表写坏了,而从站的EEPROM里保存了这个损坏状态,每次上电都从EEPROM加载坏映射。
解决方法是把从站恢复出厂设置,或者在线重写映射表。在线重写有一个前提:必须在Pre-Op状态下进行,并且要先清空映射表:
清除0x1A00子索引0为0 按顺序写0x1A00子索引1、2、3... 最后把0x1A00子索引0写成映射条目数RXPDO是0x1600,TXPDO是0x1A00,两边都要处理。
6.2 位置有反馈但不动:模式压根没切换成功
另一类问题是反馈数据正常(实际位置在变),但给定目标位置后伺服纹丝不动,或者动一下又停下来。
用ethercat sdos或者程序里读0x6061模式显示,发现从站还在别的模式,比如在Profile Position模式。这时候CSP模式下发的0x607A目标位置在从站看来不是外部周期同步命令,伺服内部需要单独的触发信号(通常还要在控制字里置位bit4)才会启动运动,所以表现为位置指令无效。
解决方法:确保上电状态机切换完成后把0x6060设为8(CSP模式),同时把控制字0x6040置位为0x0F时,一定要确认从站0x6061的显示值已经是8,再开始下发目标位置。模式和使能两个条件缺一不可。
另外,有些伺服驱动器的CSP模式要求位置环增益已经调试好,如果增益参数都是默认的甚至没调整,位置环就不会有力矩输出,从站反馈当前位置正常但电机不转。排查时用手轻轻转一下电机轴,看反馈数值是否跟随变化,如果机械上锁死了,先检查伺服使能信号和制动器状态。
6.3 周期抖动大导致看门狗复位
第三个坑是运行一段时间后从站突然掉线,然后又恢复,反复出现。看门狗触发的根本原因是主站发送帧的实际周期波动太大,超过从站设定的看门狗时间。
这就要检查前面第2部分提到的CPU隔离是否生效。在运行控制程序的同时执行:
# 查看控制线程是否跑在期望的核上 ps -eLo pid,tid,psr,comm | grep control如果实时线程的PSR列在多个核之间跳变,说明CPU亲和性设置没生效。再把中断的smp_affinity检查一遍,确认网卡中断没跑在控制线程所在核上。最后用cyclictest或者程序内部统计周期时间验证,如果抖动还是大于100微秒,把周期从1ms调到2ms,或者检查是否有其他内核线程抢占了大核。
对于多从站的情况,从站看门狗时间也可以适当调大,但不建议依赖这个手段,根因还是要让主站周期稳定。
6.4 排查工具和一条经验
排查读不到数据时,我依赖的命令有三个:ethercat pdos查映射,ethercat states查状态,ethercat data查数据。这三个配合dmesg的输出基本能覆盖绝大多数问题。养成一个习惯:每次改动从站配置前,先用ethercat sdos把原配置导出备份,折腾坏了可以恢复。这个习惯帮我省了不止一次重刷EEPROM的时间。
有个容易忽视的盲区是SDO超时。在Pre-Op状态下用SDO写ACF映射表时,如果从站没响应,IgH会报SDO超时,但不会中断主站运行。这种场景下程序可能还在继续切状态,实际上从站配置根本没完成,最终结果就是进了OP但数据不对。所以SDO操作一定要检查返回值,并且在写完映射表后重新读出来验证一遍,不要急着往下走。
说实话,RK3588做EtherCAT主站这套方案,稳定性已经足够满足产线级别的伺服控制需求,成本却比传统方案低很多。IgH主站的开源生态给了我们完全透明的调试手段,出问题不再依赖厂商支持,这是它最大的价值。希望这篇文章能帮你少走几步弯路,有问题也欢迎在评论区交流。