# DS1307 MicroPython
This is a simple and yet complete driver for the ds1307 real-time clock chip.
A demo of the driver in use can be seen on [wokwi](https://wokwi.com/projects/394263743766562817)
## Prerequisites
To be able to run this project locally you to have installed both mpremote, a Python package and the Wokwi extension for VS Code.
It is a good practice to do the installation in a virtual environment
1. Install the [Wokwi for VS Code](https://marketplace.visualstudio.com/items?itemName=Wokwi.wokwi-vscode) extension.
2. Install the [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) tool, e.g. `pip install mpremote`.
## Tests
Tests have been written for the [rtc](rtc) package using the built-in unittest module.
All tests are located in the [tests](tests) directory inside the root folder.
You do not need any special setup to run the tests other than a Python3 environment with a version 3.10 or greater.
A mock version of the I2C device in [native microPython I2C APIs](https://docs.micropython.org/en/latest/library/machine.I2C.html) was created in the [i2c](rtc/utils/i2c.py) module.
With this we can test every functionality of the [rtc](rtc) package in the development environment of choice, and also automate testing on GitHub Actions.
To run all tests execute the following command inside the root repository:
```shell
python -m unittest discover -s tests
```
## Usage
When running the RTC module is initialized to Sunday
11:57:23 03/02/2024, however you can set the date and
time using the functions described in the
detail [rtc](rtc) package documentation that follows.
1. Clone this project and open it in VS Code.
2. To open the command palette, type CTRL + F1
3. From the command palette, select "Wokwi: Start Simulator". You may need to activate your license first.
4. While the simulator is running, open a command prompt, and type:
```python
python -m mpremote connect port:rfc2217://localhost:4000 fs cp -r rtc/ : + run main.py
```
This will connect to the simulator, upload the rtc driver and run the `main.py` file on the RPi pico board.
Note: keep the simulator tab visible while running the command, otherwise the simulator will pause and the command will timeout.
# [rtc](rtc) package documentation
In the next section a detail description of the [rtc](rtc) package is give. Each module and each class is described explicitly.
## [rtc](rtc/rtc.py) Module
### RTC Class
The RTC class is the main class offering an interface for a user of the driver.
It abstracts away many of the logics and presents only functions for setting, reading, printing, and halting datetime.
```python
class RTC(object):
def __init__(self, i2c: I2C, slave_address: Optional[int] = None, hour_mode: Optional[int] = 24) -> None:
"""
Constructs a new instance and configures the RTC to start counting from 01/01/2000 00:00:00
:param i2c: I2C object for the bus the RTC is attached to
:type i2c: I2C
:param slave_address: The I2C bus address of the RTC
:type slave_address: Optional[int]
"""
...
def set_datetime(self, datetime: Tuple[int, int, int, int, int, int, int], time_period: Optional[str] = 'AM') -> bool:
"""
Sets the current datetime in the RTC.
:param datetime: The desired datetime to be set, in the structure (HH, MM, SS, DD, MM, YYYY, DAY)
:type datetime: Tuple[int, int, int, int, int, int, int]
:param time_period: Time period, default is 'AM', can be 'AM' or 'PM'
:type time_period: Optional[str]
:return: True if datetime was successfully set, False otherwise
:rtype: bool
"""
...
def halt_time(self, status: bool = True) -> None:
"""
Halts the time.
:param status: Status to set, default is True
:type status: bool
"""
...
def get_hour_mode(self) -> int:
"""
Get the hour mode.
:return: Hour mode
:rtype: int
"""
...
def set_hour_mode(self, hour_mode: int) -> None:
"""
Set the hour mode.
:param hour_mode: Hour mode to set
:type hour_mode: int
"""
...
def get_datetime(self) -> Tuple[Tuple[int, int, int, int, int, int], Tuple[str, int]]:
"""
Fetch the current datetime stored in the RTC.
:return: Current datetime and time period
:rtype: Tuple[Tuple[int, int, int, int, int, int], Tuple[str, int]]
"""
...
def print_datetime(self) -> None:
"""Print the current datetime."""
...
```
## [read_datetime_registers](rtc/read_datetime_registers.py) module
### ReadDatetimeRegister class
The ReadDatetimeRegister aggregates all methods that are
used to read register contents relating to date and time on the ds1307 chip.
```python
class ReadDatetimeRegister(object):
def __init__(self, i2c: I2C, slave_address: int) -> None:
"""
Initializes ReadDatetimeRegister.
:param i2c: I2C object
:type i2c: I2C
:param slave_address: Slave address
:type slave_address: int
"""
...
def get_seconds(self) -> int:
"""Get seconds."""
...
def get_minutes(self) -> int:
"""Get minutes."""
...
def get_hour(self) -> int:
"""Get hour."""
...
def get_date(self) -> int:
"""Get date."""
...
def get_month(self) -> int:
"""Get month."""
...
def get_year(self) -> int:
"""Get year."""
...
def get_day(self) -> int:
"""Get day."""
...
```
## [write_datetime_registers](rtc/write_datetime_registers.py) module
### WriteDatetimeRegister class
The WriteDatetimeRegister aggregates all methods that are
used to write contents to register relating to date and time on the ds1307 chip.
```python
class WriteDatetimeRegister(object):
def __init__(self, i2c: I2C, slave_address: int) -> None:
"""
Initializes WriteDatetimeRegister.
:param i2c: I2C object
:type i2c: I2C
:param slave_address: Slave address
:type slave_address: int
"""
...
def set_hour(self, value: int, hour_mode: int, time_period: Optional[str] = 'AM') -> None:
"""Set hour."""
...
def set_minutes(self, value: int) -> None:
"""Set minutes."""
...
def set_seconds(self, value: int) -> None:
"""Set seconds."""
...
def set_date(self, value: int) -> None:
"""Set date."""
...
def set_month(self, value: int) -> None:
"""Set month."""
...
def set_day(self, value: int) -> None:
"""Set day."""
...
def set_year(self, value: int) -> None:
"""Set year."""
...
def write_datetime_attribute(self, memory_address: int, tens_bounds: Tuple[int, int], units_bounds: Tuple[int, int], value: int, modifiers: Tuple[Tuple[int, int, int], ...] = tuple()) -> None:
"""Write datetime attribute."""
...
```
## [control_register](rtc/control_register.py) module
### ControlRegister class
The ControlRegister aggregates all methods that are
used to write control configurations to the control register of the ds1307 chip.
```python
class ControlRegister(object):
def __init__(self, i2c: I2C, slave_address: int) -> None:
"""
Initializes ControlRegister.
:param i2c: I2C object
:type i2c: I2C
:param slave_address: Slave address
:type slave_address: int
"""
...
def set_SQW_frequency(self, config: Tuple[int, int, int]) -> None:
"""Set SQW frequency."""
...
def get_SQW_frequency(self) -> Tuple[int, int, int]:
"""Get SQW frequency."""
...
def set_OUT(self, status: bool = True) -> None:
"""Set OUT."""
...
def get_OUT(self) -> int:
"""Get OUT."""
...
def set_SQWE(self, status: bool = True) -> None:
"""Set SQWE."""
...
def get_SQWE(self) -> int:
"""Get SQWE."""
...
def set_RS1(self, status: bool = True) -> None:
"""Set RS1."""
...
def get_RS1(self) -> int:
"""Get RS1."""
...
def set_RS0(self, status: bool = True) -> None:
"""Set RS0."""
...
def get_RS0(self) -> int:
"""Get RS0."""
...
```
## [sqw_rate_config](rtc/sqw_rate_config.py) module
### SQWRateConfig class
The SQWRateConfig class contains configurations required to set the square wave frequencies on the ds1307 chip.
```python
class SQWRateConfigs(object):
class SQWRateConfigs(object):
"""
Constants representing Square Wave Output (SQW) rate configurations for the RTC.
These configurations are tuples with the format (RS1, RS0, SQWE),
where RS1 and RS0 control the square wave output frequency, and SQWE
enables or disables the square wave output.
"""
_1Hz = (0, 0, 1)
"""
Represents a square wave output frequency of 1Hz.
Tuple order: (RS1, RS0, SQWE)
"""
_4_096kHz = (0, 1, 1)
"""
Represents a square wave output frequency of 4.096kHz.
Tuple order: (RS1, RS0, SQWE)
"""
_8_192kHz = (1, 0, 1)
"""
Represents a square wave output frequency of 8.192kHz.
Tuple order: (RS1, RS0, SQWE)
"""
_32_768kHz = (1, 1, 1)
"""
Represents a square wave output frequency of 32.768kHz.
Tuple order: (RS1, RS0, SQWE)
"""
```
## Constants in the constants module
These constants mainly contain register addresses of the
different datetime fields on the ds1307 chip that is written to or read from by this driver.
```python
SECONDS_REGISTER_ADDR = 0x00
"""
The memory address for the seconds register in the RTC.
This register stores the current seconds value.
"""
MINUTES_REGISTER_ADDR = 0x01
"""
The memory address for the minutes register in the RTC.
This register stores the current minutes value.
"""
HOURS_REGISTER_ADDR = 0x02
"""
The memory address for the hours register in the RTC.
This register stores the current hours value.
"""
DAY_REGISTER_ADDR = 0x03
"""
The memory address for the day register in the RTC.
This register stores the current day of the week.
"""
DATE_REGISTER_ADDR = 0x04
"""
The memory address for the date register in the RTC.
This register stores the current date.
"""
MONTH_REGISTER_ADDR = 0x05
"""
The memory address for the month register in the RTC.
This register stores the current month.
"""
YEAR_REGISTER_ADDR = 0x06
"""
The memory address for the year register in the RTC.
This register stores the current year.
"""
CONTROL_REGISTER_ADDR = 0x07
"""
The memory address for the control register in the RTC.
This register controls various settings of the RTC.
"""
BASE_YEAR = 2001
"""
The base year used for calculating the year value in the RTC.
This year is added to the year value stored in the RTC to get the actual year.
"""
DAYS = {1: 'SUN', 2: 'MON', 3: 'TUE', 4: 'WED', 5: 'THUR', 6: 'FRI', 7: 'SAT'}
"""
A dictionary mapping day of the week indices to their corresponding names.
The index starts from 1 for Sunday and ends at 7 for Saturday.
"""
```
## utils sub-package
### [utils](rtc/utils/utils.py) module
#### Utils class
This class aggregates all utility functions that are used by one more modules in the package.
```python
class Utils(object):
@staticmethod
def is_control_bit_set(byte: int, bit_number: int, new_status: bool) -> bool:
"""Check if control bit is set."""
...
@staticmethod
def twelve_to_twenty_four_hours(value: int, time_period: str) -> int:
"""Convert 12-hour format to 24-hour format."""
...
@staticmethod
def twenty_four_to_twelve_hours(value: int) -> tuple[int, int]:
"""Convert 24-hour format to 12-hour format."""
...
@staticmethod
def value_from_byte(byte: int, start: int, end: int) -> int:
"""Get value from byte."""
...
@staticmethod
def write_value_to_byte(value: int, byte: int, start: int, end: int) -> int:
"""Write value to byte."""
...
```
## [i2c](rtc/utils/i2c.py) module
### I2C class
The I2C class is a mock of the I2C class in micropython,
it is written to facilitate testing of the [rtc](rtc) package in the development environment of GitHub workflows.
```python
class I2C:
def __init__(self):
"""
Initializes the I2C object.
The I2C object simulates memory registers in the actual device.
"""
def writeto_mem(self, addr: int, memaddr: int, data: bytes) -> None:
"""
Writes data to a specific memory address of a device.
:param addr: The address of the device.
:type addr: int
:param memaddr: The memory address to write to.
:type memaddr: int
:param data: The data to write.
:type data: bytes
"""
def readfrom_mem(self, addr: int, memaddr: int, nbytes: int) -> bytes:
"""
Reads data from a specific memory address of a device.
If the memory address has not been written to before, it returns bytes(nbytes) of zeros.
:param addr: The address of the device.
:type addr: int
:param memaddr: The memory address to read from.
:type memaddr: int
:param nbytes: The number of bytes to read.
:type nbytes: int
:return: The data read from the memory address.
:rtype: bytes
"""
```
## License
Licensed under the MIT license. See [LICENSE](LICENSE.txt) for details.