1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * rtc-efi: RTC Class Driver for EFI-based systems |
4 | * |
5 | * Copyright (C) 2009 Hewlett-Packard Development Company, L.P. |
6 | * |
7 | * Author: dann frazier <dannf@dannf.org> |
8 | * Based on efirtc.c by Stephane Eranian |
9 | */ |
10 | |
11 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
12 | |
13 | #include <linux/kernel.h> |
14 | #include <linux/module.h> |
15 | #include <linux/stringify.h> |
16 | #include <linux/time.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/rtc.h> |
19 | #include <linux/efi.h> |
20 | |
21 | #define EFI_ISDST (EFI_TIME_ADJUST_DAYLIGHT|EFI_TIME_IN_DAYLIGHT) |
22 | |
23 | /* |
24 | * returns day of the year [0-365] |
25 | */ |
26 | static inline int |
27 | compute_yday(efi_time_t *eft) |
28 | { |
29 | /* efi_time_t.month is in the [1-12] so, we need -1 */ |
30 | return rtc_year_days(day: eft->day, month: eft->month - 1, year: eft->year); |
31 | } |
32 | |
33 | /* |
34 | * returns day of the week [0-6] 0=Sunday |
35 | */ |
36 | static int |
37 | compute_wday(efi_time_t *eft, int yday) |
38 | { |
39 | int ndays = eft->year * (365 % 7) |
40 | + (eft->year - 1) / 4 |
41 | - (eft->year - 1) / 100 |
42 | + (eft->year - 1) / 400 |
43 | + yday; |
44 | |
45 | /* |
46 | * 1/1/0000 may or may not have been a Sunday (if it ever existed at |
47 | * all) but assuming it was makes this calculation work correctly. |
48 | */ |
49 | return ndays % 7; |
50 | } |
51 | |
52 | static void |
53 | convert_to_efi_time(struct rtc_time *wtime, efi_time_t *eft) |
54 | { |
55 | eft->year = wtime->tm_year + 1900; |
56 | eft->month = wtime->tm_mon + 1; |
57 | eft->day = wtime->tm_mday; |
58 | eft->hour = wtime->tm_hour; |
59 | eft->minute = wtime->tm_min; |
60 | eft->second = wtime->tm_sec; |
61 | eft->nanosecond = 0; |
62 | eft->daylight = wtime->tm_isdst ? EFI_ISDST : 0; |
63 | eft->timezone = EFI_UNSPECIFIED_TIMEZONE; |
64 | } |
65 | |
66 | static bool |
67 | convert_from_efi_time(efi_time_t *eft, struct rtc_time *wtime) |
68 | { |
69 | memset(wtime, 0, sizeof(*wtime)); |
70 | |
71 | if (eft->second >= 60) |
72 | return false; |
73 | wtime->tm_sec = eft->second; |
74 | |
75 | if (eft->minute >= 60) |
76 | return false; |
77 | wtime->tm_min = eft->minute; |
78 | |
79 | if (eft->hour >= 24) |
80 | return false; |
81 | wtime->tm_hour = eft->hour; |
82 | |
83 | if (!eft->day || eft->day > 31) |
84 | return false; |
85 | wtime->tm_mday = eft->day; |
86 | |
87 | if (!eft->month || eft->month > 12) |
88 | return false; |
89 | wtime->tm_mon = eft->month - 1; |
90 | |
91 | if (eft->year < 1900 || eft->year > 9999) |
92 | return false; |
93 | wtime->tm_year = eft->year - 1900; |
94 | |
95 | /* day in the year [1-365]*/ |
96 | wtime->tm_yday = compute_yday(eft); |
97 | |
98 | /* day of the week [0-6], Sunday=0 */ |
99 | wtime->tm_wday = compute_wday(eft, yday: wtime->tm_yday); |
100 | |
101 | switch (eft->daylight & EFI_ISDST) { |
102 | case EFI_ISDST: |
103 | wtime->tm_isdst = 1; |
104 | break; |
105 | case EFI_TIME_ADJUST_DAYLIGHT: |
106 | wtime->tm_isdst = 0; |
107 | break; |
108 | default: |
109 | wtime->tm_isdst = -1; |
110 | } |
111 | |
112 | return true; |
113 | } |
114 | |
115 | static int efi_read_alarm(struct device *dev, struct rtc_wkalrm *wkalrm) |
116 | { |
117 | efi_time_t eft; |
118 | efi_status_t status; |
119 | |
120 | /* |
121 | * As of EFI v1.10, this call always returns an unsupported status |
122 | */ |
123 | status = efi.get_wakeup_time((efi_bool_t *)&wkalrm->enabled, |
124 | (efi_bool_t *)&wkalrm->pending, &eft); |
125 | |
126 | if (status != EFI_SUCCESS) |
127 | return -EINVAL; |
128 | |
129 | if (!convert_from_efi_time(eft: &eft, wtime: &wkalrm->time)) |
130 | return -EIO; |
131 | |
132 | return rtc_valid_tm(tm: &wkalrm->time); |
133 | } |
134 | |
135 | static int efi_set_alarm(struct device *dev, struct rtc_wkalrm *wkalrm) |
136 | { |
137 | efi_time_t eft; |
138 | efi_status_t status; |
139 | |
140 | convert_to_efi_time(wtime: &wkalrm->time, eft: &eft); |
141 | |
142 | /* |
143 | * XXX Fixme: |
144 | * As of EFI 0.92 with the firmware I have on my |
145 | * machine this call does not seem to work quite |
146 | * right |
147 | * |
148 | * As of v1.10, this call always returns an unsupported status |
149 | */ |
150 | status = efi.set_wakeup_time((efi_bool_t)wkalrm->enabled, &eft); |
151 | |
152 | dev_warn(dev, "write status is %d\n" , (int)status); |
153 | |
154 | return status == EFI_SUCCESS ? 0 : -EINVAL; |
155 | } |
156 | |
157 | static int efi_read_time(struct device *dev, struct rtc_time *tm) |
158 | { |
159 | efi_status_t status; |
160 | efi_time_t eft; |
161 | efi_time_cap_t cap; |
162 | |
163 | status = efi.get_time(&eft, &cap); |
164 | |
165 | if (status != EFI_SUCCESS) { |
166 | /* should never happen */ |
167 | dev_err_once(dev, "can't read time\n" ); |
168 | return -EINVAL; |
169 | } |
170 | |
171 | if (!convert_from_efi_time(eft: &eft, wtime: tm)) |
172 | return -EIO; |
173 | |
174 | return 0; |
175 | } |
176 | |
177 | static int efi_set_time(struct device *dev, struct rtc_time *tm) |
178 | { |
179 | efi_status_t status; |
180 | efi_time_t eft; |
181 | |
182 | convert_to_efi_time(wtime: tm, eft: &eft); |
183 | |
184 | status = efi.set_time(&eft); |
185 | |
186 | return status == EFI_SUCCESS ? 0 : -EINVAL; |
187 | } |
188 | |
189 | static int efi_procfs(struct device *dev, struct seq_file *seq) |
190 | { |
191 | efi_time_t eft, alm; |
192 | efi_time_cap_t cap; |
193 | efi_bool_t enabled, pending; |
194 | struct rtc_device *rtc = dev_get_drvdata(dev); |
195 | |
196 | memset(&eft, 0, sizeof(eft)); |
197 | memset(&alm, 0, sizeof(alm)); |
198 | memset(&cap, 0, sizeof(cap)); |
199 | |
200 | efi.get_time(&eft, &cap); |
201 | efi.get_wakeup_time(&enabled, &pending, &alm); |
202 | |
203 | seq_printf(m: seq, |
204 | fmt: "Time\t\t: %u:%u:%u.%09u\n" |
205 | "Date\t\t: %u-%u-%u\n" |
206 | "Daylight\t: %u\n" , |
207 | eft.hour, eft.minute, eft.second, eft.nanosecond, |
208 | eft.year, eft.month, eft.day, |
209 | eft.daylight); |
210 | |
211 | if (eft.timezone == EFI_UNSPECIFIED_TIMEZONE) |
212 | seq_puts(m: seq, s: "Timezone\t: unspecified\n" ); |
213 | else |
214 | /* XXX fixme: convert to string? */ |
215 | seq_printf(m: seq, fmt: "Timezone\t: %u\n" , eft.timezone); |
216 | |
217 | if (test_bit(RTC_FEATURE_ALARM, rtc->features)) { |
218 | seq_printf(m: seq, |
219 | fmt: "Alarm Time\t: %u:%u:%u.%09u\n" |
220 | "Alarm Date\t: %u-%u-%u\n" |
221 | "Alarm Daylight\t: %u\n" |
222 | "Enabled\t\t: %s\n" |
223 | "Pending\t\t: %s\n" , |
224 | alm.hour, alm.minute, alm.second, alm.nanosecond, |
225 | alm.year, alm.month, alm.day, |
226 | alm.daylight, |
227 | enabled == 1 ? "yes" : "no" , |
228 | pending == 1 ? "yes" : "no" ); |
229 | |
230 | if (alm.timezone == EFI_UNSPECIFIED_TIMEZONE) |
231 | seq_puts(m: seq, s: "Timezone\t: unspecified\n" ); |
232 | else |
233 | /* XXX fixme: convert to string? */ |
234 | seq_printf(m: seq, fmt: "Timezone\t: %u\n" , alm.timezone); |
235 | } |
236 | |
237 | /* |
238 | * now prints the capabilities |
239 | */ |
240 | seq_printf(m: seq, |
241 | fmt: "Resolution\t: %u\n" |
242 | "Accuracy\t: %u\n" |
243 | "SetstoZero\t: %u\n" , |
244 | cap.resolution, cap.accuracy, cap.sets_to_zero); |
245 | |
246 | return 0; |
247 | } |
248 | |
249 | static const struct rtc_class_ops efi_rtc_ops = { |
250 | .read_time = efi_read_time, |
251 | .set_time = efi_set_time, |
252 | .read_alarm = efi_read_alarm, |
253 | .set_alarm = efi_set_alarm, |
254 | .proc = efi_procfs, |
255 | }; |
256 | |
257 | static int __init efi_rtc_probe(struct platform_device *dev) |
258 | { |
259 | struct rtc_device *rtc; |
260 | efi_time_t eft; |
261 | efi_time_cap_t cap; |
262 | |
263 | /* First check if the RTC is usable */ |
264 | if (efi.get_time(&eft, &cap) != EFI_SUCCESS) |
265 | return -ENODEV; |
266 | |
267 | rtc = devm_rtc_allocate_device(dev: &dev->dev); |
268 | if (IS_ERR(ptr: rtc)) |
269 | return PTR_ERR(ptr: rtc); |
270 | |
271 | platform_set_drvdata(pdev: dev, data: rtc); |
272 | |
273 | rtc->ops = &efi_rtc_ops; |
274 | clear_bit(RTC_FEATURE_UPDATE_INTERRUPT, addr: rtc->features); |
275 | if (efi_rt_services_supported(EFI_RT_SUPPORTED_WAKEUP_SERVICES)) |
276 | set_bit(RTC_FEATURE_ALARM_WAKEUP_ONLY, addr: rtc->features); |
277 | else |
278 | clear_bit(RTC_FEATURE_ALARM, addr: rtc->features); |
279 | |
280 | device_init_wakeup(dev: &dev->dev, enable: true); |
281 | |
282 | return devm_rtc_register_device(rtc); |
283 | } |
284 | |
285 | static struct platform_driver efi_rtc_driver = { |
286 | .driver = { |
287 | .name = "rtc-efi" , |
288 | }, |
289 | }; |
290 | |
291 | module_platform_driver_probe(efi_rtc_driver, efi_rtc_probe); |
292 | |
293 | MODULE_AUTHOR("dann frazier <dannf@dannf.org>" ); |
294 | MODULE_LICENSE("GPL" ); |
295 | MODULE_DESCRIPTION("EFI RTC driver" ); |
296 | MODULE_ALIAS("platform:rtc-efi" ); |
297 | |