MicroPython 嵌入式项目实战:Pico DS1302 RTC 数字闹钟实现指南

1.使用 DS1302 RTC 时钟实现简易数字闹钟

以下代码可以在我们提供的资料包中 elegance-devkit v1\Demo\51 RTC_DS1302 文件夹找到。

1.1 自定义 DS1302 类的实现

在以下代码中,我们使用自定义 DS1302 类实现了获取和设置时间、日期以及使用内置的 RAM 存储自定义数据等功能,示例代码如下:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/10/3 下午2:41   
# @Author  : 李清水            
# @File    : ds1302.py       
# @Description : 自定义DS1302类控制芯片

# ======================================== 导入相关模块 ========================================

# 导入硬件相关模块
from machine import Pin
# 导入MicroPython相关模块
from micropython import const

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

# 自定义DS1302芯片控制类
class DS1302:
    """
​    DS1302类,用于通过三线接口(CLK、DIO、CS)操作DS1302实时时钟芯片。

​    该类封装了对DS1302芯片的通信,提供了读取和设置时间、日期、星期等功能。
​    支持启动和停止时钟,以及读写芯片内部的RAM寄存器。

​    Attributes:
​        clk (Pin): 时钟引脚,用于产生时钟信号。
​        dio (Pin): 数据引脚,用于读写数据。
​        cs  (Pin): 片选引脚,用于使能芯片通信。

​    Methods:
​        __init__(self, clk: Pin, dio: Pin, cs: Pin) -> None:
​            初始化DS1302类实例。

​        _dec2hex(self, dat: int) -> int:
​            将十进制数转换为十六进制数。

​        _hex2dec(self, dat: int) -> int:
​            将十六进制数转换为十进制数。

​        _write_byte(self, dat: int) -> None:
​            写入一个字节到DS1302。

​        _read_byte(self) -> int:
​            从DS1302读取一个字节。

​        _get_reg(self, reg: int) -> int:
​            获取指定寄存器的值。

​        _set_reg(self, reg: int, dat: int) -> None:
​            设置指定寄存器的值。

​        _wr(self, reg: int, dat: int) -> None:
​            写入数据到寄存器,并设置写保护。

​        start(self) -> None:
​            启动时钟。

​        stop(self) -> None:
​            停止时钟。

​        second(self) -> int:
​            获取当前秒数。

​        second(self, value: int) -> None:
​            设置秒数。

​        minute(self) -> int:
​            获取当前分钟数。

​        minute(self, value: int) -> None:
​            设置分钟数。

​        hour(self) -> int:
​            获取当前小时数。

​        hour(self, value: int) -> None:
​            设置小时数。

​        weekday(self) -> int:
​            获取当前星期数。

​        weekday(self, value: int) -> None:
​            设置星期数。

​        day(self) -> int:
​            获取当前日期。

​        day(self, value: int) -> None:
​            设置日期。

​        month(self) -> int:
​            获取当前月份。

​        month(self, value: int) -> None:
​            设置月份。

​        year(self) -> int:
​            获取当前年份。

​        year(self, value: int) -> None:
​            设置年份。

​        date_time(self, dat: list[int] | None = None) -> list[int] | None:
​            获取或设置完整的日期和时间。

​        ram(self, reg: int, dat: int | None = None) -> int | None:
​            读取或写入RAM寄存器的值。
​    """
​    ​# 类变量:定义DS1302芯片寄存器地址
    DS1302_REG_SECOND = const(0x80)  # 秒寄存器
    DS1302_REG_MINUTE = const(0x82)  # 分寄存器
    DS1302_REG_HOUR = const(0x84)    # 时寄存器
    DS1302_REG_DAY = const(0x86)     # 日寄存器
    DS1302_REG_MONTH = const(0x88)   # 月寄存器
    DS1302_REG_WEEKDAY = const(0x8A) # 星期寄存器
    DS1302_REG_YEAR = const(0x8C)    # 年寄存器
    DS1302_REG_WP = const(0x8E)      # 写保护寄存器
    DS1302_REG_CTRL = const(0x90)    # 控制寄存器
    DS1302_REG_RAM = const(0xC0)     # RAM寄存器

    def __init__(self, clk: Pin, dio: Pin, cs: Pin) -> None:
        """
​        初始化DS1302类。

​        Args:
​            clk (Pin): 时钟引脚。
​            dio (Pin): 数据引脚。
​            cs  (Pin): 片选引脚。

​        Returns:
​            None
​        """

​        ​self.clk = clk
        self.dio = dio
        self.cs = cs

        # 初始化时钟引脚为输出
        self.clk.init(Pin.OUT)
        # 初始化片选引脚为输出
        self.cs.init(Pin.OUT)

    def _dec2hex(self, dat: int) -> int:
        """
​        将十进制转换为十六进制。

​        Args:
​            dat (int): 要转换的十进制数。

​        Returns:
​            int: 转换后的十六进制数。
​        """
​        ​return (dat // 10) * 16 + (dat % 10)

    def _hex2dec(self, dat: int) -> int:
        """
​        将十六进制转换为十进制。

​        Args:
​            dat (int): 要转换的十六进制数。

​        Returns:
​            int: 转换后的十进制数。
​        """
​        ​return (dat // 16) * 10 + (dat % 16)

    def _write_byte(self, dat: int) -> None:
        """
​        写入一个字节到DS1302。

​        Args:
​            dat (int): 要写入的字节。

​        Returns:
​            None
​        """
​        ​# 设置数据引脚为输出
        self.dio.init(Pin.OUT)

        # 写入8位数据
        for i in range(8):
            # 写入每一位,上升沿写入数据
            self.dio.value((dat >> i) & 1)
            # 产生时钟信号
            self.clk.value(1)
            self.clk.value(0)

    def _read_byte(self) -> int:
        """
​        从DS1302读取一个字节。

​        Returns:
​            int: 读取到的字节。
​        """
​        ​# 初始化临时变量
        d = 0

        # 设置数据引脚为输入
        self.dio.init(Pin.IN)

        # 依次读取八位数据
        for i in range(8):
            # 读取每一位,下降沿读取数据
            d = d | (self.dio.value() << i)
            # 产生时钟信号
            self.clk.value(1)
            self.clk.value(0)
        return d

    def _get_reg(self, reg: int) -> int:
        """
​        获取指定寄存器的值。

​        Args:
​            reg (int): 寄存器地址。

​        Returns:
​            int: 寄存器的值。
​        """
​        ​# 启动通信
        self.cs.value(1)
        # 写入寄存器地址
        self._write_byte(reg)
        # 读取数据
        t = self._read_byte()
        # 结束通信
        self.cs.value(0)
        return t

    def _set_reg(self, reg: int, dat: int) -> None:
        """
​        设置指定寄存器的值。

​        Args:
​            reg (int): 寄存器地址。
​            dat (int): 要写入的数据。

​        Returns:
​            None
​        """
​        ​# 启动通信
        self.cs.value(1)
        # 写入寄存器地址
        self._write_byte(reg)
        # 写入数据
        self._write_byte(dat)
        # 结束通信
        self.cs.value(0)

    def _wr(self, reg: int, dat: int) -> None:
        """
​        写入数据到寄存器,并设置写保护。

​        Args:
​            reg (int): 寄存器地址。
​            dat (int): 要写入的数据。

​        Returns:
​            None
​        """
​        ​# 禁用写保护
        self._set_reg(DS1302.DS1302_REG_WP, 0)
        # 写入数据
        self._set_reg(reg, dat)
        # 启用写保护
        self._set_reg(DS1302.DS1302_REG_WP, 0x80)

    def start(self) -> None:
        """
​        启动时钟。
​        Args:
​            None

​        Returns:
​            None
​        """
​        ​# 读取秒寄存器
        t = self._get_reg(DS1302.DS1302_REG_SECOND + 1)
        # 取消停止位
        self._wr(DS1302.DS1302_REG_SECOND, t & 0x7f)

    def stop(self) -> None:
        """
​        停止时钟。
​        Args:
​            None

​        Returns:
​            None
​        """
​        ​# 读取秒寄存器
        t = self._get_reg(DS1302.DS1302_REG_SECOND + 1)
        # 设置停止位
        self._wr(DS1302.DS1302_REG_SECOND, t | 0x80)

    @property
    def second(self) -> int:
        """
​        获取秒。
​        Args:
​            None

​        Returns:
​            int: 当前秒。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_SECOND + 1)) % 60

    @second.setter
    def second(self, value: int) -> None:
        """
​        设置秒。

​        Args:
​            value (int): 要设置的秒数。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_SECOND, self._dec2hex(value % 60))

    @property
    def minute(self) -> int:
        """
​        获取分。
​        Args:
​            None

​        Returns:
​            int: 当前分。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_MINUTE + 1))

    @minute.setter
    def minute(self, value: int) -> None:
        """
​        设置分。

​        Args:
​            value (int): 要设置的分钟数。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_MINUTE, self._dec2hex(value % 60))

    @property
    def hour(self) -> int:
        """
​        获取时。
​        Args:
​            None

​        Returns:
​            int: 当前时。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_HOUR + 1))

    @hour.setter
    def hour(self, value: int) -> None:
        """
​        设置时。

​        Args:
​            value (int): 要设置的小时数。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_HOUR, self._dec2hex(value % 24))

    @property
    def weekday(self) -> int:
        """
​        获取星期。
​        Args:
​            None

​        Returns:
​            int: 当前星期。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_WEEKDAY + 1))

    @weekday.setter
    def weekday(self, value: int) -> None:
        """
​        设置星期。

​        Args:
​            value (int): 要设置的星期数。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_WEEKDAY, self._dec2hex(value % 8))

    @property
    def day(self) -> int:
        """
​        获取日。
​        Args:
​            None

​        Returns:
​            int: 当前日。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_DAY + 1))

    @day.setter
    def day(self, value: int) -> None:
        """
​        设置日。

​        Args:
​            value (int): 要设置的日。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_DAY, self._dec2hex(value % 32))

    @property
    def month(self) -> int:
        """
​        获取月。
​        Args:
​            None

​        Returns:
​            int: 当前月。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_MONTH + 1))

    @month.setter
    def month(self, value: int) -> None:
        """
​        设置月。

​        Args:
​            value (int): 要设置的月。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_MONTH, self._dec2hex(value % 13))

    @property
    def year(self) -> int:
        """
​        获取年。
​        Args:
​            None

​        Returns:
​            int: 当前年。
​        """
​        ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_YEAR + 1)) + 2000

    @year.setter
    def year(self, value: int) -> None:
        """
​        设置年。

​        Args:
​            value (int): 要设置的年。

​        Returns:
​            None
​        """
​        ​self._wr(DS1302.DS1302_REG_YEAR, self._dec2hex(value % 100))

    def date_time(self, dat: list[int] | None = None) -> list[int] | None:
        """
​        获取或设置日期和时间。

​        Args:
​            dat (list[int] | None): 日期和时间数据 [年, 月, 日, 星期, 时, 分, 秒]。如果为 None,则获取当前时间。

​        Returns:
​            list[int] | None: 如果 dat 为 None,返回 [年, 月, 日, 星期, 时, 分, 秒];否则返回 None。
​        """
​        ​if dat is None:
            return [self.year, self.month, self.day, self.weekday, self.hour, self.minute, self.second]
        else:
            self.year = dat[0]
            self.month = dat[1]
            self.day = dat[2]
            self.weekday = dat[3]
            self.hour = dat[4]
            self.minute = dat[5]
            self.second = dat[6]

    def ram(self, reg: int, dat: int | None = None) -> int | None:
        """
​        读取或写入RAM寄存器。

​        Args:
​            reg (int): 寄存器地址。
​            dat (int | None): 要写入的数据。如果为 None,则读取数据。

​        Returns:
​            int | None: 如果 dat 为 None,返回读取的数据;否则返回 None。
​        """
​        ​if dat is None:
            return self._get_reg(DS1302.DS1302_REG_RAM + 1 + (reg % 31) * 2)
        else:
            self._wr(DS1302.DS1302_REG_RAM + (reg % 31) * 2, dat)

# ======================================== 初始化配置 ==========================================

# ========================================  主程序  ============================================

首先,我们定义了一系列常量,表示 DS1302 芯片的寄存器地址:

# 自定义DS1302芯片控制类
class DS1302:

    # 类变量:定义DS1302芯片寄存器地址
    DS1302_REG_SECOND = const(0x80)  # 秒寄存器
    DS1302_REG_MINUTE = const(0x82)  # 分寄存器
    DS1302_REG_HOUR = const(0x84)    # 时寄存器
    DS1302_REG_DAY = const(0x86)     # 日寄存器
    DS1302_REG_MONTH = const(0x88)   # 月寄存器
    DS1302_REG_WEEKDAY = const(0x8A) # 星期寄存器
    DS1302_REG_YEAR = const(0x8C)    # 年寄存器
    DS1302_REG_WP = const(0x8E)      # 写保护寄存器
    DS1302_REG_CTRL = const(0x90)    # 控制寄存器
    DS1302_REG_RAM = const(0xC0)     # RAM寄存器

在初始化方法中设置时钟引脚(clk)、数据引脚(dio)和片选引脚(cs)。

实现 DS1302 操作的底层方法为:

  • 单字节写入 _write_byte​方法:​首先将 DIO 引脚设置为输出模式,通过循环,依次写入每一位数据,并在发送每一位数据时产生一个时钟脉冲(高电平然后低电平)。
  • 单字节读取 _read_byte​方法:​将 DIO 引脚设置为输入模式,通过循环,依次读取每一位数据,并将读取到的完整字节返回。

寄存器读写相关方法就是通过调用 _write_byte()_read_byte() 方法进行实现的:

b27be548-21ec-40f0-bf1b-95ea9f21a5a6.png

  • 寄存器值获取 ​_get_reg​方法:​首先将 CS 引脚置高,写入目标寄存器地址并读取 DS1302 芯片返回的数据,然后将 CS 引脚置低结束通信会话。
  • 寄存器值写入 _set_reg​方法:​首先将 CS 引脚置高,写入目标寄存器地址并写入需要写入的数据,然后将 CS 引脚置低结束通信会话。
  • WP 寄存器写入 _wr​方法:​该方法向 WP 写保护寄存器写入数据,首先将写保护寄存器 WP 设置为 0 来禁用写保护,然后向目标寄存器写入数据,最后将写保护寄存器 WP 设置为 0x80 以启用写保护

这些方法封装了低级的寄存器读写操作,在其他方法中通过调用这些方法即可实现控制 DS1302 芯片,例如:

  • 启动时钟 ​start​方法:​首先读取秒寄存器的当前值,然后清除最高位(STOP 位),使 DS1302 开始计时,调用关系如下图所示:

    1.png

  • 停止时钟 ​stop​方法:​首先读取秒寄存器的当前值,然后设置最高位(STOP 位),使 DS1302 停止计时,调用关系如下图所示:

2.png

在对时间数据的获取和设置的 secondminutehourweekdaydaymonthyear 方法中主要是通过读写对应寄存器实现的:

@property
def second(self) -> int:
    """
​    获取秒。
​    Args:
​        None

​    Returns:
​        int: 当前秒。
​    """
​    ​return self._hex2dec(self._get_reg(DS1302.DS1302_REG_SECOND + 1)) % 60

@second.setter
def second(self, value: int) -> None:
    """
​    设置秒。

​    Args:
​        value (int): 要设置的秒数。

​    Returns:
​        None
​    """
​    ​self._wr(DS1302.DS1302_REG_SECOND, self._dec2hex(value % 60))
  • 读取时间​:通过 _get_reg 方法读取对应寄存器的值,并使用 _hex2dec 将十六进制数据转换为十进制时间数据;
  • 写入时间​:通过 _wr 方法将十进制数据转换为十六进制后写入对应寄存器,从而设置时间。

并且,我们通过使用 Python 的 @property 装饰器,还为 DS1302 的各个时间和日期组件(秒、分、时、星期、日、月、年)提供了获取和设置的方法,我们将类的内部数据(如 DS1302 的寄存器值)封装起来,避免外部代码直接访问和修改这些内部数据;并且,外部代码能够以属性访问的方式调用方法,使得代码更加直观和易读,类似于访问普通的对象属性而非调用方法。

为方便用户使用,我们还有一个用于获取或设置完整日期和时间的 date_time 方法,如果不传参数,返回当前完整的日期和时间列表 [年, 月, 日, 星期, 时, 分, 秒],反之,如果传入一个包含日期和时间的列表,依次设置 DS1302 各个时间组件:

def date_time(self, dat: list[int] | None = None) -> list[int] | None:
    """
​    获取或设置日期和时间。

​    Args:
​        dat (list[int] | None): 日期和时间数据 [年, 月, 日, 星期, 时, 分, 秒]。如果为 None,则获取当前时间。

​    Returns:
​        list[int] | None: 如果 dat 为 None,返回 [年, 月, 日, 星期, 时, 分, 秒];否则返回 None。
​    """
​    ​if dat is None:
        return [self.year, self.month, self.day, self.weekday, self.hour, self.minute, self.second]
    else:
        self.year = dat[0]
        self.month = dat[1]
        self.day = dat[2]
        self.weekday = dat[3]
        self.hour = dat[4]
        self.minute = dat[5]
        self.second = dat[6]

在对 DS1302 芯片的内部 RAM 访问中,我们提供了读写 RAM 寄存器的 ram 方法:

  • 读取 ​RAM​: 如果不传入 dat 参数,读取指定 RAM 地址的数据
  • 写入 ​RAM​: 如果传入 dat 参数,向指定 RAM 地址写入数据
def ram(self, reg: int, dat: int | None = None) -> int | None:
    """
​    读取或写入RAM寄存器。

​    Args:
​        reg (int): 寄存器地址。
​        dat (int | None): 要写入的数据。如果为 None,则读取数据。

​    Returns:
​        int | None: 如果 dat 为 None,返回读取的数据;否则返回 None。
​    """
​    ​if dat is None:
        return self._get_reg(DS1302.DS1302_REG_RAM + 1 + (reg % 31) * 2)
    else:
        self._wr(DS1302.DS1302_REG_RAM + (reg % 31) * 2, dat)

这里,内部 RAM 起始写地址为 0xC0,而起始读地址为 0xC1 (reg % 31) * 2 确保寄存器索引在 0 到 30 范围内,并根据寄存器索引计算地址偏移量。

1.2 实验现象

在下面实验中,我们需要将风雅一号板-环境与存储采集板插入到风雅一号板-通用兼容扩展板上面,并打开风雅一号板-环境与存储采集板SWITCH2 拨码开关上 CLKDIOCS 选项:

bc7a590fe7c9906dab33dc0e80dded88.jpg

4.png

然后将风雅一号板-通用兼容扩展板插入到风雅一号板-OLED显示与交互扩展板上,同时打开风雅一号板-OLED显示与交互扩展板​ ​SWITCH1 拨码开关的 I2C_SDAI2C_SCLBUZZER 选项:

5.jfif

除此之外,请在 OLED_ADDR 拨码开关上选择一个地址,本示例将拨码置于 0x3C,因此 OLED 屏幕的 I²C 地址为 0x3C

6.png

这里,各个模块与树莓派 Pico 连接如下所示:

7.png

在以下代码中,我们使用自定义 DS1302 类,通过 DS1302 实时时钟模块和 SSD1306 OLED 显示屏,实现一个具有闹钟功能的时钟系统,系统能够显示当前的日期和时间,并在预设的闹钟时间到达时,通过蜂鸣器发出提示。

示例代码如下:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/10/3 下午2:41   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 外置RTC类实验,使用DS1302实现闹钟

# ======================================== 导入相关模块 ========================================

# 导入硬件相关模块
from machine import Pin, I2C, Timer, PWM, RTC
# 导入时间相关模块
import time
# 导入外部RTC实时时钟模块
from ds1302 import DS1302
# 从SSD1306模块中导入SSD1306_I2C类
from SSD1306 import SSD1306_I2C

# ======================================== 全局变量 ============================================

# OLED屏幕地址
OLED_ADDRESS = 0

# ======================================== 功能函数 ============================================

def display_time(timer: Timer) -> None:
    """
​    定时器回调函数,用于在OLED屏幕上显示当前时间。

​    该函数通过定时器周期性调用,从DS1302实时时钟芯片获取当前时间,
​    并在OLED屏幕上居中显示日期和时间。同时,检查当前时间是否与设置的闹钟匹配,
​    如果匹配则触发闹钟,否则停止闹钟蜂鸣器。

​    Args:
​        timer (Timer): Timer对象,用于周期性调用该函数。

​    Returns:
​        None
​    """
​    ​# 声明全局变量
    global ds1302, oled, alarm_clock

    # 获取当前时间
    current_time = ds1302.date_time()

    # 清空标题区域
    oled.fill_rect(0, 0, 128, 16, 0)

    # 显示标题
    title = "RTC Clock"
    title_width = len(title) * 8
    title_x = (128 - title_width) // 2
    oled.text(title, title_x, 0)

    # 格式化日期字符串
    date_str = "{:04}-{:02}-{:02}".format(current_time[0], current_time[1], current_time[2])
    # 计算位置以居中显示日期
    date_width = len(date_str) * 8
    date_x = (128 - date_width) // 2

    # 格式化时间字符串
    time_str = "{:02}:{:02}:{:02}".format(current_time[4], current_time[5], current_time[6])
    # 计算位置以居中显示时间
    time_width = len(time_str) * 8
    time_x = (128 - time_width) // 2

    # 清空日期和时间区域
    oled.fill_rect(0, 16, 128, 32, 0)

    # 显示日期和时间
    oled.text(date_str, date_x, 16)
    oled.text(time_str, time_x, 32)

    # 检查闹钟
    if alarm_clock.check_alarms(current_time):
        # 停止定时器
        timer.deinit()
        # 重新初始化定时器
        timer.init(period=100, mode=Timer.PERIODIC, callback=display_time)
    else:
        # 停止闹钟蜂鸣器
        alarm_clock.stop_alarm()
        # 刷新屏幕
        oled.show()

# ======================================== 自定义类 ============================================

# 自定义闹钟类
class AlarmClock:
    """
​    AlarmClock类,用于管理闹钟功能,支持设置、删除和触发闹钟。

​    该类通过OLED显示屏显示闹钟状态,并通过蜂鸣器发出闹钟提示音。
​    支持设置多个闹钟,并在指定时间触发闹钟。

​    Attributes:
​        oled (SSD1306_I2C): OLED实例,用于显示闹钟状态。
​        buzzer (PWM): 蜂鸣器控制引脚,用于发出提示音。
​        alarms (list): 存储闹钟时间的列表,格式为[(小时, 分钟), ...]。

​    Methods:
​        __init__(self, oled: SSD1306_I2C, buzzer_pin: Pin) -> None:
​            初始化AlarmClock类实例。

​        set_alarm(self, hours: int, minutes: int) -> None:
​            设置闹钟。

​        delete_alarm(self, hours: int, minutes: int) -> None:
​            删除指定的闹钟。

​        check_alarms(self, current_time: tuple[int, int, int]) -> None:
​            检查当前时间是否与设定的闹钟时间匹配。

​        trigger_alarm(self) -> None:
​            触发闹钟,打开蜂鸣器。

​        stop_alarm(self) -> None:
​            停止闹钟,关闭蜂鸣器。
​    """
​    ​def __init__(self, oled: SSD1306_I2C, buzzer_pin: Pin) -> None:
        """
​        初始化闹钟类。

​        Args:
​            oled (SSD1306_I2C): OLED实例,用于显示闹钟状态。
​            buzzer_pin (Pin): 蜂鸣器控制引脚。
​        """
​        ​# 保存OLED实例
        self.oled = oled
        # 初始化蜂鸣器引脚为PWM输出,占空比为0
        self.buzzer = PWM(buzzer_pin, freq=1000, duty_u16=0)
        # 存储闹钟时间的列表
        self.alarms = []

    def set_alarm(self, hours: int, minutes: int) -> None:
        """
​        设置闹钟。

​        Args:
​            hours (int): 小时,范围为0-23。
​            minutes (int): 分钟,范围为0-59。

​        Returns:
​            None

​        Raises:
​            ValueError: 如果小时或分钟不在合法范围内。
​        """
​        ​# 验证小时和分钟是否在合法范围内
        if (hours < 0 or hours > 23) or (minutes < 0 or minutes > 59):
            # 抛出异常
            raise ValueError("Invalid time for alarm")
            # 添加闹钟时间到列表
        self.alarms.append((hours, minutes))

    def delete_alarm(self, hours: int, minutes: int) -> None:
        """
​        删除指定的闹钟。

​        Args:
​            hours (int): 小时。
​            minutes (int): 分钟。

​        Returns:
​            None
​        """
​        ​try:
            # 从列表中删除指定的闹钟
            self.alarms.remove((hours, minutes))
        except ValueError:
            # 如果闹钟未找到,打印提示信息
            print("Alarm not found")

    def check_alarms(self, current_time: tuple[int, int, int]) -> None:
        """
​        检查当前时间是否与设定的闹钟时间匹配。

​        Args:
​            current_time (tuple[int, int, int]): 当前时间的元组(小时,分钟,秒)。

​        Returns:
​            None
​        """
​        ​# 获取当前小时和分钟
        current_hours, current_minutes = current_time[4], current_time[5]
        # 遍历所有设置的闹钟
        for alarm in self.alarms:
            # 如果闹钟时间到达
            if alarm[0] == current_hours and alarm[1] == current_minutes:
                # 触发闹钟
                self.trigger_alarm()
                # 清空屏幕
                self.oled.fill(0)
                # 在屏幕上显示“Alarm!”提示
                self.oled.text("Alarm!", 0, 0)
                # 刷新屏幕
                self.oled.show()

    def trigger_alarm(self) -> None:
        """
​        触发闹钟,打开蜂鸣器。

​        Args:
​            None

​        Returns:
​            None
​        """
​        ​# 打开蜂鸣器
        self.buzzer.duty_u16(32000)

    def stop_alarm(self) -> None:
        """
​        停止闹钟,关闭蜂鸣器。

​        Returns:
​            None
​        """
​        ​# 关闭蜂鸣器
        self.buzzer.duty_u16(0)

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试消息
print("FreakStudio: Implement an alarm clock using DS1302")

# 创建DS1302对象
ds1302 = DS1302(clk=Pin(10), dio=Pin(11), cs=Pin(12))

# 在烧录程序之前,务必使用 mpremote rtc --set 命令设置内置RTC实时时钟时间为电脑主机时间
# 接着打开REPL,输入下面命令:
#   from machine import Pin,  RTC
#   from ds1302 import DS1302
#   rtc = RTC()
#   year, month, day, weekday, hour, minute, second, _ = rtc.datetime()
#   ds1302 = DS1302(clk=Pin(10), dio=Pin(11), cs=Pin(12))
#   ds1302.date_time([year, month, day, weekday, hour, minute, second])
# 然后退出REPL模式,重启设备:mpremote reset

# 创建硬件I2C的实例,使用I2C1外设,时钟频率为400KHz,SDA引脚为6,SCL引脚为7
i2c = I2C(id=1, sda=Pin(6), scl=Pin(7), freq=400000)

# 开始扫描I2C总线上的设备,返回从机地址的列表
devices_list = i2c.scan()
print('START I2C SCANNER')

# 若devices_list为空,则没有设备连接到I2C总线上
if len(devices_list) == 0:
    print("No i2c device !")
# 若非空,则打印从机设备地址
else:
    print('i2c devices found:', len(devices_list))
    # 便利从机设备地址列表
    for device in devices_list:
        # 判断从机设备地址是否为0x3c或0x3d(即OLED SSD1306的地址)
        if device == 0x3c or device == 0x3d:
            print("I2C hexadecimal address: ", hex(device))
            OLED_ADDRESS = device

# 创建SSD1306 OLED屏幕的实例,宽度为128像素,高度为64像素,不使用外部电源
oled = SSD1306_I2C(i2c, OLED_ADDRESS, 128, 64,False)

# 创建闹钟实例
alarm_clock = AlarmClock(oled, buzzer_pin=Pin(9))
# 设置闹钟示例
alarm_clock.set_alarm(18, 3)

# 创建软件定时器实例
timer = Timer(-1)
# 每100ms调用一次display_time函数更新显示
timer.init(period=100, mode=Timer.PERIODIC, callback=display_time)

# ========================================  主程序  ============================================

# 无限循环检查闹钟是否到达
while True:
    # 获取当前时间
    current_time = ds1302.date_time()
    # 延时1s
    time.sleep(1)
    # 打印当前时间
    print("Current Time: {:02}:{:02}:{:02}".format( current_time[4], current_time[5], current_time[6]))

这里,我们在自定义 AlarmClock 类中管理闹钟的设置、删除和触发功能,当前时间达到任一闹钟设置时间时,触发蜂鸣器并在 OLED 显示提示信息,各个方法含义如下:

8.png

  • 类初始化 ​init​​ 方法:​首先保存 OLED 实例并初始化蜂鸣器,然后初始化闹钟列表 self.alarms = [],用于存储多个闹钟时间。
  • 设置闹钟 ​set_alarm​方法:​通过指定小时和分钟,将闹钟时间添加到闹钟列表中,首先进行参数验证确保 hours0-23 之间,minutes0-59 之间,然后将 (hours, minutes) 元组添加到闹钟列表。
  • 删除闹钟 ​delete_alarm​方法:​根据提供的小时和分钟,从闹钟列表中移除对应的闹钟时间;如果指定的闹钟时间不存在于列表中,捕获 ValueError 异常,并打印提示信息。
  • 检查闹钟 ​check_alarms​方法:​比较当前时间的小时和分钟与闹钟列表中的每个闹钟时间是否匹配,如果匹配,触发闹钟,在 OLED 屏幕上显示提示信息并开启蜂鸣器,返回 True;如果所有闹钟都未匹配,返回 False

然后,我们创建 DS1302 类的实例和 AlarmClock 类实例,接着,初始化一个软件定时器,设置其定时周期为 100 毫秒,以周期性模式调用 display_time 函数。在 display_time 函数中,会从 ds1302 获取当前时间,更新 OLED 屏幕上的标题(“​RTC Clock​”)、日期和时间,并调用 alarm_clock.check_alarms 检查闹钟是否触发:

  • 如果当前时间匹配某个设定的闹钟时间,会触发闹钟(启动蜂鸣器),同时在 OLED 上显示 “​Alarm​!”;
  • 如果不匹配,会停止蜂鸣器(PWM 占空比设为 0)。

而在主循环中,每秒则通过 ds1302.date_time() 获取当前时间,并将其以 “​时:分: 秒​” 的格式打印到终端上。

整体时序流程如下图所示:

9.png

需要注意的是,在烧录代码之前我们还需要进行一项工作,即同步外部 RTC 实时时钟和电脑时间。

首先用 mpremote rtc --set 命令将树莓派 Pico 内置 RTC 时间同步为电脑主机时间:

10.png

如果不提前同步内置 RTC,Pico 的内置 RTC 在断电后会丢失时间(它没有备用电池,断电后时间会重置为初始值,比如 2021-01-01 00:00:00)。此时代码读取到的内置 RTC 时间是错误的,写入 DS1302 后,整个闹钟系统的时间就会从错误的初始值开始运行(比如显示 “2021 年”),导致闹钟功能完全失效。

接着,还需打开 REPL 并依次输入以下命令,将内置 RTC 的时间同步到外部 DS1302 实时时钟:

from machine import Pin, RTC
from ds1302 import DS1302
rtc = RTC()
year, month, day, weekday, hour, minute, second, _ = rtc.datetime()
ds1302 = DS1302(clk=Pin(10), dio=Pin(11), cs=Pin(12))
ds1302.date_time([year, month, day, weekday, hour, minute, second])

11.gif

待上述的命令执行完毕后,我们退出 REPL 模式,最后通过 mpremote reset 命令重启设备,确保时间配置完全生效。

烧录代码,打开终端显示如下:

13.png

可以看到,终端不断更新当前时间,而 OLED 屏幕上同样如此:

165b981f-e5a0-44d2-ac79-44e7bd0eb560.jfif

当闹钟设定时间到达时,蜂鸣器间断发出声响,同时 OLED 屏幕上显示提示信息:

image.png

mmexport1782378087835.gif

posted @ 2026-09-02 16:25  FreakStudio  阅读(13)  评论(0)    收藏  举报