summaryrefslogtreecommitdiff
path: root/util-linux/hwclock.c
blob: 54e97e5bbb8eb641a57e5594f472109cb3a57c7c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/* vi: set sw=4 ts=4: */
/*
 * Mini hwclock implementation for busybox
 *
 * Copyright (C) 2002 Robert Griebl <griebl@gmx.de>
 *
 * Licensed under GPLv2 or later, see file LICENSE in this source tree.
*/

#include "libbb.h"
/* After libbb.h, since it needs sys/types.h on some systems */
#include <sys/utsname.h>
#include "rtc_.h"

/* diff code is disabled: it's not sys/hw clock diff, it's some useless
 * "time between hwclock was started and we saw CMOS tick" quantity.
 * It's useless since hwclock is started at a random moment,
 * thus the quantity is also random, useless. Showing 0.000000 does not
 * deprive us from any useful info.
 *
 * SHOW_HWCLOCK_DIFF code in this file shows the difference between system
 * and hw clock. It is useful, but not compatible with standard hwclock.
 * Thus disabled.
 */
#define SHOW_HWCLOCK_DIFF 0


#if !SHOW_HWCLOCK_DIFF
# define read_rtc(pp_rtcname, sys_tv, utc) read_rtc(pp_rtcname, utc)
#endif
static time_t read_rtc(const char **pp_rtcname, struct timeval *sys_tv, int utc)
{
	struct tm tm_time;
	int fd;

	fd = rtc_xopen(pp_rtcname, O_RDONLY);

	rtc_read_tm(&tm_time, fd);

#if SHOW_HWCLOCK_DIFF
	{
		int before = tm_time.tm_sec;
		while (1) {
			rtc_read_tm(&tm_time, fd);
			gettimeofday(sys_tv, NULL);
			if (before != tm_time.tm_sec)
				break;
		}
	}
#endif

	if (ENABLE_FEATURE_CLEAN_UP)
		close(fd);

	return rtc_tm2time(&tm_time, utc);
}

static void show_clock(const char **pp_rtcname, int utc)
{
#if SHOW_HWCLOCK_DIFF
	struct timeval sys_tv;
#endif
	time_t t;
	char *cp;

	t = read_rtc(pp_rtcname, &sys_tv, utc);
	cp = ctime(&t);
	strchrnul(cp, '\n')[0] = '\0';
#if !SHOW_HWCLOCK_DIFF
	printf("%s  0.000000 seconds\n", cp);
#else
	{
		long diff = sys_tv.tv_sec - t;
		if (diff < 0 /*&& tv.tv_usec != 0*/) {
			/* Why? */
			/* diff >= 0 is ok:   diff < 0, can't just use tv.tv_usec: */
			/*   45.520820          43.520820 */
			/* - 44.000000        - 45.000000 */
			/* =  1.520820        = -1.479180, not -2.520820! */
			diff++;
			/* should be 1000000 - tv.tv_usec, but then we must check tv.tv_usec != 0 */
			sys_tv.tv_usec = 999999 - sys_tv.tv_usec;
		}
		printf("%s  %ld.%06lu seconds\n", cp, diff, (unsigned long)sys_tv.tv_usec);
	}
#endif
}

static void to_sys_clock(const char **pp_rtcname, int utc)
{
	struct timeval tv;
	struct timezone tz;

	tz.tz_minuteswest = timezone/60 - 60*daylight;
	tz.tz_dsttime = 0;

	tv.tv_sec = read_rtc(pp_rtcname, NULL, utc);
	tv.tv_usec = 0;
	if (settimeofday(&tv, &tz))
		bb_perror_msg_and_die("settimeofday");
}

static void from_sys_clock(const char **pp_rtcname, int utc)
{
#if 1
	struct timeval tv;
	struct tm tm_time;
	int rtc;

	rtc = rtc_xopen(pp_rtcname, O_WRONLY);
	gettimeofday(&tv, NULL);
	/* Prepare tm_time */
	if (sizeof(time_t) == sizeof(tv.tv_sec)) {
		if (utc)
			gmtime_r((time_t*)&tv.tv_sec, &tm_time);
		else
			localtime_r((time_t*)&tv.tv_sec, &tm_time);
	} else {
		time_t t = tv.tv_sec;
		if (utc)
			gmtime_r(&t, &tm_time);
		else
			localtime_r(&t, &tm_time);
	}
#else
/* Bloated code which tries to set hw clock with better precision.
 * On x86, even though code does set hw clock within <1ms of exact
 * whole seconds, apparently hw clock (at least on some machines)
 * doesn't reset internal fractional seconds to 0,
 * making all this a pointless excercise.
 */
	/* If we see that we are N usec away from whole second,
	 * we'll sleep for N-ADJ usecs. ADJ corrects for the fact
	 * that CPU is not infinitely fast.
	 * On infinitely fast CPU, next wakeup would be
	 * on (exactly_next_whole_second - ADJ). On real CPUs,
	 * this difference between current time and whole second
	 * is less than ADJ (assuming system isn't heavily loaded).
	 */
	/* Small value of 256us gives very precise sync for 2+ GHz CPUs.
	 * Slower CPUs will fail to sync and will go to bigger
	 * ADJ values. qemu-emulated armv4tl with ~100 MHz
	 * performance ends up using ADJ ~= 4*1024 and it takes
	 * 2+ secs (2 tries with successively larger ADJ)
	 * to sync. Even straced one on the same qemu (very slow)
	 * takes only 4 tries.
	 */
#define TWEAK_USEC 256
	unsigned adj = TWEAK_USEC;
	struct tm tm_time;
	struct timeval tv;
	int rtc = rtc_xopen(pp_rtcname, O_WRONLY);

	/* Try to catch the moment when whole second is close */
	while (1) {
		unsigned rem_usec;
		time_t t;

		gettimeofday(&tv, NULL);

		t = tv.tv_sec;
		rem_usec = 1000000 - tv.tv_usec;
		if (rem_usec < adj) {
			/* Close enough */
 small_rem:
			t++;
		}

		/* Prepare tm_time from t */
		if (utc)
			gmtime_r(&t, &tm_time); /* may read /etc/xxx (it takes time) */
		else
			localtime_r(&t, &tm_time); /* same */

		if (adj >= 32*1024) {
			break; /* 32 ms diff and still no luck?? give up trying to sync */
		}

		/* gmtime/localtime took some time, re-get cur time */
		gettimeofday(&tv, NULL);

		if (tv.tv_sec < t /* we are still in old second */
		 || (tv.tv_sec == t && tv.tv_usec < adj) /* not too far into next second */
		) {
			break; /* good, we are in sync! */
		}

		rem_usec = 1000000 - tv.tv_usec;
		if (rem_usec < adj) {
			t = tv.tv_sec;
			goto small_rem; /* already close to next sec, don't sleep */
		}

		/* Try to sync up by sleeping */
		usleep(rem_usec - adj);

		/* Jump to 1ms diff, then increase fast (x2): EVERY loop
		 * takes ~1 sec, people won't like slowly converging code here!
		 */
	//bb_error_msg("adj:%d tv.tv_usec:%d", adj, (int)tv.tv_usec);
		if (adj < 512)
			adj = 512;
		/* ... and if last "overshoot" does not look insanely big,
		 * just use it as adj increment. This makes convergence faster.
		 */
		if (tv.tv_usec < adj * 8) {
			adj += tv.tv_usec;
			continue;
		}
		adj *= 2;
	}
	/* Debug aid to find "optimal" TWEAK_USEC with nearly exact sync.
	 * Look for a value which makes tv_usec close to 999999 or 0.
	 * For 2.20GHz Intel Core 2: optimal TWEAK_USEC ~= 200
	 */
	//bb_error_msg("tv.tv_usec:%d", (int)tv.tv_usec);
#endif

	tm_time.tm_isdst = 0;
	xioctl(rtc, RTC_SET_TIME, &tm_time);

	if (ENABLE_FEATURE_CLEAN_UP)
		close(rtc);
}

/*
 * At system boot, kernel may set system time from RTC,
 * but it knows nothing about timezones. If RTC is in local time,
 * then system time is wrong - it is offset by timezone.
 * This option corrects system time if RTC is in local time,
 * and (always) sets in-kernel timezone.
 *
 * This is an alternate option to --hctosys that does not read the
 * hardware clock.
 */
static void set_system_clock_timezone(int utc)
{
	struct timeval tv;
	struct tm *broken;
	struct timezone tz;

	gettimeofday(&tv, NULL);
	broken = localtime(&tv.tv_sec);
	tz.tz_minuteswest = timezone / 60;
	if (broken->tm_isdst)
		tz.tz_minuteswest -= 60;
	tz.tz_dsttime = 0;
	gettimeofday(&tv, NULL);
	if (!utc)
		tv.tv_sec += tz.tz_minuteswest * 60;
	if (settimeofday(&tv, &tz))
		bb_perror_msg_and_die("settimeofday");
}

//usage:#define hwclock_trivial_usage
//usage:	IF_FEATURE_HWCLOCK_LONG_OPTIONS(
//usage:       "[-r|--show] [-s|--hctosys] [-w|--systohc] [-t|--systz]"
//usage:       " [-l|--localtime] [-u|--utc]"
//usage:       " [-f|--rtc FILE]"
//usage:	)
//usage:	IF_NOT_FEATURE_HWCLOCK_LONG_OPTIONS(
//usage:       "[-r] [-s] [-w] [-t] [-l] [-u] [-f FILE]"
//usage:	)
//usage:#define hwclock_full_usage "\n\n"
//usage:       "Query and set hardware clock (RTC)\n"
//usage:     "\nOptions:"
//usage:     "\n	-r	Show hardware clock time"
//usage:     "\n	-s	Set system time from hardware clock"
//usage:     "\n	-w	Set hardware clock from system time"
//usage:     "\n	-t	Set in-kernel timezone, correct system time"
//usage:     "\n		if hardware clock is in local time"
//usage:     "\n	-u	Hardware clock is in UTC"
//usage:     "\n	-l	Hardware clock is in local time"
//usage:     "\n	-f FILE	Use specified device (e.g. /dev/rtc2)"

#define HWCLOCK_OPT_LOCALTIME   0x01
#define HWCLOCK_OPT_UTC         0x02
#define HWCLOCK_OPT_SHOW        0x04
#define HWCLOCK_OPT_HCTOSYS     0x08
#define HWCLOCK_OPT_SYSTOHC     0x10
#define HWCLOCK_OPT_SYSTZ       0x20
#define HWCLOCK_OPT_RTCFILE     0x40

int hwclock_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int hwclock_main(int argc UNUSED_PARAM, char **argv)
{
	const char *rtcname = NULL;
	unsigned opt;
	int utc;

#if ENABLE_FEATURE_HWCLOCK_LONG_OPTIONS
	static const char hwclock_longopts[] ALIGN1 =
		"localtime\0" No_argument "l" /* short opt is non-standard */
		"utc\0"       No_argument "u"
		"show\0"      No_argument "r"
		"hctosys\0"   No_argument "s"
		"systohc\0"   No_argument "w"
		"systz\0"     No_argument "t" /* short opt is non-standard */
		"rtc\0"       Required_argument "f"
		;
	applet_long_options = hwclock_longopts;
#endif
	opt_complementary = "r--wst:w--rst:s--wrt:t--rsw:l--u:u--l";
	opt = getopt32(argv, "lurswtf:", &rtcname);

	/* If -u or -l wasn't given check if we are using utc */
	if (opt & (HWCLOCK_OPT_UTC | HWCLOCK_OPT_LOCALTIME))
		utc = (opt & HWCLOCK_OPT_UTC);
	else
		utc = rtc_adjtime_is_utc();

	if (opt & HWCLOCK_OPT_HCTOSYS)
		to_sys_clock(&rtcname, utc);
	else if (opt & HWCLOCK_OPT_SYSTOHC)
		from_sys_clock(&rtcname, utc);
	else if (opt & HWCLOCK_OPT_SYSTZ)
		set_system_clock_timezone(utc);
	else
		/* default HWCLOCK_OPT_SHOW */
		show_clock(&rtcname, utc);

	return 0;
}