Linux:NTP服务

发布于:2025-09-08 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、ping通外网

在开发板上使用NTP服务要确保 开发板能够ping外网!

ip addr show eth0          # 查看 eth0 是否有 IP 且 UP
ip route show              # 查看默认路由是否存在
ping 192.168.100.1         # 测试能否到达网关

ip route replace default via 192.168.100.1 dev eth0
replace 会自动替换已经存在的默认路由,非常方便。确保ip route show 有下面这个节点

同时确保Linux主机开启下面这个内容:

sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -o enp2s0 -j MASQUERADE

之后板子就可以 ping 8.8.8.8ping www.baidu.com 了。

如果域名不通,再配置DNS /etc/resolv.conf

加一行:nameserver 223.5.5.5 nameserver 8.8.8.8

如果重启后 /etc/resolv.conf 被重置,需要在 启动脚本里重新写入 DNS,例如在 /etc/rc.local 或网络配置脚本里加:

echo -e "nameserver 223.5.5.5\nnameserver 8.8.8.8" > /etc/resolv.conf

我设置了开机自启动配置脚本,脚本如下(我还挂载了NFS,如果不需要可以关掉):

#!/bin/sh

# 配置板子 IP
ip addr add 192.168.100.10/24 dev eth0 2>/dev/null

# 挂载 NFS
mountpoint -q /mnt/driver_project || mount -t nfs -o vers=3,nolock 192.168.100.1:/home/dd/nfs_share /mnt/driver_project/

# 设置默认路由
ip route replace default via 192.168.100.1 dev eth0

# 更新 DNS
if ! grep -q "223.5.5.5" /etc/resolv.conf; then
    echo -e "nameserver 223.5.5.5\nnameserver 8.8.8.8" >> /etc/resolv.conf
fi

exit 0

把这个脚本放到RK3568的/etc/init.d下,做一个软连接到里面的文件,以S开头。

按照上面步骤即可ping通外网并且开机自启动。

过一段时间发现ping不通执行手动清除:

ip addr flush dev eth0
ip addr add 192.168.100.10/24 dev eth0
ip route replace default via 192.168.100.1 dev eth0 metric 100

二、lvgl下NTP服务代码

这里是有关NTP的socket网络编程,需要注意下面几个点:

1、ntp_server:NTP 服务器地址(如 ntp1.aliyun.com

2、NTP 使用 UDP 协议。SOCK_DGRAM + IPPROTO_UDP

3、NTP 默认端口是 123。

4、NTP 请求包固定 48 字节。

5、0x1b表示:00 011 011 00表示无警告 011表示NTPv3 011表示客户端模式。这是一个标准的 NTP 客户端请求包的起始字节。

6、NTP返回的时间戳在包的第 40~43 字节。

7、NTP 时间是从 1900-01-01 00:00:00 开始的秒数。

int sys_get_time_from_ntp(const char* ntp_server, int *year, int *month , int *day, int* hour, int *minute, int *second)
{
    struct addrinfo hints, *res = NULL;
    int sockfd = -1;
    memset(&hints , 0 , sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_protocol = IPPROTO_UDP; // 这里使用的是UDP
    
    /* 解析域名 */
    int gai_ret = getaddrinfo(ntp_server, "123", &hints, &res);
    if (gai_ret != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_ret));
        return -1;
    }
    /* 建立UDP socket */
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd == -1) {
        perror("socket creation failed");
        freeaddrinfo(res);
        return -1;
    }

    // NTP packet
    unsigned char buf[48] = {0};
    buf[0] = 0x1b;
    
    ssize_t sent = sendto(sockfd, buf, sizeof(buf), 0, res->ai_addr, res->ai_addrlen);
    if (sent != (ssize_t)sizeof(buf)) {
        perror("sendto");
        close(sockfd);
        freeaddrinfo(res);
        return -1;
    }

    // 使用 select 做监听,看看sockfd是否有返回数据
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(sockfd, &readfds);
    struct timeval timeout = {2, 0}; // 2s

    int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
    if (ret == 0) {
        fprintf(stderr, "receive from NTP server timeout\n");
        close(sockfd);
        freeaddrinfo(res);
        return -1;
    } else if (ret < 0) {
        perror("select error");
        close(sockfd);
        freeaddrinfo(res);
        return -1;
    }

    ssize_t rec = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
    if (rec < 0) {
        perror("recvfrom");
        close(sockfd);
        freeaddrinfo(res);
        return -1;
    } else if (rec < 48) {
        fprintf(stderr, "short ntp packet received\n");
        close(sockfd);
        freeaddrinfo(res);
        return -1;
    }

    close(sockfd);
    freeaddrinfo(res);

    uint32_t secsSince1900;
    memcpy(&secsSince1900, &buf[40], sizeof(secsSince1900));
    secsSince1900 = ntohl(secsSince1900); // 网络字节序转换为主机字节序。

    time_t unixTime = (time_t)(secsSince1900 - NTP_TIMESTAMP_DELTA);

    // set timezone to UTC+8
    setenv("TZ", "CST-8", 1);
    tzset();

    struct tm *timeinfo = localtime(&unixTime);
    if (!timeinfo) {
        perror("localtime failed");
        return -1;
    }
    if (year) *year = timeinfo->tm_year + 1900;
    if (month) *month = timeinfo->tm_mon + 1;
    if (day) *day = timeinfo->tm_mday;
    if (hour) *hour = timeinfo->tm_hour;
    if (minute) *minute = timeinfo->tm_min;
    if (second) *second = timeinfo->tm_sec;

    return 0;
}

主要的流程就是:建立UDP连接-->发送NTP请求包-->接收请求包-->解析请求包-->根据时区解析出正常的时间。

三、更新时间

void update_clock(lv_timer_t * timer)
{
    (void)timer;
    
    if (!time_label || !date_label) return;

    time_t current_time;
    struct tm *timeinfo;
    char time_str[64];
    char data_str[64];

    if (ntp_synced) {
        current_time = time(NULL);
        timeinfo = localtime(&current_time); // 这里的current_time时同步之后的时间
    } else {
        static struct tm default_time = {
            .tm_year = 100, // 2000
            .tm_mon = 0,
            .tm_mday = 1,
            .tm_hour = 0,
            .tm_min = 0,
            .tm_sec = 0
        };
        timeinfo = &default_time;
    }

    // 使用 snprintf
    snprintf(time_str, sizeof(time_str), "%02d:%02d:%02d",
             timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
    snprintf(data_str, sizeof(data_str), "%04d-%02d-%02d",
             timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday);

    pthread_mutex_lock(&lvgl_mutex);
    lv_label_set_text(time_label, time_str);
    lv_label_set_text(date_label, data_str);
    pthread_mutex_unlock(&lvgl_mutex);
}

LVGL 本身不是线程安全的,要求所有 UI 的更新必须在同一个线程里执行,否则会出现:

  • 内存冲突(不同线程同时修改 LVGL 内部的数据结构)

  • 崩溃 / UI 显示异常

所以在在最后的时候修改值的文本信息加了一把锁。这样保证了:LVGL 内部的数据修改是原子化的,不会被并发打断

void set_time(lv_obj_t *cell){
    // 创建并保存全局 date_label/time_label
    date_label = lv_label_create(cell);
    lv_label_set_text(date_label, "----/--/--");
    lv_obj_align(date_label, LV_ALIGN_CENTER, 0, -14);
    lv_obj_set_style_text_color(date_label, lv_color_hex(0xFFFFFF), 0);
    lv_obj_set_style_text_font(date_label, &lv_font_montserrat_20, 0);

    time_label = lv_label_create(cell);
    lv_label_set_text(time_label, "--:--");
    lv_obj_align(time_label, LV_ALIGN_CENTER, 0, 14);
    lv_obj_set_style_text_color(time_label, lv_color_hex(0xFFFFFF), 0);
    lv_obj_set_style_text_font(time_label, &lv_font_montserrat_28, 0);

    int year, month, day, hour, minute, second;
    if (sys_get_time_from_ntp("ntp.aliyun.com", &year, &month, &day, &hour, &minute, &second) == 0){
        ntp_synced = true;
        printf("NTP 时间同步成功:%04d-%02d-%02d %02d:%02d:%02d\n",
                year, month, day, hour, minute, second);

        struct tm timeinfo = {
            .tm_year = year - 1900,
            .tm_mon = month - 1,
            .tm_mday = day,
            .tm_hour = hour,
            .tm_min = minute,
            .tm_sec = second
        };
        time_t new_time = mktime(&timeinfo);
        // 如果你想设置系统时间,可以在这里调用 settimeofday(需 root 权限)
        // struct timeval tv = { .tv_sec = new_time, .tv_usec = 0 };
        // settimeofday(&tv, NULL);
    } else {
        ntp_synced = false;
        printf("NTP 时间同步失败, 使用默认时间\n");
    }

    // 创建 LVGL 定时器,并立即更新一次
    lv_timer_t *timer = lv_timer_create(update_clock, 1000, NULL);
    // 将 user_data 设为 NULL,因为 update_clock 使用全局 date_label/time_label
    timer->user_data = time_label;   // v8.2 里直接赋值
    update_clock(timer);  
}

这样就实现了在lvgl中的时间同步!

四、计算周几

使用系统自带函数mktime来计算

int sys_get_day_of_week(int year , int month , int day)
{   
    struct tm timeinfo = {0};
    timeinfo.tm_year = year - 1900;
    timeinfo.tm_mon = month - 1;
    timeinfo.tm_mday = day;

    timeinfo.tm_hour = 12;
    /* 自动计算星期几 */
    mktime(&timeinfo);
    return timeinfo.tm_wday;
}

网站公告

今日签到

点亮在社区的每一天
去签到