Add a roadtest for the PCF8563 RTC driver, testing many of the features including alarm and invalid time handling. Since it's the first roadtest for RTC, some helper code for handling the ABI is included. The following fixes were posted for problems identified during development of these tests: - rtc: fix use-after-free on device removal https://lore.kernel.org/lkml/20211210160951.7718-1-vincent.whitchurch@xxxxxxxx/ - rtc: pcf8563: clear RTC_FEATURE_ALARM if no irq https://lore.kernel.org/lkml/20220301131220.4011810-1-vincent.whitchurch@xxxxxxxx/ - rtc: pcf8523: fix alarm interrupt disabling https://lore.kernel.org/lkml/20211103152253.22844-1-vincent.whitchurch@xxxxxxxx/ (not the same hardware/driver, but this was the original target for test development) Signed-off-by: Vincent Whitchurch <vincent.whitchurch@xxxxxxxx> --- .../roadtest/roadtest/tests/rtc/__init__.py | 0 .../roadtest/roadtest/tests/rtc/config | 1 + .../roadtest/roadtest/tests/rtc/rtc.py | 73 ++++ .../roadtest/tests/rtc/test_pcf8563.py | 348 ++++++++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/__init__.py create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/config create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/rtc.py create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py diff --git a/tools/testing/roadtest/roadtest/tests/rtc/__init__.py b/tools/testing/roadtest/roadtest/tests/rtc/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/rtc/config b/tools/testing/roadtest/roadtest/tests/rtc/config new file mode 100644 index 000000000000..f3654f9d7c19 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/config @@ -0,0 +1 @@ +CONFIG_RTC_DRV_PCF8563=m diff --git a/tools/testing/roadtest/roadtest/tests/rtc/rtc.py b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py new file mode 100644 index 000000000000..1a2855bfc195 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +import fcntl +import struct +import typing +from pathlib import Path +from typing import Any, cast + +RTC_RD_TIME = 0x80247009 +RTC_SET_TIME = 0x4024700A +RTC_WKALM_SET = 0x4028700F +RTC_VL_READ = 0x80047013 + +RTC_IRQF = 0x80 +RTC_AF = 0x20 + +RTC_VL_DATA_INVALID = 1 << 0 + + +class RTCTime(typing.NamedTuple): + tm_sec: int + tm_min: int + tm_hour: int + tm_mday: int + tm_mon: int + tm_year: int + tm_wday: int + tm_yday: int + tm_isdst: int + + +class RTC(contextlib.AbstractContextManager): + def __init__(self, devpath: Path) -> None: + rtc = next(devpath.glob("rtc/rtc*")).name + self.filename = f"/dev/{rtc}" + + def __enter__(self) -> "RTC": + self.file = open(self.filename, "rb") + return self + + def __exit__(self, *_: Any) -> None: + self.file.close() + + def read_time(self) -> RTCTime: + s = struct.Struct("9i") + buf = bytearray(s.size) + fcntl.ioctl(self.file.fileno(), RTC_RD_TIME, buf) + return RTCTime._make(s.unpack(buf)) + + def set_time(self, tm: RTCTime) -> int: + s = struct.Struct("9i") + buf = bytearray(s.size) + s.pack_into(buf, 0, *tm) + return fcntl.ioctl(self.file.fileno(), RTC_SET_TIME, buf) + + def set_wake_alarm(self, enabled: bool, time: RTCTime) -> int: + s = struct.Struct("2B9i") + buf = bytearray(s.size) + s.pack_into(buf, 0, enabled, False, *time) + return fcntl.ioctl(self.file.fileno(), RTC_WKALM_SET, buf) + + def read(self) -> int: + s = struct.Struct("L") + buf = self.file.read(s.size) + return cast(int, s.unpack(buf)[0]) + + def read_vl(self) -> int: + s = struct.Struct("I") + buf = bytearray(s.size) + fcntl.ioctl(self.file.fileno(), RTC_VL_READ, buf) + return cast(int, s.unpack(buf)[0]) diff --git a/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py new file mode 100644 index 000000000000..a9f4c6d92762 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py @@ -0,0 +1,348 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from typing import Any, Final, Optional + +from roadtest.backend.i2c import I2CModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver + +from . import rtc + +logger = logging.getLogger(__name__) + +REG_CONTROL_STATUS_1: Final = 0x00 +REG_CONTROL_STATUS_2: Final = 0x01 +REG_VL_SECONDS: Final = 0x02 +REG_VL_MINUTES: Final = 0x03 +REG_VL_HOURS: Final = 0x04 +REG_VL_DAYS: Final = 0x05 +REG_VL_WEEKDAYS: Final = 0x06 +REG_VL_CENTURY_MONTHS: Final = 0x07 +REG_VL_YEARS: Final = 0x08 +REG_VL_MINUTE_ALARM: Final = 0x09 +REG_VL_HOUR_ALARM: Final = 0x0A +REG_VL_DAY_ALARM: Final = 0x0B +REG_VL_WEEKDAY_ALARM: Final = 0x0C +REG_CLKOUT_CONTROL: Final = 0x0D +REG_TIMER_CONTROL: Final = 0x0E +REG_TIMER: Final = 0x0F + +REG_CONTROL_STATUS_2_AIE: Final = 1 << 1 +REG_CONTROL_STATUS_2_AF: Final = 1 << 3 + +REG_VL_CENTURY_MONTHS_C: Final = 1 << 7 + +REG_VL_ALARM_AE: Final = 1 << 7 + + +class PCF8563(I2CModel): + def __init__(self, int: Optional[int] = None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.int = int + self._set_int(False) + + self.reg_addr = 0 + # Reset values from Table 27 in datasheet, with X and - bits set to 0 + self.regs = { + REG_CONTROL_STATUS_1: 0b_0000_1000, + REG_CONTROL_STATUS_2: 0b_0000_0000, + REG_VL_SECONDS: 0b_1000_0000, + REG_VL_MINUTES: 0b_0000_0000, + REG_VL_HOURS: 0b_0000_0000, + REG_VL_DAYS: 0b_0000_0000, + REG_VL_WEEKDAYS: 0b_0000_0000, + REG_VL_CENTURY_MONTHS: 0b_0000_0000, + REG_VL_YEARS: 0b_0000_0000, + REG_VL_MINUTE_ALARM: 0b_1000_0000, + REG_VL_HOUR_ALARM: 0b_1000_0000, + REG_VL_DAY_ALARM: 0b_1000_0000, + REG_VL_WEEKDAY_ALARM: 0b_1000_0000, + REG_CLKOUT_CONTROL: 0b_1000_0000, + REG_TIMER_CONTROL: 0b_0000_0011, + REG_TIMER: 0b_0000_0000, + } + + def _set_int(self, active: int) -> None: + # Active-low + self.backend.gpio.set(self.int, not active) + + def _check_alarm(self, addr: int) -> None: + alarmregs = [ + REG_VL_MINUTE_ALARM, + REG_VL_HOUR_ALARM, + REG_VL_DAY_ALARM, + REG_VL_WEEKDAY_ALARM, + ] + timeregs = [ + REG_VL_MINUTES, + REG_VL_HOURS, + REG_VL_DAYS, + REG_VL_WEEKDAYS, + ] + + if addr not in alarmregs + timeregs: + return + + af = all( + self.regs[a] == self.regs[b] + for a, b in zip(alarmregs, timeregs) + if not self.regs[a] & REG_VL_ALARM_AE + ) + self.reg_write(REG_CONTROL_STATUS_2, self.regs[REG_CONTROL_STATUS_2] | af << 3) + + def _update_irq(self) -> None: + aie = self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AIE + af = self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AF + + logger.debug(f"{aie=} {af=}") + self._set_int(aie and af) + + def reg_read(self, addr: int) -> int: + val = self.regs[addr] + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] = val + logger.debug(f"{addr=:x} {val=:x}") + self._check_alarm(addr) + self._update_irq() + + def read(self, len: int) -> bytes: + data = bytearray(len) + + for i in range(len): + data[i] = self.reg_read(self.reg_addr) + self.reg_addr = self.reg_addr + 1 + + return bytes(data) + + def write(self, data: bytes) -> None: + self.reg_addr = data[0] + + for i, byte in enumerate(data[1:]): + addr = self.reg_addr + i + self.backend.mock.reg_write(addr, byte) + self.reg_write(addr, byte) + + +class TestPCF8563(UMLTestCase): + dts = DtFragment( + src=""" +#include <dt-bindings/interrupt-controller/irq.h> + +&i2c { + rtc@$addr$ { + compatible = "nxp,pcf8563"; + reg = <0x$addr$>; + }; + + rtc@$irqaddr$ { + compatible = "nxp,pcf8563"; + reg = <0x$irqaddr$>; + interrupt-parent = <&gpio>; + interrupts = <$gpio$ IRQ_TYPE_LEVEL_LOW>; + }; +}; + """, + variables={ + "addr": DtVar.I2C_ADDR, + "irqaddr": DtVar.I2C_ADDR, + "gpio": DtVar.GPIO_PIN, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("rtc-pcf8563") + + @classmethod + def tearDownClass(cls) -> None: + # Can't rmmod since alarmtimer holds permanent reference + pass + + def setUp(self) -> None: + self.driver = I2CDriver("rtc-pcf8563") + self.hw = Hardware("i2c") + self.hw.load_model(PCF8563, int=self.dts["gpio"]) + + def tearDown(self) -> None: + self.hw.close() + + def test_read_time_invalid(self) -> None: + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID) + + with self.assertRaises(OSError) as cm: + rtcdev.read_time() + self.assertEqual(cm.exception.errno, errno.EINVAL) + + def test_no_alarm_support(self) -> None: + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + # Make sure the times are valid so we don't get -EINVAL due to + # that. + tm = rtc.RTCTime( + tm_sec=10, + tm_min=1, + tm_hour=1, + tm_mday=1, + tm_mon=0, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ) + rtcdev.set_time(tm) + + alarmtm = tm._replace(tm_sec=0, tm_min=2) + with self.assertRaises(OSError) as cm: + rtcdev.set_wake_alarm(True, alarmtm) + self.assertEqual(cm.exception.errno, errno.EINVAL) + + def test_alarm(self) -> None: + addr = self.dts["irqaddr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm = rtc.RTCTime( + tm_sec=10, + tm_min=1, + tm_hour=1, + tm_mday=1, + tm_mon=0, + tm_year=121, + tm_wday=5, + tm_yday=0, + tm_isdst=0, + ) + rtcdev.set_time(tm) + + alarmtm = tm._replace(tm_sec=0, tm_min=2) + rtcdev.set_wake_alarm(True, alarmtm) + + mock = self.hw.update_mock() + mock.assert_last_reg_write(self, REG_VL_MINUTE_ALARM, 0x02) + mock.assert_last_reg_write(self, REG_VL_HOUR_ALARM, 0x01) + mock.assert_last_reg_write(self, REG_VL_DAY_ALARM, 0x01) + mock.assert_last_reg_write(self, REG_VL_WEEKDAY_ALARM, 5) + mock.assert_last_reg_write( + self, REG_CONTROL_STATUS_2, REG_CONTROL_STATUS_2_AIE + ) + mock.reset_mock() + + self.hw.reg_write(REG_VL_MINUTES, 0x02) + self.hw.kick() + + # This waits for the interrupt + self.assertEqual(rtcdev.read() & 0xFF, rtc.RTC_IRQF | rtc.RTC_AF) + + alarmtm = tm._replace(tm_sec=0, tm_min=3) + rtcdev.set_wake_alarm(False, alarmtm) + + mock = self.hw.update_mock() + mock.assert_last_reg_write(self, REG_CONTROL_STATUS_2, 0) + + def test_read_time_valid(self) -> None: + self.hw.reg_write(REG_VL_SECONDS, 0x37) + self.hw.reg_write(REG_VL_MINUTES, 0x10) + self.hw.reg_write(REG_VL_HOURS, 0x11) + self.hw.reg_write(REG_VL_DAYS, 0x25) + self.hw.reg_write(REG_VL_WEEKDAYS, 0x00) + self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12) + self.hw.reg_write(REG_VL_YEARS, 0x21) + + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm = rtcdev.read_time() + self.assertEqual( + tm, + rtc.RTCTime( + tm_sec=37, + tm_min=10, + tm_hour=11, + tm_mday=25, + tm_mon=11, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ), + ) + + def test_set_time_after_invalid(self) -> None: + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID) + + tm = rtc.RTCTime( + tm_sec=37, + tm_min=10, + tm_hour=11, + tm_mday=25, + tm_mon=11, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ) + + rtcdev.set_time(tm) + tm2 = rtcdev.read_time() + self.assertEqual(tm, tm2) + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37) + mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10) + mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11) + mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25) + mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00) + # The driver uses the wrong polarity of the Century bit + # if the time was invalid. This probably doesn't matter(?). + mock.assert_reg_write_once(self, REG_VL_CENTURY_MONTHS, 0 << 7 | 0x12) + mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21) + + self.assertEqual(rtcdev.read_vl(), 0) + + def test_set_time_after_valid(self) -> None: + self.hw.reg_write(REG_VL_SECONDS, 0x37) + self.hw.reg_write(REG_VL_MINUTES, 0x10) + self.hw.reg_write(REG_VL_HOURS, 0x11) + self.hw.reg_write(REG_VL_DAYS, 0x25) + self.hw.reg_write(REG_VL_WEEKDAYS, 0x00) + self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12) + self.hw.reg_write(REG_VL_YEARS, 0x21) + + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm = rtc.RTCTime( + tm_sec=37, + tm_min=10, + tm_hour=11, + tm_mday=25, + tm_mon=11, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ) + + rtcdev.set_time(tm) + tm2 = rtcdev.read_time() + self.assertEqual(tm, tm2) + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37) + mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10) + mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11) + mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25) + mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00) + mock.assert_reg_write_once( + self, REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12 + ) + mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21) -- 2.34.1