4.4 从时钟程序实现:时间的"追随者"
源码版本说明
本章源码基于 ptp-lite v1.0.0 (2026-04-10)。
⚠️ 重要提示:
- 完整、最新的源码请查看
ptp_lite/目录- 本章代码片段仅用于理解原理,可能不是最新版本
- 如发现差异,以源码文件为准
- 源码变更记录请查看 CHANGELOG.md
主要源码文件:
- ptp_slave.c - 从时钟主程序
- ptp_servo.h - 伺服算法定义
- ptp_servo.c - 伺服算法实现
从时钟的职责
从时钟需要精确地追随主时钟:
从时钟任务:
1. 接收Announce消息
- 发现主时钟
- 提取主时钟信息
2. 接收Sync+Follow_Up消息
- 记录Sync接收时间t2
- 从Follow_Up获取t1
- 计算偏差:offset = t2 - t1
3. 发送Delay_Req消息
- 定期发送
- 记录发送时间t3
4. 接收Delay_Resp消息
- 获取t4
- 计算路径延迟:delay = [(t2-t1) + (t4-t3)] / 2
- 计算真实偏差:offset = (t2-t1) - delay
5. 调整系统时钟
- 使用伺服算法
- 渐进调整或跳变
伺服算法实现
ptp_servo.h
c
/**
* @file ptp_servo.h
* @brief PI伺服控制器
*/
#ifndef PTP_SERVO_H
#define PTP_SERVO_H
#include <stdint.h>
/* PI控制器参数 */
#define SERVO_KP 0.7
#define SERVO_KI 0.3
/* 步进阈值(纳秒) - 10毫秒 */
#define SERVO_STEP_THRESHOLD 10000000LL
/* 伺服状态 */
typedef enum {
SERVO_UNLOCKED,
SERVO_JUMP,
SERVO_LOCKED,
SERVO_LOCKED_STABLE
} servo_state_t;
/* PI伺服结构 */
typedef struct {
double kp;
double ki;
double integral;
double last_freq;
servo_state_t state;
int count;
} pi_servo_t;
/* 函数声明 */
void pi_servo_init(pi_servo_t *s);
double pi_servo_sample(pi_servo_t *s, int64_t offset, servo_state_t *state);
#endif /* PTP_SERVO_H */
ptp_servo.c
c
/**
* @file ptp_servo.c
* @brief PI伺服控制器实现
*/
#include <stdlib.h>
#include <stdint.h>
#include "ptp_servo.h"
void pi_servo_init(pi_servo_t *s)
{
s->kp = SERVO_KP;
s->ki = SERVO_KI;
s->integral = 0.0;
s->last_freq = 0.0;
s->state = SERVO_UNLOCKED;
s->count = 0;
}
double pi_servo_sample(pi_servo_t *s, int64_t offset, servo_state_t *state)
{
double freq_adj;
/* 状态转换逻辑 */
if (llabs(offset) > SERVO_STEP_THRESHOLD) {
/* 大偏差:跳变 */
s->state = SERVO_JUMP;
s->integral = 0;
s->count = 0;
} else {
/* 小偏差:渐进调整 */
s->count++;
if (s->count > 1) {
s->state = SERVO_LOCKED;
}
if (s->count > 10) {
s->state = SERVO_LOCKED_STABLE;
}
}
/* PI控制 - 只在LOCKED状态使用 */
if (s->state == SERVO_LOCKED || s->state == SERVO_LOCKED_STABLE) {
s->integral += offset;
freq_adj = -s->kp * offset - s->ki * s->integral;
} else {
freq_adj = 0;
}
*state = s->state;
s->last_freq = freq_adj;
return freq_adj;
}
实现要点:
-
状态转换:
- 当 |offset| > 10ms 时,进入 JUMP 状态(相位跳变)
- 当 |offset| < 10ms 且 count > 1 时,进入 LOCKED 状态(频率调整)
- 当 |offset| < 10ms 且 count > 10 时,进入 LOCKED_STABLE 状态
-
PI控制器:
- 只在 LOCKED 和 LOCKED_STABLE 状态工作
- 积分项累积历史偏差,实现精确控制
-
为什么选择 10ms 阈值:
- 小于 100ms(传统做法):更快进入频率调整
- 大于 1ms:避免噪声导致的频繁跳变
- 10ms 是一个平衡选择
时钟调整实现
从时钟需要根据伺服状态调整系统时钟:
adjust_clock 函数
c
static void adjust_clock(int64_t offset_ns, double freq_ppb, servo_state_t state)
{
struct timex tx;
int ret;
struct timespec ts;
switch (state) {
case SERVO_JUMP:
/* 使用clock_settime直接设置时间(更可靠) */
clock_gettime(CLOCK_REALTIME, &ts);
if (offset_ns < 0) {
/* 从时钟落后,需要向前调整 */
ts.tv_sec += (-offset_ns) / 1000000000LL;
ts.tv_nsec += (-offset_ns) % 1000000000LL;
if (ts.tv_nsec >= 1000000000LL) {
ts.tv_sec++;
ts.tv_nsec -= 1000000000LL;
}
} else {
/* 从时钟超前,需要向后调整 */
ts.tv_sec -= offset_ns / 1000000000LL;
ts.tv_nsec -= offset_ns % 1000000000LL;
if (ts.tv_nsec < 0) {
ts.tv_sec--;
ts.tv_nsec += 1000000000LL;
}
}
ret = clock_settime(CLOCK_REALTIME, &ts);
if (ret < 0) {
perror("clock_settime failed");
printf("Failed to adjust clock by %ld ns\n", offset_ns);
} else {
printf("CLOCK JUMP: %ld ns (new time: %ld.%09ld)\n",
offset_ns, ts.tv_sec, ts.tv_nsec);
}
break;
case SERVO_LOCKED:
case SERVO_LOCKED_STABLE:
memset(&tx, 0, sizeof(tx));
tx.modes = ADJ_FREQUENCY;
tx.freq = (long)(freq_ppb * 65.536);
ret = clock_adjtime(CLOCK_REALTIME, &tx);
if (ret < 0) {
perror("clock_adjtime FREQ failed");
} else {
printf("FREQ ADJ: %.2f ppb\n", freq_ppb);
}
break;
default:
break;
}
}
实现要点:
-
相位跳变(SERVO_JUMP):
- 使用
clock_settime直接设置时间 - 比
clock_adjtime(ADJ_SETOFFSET)更可靠 - 适用于大偏差(如几十年的偏差)
- 使用
-
频率调整(SERVO_LOCKED):
- 使用
clock_adjtime(ADJ_FREQUENCY)调整频率 - 时钟会逐渐加速或减速
- 平滑收敛,不影响应用程序
- 使用
-
错误处理:
- 检查返回值,打印错误信息
- 帮助排查权限或系统限制问题
双Socket实现
从时钟需要监听两个PTP端口:
c
int main(int argc, char *argv[])
{
int event_fd, general_fd;
/* 创建两个socket:一个监听319,一个监听320 */
event_fd = create_socket(argv[1], PTP_EVENT_PORT); // 319
general_fd = create_socket(argv[1], PTP_GENERAL_PORT); // 320
/* 使用select监听两个socket */
while (1) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(event_fd, &readfds);
FD_SET(general_fd, &readfds);
select(max_fd + 1, &readfds, NULL, NULL, &timeout);
/* 处理event端口(319)的消息 */
if (FD_ISSET(event_fd, &readfds)) {
// 处理Sync和Delay_Resp
}
/* 处理general端口(320)的消息 */
if (FD_ISSET(general_fd, &readfds)) {
// 处理Follow_Up和Announce
}
}
}
为什么需要双Socket:
根据IEEE 1588标准:
- Event端口(319):Sync, Delay_Req, Delay_Resp(需要时间戳)
- General端口(320):Follow_Up, Announce(不需要时间戳)
使用双Socket确保能收到所有PTP消息。
从时钟主程序
完整源码请查看 ptp_slave.c。
以下是关键流程说明:
主程序框架
c
int main(int argc, char *argv[])
{
int event_fd, general_fd;
/* 初始化 */
init_port_id();
pi_servo_init(&servo);
/* 创建双Socket */
event_fd = create_socket(argv[1], PTP_EVENT_PORT); // 319
general_fd = create_socket(argv[1], PTP_GENERAL_PORT); // 320
/* 主循环 */
while (1) {
// 定期发送Delay_Req
// 接收并处理PTP消息
}
}
消息处理流程
c
/* 处理Sync消息 */
static void handle_sync(ptp_sync_msg_t *msg)
{
clock_gettime(CLOCK_REALTIME, &t2); // 记录接收时间
printf("Received Sync seq=%u\n", ...);
}
/* 处理Follow_Up消息 */
static void handle_follow_up(ptp_follow_up_msg_t *msg)
{
ptp_to_timespec(&msg->precise_origin_timestamp, &t1); // 获取t1
printf("Follow_Up seq=%u: t1=%ld.%09ld t2=%ld.%09ld\n", ...);
}
/* 处理Delay_Resp消息 */
static void handle_delay_resp(ptp_delay_resp_msg_t *msg)
{
ptp_to_timespec(&msg->receive_timestamp, &t4); // 获取t4
// 计算路径延迟
delay = ((t2-t1) + (t4-t3)) / 2;
// 计算精确偏差
offset = (t2-t1) - delay;
// 调整时钟
freq = pi_servo_sample(&servo, offset, &state);
adjust_clock(offset, freq, state);
}
同步效果
第一次同步(大偏差)
bash
# 从时钟落后主时钟38年
Follow_Up: t1=1775811745.xxx t2=575585765.xxx
Delay_Resp: offset=-1200225979905902560 ns
CLOCK JUMP: -1200225979905902560 ns (new time: 1775811745.xxx)
时间立即跳变到主时钟时间。
后续同步(已锁定)
bash
# 偏差在微秒级
Follow_Up: t1=1775811746.xxx t2=1775811746.xxx
Delay_Resp: delay=178976 ns offset=50538 ns
FREQ ADJ: -0.35 ppb ← 平滑调整
时钟平滑收敛,维持同步。
完整实现
本章实现了完整的PTP从时钟:
关键特性:
- ✅ 双Socket监听(符合IEEE 1588标准)
- ✅ E2E延迟测量
- ✅ 两阶段时钟调整(JUMP + FREQ)
- ✅ PI伺服控制器
- ✅ 支持超大偏差修正(如38年)
源码文件:
- ptp_slave.c - 从时钟主程序(约330行)
- ptp_servo.h - 伺服算法定义(40行)
- ptp_servo.c - 伺服算法实现(53行)
下一节:编译运行与测试
📚 本文内容摘自本人的开源书《PTP技术书 - 从思想实验到协议实现》
全书从时间本质的思想实验出发,深度解析 IEEE 1588 协议、逐章分析 LinuxPTP 源码,并带你动手实现一个轻量级 PTP 程序(ptp-lite)。
🔗 在线阅读/下载:ptp-book
bash
git clone https://github.com/Lularible/ptp-book.git
⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。