first commit

This commit is contained in:
2025-11-10 18:05:25 +01:00
commit 85794d9f2f
10 changed files with 897 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
"""PyPCF8523 - PCF8523 Real Time Clock Driver for Python 3.14+
This library provides a Python interface for the PCF8523 RTC chip on Raspberry Pi.
Converted from Adafruit's CircuitPython library for standard Python.
Author: Converted for Python 3.14
Original: Philip R. Moyer and Radomir Dopieralski for Adafruit Industries
License: MIT
"""
__version__ = "1.0.0"
from .pcf8523 import PCF8523
__all__ = ["PCF8523"]
+295
View File
@@ -0,0 +1,295 @@
"""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