【原创】IgH EtherCAT主站详解(二十三)--DC 同步实战
DC 同步实战
8.3 — 分布式时钟配置与测量
概览
DC 同步要达到什么效果?
EtherCAT 分布式时钟 (DC) 的核心目标是让总线上所有从站的内部时钟保持纳秒级同步。在配置正确的系统中,从站间同步抖动可控制在亚微秒级(通常小于 100 ns),从而实现精确的同步采样与输出更新。
本教程基于 IgH EtherCAT Master 1.5.2 提供的 dc_user 示例程序,演示如何在实际应用中配置和使用 DC 同步功能。
DC 同步的 4 个关键 API 调用
1.
ecrt_slave_config_dc()— 配置从站 DC 参数(周期、偏移、激活字)
2.ecrt_master_application_time()— 每周期写入主站应用时间
3.ecrt_master_sync_reference_clock_to()— 同步参考时钟到指定时间
4.ecrt_master_sync_slave_clocks()— 同步所有从站时钟(漂移补偿)
示例程序运行效果
dc_user 示例以 1000 Hz(1 ms 周期)运行,启用 MEASURE_TIMING 宏后,每秒输出一次时序统计:
period ... 1000147
exec ... 987234
latency ... 12453
时序测量指标
period (周期): 两次循环启动的实际间隔,理想值为 1000000 ns (1 ms)
exec (执行时间): 单次循环的有效执行耗时
latency (延迟): 唤醒时刻到实际启动的时刻差,反映调度抖动
两种 DC 参考时钟模式
EtherCAT DC 同步有两种参考时钟模式,核心区别在于谁的时间基准作为全网同步的参考。工业实践中从站参考时钟模式更为常用,因为从站 ESC 硬件时钟抖动仅约 12ns,远优于操作系统时钟稳定性。
如何选择?
模式 A (主站参考): 适合简单 IO 同步场景,主站 CLOCK_MONOTONIC 为基准,使用
ecrt_master_sync_reference_clock_to()单向写入参考从站。
模式 B (从站参考): 适合伺服控制、多轴同步等精确场景,参考从站 DC 时钟为基准,主站通过闭环算法调整自身周期追随从站时钟。使用ecrt_master_reference_clock_time()读取参考时钟,通过调整system_time_base实现同步。本章下面分为两个大节:模式 A(L2/L3)和模式 B(L2/L3)。
技术详情
模式 A: 以主站为参考时钟 (dc_user 示例)
DC 配置完整流程
以下流程图展示了从创建主站到进入 DC 同步循环的完整步骤:
ecrt_slave_config_dc() 参数详解
这是 DC 配置的核心函数,在激活主站之前调用:
// dc_user/main.c — DC 配置调用
ecrt_slave_config_dc(sc, 0x0700, PERIOD_NS, 4400000, 0, 0);
| 参数 | 示例值 | 说明 |
|---|---|---|
assign_activate |
0x0700 |
DC 激活字,写入从站寄存器 0x0981。0x0700 = SYNC0 + SYNC1 均激活 (bit8=1, bit9=1);0x0300 = 仅 SYNC0 激活 (bit8=1, bit9=0)。具体值取决于从站 ESI 文件中的 DcSync 配置 |
sync0_cycle |
PERIOD_NS (1000000 ns) |
SYNC0 脉冲周期时间(纳秒)。本例为 1 ms,对应 1000 Hz 循环频率 |
sync0_shift |
4400000 (4.4 ms) |
SYNC0 相对于周期起始的偏移时间(纳秒)。用于补偿传播延迟,确保从站在数据到达后才触发采样 |
sync1_cycle |
0 |
SYNC1 脉冲周期时间(纳秒)。设为 0 表示不使用 SYNC1 |
sync1_shift |
0 |
SYNC1 相对于 SYNC0 的偏移时间(纳秒)。设为 0 表示不使用 SYNC1 |
AssignActivate 激活字说明
assign_activate的值决定了 DC 同步模式。常见值:
0x0300: 仅 SYNC0 激活 — 适用于大多数简单同步场景
0x0700: SYNC0 + SYNC1 同时激活 — 适用于需要双同步信号的高级场景(如需要区分采样和输出时刻)该值通常来自从站 ESI (EtherCAT Slave Information) XML 文件中的
<Dc>配置节点。
完整代码走读: dc_user/main.c
1. 宏定义与常量
// dc_user/main.c — 核心宏定义
#define FREQUENCY 1000 // 循环频率 1000 Hz
#define CLOCK_TO_USE CLOCK_MONOTONIC // 使用单调时钟,不受系统时间跳变影响
#define MEASURE_TIMING // 启用时序测量
#define NSEC_PER_SEC (1000000000L) // 1 秒 = 10^9 纳秒
#define PERIOD_NS (NSEC_PER_SEC / FREQUENCY) // 周期 = 1000000 ns = 1 ms
// 时间差计算宏
#define DIFF_NS(A, B) (((B).tv_sec - (A).tv_sec) * NSEC_PER_SEC + \
(B).tv_nsec - (A).tv_nsec)
// timespec 转 64 位纳秒
#define TIMESPEC2NS(T) ((uint64_t) (T).tv_sec * NSEC_PER_SEC + (T).tv_nsec)
2. 主站初始化与 DC 配置
// dc_user/main.c — main() 函数关键步骤
int main(int argc, char **argv)
{
ec_slave_config_t *sc;
// 锁定内存,防止页面换出导致延迟抖动
if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
perror("mlockall failed");
return -1;
}
// 步骤 1: 请求主站
master = ecrt_request_master(0);
// 步骤 2: 创建过程数据域
domain1 = ecrt_master_create_domain(master);
// 步骤 3: 配置从站(总线耦合器 EK1100)
sc = ecrt_master_slave_config(master, BusCouplerPos, Beckhoff_EK1100);
// 步骤 3: 配置数字量输出从站 EL2008
sc = ecrt_master_slave_config(master, DigOutSlavePos, Beckhoff_EL2008);
// 注册 PDO 入口
off_dig_out = ecrt_slave_config_reg_pdo_entry(sc, 0x7000, 1, domain1, NULL);
// 步骤 3: 配置计数器从站
sc = ecrt_master_slave_config(master, CounterSlavePos, IDS_Counter);
off_counter_in = ecrt_slave_config_reg_pdo_entry(sc, 0x6020, 0x11, domain1, NULL);
off_counter_out = ecrt_slave_config_reg_pdo_entry(sc, 0x7020, 1, domain1, NULL);
// 步骤 4: 配置 DC 同步 —— 关键!
// 激活 SYNC0+SYNC1, 周期 1ms, 偏移 4.4ms
ecrt_slave_config_dc(sc, 0x0700, PERIOD_NS, 4400000, 0, 0);
// 步骤 5: 激活主站
ecrt_master_activate(master);
// 步骤 6: 获取过程数据内存映射
domain1_pd = ecrt_domain_data(domain1);
// 设置实时调度优先级 (SCHED_FIFO)
struct sched_param param = {};
param.sched_priority = sched_get_priority_max(SCHED_FIFO);
sched_setscheduler(0, SCHED_FIFO, ¶m);
// 步骤 7: 进入 DC 同步循环
cyclic_task();
return 0;
}
3. 时间辅助函数
// dc_user/main.c — timespec_add() 纳秒级时间加法
// 精确的 timespec 加法,正确处理纳秒进位
struct timespec timespec_add(struct timespec time1, struct timespec time2)
{
struct timespec result;
if ((time1.tv_nsec + time2.tv_nsec) >= NSEC_PER_SEC) {
result.tv_sec = time1.tv_sec + time2.tv_sec + 1;
result.tv_nsec = time1.tv_nsec + time2.tv_nsec - NSEC_PER_SEC;
} else {
result.tv_sec = time1.tv_sec + time2.tv_sec;
result.tv_nsec = time1.tv_nsec + time2.tv_nsec;
}
return result;
}
4. DC 同步循环 (cyclic_task)
// dc_user/main.c — cyclic_task() DC 同步循环核心
void cyclic_task()
{
struct timespec wakeupTime, time;
#ifdef MEASURE_TIMING
struct timespec startTime, endTime, lastStartTime = {};
uint32_t period_ns = 0, exec_ns = 0, latency_ns = 0,
latency_min_ns = 0, latency_max_ns = 0,
period_min_ns = 0, period_max_ns = 0,
exec_min_ns = 0, exec_max_ns = 0;
#endif
// 获取当前时间作为首次唤醒基准
clock_gettime(CLOCK_TO_USE, &wakeupTime);
while(1) {
// 计算下一个绝对唤醒时间 = 当前唤醒时间 + 周期
wakeupTime = timespec_add(wakeupTime, cycletime);
// 使用绝对时间休眠 —— 确定性的周期唤醒
// TIMER_ABSTIME: 休眠到绝对时刻,避免累积误差
clock_nanosleep(CLOCK_TO_USE, TIMER_ABSTIME, &wakeupTime, NULL);
// ====== DC 关键调用 1: 写入应用时间 ======
// 使用目标时间(非测量时间),因为更稳定
ecrt_master_application_time(master, TIMESPEC2NS(wakeupTime));
#ifdef MEASURE_TIMING
clock_gettime(CLOCK_TO_USE, &startTime);
latency_ns = DIFF_NS(wakeupTime, startTime); // 唤醒延迟
period_ns = DIFF_NS(lastStartTime, startTime); // 实际周期
exec_ns = DIFF_NS(lastStartTime, endTime); // 执行时间
lastStartTime = startTime;
// 更新 min/max 统计
if (latency_ns > latency_max_ns) latency_max_ns = latency_ns;
if (latency_ns < latency_min_ns) latency_min_ns = latency_ns;
if (period_ns > period_max_ns) period_max_ns = period_ns;
if (period_ns < period_min_ns) period_min_ns = period_ns;
if (exec_ns > exec_max_ns) exec_max_ns = exec_ns;
if (exec_ns < exec_min_ns) exec_min_ns = exec_ns;
#endif
// 接收过程数据
ecrt_master_receive(master);
ecrt_domain_process(domain1);
// 可选: 检查域状态
check_domain1_state();
// 1 Hz 统计输出
if (counter) {
counter--;
} else {
counter = FREQUENCY;
check_master_state();
#ifdef MEASURE_TIMING
printf("period %10u ... %10u\n", period_min_ns, period_max_ns);
printf("exec %10u ... %10u\n", exec_min_ns, exec_max_ns);
printf("latency %10u ... %10u\n", latency_min_ns, latency_max_ns);
// 重置统计
period_max_ns = 0; period_min_ns = 0xffffffff;
exec_max_ns = 0; exec_min_ns = 0xffffffff;
latency_max_ns = 0; latency_min_ns = 0xffffffff;
#endif
blink = !blink;
}
// 写入过程数据
EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x66 : 0x99);
EC_WRITE_U8(domain1_pd + off_counter_out, blink ? 0x00 : 0x02);
// ====== DC 关键调用 2: 同步参考时钟 ======
if (sync_ref_counter) {
sync_ref_counter--;
} else {
sync_ref_counter = 1; // 每周期同步一次
clock_gettime(CLOCK_TO_USE, &time);
ecrt_master_sync_reference_clock_to(master, TIMESPEC2NS(time));
}
// ====== DC 关键调用 3: 同步所有从站时钟 ======
ecrt_master_sync_slave_clocks(master);
// 发送过程数据
ecrt_domain_queue(domain1);
ecrt_master_send(master);
#ifdef MEASURE_TIMING
clock_gettime(CLOCK_TO_USE, &endTime);
#endif
}
}
时序测量输出分析
启用 MEASURE_TIMING 后,程序每秒输出三项时序统计的最小/最大值:
| 指标 | 理想值 | 可接受范围 | 异常阈值 | 可能原因 |
|---|---|---|---|---|
| period (周期) | 1000000 ns | 999000 ~ 1001000 ns | 偏差 > 10 us | 调度优先级不足、非 RT 内核、其他高优先级中断 |
| exec (执行时间) | 取决于负载 | < 周期的 80% | 接近或超过周期 | PDO 过多、printf 输出阻塞、内存未锁定 |
| latency (延迟) | 0 ns | < 50 us | > 100 us | 内核未开启 PREEMPT_RT、未使用 SCHED_FIFO、GC 或电源管理干扰 |
模式 B: 以从站为参考时钟 (rtai_rtdm_dc 示例)
以下内容基于 examples/rtai_rtdm_dc/main.c,演示如何将从站 DC 时钟作为全网时间基准,并在主站应用中通过同步算法调整运行周期。
⚠️ 模式切换
代码中通过宏
SYNC_MASTER_TO_REF切换模式:
SYNC_MASTER_TO_REF = 0: 模式 A (主站 → 参考从站) — 使用ecrt_master_sync_reference_clock_to()
SYNC_MASTER_TO_REF = 1: 模式 B (参考从站 → 主站) — 使用ecrt_master_reference_clock_time()+ 同步算法
rtai_rtdm_dc 示例默认SYNC_MASTER_TO_REF = 1。
核心架构
在从站参考时钟模式下,主站不再是时间基准的提供者,而是主动追随参考从站的时间。其工作原理可以用一个闭环控制系统来描述:
关键 API 差异对比
| 维度 | 模式 A (主站参考) | 模式 B (从站参考) |
|---|---|---|
| 参考时钟写入 | ecrt_master_sync_reference_clock_to(master, time) |
不调用 (参考从站时钟自主运行) |
| 参考时钟读取 | 不需要 | ecrt_master_reference_clock_time(master, &ref_time) |
| 从站时钟漂移补偿 | ecrt_master_sync_slave_clocks(master) |
ecrt_master_sync_slave_clocks(master) (相同) |
| 主站周期调整 | 固定周期 clock_nanosleep() |
动态调整 system_time_base,通过 rt_sleep_until() 实现可变唤醒 |
| 参考时钟选择 | 自动 (第一个有 DC 能力的从站) | ecrt_master_select_reference_clock(master, sc) 建议显式指定 |
| 时间基准 | CLOCK_MONOTONIC |
rt_get_time_ns() - system_time_base (可调整) |
rtai_rtdm_dc 代码走读
1. DC 同步变量定义
// rtai_rtdm_dc/main.c — DC 变量
#define DC_FILTER_CNT 1024 // 漂移率采样窗口
#define SYNC_MASTER_TO_REF 1 // 1=从站参考, 0=主站参考
static uint64_t dc_start_time_ns = 0LL; // 同步启动时刻
static uint64_t dc_time_ns = 0; // 当前应用时间
static uint8_t dc_started = 0; // 是否已检测到首次偏差
static int32_t dc_diff_ns = 0; // 当前归一化偏差: app_time - ref_time
static int32_t prev_dc_diff_ns = 0; // 上一周期偏差 (用于计算 delta)
static int64_t dc_diff_total_ns = 0LL; // 偏差累积量
static int64_t dc_delta_total_ns = 0LL; // 偏差变化率累积量
static int dc_filter_idx = 0; // 采样计数器
static int64_t dc_adjust_ns; // 累积调整量 (单位: ns)
static int64_t system_time_base = 0LL; // 时间基准偏移 (核心变量!)
system_time_base 的作用
system_time_base是主站时间基准的偏移量。主站每次获取时间时:
app_time = rt_get_time_ns() - system_time_base同步算法通过调整
system_time_base来改变主站感知的时间,从而改变rt_sleep_until()的唤醒时刻,使主站周期逐渐与参考从站对齐。
2. sync_distributed_clocks() — 每周期同步函数
// rtai_rtdm_dc/main.c — sync_distributed_clocks()
void sync_distributed_clocks(void)
{
uint32_t ref_time = 0;
uint64_t prev_app_time = dc_time_ns;
// 获取当前调整后的系统时间
dc_time_ns = system_time_ns();
// ★ 关键: 读取参考从站的当前时钟时间
ecrt_master_reference_clock_time(master, &ref_time);
// ★ 计算偏差: 上一周期的 app_time - 参考从站当前时间
// 注意: ref_time 是 32 位截断值
dc_diff_ns = (uint32_t) prev_app_time - ref_time;
// 同步所有从站时钟到参考从站 (漂移补偿)
ecrt_master_sync_slave_clocks(master);
}
3. update_master_clock() — 核心同步算法
// rtai_rtdm_dc/main.c — update_master_clock()
void update_master_clock(void)
{
// 步骤 1: 计算偏差变化率 (delta)
int32_t delta = dc_diff_ns - prev_dc_diff_ns;
prev_dc_diff_ns = dc_diff_ns;
// 步骤 2: 归一化偏差到 [-T/2, +T/2]
dc_diff_ns =
((dc_diff_ns + (cycle_ns / 2)) % cycle_ns) - (cycle_ns / 2);
// 步骤 3: 首次启动检测
if (!dc_started) {
dc_started = (dc_diff_ns != 0);
if (dc_started) {
dc_start_time_ns = dc_time_ns;
}
return;
}
// 步骤 4: 累积偏差和变化率
dc_diff_total_ns += dc_diff_ns;
dc_delta_total_ns += delta;
dc_filter_idx++;
// 步骤 5: 每 1024 周期更新一次漂移补偿
if (dc_filter_idx >= DC_FILTER_CNT) {
// 平均漂移率 (D 项)
dc_adjust_ns +=
((dc_delta_total_ns + (DC_FILTER_CNT / 2)) / DC_FILTER_CNT);
// 漂移方向修正 (极弱 P 项: 仅 ±1 ns)
dc_adjust_ns += sign(dc_diff_total_ns / DC_FILTER_CNT);
// 限幅 ±1000 ns
if (dc_adjust_ns < -1000) dc_adjust_ns = -1000;
if (dc_adjust_ns > 1000) dc_adjust_ns = 1000;
// 重置累积器
dc_diff_total_ns = 0LL;
dc_delta_total_ns = 0LL;
dc_filter_idx = 0;
}
// 步骤 6: 每周期应用调整
// dc_adjust_ns: 长期漂移补偿 (每 1024 周期更新)
// sign(dc_diff_ns): 即时微调 (仅 ±1 ns)
system_time_base += dc_adjust_ns + sign(dc_diff_ns);
}
4. 循环主体
// rtai_rtdm_dc/main.c — my_cyclic() 循环
void my_cyclic(void)
{
// 设置首次唤醒时间
wakeup_time = system_time_ns() + 10 * cycle_ns;
start_rt_timer(nano2count(cycle_ns));
rt_make_hard_real_time();
while (run) {
// 使用可调整的系统时间等待 (关键!)
wait_period(); // 内部: rt_sleep_until(system2count(wakeup_time))
// 接收过程数据
ecrt_master_receive(master);
ecrt_domain_process(domain1);
// ... 处理 PDO ...
// 队列过程数据
ecrt_domain_queue(domain1);
// ★ DC 同步: 在 send 前调用,设置最精确的时间
sync_distributed_clocks();
// 发送 EtherCAT 帧
ecrt_master_send(master);
// ★ 更新主站时钟: 在 send 后调用,减少 sync_distributed_clocks 的抖动
update_master_clock();
}
}
调用顺序关键点
参考时钟选择
在从站参考模式下,建议显式指定参考时钟从站,而非依赖自动选择:
// rtai_rtdm_dc/main.c — 参考时钟选择
// 显式选择总线耦合器 EK1100 作为参考时钟
// NULL = 自动选择第一个有 DC 能力的从站
ret = ecrt_master_select_reference_clock(master, sc_ek1100);
if (ret < 0) {
fprintf(stderr, "Failed to select reference clock: %s\n",
strerror(-ret));
return ret;
}
参考时钟选择建议
- 通常选择总线耦合器 (如 EK1100) 作为参考时钟,因为它位于总线最前端
- 也可选择具有高精度 DC 的伺服驱动器
- 传入
NULL则自动选择第一个有 DC 能力的从站- 参考时钟从站必须支持 DC (寄存器 0x0981 可写)
常见问题与解决方案
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 周期不稳定,period 波动大 | 未使用实时调度策略 | 调用 sched_setscheduler() 设置 SCHED_FIFO;使用 PREEMPT_RT 或 Xenomai 内核 |
| latency 偶尔出现尖峰 | 内存页面换出、中断干扰 | 调用 mlockall(MCL_CURRENT | MCL_FUTURE) 锁定内存;关闭 CPU 频率调节 |
| 从站 AL 状态无法进入 OP | DC 参数不匹配(周期或激活字错误) | 对照从站 ESI 文件检查 ecrt_slave_config_dc() 参数;确认 assign_activate 值 |
| WC (Working Counter) 不等于 3 | 从站未响应或帧丢失 | 检查物理连接;确认从站地址和位置正确;检查网线质量 |
| DC 同步报错:时钟偏差过大 | 参考时钟未正确同步 | 确认参考时钟从站存在且有 DC 能力;检查 ecrt_master_sync_reference_clock_to() 是否每周期调用 |
| printf 输出影响实时性能 | 标准 I/O 引起阻塞 | 在生产代码中移除或异步化 printf 调用;使用无锁环形缓冲记录日志 |
深入源码
模式 A 源码机制
DC 同步时序: 循环内部序列
下图展示了 cyclic_task() 单个循环周期中各步骤的时间关系:
ecrt_master_application_time() 内部机制
该函数将应用程序的当前时间通知给主站内核模块,用于 DC 参考时钟同步。
为什么用目标时间而非测量时间?
dc_user示例使用wakeupTime(目标唤醒时间)而非startTime(实际启动时间)来调用ecrt_master_application_time()。这是因为:
- 稳定性: 目标时间是精确计算的,不受调度抖动影响
- 单调性: 目标时间严格递增(每次加固定周期),而实际时间可能因调度延迟出现波动
- 确定性: 参考时钟从站收到的应该是"应该到达的时间",而不是"实际到达的时间"
ecrt_master_sync_reference_clock_to() 内部机制
该函数将主站应用时间同步到 DC 参考时钟从站,使用 ARMW (Auto Increment Read Multiple Write) 命令:
// 内核实现逻辑 (master/ioctl.c)
// ecrt_master_sync_reference_clock_to() 对应的 ioctl 处理
static ATTRIBUTES int ec_ioctl_sync_ref(
ec_master_t *master, void *arg, size_t size)
{
uint64_t app_time = *((uint64_t *) arg);
// 将应用时间写入 ref_sync_datagram 的数据区
// ref_sync_datagram 类型: FPWR (Configured Address Physical Write)
// 目标寄存器: 0x0910 (DC 系统时间, 64 位)
EC_WRITE_U64(master->ref_sync_datagram.data, app_time);
master->ref_sync_datagram.state = EC_DATAGRAM_INITIALIZED;
return 0;
}
ecrt_master_sync_slave_clocks() 内部机制
该函数执行全网从站时钟的漂移补偿。它通过 FRMW (Configured Address Read, Multiple Write) 广播命令实现:
ARMW vs FRMW 的区别
ARMW (Auto Increment Read Multiple Write): 使用自增地址,读取参考从站数据并广播写入后续从站。适用于首次同步。
FRMW (Configured Address Read Multiple Write): 使用配置地址,读取指定从站数据并广播写入其他从站。适用于运行时周期性漂移补偿。
IgH 主站在运行时通常使用 FRMW,地址指向参考时钟从站的配置地址。
clock_nanosleep 与确定性周期
dc_user 使用 clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...) 实现确定性周期唤醒,其关键在于 TIMER_ABSTIME 标志:
| 特性 | 相对时间 (默认) | 绝对时间 (TIMER_ABSTIME) |
|---|---|---|
| 唤醒基准 | 从调用时刻起延迟指定时间 | 休眠到指定的绝对时刻 |
| 累积误差 | 每次循环会累积处理时间的误差 | 不会累积,始终对齐到精确周期 |
| 唤醒公式 | now + period |
start + N * period |
| 适用场景 | 非实时应用 | 实时周期任务(推荐) |
// 绝对时间唤醒模式 (推荐)
// 初始化: 获取当前时间
clock_gettime(CLOCK_TO_USE, &wakeupTime);
while (1) {
// 计算下一个绝对唤醒时刻
wakeupTime = timespec_add(wakeupTime, cycletime);
// 休眠到绝对时刻 —— 无累积误差
clock_nanosleep(CLOCK_TO_USE, TIMER_ABSTIME, &wakeupTime, NULL);
// ... 周期任务 ...
}
模式 B 同步算法深度分析
以下对 update_master_clock() 中的同步算法进行数学建模和问题分析,并给出改进方案。
算法数学建模
原始算法本质上是一个类 PD 控制器(比例-微分),而非标准 PI 控制器:
| 控制项 | 原始算法实现 | 数学表达 | 问题 |
|---|---|---|---|
| P (比例) | sign(dc_diff_ns) |
u_P = sign(e(n)) = ±1 ns | 极弱! 误差 100ns 仅修正 1ns,增益仅 0.01 |
| D (微分/漂移率) | (dc_delta_total_ns + N/2) / N |
u_D = avg(Δe) / 1024 | 1024 采样窗口造成 ~1s 延迟 (1ms 周期时) |
| 积分累加 | dc_adjust_ns |
Σ u_D + sign(avg(e)) | 每 1024 周期才更新一次,响应极慢 |
| 输出 | system_time_base += dc_adjust_ns + sign(e) |
base += Σu_D + u_P | 每周期应用 dc_adjust_ns (含历史累积) + 即时修正 |
收敛性分析
原始算法的 4 个关键问题
| # | 问题 | 代码位置 | 影响 |
|---|---|---|---|
| 1 | P 项增益过小: sign(dc_diff_ns) 仅输出 ±1 ns |
system_time_base += dc_adjust_ns + sign(dc_diff_ns) |
100us 初始偏差需要 ~100s 收敛,远超实际可接受范围 |
| 2 | 1024 采样窗口过长: 漂移率更新延迟 ~1s | dc_filter_idx >= DC_FILTER_CNT |
温度变化引起的时钟漂移 (~50ppm) 响应太慢 |
| 3 | 无标准 I 项: 缺少偏差积分消除稳态误差 | 整个 update_master_clock() |
当 P 项和 D 项平衡后可能存在残余稳态偏差 |
| 4 | dc_adjust_ns 单向累积: 缺少抗饱和机制 | dc_adjust_ns += ... (只加不减) |
如果初始阶段累积过大,后续反向调整困难 |
改进方案: 标准 PI 控制器
将原始的 crude PD 结构替换为标准 PI 控制器,实现更快速、更稳定的收敛:
/* ========= PI 控制器参数 (可根据总线规模调整) ========= */
#define DC_KP 0.05 /* 比例增益: 100ns偏差 → 5ns修正 */
#define DC_KI 0.0005 /* 积分增益: 消除稳态偏差 */
#define DC_INTEGRAL_MAX 100000 /* 积分器抗饱和上限 (ns) */
#define DC_OUTPUT_MAX 1000 /* 单周期输出限幅 (ns) */
#define DC_FILTER_CNT 1024 /* 保持 1024 用于归一化 */
/* ========= PI 控制器状态 ========= */
static int64_t dc_integral_ns = 0; /* 积分项累积值 */
static int32_t dc_diff_ns = 0; /* 当前偏差 */
static int32_t prev_dc_diff_ns = 0; /* 上一周期偏差 */
static int64_t system_time_base = 0; /* 时间基准偏移 */
static uint8_t dc_started = 0;
/* ========= 改进的 sync_distributed_clocks (不变) ========= */
void sync_distributed_clocks(void)
{
uint32_t ref_time = 0;
uint64_t prev_app_time = dc_time_ns;
dc_time_ns = system_time_ns();
ecrt_master_reference_clock_time(master, &ref_time);
dc_diff_ns = (uint32_t) prev_app_time - ref_time;
ecrt_master_sync_slave_clocks(master);
}
/* ========= 改进的 update_master_clock: PI 控制器 ========= */
void update_master_clock_pi(void)
{
/* 步骤 1: 计算偏差变化率 (用于诊断, PI 不直接使用) */
int32_t delta = dc_diff_ns - prev_dc_diff_ns;
prev_dc_diff_ns = dc_diff_ns;
/* 步骤 2: 归一化偏差到 [-T/2, +T/2] (处理 32 位回绕) */
dc_diff_ns =
((dc_diff_ns + (cycle_ns / 2)) % cycle_ns) - (cycle_ns / 2);
/* 步骤 3: 首次启动检测 */
if (!dc_started) {
dc_started = (dc_diff_ns != 0);
return;
}
/* 步骤 4: P 项 — 按比例修正当前偏差 */
int32_t p_term = (int32_t)(DC_KP * dc_diff_ns);
/* 步骤 5: I 项 — 累积偏差消除稳态误差 */
dc_integral_ns += dc_diff_ns;
/* 抗饱和: 积分器钳位 */
if (dc_integral_ns > DC_INTEGRAL_MAX) dc_integral_ns = DC_INTEGRAL_MAX;
if (dc_integral_ns < -DC_INTEGRAL_MAX) dc_integral_ns = -DC_INTEGRAL_MAX;
int32_t i_term = (int32_t)(DC_KI * dc_integral_ns);
/* 步骤 6: 总输出 = P + I, 限幅 */
int32_t output = p_term + i_term;
if (output > DC_OUTPUT_MAX) output = DC_OUTPUT_MAX;
if (output < -DC_OUTPUT_MAX) output = -DC_OUTPUT_MAX;
/* 步骤 7: 应用到时间基准 */
system_time_base += output;
}
新旧算法对比
| 对比维度 | 原始算法 | 改进 PI 算法 |
|---|---|---|
| 控制器类型 | 弱 P + D (漂移率) | 标准 P + I |
| P 项增益 | sign(e) → 增益 1/|e| (非线性, 极弱) | Kp = 0.05 (线性, 可调) |
| 积分/消除偏差 | 无标准 I 项, 依赖 D 项累积 | Ki × Σe, 带抗饱和 |
| 更新频率 | D 项: 每 1024 周期; P 项: 每周期 | 每周期更新 P + I |
| 100us 偏差收敛 | ~20-50 秒 (20000-50000 周期) | ~50-200 周期 (50-200 ms) |
| 稳态精度 | ±50-100 ns (可能存在残余偏差) | ±10 ns (I 项消除稳态偏差) |
| 参数调整 | 仅 DC_FILTER_CNT | Kp, Ki, 积分上限, 输出限幅 (4 个参数) |
PI 参数调节指南
| 参数 | 默认值 | 范围 | 调节建议 |
|---|---|---|---|
Kp |
0.05 | [0.01, 0.1] | 从站少/周期短 → 增大; 从站多/周期长 → 减小。太大则振荡, 太小则收敛慢 |
Ki |
0.0005 | [0.0001, 0.001] | 消除稳态偏差。通常比 Kp 小 100 倍。太大则低频振荡 |
INTEGRAL_MAX |
100000 ns | [10000, 1000000] | 抗饱和上限。防止积分器累积过大导致过冲 |
OUTPUT_MAX |
1000 ns | [100, 5000] | 单周期最大调整量。不超过周期的 0.1% |
收敛曲线示意
⚠️ 实际部署注意
- 首次启动: 主站与参考从站初始偏差可能达数十微秒 (传播延迟 + 时钟偏移),PI 算法在 50-200 周期内即可收敛
- 实时要求: PI 计算必须在周期任务内完成 (通常 <1us),不可放入非实时上下文
- 温度漂移: ESC 晶振温漂约 ±50 ppm (即 1ms 周期偏差 ±50ns),I 项可自动跟踪补偿
- 与 SYNC0 对齐: 目标是使主站唤醒时刻与从站 SYNC0 脉冲对齐,dc_diff_ns ≈ 0 表示完美对齐
- RTAI/Xenomai 差异: 示例使用 RTAI API (
rt_sleep_until,rt_get_time_ns),Xenomai 用户空间应替换为对应的 RTDM API
深入了解
- 分布式时钟 (DC) 原理 — DC 系统时间、SYNC 信号、漂移补偿的完整原理
- 启动配置流程 — DC 配置在主站启动序列中的位置
- Slave Config FSM — DC 配置状态机的实现细节

浙公网安备 33010602011771号