背景說明
我的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.當然你可以用別的方式表示年。
編程是一件浪費時間的運動,有時候會對心理產生不良影響。