As part of a team effort to improve the upstream Linux kernel support for the Renesas RZ/N1 ARM processor, we had to write from scratch a new RTC driver for this SoC. The RTC subsystem API is rather straightforward but, as most kernel subsystems, the documentation about it is rather sparse. So what are the steps to write a basic RTC driver? Here are some pointers.
The registration
The core expects drivers to allocate, initialize and then register a struct rtc_device
with the device managed helpers: devm_rtc_allocate_device()
and devm_rtc_register_device()
. Between these two function calls, one will be required to provide at least a set of struct rtc_class_ops
which contains the various callbacks used to access the device from the core, as well as setting a few information about the device.
The kind of information expected is the support for various features (rtcdev->features
bitmap) as well as the maximum continuous time range supported by your RTC. If you do not know the actual date after which your device stops being reliable, you can use the rtc-range
test tool from rtc-tools
, available at https://git.kernel.org/pub/scm/linux/kernel/git/abelloni/rtc-tools.git (also available as a Buildroot package). It will check the consistency of your driver against a number of common known-to-be-failing situations.
Time handling
The most basic operations to provide are ->read_time()
and ->set_time()
. Both functions should play with a struct rtc_time
which describes time and date with members for the year, month, day of the month, hours (in 24-hour mode), minutes and seconds. The week day member is ignored by userspace and is not expected to be set properly, unless it is actively used by the RTC, for example to set alarms. There are then three popular ways of storing time in the RTC world:
- either using the binary values of each of these fields
- or using a Binary Coded Decimal (BCD) version of these fields
- or, finally, by storing a timestamp in seconds since the epoch
In BCD, each decimal digit is encoded using four bits, eg. the number 12 could either be coded by 0x0C in hexadecimal, or 0x12 in BCD, which is easier to read with a human eye.
The three representations are absolutely equivalent and you are free to convert the time from one system to another when needed:
- #1 <-> #2 conversions are done with
bcd2bin()
andbin2bcd()
(fromlinux/bcd.h
) - #1 <-> #3 conversions are done with
rtc_time64_to_tm()
andrtc_tm_to_time64()
(fromlinux/rtc.h
)
While debugging, it is likely that you will end up dumping these time structures. Note that struct rtc_time
is aligned on struct tm
, this means that the year field is the number of years since 1900 and the month field is the number of months since January, in the range 0 to 11. Anyway, dumping these fields manually is a loss of time, it is advised instead to use the dedicated RTC printk specifiers which will handle the conversion for you: %ptR
for a struct rtc_time
, %ptT
for a time64_t
.
Of course, when reading the actual time from multiple registers on the device and filling those fields, be aware that you should handle possible wrapping situations. Either the device has an internal latching mechanism for that (eg. the front-end of the registers that you must read are all frozen upon a specific action) or you need to verify this manually by, for instance, monitoring the seconds register and try another read if it changed between the beginning and the end of the retrieval.
If your device continuous time range ended before 2000 you may want to shift the default hardware range further by providing the start-year
device tree property. The core will then shift the Epoch further for you.
Finally, once done, you can verify your implementation by playing with the rtc
test tool (also from rtc-tools
).
Supporting alarms
One common RTC feature is the ability to trigger alarms at specific times. Of course it’s even better if your RTC can wake-up the system.
If the device or the way it is integrated doesn’t support alarms, this should be advertised at registration time by clearing the relevant bit (RTC_FEATURE_ALARM
, RTC_FEATURE_UPDATE_INTERRUPT
). In the other situations, it is relevant to indicate whether the RTC has a second, 2-seconds or minute resolution by setting the appropriate flag (RTC_FEATURE_ALARM_RES_2S
, RTC_FEATURE_ALARM_RES_MINUTE
). Mind when testing that querying an alarm time below this resolution will return a -ETIME
error.
When implementing the ->read_alarm()
, ->set_alarm()
and ->alarm_irq_enable()
hooks, be aware that the update and periodic alarms are now implemented in the core, using HR timers rather than with the RTC so you should focus on the regular alarm. The read/set hooks naturally allow to read and change the alarm settings. A struct rtc_wkalrm *alrm
is passed as parameter, alrm->time
is the struct rtc_time
and alrm->enabled
the state of the alarm (which must be set in ->set_alarm()
). The third hook is an asynchronous way to enable/disable the alarm IRQ.
The interrupt handler for the alarm is required to call rtc_update_irq()
to signal the core that an alarm happened, providing the RTC device, the number of alarms reported (usually one), and the RTC_IRQF
flag OR’ed with the relevant alarm flag (likely, RTC_AF
for the main alarm).
Oscillator offset compensation
RTC counters rely on very precise clock sources to deliver accurate times. To handle the situation where the source is not matching the expected precision, which is the case with most cheap oscillators on the market, some RTCs have a mechanism allowing to compensate for the frequency variation by incrementing or skipping the RTC counters at a regular interval in order to get closer to the reality.
The RTC subsystem offers a set of callbacks, ->read_offset()
and a ->set_offset()
, where a signed offset is passed in ppb (parts per billion).
As an example, if an oscillator is below its targeted frequency of 32768 Hz and is measured to run at 32767.7 Hz, we need to offset the counter by 1 - (32767.7/32768) = 9155 ppb
. If the RTC is capable of offsetting the main counter once every 20s it means that every 20s, this counter (which gets decremented at the frequency of the oscillator to produce the “seconds”) will start at a different value than 32768. Adding 1 to this counter every 20s would basically mean earning 1 / (32768 * 20) = 1526 ppb
. Our target being 9155 ppb, we must offset the counter by 9155 / 1526 = 6
every 20s to get a compensated rate of 32767.7 + (6 / 20) = 32768 Hz
.
Upstreaming status of the RZ/N1 RTC driver
The RZ/N1 RTC driver has all the features listed above and made its way into the v5.18 Linux kernel release. Hopefully this little reference sheet will encourage others to finalize and send new RTC drivers upstream!
Good thing Alexandre is the RTC maintainer 🙂