内核驱动 (三)Linux系统时钟RTC

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/JerryGou/article/details/82557837

RTC,实时时钟芯片,用来在系统断电时,利用备用的锂电池继续记录时间。

一、RTC介绍

    (1)real time clock,真实时间,就是所谓的xx年x月x日x时x分x秒星期x
    (2)RTC是SoC中一个内部外设,RTC有自己独立的晶振提供RTC时钟源(32.768KHz),内部有一些寄存器用来记录时间(年月日时分秒星期)。一般情况下为了在系统关机时时间仍然在走,还会给RTC提供一个电池供电。
    (3)RTC的时间寄存器们(SEC,MIN,HOUR,YEAR...)使用的是BCD码保存时间数据。

注:什么是BCD码?
    (1)RTC中所有的时间(年月日时分秒星期,包括闹钟)都是用BCD码编码的。
    (2)BCD码本质上是对数字的一种编码。用来解决这种问题:由56得到0x56(或者反过来)。也就是说我们希望十进制的56可以被编码成56(这里的56不是十进制56,而是两个数字5和6).
    (3)BCD码的作用在于可以将十进制数拆成组成这个十进制数的各个数字的编码,变成编码后就没有位数的限制了。譬如我有一个很大的数123456789123456789,如果这个数纯粹当数字肯定超出了int的范围,计算机无法直接处理。要想让计算机处理这个数,计算机首先得能表达这个数,表达的方式就是先把这个数转成对应的BCD码(123456789123456789)
    (4)BCD码在计算机中可以用十六进制的形式来表示。也就是说十进制的56转成BCD码后是56,在计算机中用0x56来表达(暂时存储与运算)。
    (5)所以我们需要写两个函数进行十进制和BCD的互转,示例代码如下:

static unsigned int bcd_2_dec(unsigned int bcd)
{
    volatile unsigned int dec_h, dec_l;
    dec_h = ((bcd &(0xf << 4))>>4);
    dec_l = bcd&0xf;
    
    return dec_h*10 + dec_l;
}

static unsigned int dec_2_bcd(unsigned int dec)
{
    return ((dec/10)<<4)|(dec%10);
}

二、S5PV210的RTC

2.1、S5PV210的RTC的功能特点

1、支持BCD数字
2、支持alarm功能
3、支持tick功能
4、支持毫秒级tick time

操作步骤
    1、使能RTC控制器,使能rtc 控制器
    2、使能rtc tick timer,这里应该是中断允许。
    3、向寄存器写时间相关的日期,操作的时候可以读取。

2.2、S5PV210的RTC分析

如图,RTC实时时钟的框架图,XTIrtc和XTOrtc产生脉冲信号,即外部晶振。传给2^15的一个时钟分频器,得到一个128Hz的频率,这个频率用来产生滴答计数。当时钟计数为0时,产生一个TIME TICK中断信号。时钟控制器用来控制RTC实时时钟的功能。复位寄存器用来重置SEC和MIN寄存器。闰年发生器用来产生闰年逻辑。报警发生器用来控制是否产生报警信号。

2.2.1、分析

    (1)使用独立的晶振。
    (2)对外可以输出两种TICK信号:TICK中断和TICK唤醒。
    (3)对外可以输出两种ALARM信号:ALARM中断和ALARM唤醒。(闹钟

Time Tick Generator:产生tick,说明这个RTC可以用来生成tick给操作系统用。
Clock Divider:分频器,但RTC的分频器是设定好了的,所以软件操作不需要设置。
Control Register:控制各大部门的,主要是用来控制enable和disable。
Reset Register:复位。
Leap Year Generator:闰年生成器。
Alarm Generator:闹钟生成器。
SEC,MIN,HOUR...:记录各个时间级的寄存器。

2.2.2、重要寄存器和操作注意

重要寄存器简介
    (1)INTP 中断挂起寄存器
    (2)RTCCON RTC控制寄存器
    (3)RTCALM ALMxxx 闹钟功能有关的寄存器
    (4)BCDxxx 时间寄存器

操作注意
    (1)基础使用的RTC是不需要初始化的。
    (2)为了避免RTC的时间寄存器被误操作,所以默认情况下RTC读写是禁止的,因此当我们要更改RTC时间时,应该先打开RTC的读写开关,然后再进行读写操作,操作完了后立即关闭读写开关。
    (3)RTC各个时间级的ALARM是独立的,如:只使用ALMSEC,并设置ALMSEC为1,那么每次SEC为1是都会发出中断。

三、注册RTC平台设备

./arch/arm/plat-samsung/devs.c

static struct resource s3c_rtc_resource[] = {
	[0] = DEFINE_RES_MEM(S3C_PA_RTC, SZ_256),   //RTC寄存器地址,内存资源
	[1] = DEFINE_RES_IRQ(IRQ_RTC_ALARM),    //RTC(闹钟)告警中断,IRQ资源
	[2] = DEFINE_RES_IRQ(IRQ_RTC_TIC),  //RTC时钟节拍中断,IRQ资源
};

struct platform_device s3c_device_rtc = {
	.name		= "s5pv210-rtc",
	.id		= -1,
	.num_resources	= ARRAY_SIZE(s3c_rtc_resource),
	.resource	= s3c_rtc_resource,
};

中断资源 NTERRUPT SOURCE

四、平台RTC设备驱动

./drivers/rtc/rtc-s3c.c

static struct platform_driver s3c_rtc_driver = {
	.probe		= s3c_rtc_probe,
	.remove		= s3c_rtc_remove,
	.id_table	= s3c_rtc_driver_ids,
	.driver		= {
		.name	= "s3c-rtc",
		.owner	= THIS_MODULE,
		.pm	= &s3c_rtc_pm_ops,
		.of_match_table	= of_match_ptr(s3c_rtc_dt_match),
	},
};

module_platform_driver(s3c_rtc_driver);

内核驱动匹配成功执行s3c_rtc_probe函数

static int s3c_rtc_probe(struct platform_device *pdev)
{
	struct rtc_device *rtc;     //rtc设备结构体
	struct rtc_time rtc_tm;
	struct resource *res;
	int ret;
	int tmp;

	dev_dbg(&pdev->dev, "%s: probe=%p\n", __func__, pdev);

	/* find the IRQs */
    /*获得IRQ资源中的第二个,即TICK节拍时间中断号*/  
    s3c_rtc_tickno = platform_get_irq(pdev, 1); 
	if (s3c_rtc_tickno < 0) {
		dev_err(&pdev->dev, "no irq for rtc tick\n");
		return s3c_rtc_tickno;
	}

    /*获取IRQ资源中的第一个,即RTC报警中断*/  
	s3c_rtc_alarmno = platform_get_irq(pdev, 0); 
	if (s3c_rtc_alarmno < 0) {
		dev_err(&pdev->dev, "no irq for alarm\n");
		return s3c_rtc_alarmno;
	}

	dev_dbg(&pdev->dev, "s3c2410_rtc: tick irq %d, alarm irq %d\n",
		 s3c_rtc_tickno, s3c_rtc_alarmno);

	/* get the memory region */
    /*获取RTC平台设备所使用的IO端口资源*/  
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 
    
    /*将IO端口占用的IO空间映射到虚拟地址,s3c_rtc_base是这段虚拟地址的起始地址*/  
	s3c_rtc_base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(s3c_rtc_base))
		return PTR_ERR(s3c_rtc_base);

	rtc_clk = devm_clk_get(&pdev->dev, "rtc");
	if (IS_ERR(rtc_clk)) {
		dev_err(&pdev->dev, "failed to find rtc clock source\n");
		ret = PTR_ERR(rtc_clk);
		rtc_clk = NULL;
		return ret;
	}

	clk_prepare_enable(rtc_clk);

	/* check to see if everything is setup correctly */
    /*硬件相关设置,对RTCCON第0位进行操作,使能RTC*/  
	s3c_rtc_enable(pdev, 1);

	dev_dbg(&pdev->dev, "s3c2410_rtc: RTCCON=%02x\n",
		 readw(s3c_rtc_base + S3C2410_RTCCON));

     /*让电源管理支持唤醒功能*/  
	device_init_wakeup(&pdev->dev, 1);

	/* register RTC and exit */

    /*注册rtc设备,名为"s3c",与s3c_rtcops这个rtc_class_ops进行关联*/  
	rtc = devm_rtc_device_register(&pdev->dev, "s3c", &s3c_rtcops,
				  THIS_MODULE); 

	if (IS_ERR(rtc)) {
		dev_err(&pdev->dev, "cannot attach rtc\n");
		ret = PTR_ERR(rtc);
		goto err_nortc;
	}

	s3c_rtc_cpu_type = s3c_rtc_get_driver_data(pdev);

	/* Check RTC Time */

	s3c_rtc_gettime(NULL, &rtc_tm);

	if (rtc_valid_tm(&rtc_tm)) {
		rtc_tm.tm_year	= 100;
		rtc_tm.tm_mon	= 0;
		rtc_tm.tm_mday	= 1;
		rtc_tm.tm_hour	= 0;
		rtc_tm.tm_min	= 0;
		rtc_tm.tm_sec	= 0;

		s3c_rtc_settime(NULL, &rtc_tm);

		dev_warn(&pdev->dev, "warning: invalid RTC value so initializing it\n");
	}

	if (s3c_rtc_cpu_type != TYPE_S3C2410)
		rtc->max_user_freq = 32768;
	else
		rtc->max_user_freq = 128;

	if (s3c_rtc_cpu_type == TYPE_S3C2416 || s3c_rtc_cpu_type == TYPE_S3C2443) {
		tmp = readw(s3c_rtc_base + S3C2410_RTCCON);
		tmp |= S3C2443_RTCCON_TICSEL;
		writew(tmp, s3c_rtc_base + S3C2410_RTCCON);
	}

    /*将rtc这个rtc_device存放在&pdev->dev->driver_data*/  
	platform_set_drvdata(pdev, rtc);

     /*对TICNT第7位进行操作,使能节拍时间计数寄存器*/  
	s3c_rtc_setfreq(&pdev->dev, 1);

    //申请闹钟中断
	ret = devm_request_irq(&pdev->dev, s3c_rtc_alarmno, s3c_rtc_alarmirq,
			  0,  "s3c2410-rtc alarm", rtc);
	if (ret) {
		dev_err(&pdev->dev, "IRQ%d error %d\n", s3c_rtc_alarmno, ret);
		goto err_alarm_irq;
	}

    //申请计时中断
	ret = devm_request_irq(&pdev->dev, s3c_rtc_tickno, s3c_rtc_tickirq,
			  0,  "s3c2410-rtc tick", rtc);
	if (ret) {
		dev_err(&pdev->dev, "IRQ%d error %d\n", s3c_rtc_tickno, ret);
		goto err_alarm_irq;
	}

	clk_disable(rtc_clk);

	return 0;

 err_alarm_irq:
	platform_set_drvdata(pdev, NULL);

 err_nortc:
	s3c_rtc_enable(pdev, 0);
	clk_disable_unprepare(rtc_clk);

	return ret;
}

显然最终会调用devm_rtc_device_register()函数来向内核注册rtc_device设备,注册成功会返回一个已注册好的rtc_device,而s3c_rtcops是一个rtc_class_ops结构体,里面就是保存如何操作这个rtc设备的函数,比如读写RTC时间,读写闹钟时间等,注册后会保存在rtc_device->ops里该函数在drivers/rtc/class.c文件内被定义。class.c文件主要定义了RTC子系统,而内核初始化,便会进入class.c,进入rtc_init()->rtc_dev_init(),来注册字符设备

err = alloc_chrdev_region(&rtc_devt, 0, RTC_DEV_MAX, "rtc");
        // RTC_DEV_MAX=16,表示只注册0~15个次设备号,设备编号保存在rtc_devt中 

devm_rtc_device_register函数注册RTC设备

struct rtc_device *devm_rtc_device_register(struct device *dev,
					const char *name,
					const struct rtc_class_ops *ops,
					struct module *owner)
{
	struct rtc_device **ptr, *rtc;  //定义一个rtc_device结构体

	ptr = devres_alloc(devm_rtc_device_release, sizeof(*ptr), GFP_KERNEL);
	if (!ptr)
		return ERR_PTR(-ENOMEM);

	rtc = rtc_device_register(name, dev, ops, owner);
	if (!IS_ERR(rtc)) {
		*ptr = rtc;
		devres_add(dev, ptr);
	} else {
		devres_free(ptr);
	}

	return rtc;
}

struct rtc_device *rtc_device_register(const char *name, struct device *dev,
					const struct rtc_class_ops *ops,
					struct module *owner)
{
	struct rtc_device *rtc;
	struct rtc_wkalrm alrm;
	int id, err;

	id = ida_simple_get(&rtc_ida, 0, 0, GFP_KERNEL);
	if (id < 0) {
		err = id;
		goto exit;
	}

	rtc = kzalloc(sizeof(struct rtc_device), GFP_KERNEL);   //分配rtc_device结构体为全局变量
	if (rtc == NULL) {
		err = -ENOMEM;
		goto exit_ida;
	}

    /* 设置rtc_device */
	rtc->id = id;
	rtc->ops = ops;     //将s3c_rtcops保存在rtc_device->ops里
	rtc->owner = owner;
	rtc->irq_freq = 1;
	rtc->max_user_freq = 64;
	rtc->dev.parent = dev;
	rtc->dev.class = rtc_class;
	rtc->dev.release = rtc_device_release;

	mutex_init(&rtc->ops_lock);
	spin_lock_init(&rtc->irq_lock);
	spin_lock_init(&rtc->irq_task_lock);
	init_waitqueue_head(&rtc->irq_queue);

	/* Init timerqueue */
	timerqueue_init_head(&rtc->timerqueue);
	INIT_WORK(&rtc->irqwork, rtc_timer_do_work);
	/* Init aie timer */
	rtc_timer_init(&rtc->aie_timer, rtc_aie_update_irq, (void *)rtc);
	/* Init uie timer */
	rtc_timer_init(&rtc->uie_rtctimer, rtc_uie_update_irq, (void *)rtc);
	/* Init pie timer */
	hrtimer_init(&rtc->pie_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	rtc->pie_timer.function = rtc_pie_update_irq;
	rtc->pie_enabled = 0;

	/* Check to see if there is an ALARM already set in hw */
	err = __rtc_read_alarm(rtc, &alrm);

	if (!err && !rtc_valid_tm(&alrm.time))
		rtc_initialize_alarm(rtc, &alrm);

	strlcpy(rtc->name, name, RTC_DEVICE_NAME_SIZE);
	dev_set_name(&rtc->dev, "rtc%d", id);

 	rtc_dev_prepare(rtc);   //1.做提前准备,初始化cdev结构体

	err = device_register(&rtc->dev);
	if (err) {
		put_device(&rtc->dev);
		goto exit_kfree;
	}

	rtc_dev_add_device(rtc);    //2.在/dev下创建rtc相关文件,将cdev添加到系统中
	rtc_sysfs_add_device(rtc);  //3.在/sysfs下创建rtc相关文件
	rtc_proc_add_device(rtc);   //4.在/proc下创建rtc相关文件

	dev_info(dev, "rtc core: registered %s as %s\n",
			rtc->name, dev_name(&rtc->dev));

	return rtc;

exit_kfree:
	kfree(rtc);

exit_ida:
	ida_simple_remove(&rtc_ida, id);

exit:
	dev_err(dev, "rtc core: unable to register %s, err = %d\n",
			name, err);
	return ERR_PTR(err);
}
EXPORT_SYMBOL_GPL(rtc_device_register);

上面的rtc_dev_prepare(rtc)和rtc_dev_add_device(rtc)主要做了以下两个(位于./drivers/rtc/rtc-dev.c):

cdev_init(&rtc->char_dev, &rtc_dev_fops);       绑定file_operations  
cdev_add(&rtc->char_dev, rtc->dev.devt, 1);   注册rtc->char_dev字符设备,添加一个从设备到系统中
所以“s5pv210-rtc”平台设备驱动的.probe主要做了以下几件事:
•1.设置RTC相关寄存器
•2.分配rtc_device结构体
•3.设置rtc_device结构体 
•    -> 3.1 将struct rtc_class_ops s3c_rtcops放入rtc_device->ops,实现对RTC读写时间等操作
•4. 注册rtc->char_dev字符设备,且该字符设备的操作结构体为: struct file_operations rtc_dev_fops 

上面的file_operations操作结构体rtc_dev_fops 的成员,如下图所示:

static const struct file_operations rtc_dev_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.read		= rtc_dev_read,
	.poll		= rtc_dev_poll,
	.unlocked_ioctl	= rtc_dev_ioctl,
	.open		= rtc_dev_open,
	.release	= rtc_dev_release,
	.fasync		= rtc_dev_fasync,
};

1、当我们应用层open(”/dev/rtcXX”)时,就会调用rtc_dev_fops-> rtc_dev_open()

static int rtc_dev_open(struct inode *inode, struct file *file)
{
	int err;
	struct rtc_device *rtc = container_of(inode->i_cdev,
					struct rtc_device, char_dev);   //获取对应的rtc_device
	const struct rtc_class_ops *ops = rtc->ops;     //最终等于s3c_rtcops

	if (test_and_set_bit_lock(RTC_DEV_BUSY, &rtc->flags))
		return -EBUSY;
    
    //设置file结构体的私有成员等于rtc_device,再次执行ioctl等函数时,直接就可以提取file->private_data即可
	file->private_data = rtc;   
	err = ops->open ? ops->open(rtc->dev.parent) : 0;   //调用s3c_rtcops->open
	if (err == 0) {
		spin_lock_irq(&rtc->irq_lock);
		rtc->irq_data = 0;
		spin_unlock_irq(&rtc->irq_lock);

		return 0;
	}

	/* something has gone wrong */
	clear_bit_unlock(RTC_DEV_BUSY, &rtc->flags);
	return err;
}

显然最终还是调用rtc_device下的s3c_rtcops->open:

static const struct rtc_class_ops s3c_rtcops = {
	.read_time	= s3c_rtc_gettime,
	.set_time	= s3c_rtc_settime,
	.read_alarm	= s3c_rtc_getalarm,
	.set_alarm	= s3c_rtc_setalarm,
	.proc		= s3c_rtc_proc,
	.alarm_irq_enable = s3c_rtc_setaie,
};

s3c_rtc_open()函数里面主要是申请了两个中断,一个是闹钟中断,一个是计时中断;
新内核直接将此内容加到s3c_rtc_probe()函数中

static int s3c_rtc_open(struct device *dev)
{     
    struct platform_device *pdev = to_platform_device(dev);    
    struct rtc_device *rtc_dev = platform_get_drvdata(pdev);      
    int ret;

    //申请闹钟中断
    ret = request_irq(s3c_rtc_alarmno, s3c_rtc_alarmirq,IRQF_DISABLED,  
                        "s3c2410-rtc alarm", rtc_dev);                              
    if (ret) {
        dev_err(dev, "IRQ%d error %d\n", s3c_rtc_alarmno, ret);
        return ret;
    }

    //申请计时中断 
    ret = request_irq(s3c_rtc_tickno, s3c_rtc_tickirq,IRQF_DISABLED,  
                        "s3c2410-rtc tick", rtc_dev);  
    if (ret) {
        dev_err(dev, "IRQ%d error %d\n", s3c_rtc_tickno, ret);
        goto tick_err;
    }

    return ret;

tick_err:
    free_irq(s3c_rtc_alarmno, rtc_dev);
    return ret;
}

2、 当我们应用层open后,使用 ioctl(int fd, unsigned long cmd, ...)时,就会调用rtc_dev_fops-> rtc_dev_ioctl ()

static int rtc_dev_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)
{
    struct rtc_device *rtc = file->private_data;  //提取rtc_device
    void __user *uarg = (void __user *) arg;
        ... ...

    switch (cmd) {
        case RTC_EPOCH_SET:
        case RTC_SET_TIME:      //设置时间
            if (!capable(CAP_SYS_TIME))
                return -EACCES;
            break;
        case RTC_IRQP_SET:   //改变中断触发速度
        ... ...
    }
    
    ... ...
    
    switch (cmd) {
        case RTC_ALM_READ:    //读闹钟时间
            err = rtc_read_alarm(rtc, &alarm);              //调用s3c_rtcops-> read_alarm
            if (err < 0)
                return err;

            if (copy_to_user(uarg, &alarm.time, sizeof(tm)))  //长传时间数据
                return -EFAULT;
            break;
        case RTC_ALM_SET:              //设置闹钟时间 , 调用s3c_rtcops-> set_alarm
              ... ...
        case RTC_RD_TIME:              //读RTC时间, 调用s3c_rtcops-> read_alarm
              ... ...
        case RTC_SET_TIME:      //写RTC时间,调用s3c_rtcops-> set_time
              ... ...
        case RTC_IRQP_SET:      //改变中断触发频率,调用s3c_rtcops-> irq_set_freq
              ... ...
    }
}

3、read()调用s3c_rtcops下的成员函数,我们以s3c_rtcops-> read_time()函数为例,读出时间的:

static int s3c_rtc_gettime(struct device *dev, struct rtc_time *rtc_tm)
{
	unsigned int have_retried = 0;
	void __iomem *base = s3c_rtc_base;  //获取RTC相关寄存器基地址

	clk_enable(rtc_clk);
 retry_get_time:
    /* 获取年、月、日、时、分、秒寄存器 */
	rtc_tm->tm_min  = readb(base + S3C2410_RTCMIN);
	rtc_tm->tm_hour = readb(base + S3C2410_RTCHOUR);
	rtc_tm->tm_mday = readb(base + S3C2410_RTCDATE);
	rtc_tm->tm_mon  = readb(base + S3C2410_RTCMON);
	rtc_tm->tm_year = readb(base + S3C2410_RTCYEAR);
	rtc_tm->tm_sec  = readb(base + S3C2410_RTCSEC);

	/* the only way to work out whether the system was mid-update
	 * when we read it is to check the second counter, and if it
	 * is zero, then we re-try the entire read
	 */
    /* 判断秒寄存器中是0,则表示过去了一分钟,那么小时、天、月等寄存器
                    中的值都可能已经变化,需要重新读取这些寄存器的值 */
	if (rtc_tm->tm_sec == 0 && !have_retried) {
		have_retried = 1;
		goto retry_get_time;
	}

    /* 将获取的寄存器值,转换为真正的时间数据 */
	rtc_tm->tm_sec = bcd2bin(rtc_tm->tm_sec);
	rtc_tm->tm_min = bcd2bin(rtc_tm->tm_min);
	rtc_tm->tm_hour = bcd2bin(rtc_tm->tm_hour);
	rtc_tm->tm_mday = bcd2bin(rtc_tm->tm_mday);
	rtc_tm->tm_mon = bcd2bin(rtc_tm->tm_mon);
	rtc_tm->tm_year = bcd2bin(rtc_tm->tm_year);

	rtc_tm->tm_year += 100; //寄存器中存放的是从1900年开始的时间,所以需要+100

	dev_dbg(dev, "read time %04d.%02d.%02d %02d:%02d:%02d\n",
		 1900 + rtc_tm->tm_year, rtc_tm->tm_mon, rtc_tm->tm_mday,
		 rtc_tm->tm_hour, rtc_tm->tm_min, rtc_tm->tm_sec);

	rtc_tm->tm_mon -= 1;

	clk_disable(rtc_clk);
	return rtc_valid_tm(rtc_tm);
}

同样, 在s3c_rtcops-> set_time()函数里,也是向相关寄存器写入RTC时间

所以,总结如下所示:
    • rtc_device->char_dev:字符设备,与应用层、以及更底层的函数打交道
    • rtc_device->ops:更底层的操作函数,直接操作硬件相关的寄存器,被rtc_device->char_dev调用

五、测试运行

5.1、设置RTC时间

Linux中有两个时钟:硬件时钟(210里寄存器的时钟)、系统时钟(内核中的时钟)

所以有两个不同的命令:date命令、hwclock命令

5.2、date命令

如果觉得不方便也可以指定格式显示日期,需要在字符串前面加”+”
如下图所示,输入了  date "+ %Y/%m/%d %H:%M:%S"

%M:表示秒
%m:表示月
%Y:表示年,当只需要最后两位数字,输入%y即可
date命令设置时间格式如下:
date 月日时分年.
如下图所示,输入date 111515292017.20,即可设置好系统时钟

5.3、hwclock命令

常用参数如下所示
    -r, --show          读取并打印硬件时钟(read hardware clock and print result)
    -s, --hctosys       将硬件时钟同步到系统时钟(set the system time from the hardware clock)
    -w, --systohc       将系统时钟同步到硬件时钟(set the hardware clock to the current system time)
如下图所示,使用hwclock -w,即可同步硬件时钟

然后重启后,使用date命令,看到时间正常

猜你喜欢

转载自blog.csdn.net/JerryGou/article/details/82557837