296 lines
9.3 KiB
Python
296 lines
9.3 KiB
Python
"""PCF8523 Real Time Clock Driver
|
|
|
|
This module provides an interface to the PCF8523 RTC chip via I2C.
|
|
Designed for Raspberry Pi and similar Linux boards with I2C support.
|
|
|
|
SPDX-FileCopyrightText: 2016 Philip R. Moyer for Adafruit Industries
|
|
SPDX-FileCopyrightText: 2016 Radomir Dopieralski for Adafruit Industries
|
|
SPDX-FileCopyrightText: 2025 Converted for Python 3.14
|
|
|
|
SPDX-License-Identifier: MIT
|
|
"""
|
|
|
|
import time
|
|
from typing import Optional
|
|
|
|
try:
|
|
from smbus2 import SMBus
|
|
except ImportError:
|
|
raise ImportError("smbus2 is required. Install with: pip install smbus2")
|
|
|
|
|
|
# Power management constants
|
|
STANDARD_BATTERY_SWITCHOVER_AND_DETECTION = 0b000
|
|
BATTERY_SWITCHOVER_OFF = 0b111
|
|
|
|
# I2C address for PCF8523
|
|
PCF8523_ADDRESS = 0x68
|
|
|
|
# Register addresses
|
|
_CONTROL_1 = 0x00
|
|
_CONTROL_2 = 0x01
|
|
_CONTROL_3 = 0x02
|
|
_SECONDS = 0x03
|
|
_MINUTES = 0x04
|
|
_HOURS = 0x05
|
|
_DAYS = 0x06
|
|
_WEEKDAYS = 0x07
|
|
_MONTHS = 0x08
|
|
_YEARS = 0x09
|
|
_MINUTE_ALARM = 0x0A
|
|
_HOUR_ALARM = 0x0B
|
|
_DAY_ALARM = 0x0C
|
|
_WEEKDAY_ALARM = 0x0D
|
|
_OFFSET = 0x0E
|
|
_TMR_CLKOUT_CTRL = 0x0F
|
|
|
|
|
|
def _bcd2bin(value: int) -> int:
|
|
"""Convert binary coded decimal to binary."""
|
|
return value - 6 * (value >> 4)
|
|
|
|
|
|
def _bin2bcd(value: int) -> int:
|
|
"""Convert binary to binary coded decimal."""
|
|
return value + 6 * (value // 10)
|
|
|
|
|
|
class PCF8523:
|
|
"""Interface to the PCF8523 RTC.
|
|
|
|
Args:
|
|
i2c_bus: I2C bus number (typically 1 for Raspberry Pi)
|
|
address: I2C address of the PCF8523 (default: 0x68)
|
|
|
|
Example:
|
|
>>> rtc = PCF8523(1) # Use I2C bus 1
|
|
>>> print(rtc.datetime)
|
|
time.struct_time(tm_year=2025, tm_mon=11, tm_mday=9, ...)
|
|
"""
|
|
|
|
def __init__(self, i2c_bus: int = 1, address: int = PCF8523_ADDRESS):
|
|
"""Initialize the PCF8523 driver.
|
|
|
|
Args:
|
|
i2c_bus: I2C bus number (default: 1)
|
|
address: I2C device address (default: 0x68)
|
|
"""
|
|
self._bus = SMBus(i2c_bus)
|
|
self._address = address
|
|
|
|
def _read_byte(self, register: int) -> int:
|
|
"""Read a single byte from a register."""
|
|
return self._bus.read_byte_data(self._address, register)
|
|
|
|
def _write_byte(self, register: int, value: int) -> None:
|
|
"""Write a single byte to a register."""
|
|
self._bus.write_byte_data(self._address, register, value)
|
|
|
|
def _read_bit(self, register: int, bit: int) -> bool:
|
|
"""Read a specific bit from a register."""
|
|
value = self._read_byte(register)
|
|
return bool((value >> bit) & 1)
|
|
|
|
def _write_bit(self, register: int, bit: int, value: bool) -> None:
|
|
"""Write a specific bit in a register."""
|
|
reg_value = self._read_byte(register)
|
|
if value:
|
|
reg_value |= (1 << bit)
|
|
else:
|
|
reg_value &= ~(1 << bit)
|
|
self._write_byte(register, reg_value)
|
|
|
|
def _read_bits(self, register: int, bit: int, length: int) -> int:
|
|
"""Read multiple bits from a register."""
|
|
value = self._read_byte(register)
|
|
mask = (1 << length) - 1
|
|
return (value >> bit) & mask
|
|
|
|
def _write_bits(self, register: int, bit: int, length: int, value: int) -> None:
|
|
"""Write multiple bits to a register."""
|
|
reg_value = self._read_byte(register)
|
|
mask = ((1 << length) - 1) << bit
|
|
reg_value = (reg_value & ~mask) | ((value << bit) & mask)
|
|
self._write_byte(register, reg_value)
|
|
|
|
@property
|
|
def lost_power(self) -> bool:
|
|
"""True if the device has lost power since the time was set."""
|
|
return self._read_bit(_CONTROL_3, 7)
|
|
|
|
@lost_power.setter
|
|
def lost_power(self, value: bool) -> None:
|
|
"""Clear or set the power lost flag."""
|
|
self._write_bit(_CONTROL_3, 7, value)
|
|
|
|
@property
|
|
def power_management(self) -> int:
|
|
"""Power management state that dictates battery switchover.
|
|
|
|
Defaults to BATTERY_SWITCHOVER_OFF (0b111).
|
|
"""
|
|
return self._read_bits(_CONTROL_3, 5, 3)
|
|
|
|
@power_management.setter
|
|
def power_management(self, value: int) -> None:
|
|
"""Set power management mode."""
|
|
self._write_bits(_CONTROL_3, 5, 3, value)
|
|
|
|
@property
|
|
def datetime(self) -> time.struct_time:
|
|
"""Get the current date and time as a time.struct_time object."""
|
|
# Read all time registers at once
|
|
buffer = self._bus.read_i2c_block_data(self._address, _SECONDS, 7)
|
|
|
|
# Convert BCD to binary
|
|
seconds = _bcd2bin(buffer[0] & 0x7F)
|
|
minutes = _bcd2bin(buffer[1] & 0x7F)
|
|
hours = _bcd2bin(buffer[2] & 0x3F)
|
|
days = _bcd2bin(buffer[3] & 0x3F)
|
|
weekday = buffer[4] & 0x07
|
|
months = _bcd2bin(buffer[5] & 0x1F)
|
|
years = _bcd2bin(buffer[6]) + 2000
|
|
|
|
return time.struct_time((years, months, days, hours, minutes, seconds,
|
|
weekday, -1, -1))
|
|
|
|
@datetime.setter
|
|
def datetime(self, value: time.struct_time) -> None:
|
|
"""Set the current date and time from a time.struct_time object.
|
|
|
|
Args:
|
|
value: time.struct_time with year, month, day, hour, minute, second, weekday
|
|
"""
|
|
# Enable battery switchover and clear lost power flag
|
|
self.power_management = STANDARD_BATTERY_SWITCHOVER_AND_DETECTION
|
|
|
|
# Convert to BCD and write to registers
|
|
buffer = [
|
|
_bin2bcd(value.tm_sec) & 0x7F,
|
|
_bin2bcd(value.tm_min) & 0x7F,
|
|
_bin2bcd(value.tm_hour) & 0x3F,
|
|
_bin2bcd(value.tm_mday) & 0x3F,
|
|
value.tm_wday & 0x07,
|
|
_bin2bcd(value.tm_mon) & 0x1F,
|
|
_bin2bcd(value.tm_year - 2000) & 0xFF,
|
|
]
|
|
|
|
self._bus.write_i2c_block_data(self._address, _SECONDS, buffer)
|
|
|
|
# Clear the power lost flag
|
|
self.lost_power = False
|
|
|
|
@property
|
|
def alarm_interrupt(self) -> bool:
|
|
"""True if the interrupt pin will output when alarm is alarming."""
|
|
return self._read_bit(_CONTROL_1, 1)
|
|
|
|
@alarm_interrupt.setter
|
|
def alarm_interrupt(self, value: bool) -> None:
|
|
"""Enable or disable alarm interrupt output."""
|
|
self._write_bit(_CONTROL_1, 1, value)
|
|
|
|
@property
|
|
def alarm_status(self) -> bool:
|
|
"""True if alarm is alarming. Set to False to reset."""
|
|
return self._read_bit(_CONTROL_2, 3)
|
|
|
|
@alarm_status.setter
|
|
def alarm_status(self, value: bool) -> None:
|
|
"""Clear alarm status flag."""
|
|
self._write_bit(_CONTROL_2, 3, value)
|
|
|
|
@property
|
|
def battery_low(self) -> bool:
|
|
"""True if the battery is low and should be replaced."""
|
|
return self._read_bit(_CONTROL_3, 2)
|
|
|
|
@property
|
|
def high_capacitance(self) -> bool:
|
|
"""True for high oscillator capacitance (12.5pF), False for lower (7pF)."""
|
|
return self._read_bit(_CONTROL_1, 7)
|
|
|
|
@high_capacitance.setter
|
|
def high_capacitance(self, value: bool) -> None:
|
|
"""Set oscillator capacitance mode."""
|
|
self._write_bit(_CONTROL_1, 7, value)
|
|
|
|
@property
|
|
def calibration_schedule_per_minute(self) -> bool:
|
|
"""False to apply calibration offset every 2 hours (1 LSB = 4.340ppm);
|
|
True to offset every minute (1 LSB = 4.069ppm).
|
|
"""
|
|
return self._read_bit(_OFFSET, 7)
|
|
|
|
@calibration_schedule_per_minute.setter
|
|
def calibration_schedule_per_minute(self, value: bool) -> None:
|
|
"""Set calibration schedule mode."""
|
|
self._write_bit(_OFFSET, 7, value)
|
|
|
|
@property
|
|
def calibration(self) -> int:
|
|
"""Calibration offset to apply, from -64 to +63."""
|
|
value = self._read_bits(_OFFSET, 0, 7)
|
|
# Convert to signed integer
|
|
if value > 63:
|
|
value -= 128
|
|
return value
|
|
|
|
@calibration.setter
|
|
def calibration(self, value: int) -> None:
|
|
"""Set calibration offset (-64 to +63)."""
|
|
if not -64 <= value <= 63:
|
|
raise ValueError("Calibration must be between -64 and +63")
|
|
|
|
# Convert to unsigned for register
|
|
if value < 0:
|
|
value += 128
|
|
|
|
self._write_bits(_OFFSET, 0, 7, value)
|
|
|
|
def set_alarm(self, minute: Optional[int] = None, hour: Optional[int] = None,
|
|
day: Optional[int] = None, weekday: Optional[int] = None) -> None:
|
|
"""Set an alarm.
|
|
|
|
Args:
|
|
minute: Minute to trigger (0-59) or None to disable
|
|
hour: Hour to trigger (0-23) or None to disable
|
|
day: Day to trigger (1-31) or None to disable
|
|
weekday: Weekday to trigger (0-6) or None to disable
|
|
|
|
Note: Alarms only fire at full minutes (seconds are ignored).
|
|
"""
|
|
alarm_regs = [0x80, 0x80, 0x80, 0x80] # All disabled by default
|
|
|
|
if minute is not None:
|
|
alarm_regs[0] = _bin2bcd(minute) & 0x7F
|
|
|
|
if hour is not None:
|
|
alarm_regs[1] = _bin2bcd(hour) & 0x3F
|
|
|
|
if day is not None:
|
|
alarm_regs[2] = _bin2bcd(day) & 0x3F
|
|
|
|
if weekday is not None:
|
|
alarm_regs[3] = weekday & 0x07
|
|
|
|
self._bus.write_i2c_block_data(self._address, _MINUTE_ALARM, alarm_regs)
|
|
|
|
def clear_alarm(self) -> None:
|
|
"""Disable and clear the alarm."""
|
|
self.alarm_status = False
|
|
self.set_alarm() # Disable all alarm fields
|
|
|
|
def close(self) -> None:
|
|
"""Close the I2C bus connection."""
|
|
self._bus.close()
|
|
|
|
def __enter__(self):
|
|
"""Context manager entry."""
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Context manager exit."""
|
|
self.close()
|
|
return False
|