Commit | Line | Data |
---|---|---|
435af897 JN |
1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* | |
3 | * The Netronix embedded controller is a microcontroller found in some | |
4 | * e-book readers designed by the original design manufacturer Netronix, Inc. | |
5 | * It contains RTC, battery monitoring, system power management, and PWM | |
6 | * functionality. | |
7 | * | |
8 | * This driver implements access to the RTC time and date. | |
9 | * | |
10 | * Copyright 2020 Jonathan Neuschäfer <j.neuschaefer@gmx.net> | |
11 | */ | |
12 | ||
13 | #include <linux/mfd/ntxec.h> | |
14 | #include <linux/module.h> | |
15 | #include <linux/platform_device.h> | |
16 | #include <linux/regmap.h> | |
17 | #include <linux/rtc.h> | |
18 | #include <linux/types.h> | |
19 | ||
20 | struct ntxec_rtc { | |
21 | struct device *dev; | |
22 | struct ntxec *ec; | |
23 | }; | |
24 | ||
25 | #define NTXEC_REG_WRITE_YEAR 0x10 | |
26 | #define NTXEC_REG_WRITE_MONTH 0x11 | |
27 | #define NTXEC_REG_WRITE_DAY 0x12 | |
28 | #define NTXEC_REG_WRITE_HOUR 0x13 | |
29 | #define NTXEC_REG_WRITE_MINUTE 0x14 | |
30 | #define NTXEC_REG_WRITE_SECOND 0x15 | |
31 | ||
32 | #define NTXEC_REG_READ_YEAR_MONTH 0x20 | |
33 | #define NTXEC_REG_READ_MDAY_HOUR 0x21 | |
34 | #define NTXEC_REG_READ_MINUTE_SECOND 0x23 | |
35 | ||
36 | static int ntxec_read_time(struct device *dev, struct rtc_time *tm) | |
37 | { | |
38 | struct ntxec_rtc *rtc = dev_get_drvdata(dev); | |
39 | unsigned int value; | |
40 | int res; | |
41 | ||
42 | retry: | |
43 | res = regmap_read(rtc->ec->regmap, NTXEC_REG_READ_MINUTE_SECOND, &value); | |
44 | if (res < 0) | |
45 | return res; | |
46 | ||
47 | tm->tm_min = value >> 8; | |
48 | tm->tm_sec = value & 0xff; | |
49 | ||
50 | res = regmap_read(rtc->ec->regmap, NTXEC_REG_READ_MDAY_HOUR, &value); | |
51 | if (res < 0) | |
52 | return res; | |
53 | ||
54 | tm->tm_mday = value >> 8; | |
55 | tm->tm_hour = value & 0xff; | |
56 | ||
57 | res = regmap_read(rtc->ec->regmap, NTXEC_REG_READ_YEAR_MONTH, &value); | |
58 | if (res < 0) | |
59 | return res; | |
60 | ||
61 | tm->tm_year = (value >> 8) + 100; | |
62 | tm->tm_mon = (value & 0xff) - 1; | |
63 | ||
64 | /* | |
65 | * Read the minutes/seconds field again. If it changed since the first | |
66 | * read, we can't assume that the values read so far are consistent, | |
67 | * and should start from the beginning. | |
68 | */ | |
69 | res = regmap_read(rtc->ec->regmap, NTXEC_REG_READ_MINUTE_SECOND, &value); | |
70 | if (res < 0) | |
71 | return res; | |
72 | ||
73 | if (tm->tm_min != value >> 8 || tm->tm_sec != (value & 0xff)) | |
74 | goto retry; | |
75 | ||
76 | return 0; | |
77 | } | |
78 | ||
79 | static int ntxec_set_time(struct device *dev, struct rtc_time *tm) | |
80 | { | |
81 | struct ntxec_rtc *rtc = dev_get_drvdata(dev); | |
82 | ||
83 | /* | |
84 | * To avoid time overflows while we're writing the full date/time, | |
85 | * set the seconds field to zero before doing anything else. For the | |
86 | * next 59 seconds (plus however long it takes until the RTC's next | |
87 | * update of the second field), the seconds field will not overflow | |
88 | * into the other fields. | |
89 | */ | |
90 | struct reg_sequence regs[] = { | |
91 | { NTXEC_REG_WRITE_SECOND, ntxec_reg8(0) }, | |
92 | { NTXEC_REG_WRITE_YEAR, ntxec_reg8(tm->tm_year - 100) }, | |
93 | { NTXEC_REG_WRITE_MONTH, ntxec_reg8(tm->tm_mon + 1) }, | |
94 | { NTXEC_REG_WRITE_DAY, ntxec_reg8(tm->tm_mday) }, | |
95 | { NTXEC_REG_WRITE_HOUR, ntxec_reg8(tm->tm_hour) }, | |
96 | { NTXEC_REG_WRITE_MINUTE, ntxec_reg8(tm->tm_min) }, | |
97 | { NTXEC_REG_WRITE_SECOND, ntxec_reg8(tm->tm_sec) }, | |
98 | }; | |
99 | ||
100 | return regmap_multi_reg_write(rtc->ec->regmap, regs, ARRAY_SIZE(regs)); | |
101 | } | |
102 | ||
103 | static const struct rtc_class_ops ntxec_rtc_ops = { | |
104 | .read_time = ntxec_read_time, | |
105 | .set_time = ntxec_set_time, | |
106 | }; | |
107 | ||
108 | static int ntxec_rtc_probe(struct platform_device *pdev) | |
109 | { | |
110 | struct rtc_device *dev; | |
111 | struct ntxec_rtc *rtc; | |
112 | ||
113 | pdev->dev.of_node = pdev->dev.parent->of_node; | |
114 | ||
115 | rtc = devm_kzalloc(&pdev->dev, sizeof(*rtc), GFP_KERNEL); | |
116 | if (!rtc) | |
117 | return -ENOMEM; | |
118 | ||
119 | rtc->dev = &pdev->dev; | |
120 | rtc->ec = dev_get_drvdata(pdev->dev.parent); | |
121 | platform_set_drvdata(pdev, rtc); | |
122 | ||
123 | dev = devm_rtc_allocate_device(&pdev->dev); | |
124 | if (IS_ERR(dev)) | |
125 | return PTR_ERR(dev); | |
126 | ||
127 | dev->ops = &ntxec_rtc_ops; | |
128 | dev->range_min = RTC_TIMESTAMP_BEGIN_2000; | |
129 | dev->range_max = 9025257599LL; /* 2255-12-31 23:59:59 */ | |
130 | ||
131 | return devm_rtc_register_device(dev); | |
132 | } | |
133 | ||
134 | static struct platform_driver ntxec_rtc_driver = { | |
135 | .driver = { | |
136 | .name = "ntxec-rtc", | |
137 | }, | |
138 | .probe = ntxec_rtc_probe, | |
139 | }; | |
140 | module_platform_driver(ntxec_rtc_driver); | |
141 | ||
142 | MODULE_AUTHOR("Jonathan Neuschäfer <j.neuschaefer@gmx.net>"); | |
143 | MODULE_DESCRIPTION("RTC driver for Netronix EC"); | |
144 | MODULE_LICENSE("GPL"); | |
145 | MODULE_ALIAS("platform:ntxec-rtc"); |