3.5 伺服控制器:让时钟追上主人的艺术
同步的核心问题
当PTP端口进入SLAVE状态后,面临的核心问题是:
如何调整本地时钟,使其与主时钟保持同步?
这就是伺服控制器(Servo)的任务。
伺服控制器概述
伺服的作用
伺服控制器的输入:
- offset:本地时钟与主时钟的偏差(纳秒)
- local_ts:本地时间戳
- weight:样本权重
伺服控制器的输出:
- ppb:频率调整值(parts per billion,十亿分之一)
伺服控制器的目标:
- 最小化offset
- 使本地时钟频率与主时钟一致
LinuxPTP支持的伺服类型
/* servo.h, 第33-39行 */
enum servo_type {
CLOCK_SERVO_PI, /* PI控制器(最常用) */
CLOCK_SERVO_LINREG, /* 线性回归滤波器 */
CLOCK_SERVO_NTPSHM, /* NTP共享内存 */
CLOCK_SERVO_NULLF, /* 空滤波器 */
CLOCK_SERVO_REFCLOCK_SOCK, /* 参考时钟套接字 */
};
各伺服类型的特点:
PI控制器:
- 比例-积分控制器
- 经典的控制理论方法
- 参数可调,适应性强
- 适合大多数场景
线性回归滤波器:
- 基于统计学方法
- 自适应窗口大小
- 对异常值鲁棒
- 适合高精度场景
NTP共享内存:
- 与NTP共享时间信息
- 用于混合部署
- 不直接调整时钟
空滤波器:
- 不进行任何滤波
- 直接传递offset
- 用于调试或特殊用途
参考时钟套接字:
- 从外部参考时钟获取时间
- 用于GPS等外部源
伺服接口设计
基类设计
/* servo_private.h,第27-49行 */
struct servo {
/* 公共属性 */
double max_frequency; /* 最大频率调整 */
double step_threshold; /* 阶跃阈值 */
double first_step_threshold; /* 首次阶跃阈值 */
int first_update; /* 是否首次更新 */
int64_t offset_threshold; /* 偏差阈值 */
int num_offset_values; /* 检查次数 */
int curr_offset_values; /* 当前计数 */
/* 虚函数 */
void (*destroy)(struct servo *servo);
double (*sample)(struct servo *servo,
int64_t offset, uint64_t local_ts, double weight,
enum servo_state *state);
void (*sync_interval)(struct servo *servo, double interval);
void (*reset)(struct servo *servo);
double (*rate_ratio)(struct servo *servo);
void (*leap)(struct servo *servo, int leap);
};
面向对象设计:
LinuxPTP用C语言实现了面向对象设计:
基类:struct servo
- 定义虚函数指针
- 定义公共属性
派生类:
- struct pi_servo(PI控制器)
- struct linreg_servo(线性回归)
- struct ntpshm_servo(NTP共享内存)
- ...
派生类包含基类作为第一个成员:
struct pi_servo {
struct servo servo; /* 基类 */
/* 派生类特有成员 */
double kp;
double ki;
...
};
这样可以用基类指针操作派生类对象。
伺服创建函数
/* servo.c, 第33-93行 */
struct servo *servo_create(struct config *cfg, enum servo_type type,
double fadj, int max_ppb, int sw_ts)
{
struct servo *servo;
/* 步骤1:根据类型创建具体伺服 */
switch (type) {
case CLOCK_SERVO_PI:
servo = pi_servo_create(cfg, fadj, sw_ts);
break;
case CLOCK_SERVO_LINREG:
servo = linreg_servo_create(fadj);
break;
case CLOCK_SERVO_NTPSHM:
servo = ntpshm_servo_create(cfg);
break;
case CLOCK_SERVO_NULLF:
servo = nullf_servo_create();
break;
case CLOCK_SERVO_REFCLOCK_SOCK:
servo = refclock_sock_servo_create(cfg);
break;
default:
return NULL;
}
if (!servo)
return NULL;
/* 步骤2:设置公共参数 */
servo_step_threshold = config_get_double(cfg, NULL, "step_threshold");
if (servo_step_threshold > 0.0) {
servo->step_threshold = servo_step_threshold * NSEC_PER_SEC;
} else {
servo->step_threshold = 0.0;
}
servo_first_step_threshold =
config_get_double(cfg, NULL, "first_step_threshold");
if (servo_first_step_threshold > 0.0) {
servo->first_step_threshold =
servo_first_step_threshold * NSEC_PER_SEC;
} else {
servo->first_step_threshold = 0.0;
}
servo->max_frequency = max_ppb;
servo->first_update = 1;
return servo;
}
采样函数
/* servo.c, 第116-150行 */
double servo_sample(struct servo *servo,
int64_t offset,
uint64_t local_ts,
double weight,
enum servo_state *state)
{
double r;
/* 调用具体伺服的采样函数 */
r = servo->sample(servo, offset, local_ts, weight, state);
/* 根据状态更新公共属性 */
switch (*state) {
case SERVO_UNLOCKED:
servo->curr_offset_values = servo->num_offset_values;
break;
case SERVO_JUMP:
servo->curr_offset_values = servo->num_offset_values;
servo->first_update = 0;
break;
case SERVO_LOCKED:
if (check_offset_threshold(servo, offset)) {
*state = SERVO_LOCKED_STABLE;
}
servo->first_update = 0;
break;
case SERVO_LOCKED_STABLE:
break;
}
return r;
}
PI控制器详解
PI控制器原理
PI控制器 = 比例(P)+ 积分(I)
控制方程:
output = Kp × offset + Ki × ∫offset dt + drift
其中:
- Kp:比例增益
- Ki:积分增益
- offset:当前偏差
- drift:累积的频率漂移
比例项(P):
- 响应当前偏差
- 偏差越大,调整越大
- 快速响应
积分项(I):
- 响应历史累积偏差
- 消除稳态误差
- 长期稳定
PI控制器结构
/* pi.c, 第38-56行 */
struct pi_servo {
struct servo servo; /* 基类 */
/* 历史数据 */
int64_t offset[2]; /* 最近两次offset */
uint64_t local[2]; /* 最近两次本地时间戳 */
/* 状态变量 */
double drift; /* 累积频率漂移 */
double kp; /* 当前比例增益 */
double ki; /* 当前积分增益 */
double last_freq; /* 上次频率调整 */
int count; /* 采样计数 */
/* 配置参数 */
double configured_pi_kp;
double configured_pi_ki;
double configured_pi_kp_scale;
double configured_pi_kp_exponent;
double configured_pi_kp_norm_max;
double configured_pi_ki_scale;
double configured_pi_ki_exponent;
double configured_pi_ki_norm_max;
};
PI控制器采样函数
/* pi.c, 第64-155行 */
static double pi_sample(struct servo *servo,
int64_t offset,
uint64_t local_ts,
double weight,
enum servo_state *state)
{
struct pi_servo *s = container_of(servo, struct pi_servo, servo);
double ki_term, ppb = s->last_freq;
double freq_est_interval, localdiff;
switch (s->count) {
case 0:
/* 第一次采样:只记录数据 */
s->offset[0] = offset;
s->local[0] = local_ts;
*state = SERVO_UNLOCKED;
s->count = 1;
break;
case 1:
/* 第二次采样:估计频率偏差 */
s->offset[1] = offset;
s->local[1] = local_ts;
/* 确保时间顺序正确 */
if (s->local[0] >= s->local[1]) {
*state = SERVO_UNLOCKED;
s->count = 0;
break;
}
/* 检查时间间隔是否足够 */
localdiff = (s->local[1] - s->local[0]) / 1e9;
localdiff += localdiff * FREQ_EST_MARGIN;
freq_est_interval = 0.016 / s->ki;
if (freq_est_interval > 1000.0) {
freq_est_interval = 1000.0;
}
if (localdiff < freq_est_interval) {
*state = SERVO_UNLOCKED;
break;
}
/* 估计频率偏差 */
s->drift += (1e9 - s->drift) * (s->offset[1] - s->offset[0]) /
(s->local[1] - s->local[0]);
/* 限制频率范围 */
if (s->drift < -servo->max_frequency)
s->drift = -servo->max_frequency;
else if (s->drift > servo->max_frequency)
s->drift = servo->max_frequency;
/* 判断是否需要阶跃 */
if ((servo->first_update &&
servo->first_step_threshold &&
servo->first_step_threshold < llabs(offset)) ||
(servo->step_threshold &&
servo->step_threshold < llabs(offset)))
*state = SERVO_JUMP;
else
*state = SERVO_LOCKED;
ppb = s->drift;
s->count = 2;
break;
case 2:
/* 正常工作:PI控制 */
/* 检查是否需要重置 */
if (servo->step_threshold &&
servo->step_threshold < llabs(offset)) {
*state = SERVO_UNLOCKED;
s->count = 0;
break;
}
/* PI控制方程 */
ki_term = s->ki * offset * weight;
ppb = s->kp * offset * weight + s->drift + ki_term;
/* 限制输出范围 */
if (ppb < -servo->max_frequency) {
ppb = -servo->max_frequency;
} else if (ppb > servo->max_frequency) {
ppb = servo->max_frequency;
} else {
s->drift += ki_term; /* 更新积分项 */
}
*state = SERVO_LOCKED;
break;
}
s->last_freq = ppb;
return ppb;
}
三阶段采样详解:
阶段0(count=0):初始化
- 记录第一个样本
- 状态:UNLOCKED
- 输出:上次频率(无新调整)
阶段1(count=1):频率估计
- 记录第二个样本
- 计算两个样本之间的频率偏差
- 公式:drift = (offset[1] - offset[0]) / (time[1] - time[0])
- 这是对本地振荡器频率偏差的估计
- 状态:LOCKED或JUMP
- 输出:估计的drift
阶段2(count=2):正常PI控制
- 每次采样都执行PI控制
- 公式:ppb = kp × offset + drift + ki × offset
- 状态:LOCKED
- 输出:PI控制器输出
PI参数调整
/* pi.c, 第157-171行 */
static void pi_sync_interval(struct servo *servo, double interval)
{
struct pi_servo *s = container_of(servo, struct pi_servo, servo);
/* 根据Sync间隔自动调整Kp */
s->kp = s->configured_pi_kp_scale *
pow(interval, s->configured_pi_kp_exponent);
if (s->kp > s->configured_pi_kp_norm_max / interval)
s->kp = s->configured_pi_kp_norm_max / interval;
/* 根据Sync间隔自动调整Ki */
s->ki = s->configured_pi_ki_scale *
pow(interval, s->configured_pi_ki_exponent);
if (s->ki > s->configured_pi_ki_norm_max / interval)
s->ki = s->configured_pi_ki_norm_max / interval;
pr_debug("PI servo: sync interval %.3f kp %.3f ki %.6f",
interval, s->kp, s->ki);
}
自适应参数的意义:
为什么要根据Sync间隔调整参数?
Sync间隔长:
- 两个样本之间间隔长
- 频率估计更准确
- 但响应慢
- 需要更大的Kp和Ki
Sync间隔短:
- 两个样本之间间隔短
- 频率估计可能不准
- 但响应快
- 需要更小的Kp和Ki
公式:
kp = kp_scale × interval^kp_exponent
ki = ki_scale × interval^ki_exponent
默认值:
- kp_scale = 0.7(硬件时间戳)或 0.1(软件时间戳)
- kp_exponent = 0.0(如果用户未指定)
- ki_scale = 0.3(硬件时间戳)或 0.001(软件时间戳)
- ki_exponent = 0.0(如果用户未指定)
这意味着:
- 默认情况下,Kp和Ki不随间隔变化
- 但用户可以通过配置文件设置指数
PI控制器创建
/* pi.c, 第180-231行 */
struct servo *pi_servo_create(struct config *cfg, double fadj, int sw_ts)
{
struct pi_servo *s;
s = calloc(1, sizeof(*s));
if (!s)
return NULL;
/* 设置虚函数 */
s->servo.destroy = pi_destroy;
s->servo.sample = pi_sample;
s->servo.sync_interval = pi_sync_interval;
s->servo.reset = pi_reset;
/* 初始化状态 */
s->drift = fadj; /* 初始频率偏差 */
s->last_freq = fadj;
s->kp = 0.0;
s->ki = 0.0;
/* 读取配置 */
s->configured_pi_kp = config_get_double(cfg, NULL, "pi_proportional_const");
s->configured_pi_ki = config_get_double(cfg, NULL, "pi_integral_const");
s->configured_pi_kp_scale = config_get_double(cfg, NULL, "pi_proportional_scale");
s->configured_pi_kp_exponent = config_get_double(cfg, NULL, "pi_proportional_exponent");
s->configured_pi_kp_norm_max = config_get_double(cfg, NULL, "pi_proportional_norm_max");
s->configured_pi_ki_scale = config_get_double(cfg, NULL, "pi_integral_scale");
s->configured_pi_ki_exponent = config_get_double(cfg, NULL, "pi_integral_exponent");
s->configured_pi_ki_norm_max = config_get_double(cfg, NULL, "pi_integral_norm_max");
/* 如果用户指定了Kp和Ki,使用固定值 */
if (s->configured_pi_kp && s->configured_pi_ki) {
s->configured_pi_kp_scale = s->configured_pi_kp;
s->configured_pi_ki_scale = s->configured_pi_ki;
s->configured_pi_kp_exponent = 0.0;
s->configured_pi_ki_exponent = 0.0;
s->configured_pi_kp_norm_max = MAX_KP_NORM_MAX;
s->configured_pi_ki_norm_max = MAX_KI_NORM_MAX;
}
/* 否则使用自适应值 */
else if (!s->configured_pi_kp_scale || !s->configured_pi_ki_scale) {
if (sw_ts) {
s->configured_pi_kp_scale = SWTS_KP_SCALE; /* 0.1 */
s->configured_pi_ki_scale = SWTS_KI_SCALE; /* 0.001 */
} else {
s->configured_pi_kp_scale = HWTS_KP_SCALE; /* 0.7 */
s->configured_pi_ki_scale = HWTS_KI_SCALE; /* 0.3 */
}
}
return &s->servo;
}
线性回归滤波器详解
原理
线性回归滤波器的思想:
- 收集多个(offset, time)样本点
- 用线性回归拟合一条直线
- 直线的斜率 = 频率偏差
- 直线的截距 = 初始偏差
优点:
- 利用多个样本,减少噪声影响
- 自适应窗口大小
- 对异常值鲁棒
数学公式:
给定n个点(x_i, y_i),回归直线:
y = slope × x + intercept
其中:
slope = (n∑xy - ∑x∑y) / (n∑x² - (∑x)²)
intercept = (∑y - slope×∑x) / n
线性回归结构
/* linreg.c, 第58-84行 */
struct linreg_servo {
struct servo servo;
/* 样本缓冲区 */
struct point points[MAX_POINTS]; /* 最多64个点 */
struct point reference; /* 参考点 */
/* 缓冲区管理 */
unsigned int num_points; /* 当前点数 */
unsigned int last_point; /* 最新点索引 */
/* 回归结果 */
struct result results[MAX_SIZE - MIN_SIZE + 1]; /* 多个窗口大小 */
unsigned int size; /* 当前窗口大小 */
/* 状态变量 */
double x_remainder;
uint64_t last_update;
double clock_freq; /* 当前频率 */
double update_interval;
double frequency_ratio; /* 频率比 */
int leap; /* 闰秒 */
};
线性回归采样
/* linreg.c中的采样函数(简化) */
static double linreg_sample(struct servo *servo,
int64_t offset,
uint64_t local_ts,
double weight,
enum servo_state *state)
{
struct linreg_servo *s = container_of(servo, struct linreg_servo, servo);
/* 步骤1:添加新样本点 */
add_point(s, local_ts, offset, weight);
/* 步骤2:执行多个窗口大小的回归 */
for (i = 0; i < ARRAY_SIZE(s->results); i++) {
regression(s, i, &slope, &intercept);
s->results[i].slope = slope;
s->results[i].intercept = intercept;
}
/* 步骤3:选择最佳窗口大小 */
s->size = select_best_size(s);
/* 步骤4:计算频率调整 */
freq = s->results[s->size].slope;
/* 步骤5:更新状态 */
*state = s->num_points >= MIN_POINTS ? SERVO_LOCKED : SERVO_UNLOCKED;
return freq;
}
自适应窗口选择
/* 选择最佳窗口大小 */
static unsigned int select_best_size(struct linreg_servo *s)
{
double err, min_err = HUGE_VAL;
unsigned int i, best = 0;
/* 比较不同窗口大小的预测误差 */
for (i = 0; i < ARRAY_SIZE(s->results); i++) {
err = s->results[i].err; /* 预测误差 */
if (err < min_err) {
min_err = err;
best = i;
}
}
return best;
}
自适应原理:
线性回归滤波器维护多个窗口大小:
- 窗口大小:4, 8, 16, 32, 64(可配置)
- 每个窗口独立计算回归
- 跟踪每个窗口的预测误差
- 选择预测误差最小的窗口
好处:
- 样本少时,用小窗口(快速响应)
- 样本多时,用大窗口(更稳定)
- 自动适应不同场景
伺服状态详解
四种状态
/* servo.h, 第44-67行 */
enum servo_state {
SERVO_UNLOCKED, /* 未锁定 */
SERVO_JUMP, /* 需要跳变 */
SERVO_LOCKED, /* 已锁定 */
SERVO_LOCKED_STABLE, /* 稳定锁定 */
};
状态含义:
SERVO_UNLOCKED:
- 伺服还没有足够的数据
- 或者偏差过大需要重置
- 不能可靠地调整时钟
SERVO_JUMP:
- 偏差超过阈值
- 需要时钟跳变
- 用于快速修正大偏差
SERVO_LOCKED:
- 伺服正在跟踪
- 偏差在正常范围
- 进行正常的频率调整
SERVO_LOCKED_STABLE:
- 连续多次偏差小于阈值
- 伺服已稳定
- 可以考虑降低Sync频率
状态转换
/* servo.c中的状态处理 */
double servo_sample(struct servo *servo, int64_t offset, ...)
{
r = servo->sample(servo, offset, local_ts, weight, state);
switch (*state) {
case SERVO_UNLOCKED:
/* 重置稳定计数 */
servo->curr_offset_values = servo->num_offset_values;
break;
case SERVO_JUMP:
/* 重置稳定计数 */
servo->curr_offset_values = servo->num_offset_values;
servo->first_update = 0;
break;
case SERVO_LOCKED:
/* 检查是否达到稳定 */
if (check_offset_threshold(servo, offset)) {
*state = SERVO_LOCKED_STABLE;
}
servo->first_update = 0;
break;
case SERVO_LOCKED_STABLE:
/* 这个状态只从这里设置 */
break;
}
return r;
}
static int check_offset_threshold(struct servo *s, int64_t offset)
{
long long int abs_offset = llabs(offset);
if (s->offset_threshold) {
if (abs_offset < s->offset_threshold) {
if (s->curr_offset_values)
s->curr_offset_values--;
} else {
s->curr_offset_values = s->num_offset_values;
}
return s->curr_offset_values ? 0 : 1;
}
return 0;
}
稳定检测逻辑:
配置参数:
- servo_offset_threshold:偏差阈值
- servo_num_offset_values:检查次数
工作原理:
1. 每次采样,检查|offset|是否小于阈值
2. 如果小于,计数器减1
3. 如果大于,计数器重置
4. 当计数器减到0,进入LOCKED_STABLE
示例:
配置:offset_threshold = 100ns, num_offset_values = 5
样本1:|offset| = 80ns < 100ns → count = 4
样本2:|offset| = 90ns < 100ns → count = 3
样本3:|offset| = 150ns > 100ns → count = 5(重置)
样本4:|offset| = 70ns < 100ns → count = 4
样本5:|offset| = 60ns < 100ns → count = 3
样本6:|offset| = 80ns < 100ns → count = 2
样本7:|offset| = 90ns < 100ns → count = 1
样本8:|offset| = 85ns < 100ns → count = 0 → LOCKED_STABLE
这确保:
- 连续N次偏差都小于阈值才认为稳定
- 任何一次超差都会重置计数
阶跃阈值配置
阶跃的作用
当偏差很大时:
- 逐渐调整太慢
- 需要时钟跳变
阶跃阈值决定何时跳变:
- step_threshold:正常运行时的阈值
- first_step_threshold:首次同步的阈值
为什么区分首次和正常?
首次同步:
- 设备刚启动,偏差可能很大(毫秒甚至秒级)
- 允许更大的首次跳变
- 快速进入同步状态
正常运行:
- 偏差应该在纳秒级
- 只允许小的跳变
- 避免服务中断
配置示例
# /etc/linuxptp/ptp4l.conf
[global]
# 阈值为0表示禁用阶跃
step_threshold 0.0
# 首次阶跃阈值:100微秒
first_step_threshold 0.0001
# 正常阶跃阈值:10微秒
step_threshold 0.00001
# 最大频率调整:500000 ppb(500 ppm)
max_frequency 500000
实际效果:
场景:设备刚启动,偏差 = 1秒
配置:
first_step_threshold = 0.1秒
step_threshold = 0.00001秒
处理:
1. |offset| = 1秒 > 0.1秒
2. 状态:SERVO_JUMP
3. 执行时钟跳变:clock_step(1秒)
4. 偏差变为0
5. 进入正常同步
后续运行中:
1. |offset| = 20微秒 > 10微秒
2. 状态:SERVO_JUMP
3. 执行时钟跳变:clock_step(20微秒)
4. 继续同步
配置参数详解
PI控制器参数
[global]
# 比例常数(固定模式)
pi_proportional_const 0.0
# 积分常数(固定模式)
pi_integral_const 0.0
# 比例缩放因子(自适应模式)
pi_proportional_scale 0.7
# 比例指数
pi_proportional_exponent 0.0
# 比例归一化最大值
pi_proportional_norm_max 1.0
# 积分缩放因子
pi_integral_scale 0.3
# 积分指数
pi_integral_exponent 0.0
# 积分归一化最大值
pi_integral_norm_max 2.0
两种配置模式:
模式一:固定参数
pi_proportional_const = 0.7
pi_integral_const = 0.3
效果:
- Kp固定为0.7
- Ki固定为0.3
- 不随Sync间隔变化
模式二:自适应参数
pi_proportional_scale = 0.7
pi_proportional_exponent = 0.5
效果:
- Kp = 0.7 × interval^0.5
- Ki = 0.3 × interval^0.5
- 随Sync间隔自动调整
实际调优建议
硬件时间戳场景
[global]
# 适合硬件时间戳的默认参数
pi_proportional_scale 0.7
pi_integral_scale 0.3
# 阶跃阈值
step_threshold 0.00001 # 10微秒
# 稳定检测
servo_offset_threshold 100 # 100纳秒
servo_num_offset_values 5
软件时间戳场景
[global]
# 软件时间戳需要更保守的参数
pi_proportional_scale 0.1
pi_integral_scale 0.001
# 更大的阶跃阈值
step_threshold 0.001 # 1毫秒
高精度场景
[global]
# 更激进的参数
pi_proportional_scale 1.0
pi_integral_scale 0.5
# 更小的阶跃阈值
step_threshold 0.000001 # 1微秒
first_step_threshold 0.0001 # 100微秒
# 更严格的稳定要求
servo_offset_threshold 50 # 50纳秒
servo_num_offset_values 10
小结:伺服控制器的设计智慧
面向对象设计:
- 基类定义接口
- 派生类实现具体算法
- 统一的调用方式
三阶段采样:
- 初始化 → 频率估计 → 正常控制
- 逐步进入稳定状态
自适应参数:
- 根据Sync间隔自动调整
- 适应不同网络条件
多种算法选择:
- PI控制器:经典可靠
- 线性回归:鲁棒性强
- 空滤波器:调试用
下集预告
伺服控制器决定如何调整时钟,但调整需要通过PHC(PTP硬件时钟)接口。
下一节,我们将分析PHC操作和时钟调整——看看LinuxPTP如何与硬件时钟交互。
【悬念留给3.6】
PHC是Linux内核提供的PTP硬件时钟子系统。
LinuxPTP通过以下接口操作PHC:
- clock_gettime:读取时间
- clock_adjtime:调整频率
- clock_settime:设置时间
这些操作封装在phc.c和clockadj.c中。
如何在用户态操作内核时钟?
下一节,我们详细解读。