C/C++更新linux系统时间和硬件时间的方法

背景说明

我的linux板卡是不联网的,但是MCU所在的板卡通过2G模块连接物联网。但是linux板卡有时需要记录相对准确的时间。理想情况是安装一个带电池的RTC模块通过I2C连接到linux板卡上。但我对时间的准确度要求并不是很高。差个1秒2秒的问题不大。添加额外硬件就得不偿失。MCU和linux板卡本来已经有UART进行数据互传。所以我决定通过MCU将自己的RTC时间通过UART传递给linux板卡,linux板卡接收到时间之后,在添加一定的延时来实现时间的基本同步。当然如果想更好的通过可以尝试数据互传计算偏差的方法。但这不是本贴的重点。

关于linux时间的说明

linux板卡本质上有两个时间。一个是系统时间,一个是硬件时钟(RTC时间)。linux启动时从RTC读取时间同步到系统时间。而每隔一段时间(linux据说时11分钟,我不知道这个数据的来源。有知道的可以告诉我)系统回自动将系统时间同步到硬件。这就产生了一个问题,如果你在系统启动时直接更改RTC的时间很难说会不会被系统时间冲刷掉。另一方面,如果你更改和系统时间但是没有立即更新硬件时间。那么恰好你在更新完时间没多久就关闭了系统,这时候硬件时钟可能还是之前的时间。那么最好的方式是更新晚系统时间之后然后同步系统时间到硬件时钟。
很多ARM CPU本身自带RTC模块的,但是大多数此类板卡不带RTC备份电池电路。所以掉电之后时间就不准确了,但是休眠不断电的情况下,RTC还是在运行的。

linux时间相关的两个头文件

linux上有两个叫做time.h的头文件。包含的方法如下:

#include <time.h>
#include <sys/time.h>

头一个time.h主要包含获取和转换时间的很多函数。
第二个主要是gettimeofday和settimeofday等函数。两者并不一样,请注意。
另外本程序用到了settimeofday可能会造成编译上的问题,请看我上一个帖子的解决办法。

几个关键函数,结构,语句的说明

函数

图省事,前三个函数的说明摘自runoob.
1、time_t time(time_t *timer)
计算当前日历时间,并把它编码成 time_t 格式。
2、time_t mktime(struct tm *timeptr)
把 timeptr 所指向的结构转换为一个依据本地时区的 time_t 值。
3、struct tm *gmtime(const time_t *timer)
timer 的值被分解为 tm 结构,并用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示。
4、int settimeofday ( const struct timeval *tv,const struct timezone *tz);
用于更新系统时间。相关函数是stime,但这个函数可以更新精度可以到微秒。

结构

下面两个结构体(描述摘自runboob关于时间的说明)很关键:
struct tm {
int tm_sec; /* 秒,范围从 0 到 59 /
int tm_min; /
分,范围从 0 到 59 /
int tm_hour; /
小时,范围从 0 到 23 /
int tm_mday; /
一月中的第几天,范围从 1 到 31 /
int tm_mon; /
月,范围从 0 到 11 /
int tm_year; /
自 1900 年起的年数 /
int tm_wday; /
一周中的第几天,范围从 0 到 6 /
int tm_yday; /
一年中的第几天,范围从 0 到 365 /
int tm_isdst; /
夏令时 */
};
请注意月份,不是从1~12.这个月份加1才是我们常用的月份。别的夏令时有需要的也查一下吧。
struct timeval
{
long tv_sec; //
long tv_usec; /微秒/
};

语句

hwclock是本次主要使用的语句。另一个语句date可用于你查看时间是否设置正确。

Usage:
 hwclock [function] [option...]

Time clocks utility.

Functions:
 -r, --show           display the RTC time
     --get            display drift corrected RTC time
     --set            set the RTC according to --date
 -s, --hctosys        set the system time from the RTC
 -w, --systohc        set the RTC from the system time
     --systz          send timescale configurations to the kernel
 -a, --adjust         adjust the RTC to account for systematic drift
     --predict        predict the drifted RTC time according to --date

Options:
 -u, --utc            the RTC timescale is UTC
 -l, --localtime      the RTC timescale is Local
 -f, --rtc <file>     use an alternate file to /dev/rtc0
     --directisa      use the ISA bus instead of /dev/rtc0 access
     --date <time>    date/time input for --set and --predict
     --update-drift   update the RTC drift factor
     --noadjfile      do not use /etc/adjtime
     --adjfile <file> use an alternate file to /etc/adjtime
     --test           dry run; implies --debug
 -D, --debug          display more details

 -h, --help           display this help
 -V, --version        display version

几个关键说明,windows的wsl不支持此命令。
-u可以显示utc时间-l可以显示本地时间–show可以显示时间。
我们本次使用的是"hwclock --systohc",用于将系统时间更新到硬件。当然你也可以用c语言直接实现,但是过程稍微繁琐。这里直接用c调用bash命令实现。相关如果你直接更新的rtc,也可以通过"hwclock --hctosys"来实现对系统时间的更新。防止二者不匹配。

date的命令解释很长,请自行查阅。这里提供两个:“date"和"date -u”.前者是本地时间,后者显示utc时间。

具体实现

根据我上面的思路和函数描述
函数中添加了很多与调试相关的代码,可以酌情删减。主要是printf函数。
我这里用8各byte数据来传递时间。按照顺序:年月日时分秒各一个字节,毫秒两个字节。当然你可以直接传递微妙,程序只需要做简单修改即可。
不多说了,测试程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <string.h>

/*Message List:
(u8)year + 2000,
(u8)month,
(u8)date,
(u8)hour
(u8)min
(u8)seconds
(u16)milliseconds
there are 8 bytes totally
*/
#define mcu_opi_oddsef_ms 200

void opi_utc_update(uint8_t *msgBuf, uint16_t size)
{
    time_t rawTime;
    //struct tm *pNowTM;
    struct tm nowTM;
    struct timeval nowTV;
    int microSec = 0;

    if (size < 8)
        return;

    rawTime = time(NULL);
    printf("RawTime 1 is %lu\r\n", rawTime);
    // pNowTM = gmtime(&rawTime);
    // printf("Year:%d, month:%d, day is %d\r\n", pNowTM->tm_year, pNowTM->tm_mon, pNowTM->tm_mday);
    // printf("Hour:%d, Mins:%d, Seconds is %d\r\n", pNowTM->tm_hour, pNowTM->tm_min, pNowTM->tm_sec);
    // printf("Timezone: %s\r\n", pNowTM->tm_zone);
    // printf("Will test mktime\r\n");
    // pNowTM->tm_sec += 1;
    // rawTime = mktime(pNowTM);
    // printf("RawTime 2 is %lu\r\n", rawTime);

    /*Set my time*/
    nowTM.tm_year = 100 + (int)msgBuf[0];
    nowTM.tm_mon = msgBuf[1] - 1;
    nowTM.tm_mday = msgBuf[2];
    nowTM.tm_hour = msgBuf[3];
    nowTM.tm_min = msgBuf[4];
    nowTM.tm_sec = msgBuf[5];

    rawTime = mktime(&nowTM);
    printf("gmtoff after mktime: %s\r\n", nowTM.tm_gmtoff);
    if (nowTM.tm_gmtoff != 0)
        rawTime += nowTM.tm_gmtoff;

    printf("RawTime 3 is %lu\r\n", rawTime);
    microSec = msgBuf[6];
    microSec <<= 8;
    microSec += msgBuf[7];
    microSec += mcu_opi_oddsef_ms;
    if (microSec > 1000)
    {
        microSec %= 1000;
        rawTime += 1;
    }
    microSec *= 1000;
    /*Set time of day*/
    nowTV.tv_sec = rawTime;
    nowTV.tv_usec = microSec;
    if (settimeofday(&nowTV, NULL) != 0)
    {
        printf("Can not set system time\r\n");
        return;
    }
    /*update hardware time*/
    int rc = system("hwclock --systohc");
    printf("return value is %d \r\n", rc);
}

int main(void)
{
    uint8_t testTim[8] = {20, 7, 2, 10, 42, 35, 0, 200};
    printf("It is a test of time\r\n");
    opi_utc_update(testTim, 8);
}

程序很短,可以轻松修改。
下面是Makefile文件

CC = gcc
CFLAGS = -lm -lc -std=c11 -Wall -O2 -pipe -g -D_GNU_SOURCE

TARGET = hwcs
OBJS = hwc_sync.o 

$(TARGET): $(OBJS)
	$(CC)  -o $(TARGET) $(OBJS) $(CFLAGS)
#%.o:%.c
#	$(CC) $(CFLAGS) -o $@ -c $<
%.o : %.cpp
	$(CC) $(CFLAGS) -o $@ -c $<


clean: 
	rm *.o $(TARGET)

总结

代码虽短,调试废了很多功夫。我传递的UTC时间,在假定不知道timezone的时候。用mktime这个函数直接转换的是按照当地时间转换的。所以,还需要将转换的时间加上gmtoff才可以。
另一个细节需要说明的是,计算time_t的时间基准是1900年,我这里的时间基准是2000年。所以在程序中加了100.当然你可以用别的方式表示年。
编程是一件浪费时间的运动,有时候会对心理产生不良影响。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章