first commit
This commit is contained in:
commit
85794d9f2f
67
.gitignore
vendored
Normal file
67
.gitignore
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Adafruit Industries
|
||||
Copyright (c) 2025 Duncan Tourolle (Python conversion)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
include pyproject.toml
|
||||
recursive-include examples *.py
|
||||
194
README.md
Normal file
194
README.md
Normal file
@ -0,0 +1,194 @@
|
||||
# PyPCF8523 - Python Driver for PCF8523 Real Time Clock
|
||||
|
||||
A Python 3.8+ driver for the PCF8523 Real Time Clock (RTC) chip, designed for Raspberry Pi and similar Linux boards with I2C support.
|
||||
|
||||
This library is a Python conversion of [Adafruit's CircuitPython PCF8523 library](https://github.com/adafruit/Adafruit_CircuitPython_PCF8523), adapted to work with standard Python and smbus2.
|
||||
|
||||
## Features
|
||||
|
||||
- **Battery-backed RTC**: Maintains accurate time even when main power is lost
|
||||
- **Dual voltage support**: Works with 3.3V or 5V logic
|
||||
- **Alarm functionality**: Set alarms with minute precision
|
||||
- **Calibration support**: Adjust clock accuracy with offset calibration
|
||||
- **Low battery detection**: Monitor backup battery status
|
||||
- **Power management**: Configurable battery switchover modes
|
||||
- **Python 3.8+ compatible**: Modern Python with type hints
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
- Raspberry Pi (any model with I2C) or compatible Linux board
|
||||
- PCF8523 RTC module (e.g., Adafruit PCF8523 breakout)
|
||||
- I2C connection to the RTC module
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Enable I2C on your Raspberry Pi:
|
||||
```bash
|
||||
sudo raspi-config
|
||||
# Navigate to: Interface Options > I2C > Enable
|
||||
```
|
||||
|
||||
### Install the package
|
||||
|
||||
```bash
|
||||
pip install smbus2
|
||||
pip install -e /path/to/PyPCF8523
|
||||
```
|
||||
|
||||
Or install dependencies directly:
|
||||
```bash
|
||||
cd /path/to/PyPCF8523
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
import time
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
# Initialize RTC on I2C bus 1 (default for Raspberry Pi)
|
||||
rtc = PCF8523(i2c_bus=1)
|
||||
|
||||
# Set the current time
|
||||
current_time = time.localtime()
|
||||
rtc.datetime = current_time
|
||||
|
||||
# Read the current time
|
||||
dt = rtc.datetime
|
||||
print(f"Current time: {dt.tm_year}-{dt.tm_mon:02d}-{dt.tm_mday:02d} "
|
||||
f"{dt.tm_hour:02d}:{dt.tm_min:02d}:{dt.tm_sec:02d}")
|
||||
```
|
||||
|
||||
### Using Context Manager
|
||||
|
||||
```python
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
with PCF8523(1) as rtc:
|
||||
print(f"RTC Time: {rtc.datetime}")
|
||||
|
||||
# Check if power was lost
|
||||
if rtc.lost_power:
|
||||
print("Warning: RTC lost power, time may be incorrect")
|
||||
```
|
||||
|
||||
### Setting an Alarm
|
||||
|
||||
```python
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
rtc = PCF8523(1)
|
||||
|
||||
# Set alarm for 8:30 AM every day
|
||||
rtc.set_alarm(minute=30, hour=8)
|
||||
|
||||
# Enable alarm interrupt
|
||||
rtc.alarm_interrupt = True
|
||||
|
||||
# Check if alarm triggered
|
||||
if rtc.alarm_status:
|
||||
print("Alarm triggered!")
|
||||
rtc.alarm_status = False # Clear the alarm
|
||||
```
|
||||
|
||||
### Calibration
|
||||
|
||||
```python
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
rtc = PCF8523(1)
|
||||
|
||||
# Set calibration offset (-64 to +63)
|
||||
# Positive values speed up the clock, negative values slow it down
|
||||
rtc.calibration = 5
|
||||
|
||||
# Set calibration schedule
|
||||
rtc.calibration_schedule_per_minute = True # Apply every minute
|
||||
# or
|
||||
rtc.calibration_schedule_per_minute = False # Apply every 2 hours
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### PCF8523 Class
|
||||
|
||||
#### Constructor
|
||||
```python
|
||||
PCF8523(i2c_bus=1, address=0x68)
|
||||
```
|
||||
- `i2c_bus`: I2C bus number (default: 1)
|
||||
- `address`: I2C device address (default: 0x68)
|
||||
|
||||
#### Properties
|
||||
|
||||
- **`datetime`** (struct_time): Get or set the current date and time
|
||||
- **`lost_power`** (bool): True if device lost power since time was set
|
||||
- **`power_management`** (int): Battery switchover mode (0-7)
|
||||
- **`alarm_interrupt`** (bool): Enable/disable alarm interrupt output
|
||||
- **`alarm_status`** (bool): Check if alarm triggered (write False to clear)
|
||||
- **`battery_low`** (bool): True if backup battery is low (read-only)
|
||||
- **`high_capacitance`** (bool): Oscillator capacitance mode
|
||||
- **`calibration`** (int): Clock calibration offset (-64 to +63)
|
||||
- **`calibration_schedule_per_minute`** (bool): Calibration schedule mode
|
||||
|
||||
#### Methods
|
||||
|
||||
- **`set_alarm(minute=None, hour=None, day=None, weekday=None)`**: Set alarm
|
||||
- **`clear_alarm()`**: Disable and clear alarm
|
||||
- **`close()`**: Close I2C bus connection
|
||||
|
||||
## Pin Connections (Raspberry Pi)
|
||||
|
||||
| PCF8523 | Raspberry Pi |
|
||||
|---------|--------------|
|
||||
| VCC | 3.3V (Pin 1) |
|
||||
| GND | GND (Pin 6) |
|
||||
| SDA | SDA (Pin 3) |
|
||||
| SCL | SCL (Pin 5) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I2C Device Not Found
|
||||
|
||||
Check if the device is detected:
|
||||
```bash
|
||||
i2cdetect -y 1
|
||||
```
|
||||
You should see `68` in the output grid.
|
||||
|
||||
### Permission Denied
|
||||
|
||||
Add your user to the i2c group:
|
||||
```bash
|
||||
sudo usermod -a -G i2c $USER
|
||||
```
|
||||
Then log out and back in.
|
||||
|
||||
### Accuracy Issues
|
||||
|
||||
The PCF8523 can drift up to 2 seconds per day. For critical timing applications:
|
||||
1. Use the calibration feature to compensate for drift
|
||||
2. Consider periodic synchronization with NTP
|
||||
3. Monitor temperature (affects crystal oscillator)
|
||||
|
||||
## Credits
|
||||
|
||||
- **Original CircuitPython Library**: [Adafruit Industries](https://github.com/adafruit/Adafruit_CircuitPython_PCF8523)
|
||||
- **Original Authors**: Philip R. Moyer and Radomir Dopieralski
|
||||
- **Python Conversion**: Duncan Tourolle
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a conversion of Adafruit's CircuitPython library. For core functionality improvements, please contribute to the [original repository](https://github.com/adafruit/Adafruit_CircuitPython_PCF8523).
|
||||
|
||||
For Python-specific issues or improvements, feel free to submit issues or pull requests.
|
||||
82
examples/alarm_example.py
Normal file
82
examples/alarm_example.py
Normal file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Alarm example for PyPCF8523 RTC driver.
|
||||
|
||||
This example demonstrates how to use the alarm functionality:
|
||||
- Setting an alarm
|
||||
- Checking alarm status
|
||||
- Clearing an alarm
|
||||
|
||||
Hardware setup:
|
||||
- Connect PCF8523 to Raspberry Pi I2C bus 1
|
||||
- Optionally connect INT pin to a GPIO for interrupt handling
|
||||
"""
|
||||
|
||||
import time
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
|
||||
def main():
|
||||
print("PCF8523 Alarm Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Initialize the RTC
|
||||
rtc = PCF8523(i2c_bus=1)
|
||||
|
||||
# Get current time
|
||||
current = rtc.datetime
|
||||
print(f"Current time: {current.tm_hour:02d}:{current.tm_min:02d}:{current.tm_sec:02d}")
|
||||
|
||||
# Set an alarm for 2 minutes from now
|
||||
alarm_minute = (current.tm_min + 2) % 60
|
||||
alarm_hour = current.tm_hour
|
||||
if alarm_minute < current.tm_min: # Handle hour rollover
|
||||
alarm_hour = (alarm_hour + 1) % 24
|
||||
|
||||
print(f"Setting alarm for: {alarm_hour:02d}:{alarm_minute:02d}")
|
||||
rtc.set_alarm(minute=alarm_minute, hour=alarm_hour)
|
||||
|
||||
# Enable the alarm interrupt
|
||||
rtc.alarm_interrupt = True
|
||||
print("Alarm interrupt enabled")
|
||||
|
||||
# Clear any existing alarm status
|
||||
rtc.alarm_status = False
|
||||
|
||||
print("\nWaiting for alarm... (Press Ctrl+C to stop)")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Read current time
|
||||
current = rtc.datetime
|
||||
time_str = f"{current.tm_hour:02d}:{current.tm_min:02d}:{current.tm_sec:02d}"
|
||||
|
||||
# Check if alarm triggered
|
||||
if rtc.alarm_status:
|
||||
print(f"\n🔔 ALARM! Triggered at {time_str}")
|
||||
|
||||
# Clear the alarm
|
||||
rtc.alarm_status = False
|
||||
print("Alarm cleared")
|
||||
|
||||
# Optionally disable the alarm
|
||||
# rtc.clear_alarm()
|
||||
# print("Alarm disabled")
|
||||
|
||||
break
|
||||
else:
|
||||
print(f"Current time: {time_str} - Waiting for alarm...", end="\r")
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nExample stopped by user")
|
||||
finally:
|
||||
# Clean up
|
||||
rtc.clear_alarm()
|
||||
rtc.close()
|
||||
print("Alarm cleared and RTC connection closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
101
examples/calibration_example.py
Normal file
101
examples/calibration_example.py
Normal file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Calibration example for PyPCF8523 RTC driver.
|
||||
|
||||
This example demonstrates how to calibrate the RTC for better accuracy:
|
||||
- Reading calibration settings
|
||||
- Adjusting calibration offset
|
||||
- Setting calibration schedule
|
||||
|
||||
The PCF8523 can drift up to 2 seconds per day. Calibration helps
|
||||
compensate for this drift.
|
||||
|
||||
Calibration offset range: -64 to +63
|
||||
- Positive values speed up the clock
|
||||
- Negative values slow it down
|
||||
|
||||
Calibration schedule:
|
||||
- Per minute: 1 LSB = 4.069 ppm
|
||||
- Per 2 hours: 1 LSB = 4.340 ppm
|
||||
"""
|
||||
|
||||
import time
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
|
||||
def main():
|
||||
print("PCF8523 Calibration Example")
|
||||
print("=" * 50)
|
||||
|
||||
# Initialize the RTC
|
||||
rtc = PCF8523(i2c_bus=1)
|
||||
|
||||
# Read current calibration settings
|
||||
current_offset = rtc.calibration
|
||||
per_minute = rtc.calibration_schedule_per_minute
|
||||
|
||||
print(f"Current calibration offset: {current_offset}")
|
||||
print(f"Calibration schedule: {'Per minute' if per_minute else 'Per 2 hours'}")
|
||||
|
||||
# Calculate ppm (parts per million) offset
|
||||
ppm_per_lsb = 4.069 if per_minute else 4.340
|
||||
ppm_offset = current_offset * ppm_per_lsb
|
||||
print(f"Approximate offset: {ppm_offset:.2f} ppm")
|
||||
|
||||
# Example: Set calibration
|
||||
print("\n" + "-" * 50)
|
||||
print("Example calibration adjustment:")
|
||||
print("-" * 50)
|
||||
|
||||
# If your RTC is running fast (gaining time), use negative offset
|
||||
# If your RTC is running slow (losing time), use positive offset
|
||||
|
||||
# Example: Clock gains 2 seconds per day
|
||||
# 2 seconds / 86400 seconds = 23.15 ppm
|
||||
# Offset needed: 23.15 / 4.069 ≈ -6 (per minute mode)
|
||||
|
||||
new_offset = 0 # Change this based on your measurements
|
||||
print(f"\nTo set calibration offset to {new_offset}:")
|
||||
print(f" rtc.calibration = {new_offset}")
|
||||
|
||||
if new_offset != 0:
|
||||
print("\nUncomment the following lines to apply:")
|
||||
print(" # rtc.calibration_schedule_per_minute = True")
|
||||
print(f" # rtc.calibration = {new_offset}")
|
||||
print(f" # This would give approximately {new_offset * 4.069:.2f} ppm offset")
|
||||
|
||||
# Uncomment to actually apply calibration:
|
||||
# rtc.calibration_schedule_per_minute = True
|
||||
# rtc.calibration = new_offset
|
||||
|
||||
# How to measure drift:
|
||||
print("\n" + "=" * 50)
|
||||
print("How to measure and calibrate your RTC:")
|
||||
print("=" * 50)
|
||||
print("1. Set the RTC to accurate time (sync with NTP)")
|
||||
print("2. Wait 24-48 hours")
|
||||
print("3. Compare RTC time with accurate time")
|
||||
print("4. Calculate drift in seconds per day")
|
||||
print("5. Convert to ppm: (drift_seconds / 86400) * 1,000,000")
|
||||
print("6. Calculate offset: ppm / 4.069 (per minute mode)")
|
||||
print("7. Apply opposite sign: if fast use negative, if slow use positive")
|
||||
print("8. Set the calibration offset")
|
||||
print("\nExample:")
|
||||
print(" If RTC gains 2 seconds/day:")
|
||||
print(" 2 / 86400 * 1000000 = 23.15 ppm")
|
||||
print(" 23.15 / 4.069 = 5.69 ≈ 6")
|
||||
print(" Use offset = -6 (negative because it's fast)")
|
||||
|
||||
# Check battery status
|
||||
print("\n" + "-" * 50)
|
||||
if rtc.battery_low:
|
||||
print("⚠️ WARNING: Backup battery is low!")
|
||||
else:
|
||||
print("✓ Backup battery is OK")
|
||||
|
||||
# Clean up
|
||||
rtc.close()
|
||||
print("\nRTC connection closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
75
examples/simple_test.py
Normal file
75
examples/simple_test.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simple test example for PyPCF8523 RTC driver.
|
||||
|
||||
This example demonstrates basic usage of the PCF8523 RTC:
|
||||
- Reading the current time
|
||||
- Setting the time
|
||||
- Checking power loss status
|
||||
|
||||
Hardware setup:
|
||||
- Connect PCF8523 to Raspberry Pi I2C bus 1
|
||||
- VCC -> 3.3V, GND -> GND, SDA -> GPIO2, SCL -> GPIO3
|
||||
"""
|
||||
|
||||
import time
|
||||
from pypcf8523 import PCF8523
|
||||
|
||||
# Days of the week for display
|
||||
DAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
|
||||
|
||||
|
||||
def main():
|
||||
# Initialize the RTC on I2C bus 1
|
||||
print("Initializing PCF8523 RTC...")
|
||||
rtc = PCF8523(i2c_bus=1)
|
||||
|
||||
# Check if the RTC lost power
|
||||
if rtc.lost_power:
|
||||
print("WARNING: RTC lost power. Setting time to system time...")
|
||||
|
||||
# Set the RTC to the current system time
|
||||
# In a real application, you might want to sync with NTP first
|
||||
current_time = time.localtime()
|
||||
rtc.datetime = current_time
|
||||
print(f"Time set to: {time.strftime('%Y-%m-%d %H:%M:%S', current_time)}")
|
||||
else:
|
||||
print("RTC power OK")
|
||||
|
||||
# To manually set the time, uncomment and modify this section:
|
||||
# ================================================================
|
||||
# import time
|
||||
# # Set to a specific time: 2025-11-09 15:30:00 (Saturday)
|
||||
# set_time = time.struct_time((2025, 11, 9, 15, 30, 0, 5, -1, -1))
|
||||
# rtc.datetime = set_time
|
||||
# print(f"Time manually set to: {time.strftime('%Y-%m-%d %H:%M:%S', set_time)}")
|
||||
# ================================================================
|
||||
|
||||
print("\nReading time from RTC (Press Ctrl+C to stop):")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Read the current time from the RTC
|
||||
current = rtc.datetime
|
||||
|
||||
# Format and display the time
|
||||
day_name = DAYS[current.tm_wday]
|
||||
time_str = (f"{day_name} "
|
||||
f"{current.tm_year}/{current.tm_mon:02d}/{current.tm_mday:02d} "
|
||||
f"{current.tm_hour:02d}:{current.tm_min:02d}:{current.tm_sec:02d}")
|
||||
|
||||
print(time_str)
|
||||
|
||||
# Wait one second before next read
|
||||
time.sleep(1.0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nTest stopped by user")
|
||||
finally:
|
||||
# Clean up
|
||||
rtc.close()
|
||||
print("RTC connection closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
pypcf8523/__init__.py
Normal file
15
pypcf8523/__init__.py
Normal 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
pypcf8523/pcf8523.py
Normal file
295
pypcf8523/pcf8523.py
Normal 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
|
||||
42
pyproject.toml
Normal file
42
pyproject.toml
Normal file
@ -0,0 +1,42 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pypcf8523"
|
||||
version = "1.0.0"
|
||||
description = "Python 3.14+ driver for the PCF8523 Real Time Clock"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Duncan Tourolle", email = "duncan@tourolle.paris"}
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Topic :: System :: Hardware",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
keywords = ["pcf8523", "rtc", "real-time-clock", "i2c", "raspberry-pi", "hardware"]
|
||||
dependencies = [
|
||||
"smbus2>=0.4.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/adafruit/Adafruit_CircuitPython_PCF8523"
|
||||
"Original CircuitPython Library" = "https://github.com/adafruit/Adafruit_CircuitPython_PCF8523"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pypcf8523"]
|
||||
package-dir = {"pypcf8523" = "pypcf8523"}
|
||||
Loading…
x
Reference in New Issue
Block a user