MicroPython 开发:树莓派 Pico RTC 控制方法与 NTP 时间同步实现

1.软件控制方法

1.1 machine.RTC 类的构造方法

machine.RTC 类的构造方法如下:

1.2 machine.RTC 类的其他方法

machine.RTC 类还有其他方法:

连接树莓派 Pico,使用 dir 命令,可以看到内置的 RTC 大部分功能无法使用,若在项目中需要用到 RTC 的唤醒和闹钟功能只能使用 Pico 的 SDK 编程或借助外置 RTC 芯片来实现。

2.应用实例:获取网络时间并设置 RTC 实时时钟

首先,我们需要将电脑和**风雅一号板-以太网与SD卡扩展板相连接,具体流程可以参考我们教材中关于 ETH 以太网章节中内容(注意,进行该实验时需要将 风雅一号板-以太网与SD卡扩展板上的网口和路由器连接**):

NTP(网络时间协议)服务器用于提供精确的时间同步服务,NTP 服务器能够将网络时间同步到一个标准的时间源,从而保证设备上的时间与世界标准时间(UTC)保持一致。

常用的公共 NTP 服务器地址如下:

这里,我们首要选择的 NTP 服务器为中国授时公开 NTP 服务器,地址为 'cn.ntp.org.cn',在选择 NTP 服务器前,我们需要尝试对地址进行 ping 测试,确保连接无误。

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

在下面的代码中,我们从 NTP 服务器获取时间并设置 RTC 实时时钟,同时在主循环中,每秒打印 RTC 的当前时间,示例代码如下:

Python 复制代码
# Python env   : MicroPython v1.23.0 on Wiznet W5500
# -*- coding: utf-8 -*-        
# @Time    : 2024/8/14 下午2:30   
# @Author  : 李清水            
# @File    : main.py       
# @Description : RTC实时时钟类使用,获取NTP服务器时间设定RTC实时时钟

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

# 导入硬件相关模块
from machine import Pin,SPI,RTC
# 导入时间相关模块
import time
# 导入网络相关模块
import network
import socket
# 导入访问和设置系统时间的模块
import ntptime

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

# 设备IP地址
ip = '192.168.1.20'
sn = '255.255.255.0'
gw = '192.168.1.1'
dns = '114.114.114.114'

# 用于网络连接的元组对象
netinfo = (ip, sn, gw, dns)

# 设置多个备选NTP服务器
ntp_servers = [
    'cn.ntp.org.cn',
    'ntp.aliyun.com',
    'time1.cloud.tencent.com',
    'pool.ntp.org'
]

# 当前使用的NTP服务器
current_ntp_server = 0

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

# 初始化网络连接
def w5x00_init() -> network.WIZNET5K:
    """
​    初始化 WIZNET5K 以太网模块,使用 SPI 接口进行通信,并设置静态 IP 地址。

​    Args:
​        None: 无参数。

​    Returns:
​        network.WIZNET5K: 已初始化的 WIZNET5K 以太网实例。

​    Raises:
​        OSError: 如果网络接口未能连接。
​    """

​    ​# 声明全局变量
    global netinfo

    # 初始化SPI对象
    spi = SPI(0, 2_000_000, mosi=Pin(19), miso=Pin(16), sck=Pin(18))
    # 实例化network.WIZNET5K类,传入使用的片选引脚CS和复位引脚RST
    nic = network.WIZNET5K(spi, Pin(17), Pin(20))
    # 激活网络接口
    nic.active(True)

    try:
        print("\r\nConfiguring DHCP")
        # 尝试使用DHCP动态获取IP地址、子网掩码、网关和DNS服务器等网络配置
        nic.ifconfig('dhcp')
    except:
        print("\r\nDHCP fails, use static configuration")
        # 如果DHCP获取失败,使用静态IP地址配置
        nic.ifconfig(netinfo)

    # 等待网络连接
    max_attempts = 10
    attempt = 0
    while not nic.isconnected() and attempt < max_attempts:
        attempt += 1
        print(f"Waiting for network connection... Attempt {attempt}/{max_attempts}")
        time.sleep(1)
        # 输出寄存器信息
        print(nic.regs())

    if not nic.isconnected():
        raise OSError("Failed to establish network connection")

    # 打印设置的IP地址
    print('ip :', nic.ifconfig()[0])
    print('sn :', nic.ifconfig()[1])
    print('gw :', nic.ifconfig()[2])
    print('dns:', nic.ifconfig()[3])

    # 测试DNS解析
    try:
        addr = socket.getaddrinfo(ntp_servers[0], 80)[0][-1]
        print(f"DNS resolution test successful: {ntp_servers[0]} -> {addr[0]}")
    except Exception as e:
        print(f"DNS resolution test failed: {e}")

    return nic

# 修改ntptime的超时设置
def set_ntptime_timeout(timeout_ms: int) -> None:
    """
​    修改ntptime模块的超时设置,确保在网络延迟时能正确等待响应。

​    Args:
​        timeout_ms: 超时时间,单位为毫秒。

​    Returns:
​        None: 无返回值。
​    """
​    ​# 如果ntptime模块有timeout属性,直接设置
    if hasattr(ntptime, 'timeout'):
        ntptime.timeout = timeout_ms
    else:
        # 否则尝试修改内部实现
        import usocket as socket
        def _ntp_time(host):
            NTP_DELTA = 2208988800
            NTP_QUERY = bytearray(48)
            NTP_QUERY[0] = 0x1B
            addr = socket.getaddrinfo(host, 123)[0][-1]
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            try:
                s.settimeout(timeout_ms / 1000)
                s.sendto(NTP_QUERY, addr)
                msg = s.recv(48)
            finally:
                s.close()
            val = int.from_bytes(msg[40:44], 'big')
            return val - NTP_DELTA

        # 替换ntptime的time函数
        ntptime.time = _ntp_time

# 获取网络时间
def get_network_time() -> time.struct_time:
    """
​    从 NTP 服务器获取网络时间,并设置系统时钟。
​    同时将时间转换为北京时间(UTC+8)。

​    Args:
​        None: 无参数。

​    Returns:
​        time.struct_time: 当前的本地时间。

​    Raises:
​        Exception: 如果获取网络时间失败,会在循环中不断尝试。
​    """
​    ​global current_ntp_server, ntp_servers

    # 设置NTP超时为5秒
    set_ntptime_timeout(5000)

    max_retries = 5
    retry_count = 0

    while retry_count < max_retries:
        try:
            # 切换到当前NTP服务器
            ntptime.host = ntp_servers[current_ntp_server]
            print(f"Trying to sync time with {ntptime.host}...")

            # 从 NTP 服务器获取时间并设置系统时钟,注意是UTC时间
            ntptime.settime()

            # 获取当前的本地时间
            nowtime = time.localtime()
            # 转换为北京时间(UTC+8)
            beijing_time = convert_to_beijing_time(nowtime)
            # 打印当前时间
            print(f'Successfully synced with {ntptime.host}. Current time: {beijing_time}')

            # 设置RTC时钟
            rtc.datetime((
                beijing_time[0], beijing_time[1], beijing_time[2],
                beijing_time[6], beijing_time[3], beijing_time[4],
                beijing_time[5], 0
            ))
            print("RTC time set successfully")

            return beijing_time

        except Exception as e:
            print(f'Failed to get time from {ntptime.host}: {e}')
            retry_count += 1

            # 切换到下一个NTP服务器
            current_ntp_server = (current_ntp_server + 1) % len(ntp_servers)

            # 等待一段时间再重试
            time.sleep(2)

    # 如果所有尝试都失败,抛出异常
    raise Exception("All NTP servers failed to respond after multiple attempts")

# 将UTC时间转换为北京时间(UTC+8)
def convert_to_beijing_time(utc_time: time.struct_time) -> time.struct_time:
    """
​    将UTC时间转换为北京时间(UTC+8)。

​    Args:
​        utc_time (time.struct_time): UTC时间元组。

​    Returns:
​        time.struct_time: 北京时间元组。
​    """
​    ​# 将时间元组转换为时间戳(秒)
    utc_timestamp = time.mktime(utc_time)
    # 增加8小时(8 * 3600秒)
    beijing_timestamp = utc_timestamp + 8 * 3600
    # 转换回时间元组
    return time.localtime(beijing_timestamp)

# RTC时间转换为与 time.localtime() 兼容的格式
def rtc_to_localtime(rtc_time: tuple) -> tuple:
    """
​    将 RTC.datetime() 返回的 8 元组转换为与 time.localtime() 兼容的 8 元组格式。

​    Args:
​        rtc_time (tuple): RTC.datetime() 返回的 8 元组 (year, month, day, weekday, hour, minute, second, subseconds)。

​    Returns:
​        tuple: 8 元组格式 (year, month, day, hour, minute, second, weekday, yearday)。
​    """
​    ​# 忽略 subseconds
    year, month, day, weekday, hour, minute, second, _ = rtc_time
    # 计算时间戳
    yearday = time.mktime((year, month, day, hour, minute, second, 0, 0, 0))
    # 获取 yearday(一年中的第几天)
    yearday = time.localtime(yearday)[7]

    return (year, month, day, hour, minute, second, weekday, yearday, -1)

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

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

# 延时3s,等待设备上电
time.sleep(2)
# 打印调试信息
print("FreakStudio: Using NTP to set RTC Timer")

# 初始化w5500模块
nic = w5x00_init()
# 构造RTC实例
rtc = RTC()

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

# 获取网络时间
get_network_time()

# 主循环
while True:
    # 获取 RTC 时间,并转换为与 time.localtime() 兼容的格式
    rtc_time = rtc_to_localtime(rtc.datetime())
    # 转换为北京时间(RTC 已经是本地时间,不需要加 8 小时)
    beijing_time = convert_to_beijing_time(rtc_time)
    # 打印 RTC 时间并延时1s
    print("RTC time:", beijing_time)
    time.sleep(1)

我们在 get_network_time() 方法中首先调用 set_ntptime_timeout(5000) 修改 ntptime 模块的超时时间为 5 秒:

  • ntptime 模块本身有 timeout 属性,直接赋值;
  • 若没有,则重写 ntptime.time() 函数,通过 s.settimeout(5) 设置 UDP 通信超时 ------ 避免 NTP 服务器响应慢导致程序卡死。
Python 复制代码
# 修改ntptime的超时设置
def set_ntptime_timeout(timeout_ms: int) -> None:
    """
​    修改ntptime模块的超时设置,确保在网络延迟时能正确等待响应。

​    Args:
​        timeout_ms: 超时时间,单位为毫秒。

​    Returns:
​        None: 无返回值。
​    """
​    ​# 如果ntptime模块有timeout属性,直接设置
    if hasattr(ntptime, 'timeout'):
        ntptime.timeout = timeout_ms
    else:
        # 否则尝试修改内部实现
        import usocket as socket
        def _ntp_time(host):
            NTP_DELTA = 2208988800
            NTP_QUERY = bytearray(48)
            NTP_QUERY[0] = 0x1B
            addr = socket.getaddrinfo(host, 123)[0][-1]
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            try:
                s.settimeout(timeout_ms / 1000)
                s.sendto(NTP_QUERY, addr)
                msg = s.recv(48)
            finally:
                s.close()
            val = int.from_bytes(msg[40:44], 'big')
            return val - NTP_DELTA

        # 替换ntptime的time函数
        ntptime.time = _ntp_time
        
# 获取网络时间
def get_network_time() -> time.struct_time:
    """
​    从 NTP 服务器获取网络时间,并设置系统时钟。
​    同时将时间转换为北京时间(UTC+8)。

​    Args:
​        None: 无参数。

​    Returns:
​        time.struct_time: 当前的本地时间。

​    Raises:
​        Exception: 如果获取网络时间失败,会在循环中不断尝试。
​    """
​    ​global current_ntp_server, ntp_servers

    # 设置NTP超时为5秒
    set_ntptime_timeout(5000)

get_network_time() 函数通过 "重试机制 + 服务器切换" 确保 NTP 同步成功:

  • 重试次数限制:最多重试 5 次(max_retries=5),避免无限重试;
  • 服务器切换:每次失败后,通过 current_ntp_server = (current_ntp_server + 1) % len(ntp_servers) 切换到下一个 NTP 服务器(循环使用列表中的 4 个服务器);
  • 延时重试:每次失败后 time.sleep(2)------ 避免短时间内频繁请求导致服务器限流。
Python 复制代码
max_retries = 5
retry_count = 0

while retry_count < max_retries:
    try:
        # 切换到当前NTP服务器
        ntptime.host = ntp_servers[current_ntp_server]
        print(f"Trying to sync time with {ntptime.host}...")
        ... ...
    
    except Exception as e:
        print(f'Failed to get time from {ntptime.host}: {e}')
        retry_count += 1

        # 切换到下一个NTP服务器
        current_ntp_server = (current_ntp_server + 1) % len(ntp_servers)

        # 等待一段时间再重试
        time.sleep(2)

# 如果所有尝试都失败,抛出异常
raise Exception("All NTP servers failed to respond after multiple attempts")

接着进行 NTP 时间获取与 UTC 转北京时间,调用 ntptime.settime()------ 从当前 NTP 服务器获取 UTC 时间(世界协调时间),并自动设置 MicroPython 的系统时钟;

Python 复制代码
# 从 NTP 服务器获取时间并设置系统时钟,注意是UTC时间
ntptime.settime()

# 获取当前的本地时间
nowtime = time.localtime()

这里,time.localtime() 方法获取的是一个时间元组,格式如下:

Python 复制代码
(year, month, day, hour, minute, second, weekday, yearday)

具体含义如下表所示:

这里,世界标准时间(UTC)和我们生活中常用的北京时间时差为八小时,在获取 UTC 时间后,需要增加 8 小时来得到北京时间,这里我们通过下面代码中的 convert_to_beijing_time() 函数进行实现:

python 复制代码
# 将UTC时间转换为北京时间(UTC+8)
def convert_to_beijing_time(utc_time: time.struct_time) -> time.struct_time:
    """
​    将UTC时间转换为北京时间(UTC+8)。

​    Args:
​        utc_time (time.struct_time): UTC时间元组。

​    Returns:
​        time.struct_time: 北京时间元组。
​    """
​    ​# 将时间元组转换为时间戳(秒)
    utc_timestamp = time.mktime(utc_time)
    # 增加8小时(8 * 3600秒)
    beijing_timestamp = utc_timestamp + 8 * 3600
    # 转换回时间元组
    return time.localtime(beijing_timestamp)

convert_to_beijing_time() 函数首先通过 time.mktime() 方法将时间元组 (year, month, day, hour, minute, second, ...) 转换成时间戳,所谓时间戳,就是从 1970 年 1 月 1 日(UTC/GMT 的午夜,即 1970-01-01 00:00:00 UTC)开始所经过的秒数,然后让 UTC 时间加 8 小时,转换为北京时间。

之后通过 time.localtime() 方法把时间戳转换回 time.localtime() 格式的时间元组,从而实现将 UTC 时间转换为北京时间。

在主循环中,我们首先获取 RTC 时间,然后在 rtc_to_localtime() 函数中,我们将 RTC.datetime() 时间格式转换为 time.localtime() 时间格式:

sql 复制代码
# RTC时间转换为与 time.localtime() 兼容的格式
def rtc_to_localtime(rtc_time: tuple) -> tuple:
    """
​    将 RTC.datetime() 返回的 8 元组转换为与 time.localtime() 兼容的 8 元组格式。

​    Args:
​        rtc_time (tuple): RTC.datetime() 返回的 8 元组 (year, month, day, weekday, hour, minute, second, subseconds)。

​    Returns:
​        tuple: 8 元组格式 (year, month, day, hour, minute, second, weekday, yearday)。
​    """
​    ​# 忽略 subseconds
    year, month, day, weekday, hour, minute, second, _ = rtc_time
    # 计算时间戳
    yearday = time.mktime((year, month, day, hour, minute, second, 0, 0, 0))
    # 获取 yearday(一年中的第几天)
    yearday = time.localtime(yearday)[7]

    return (year, month, day, hour, minute, second, weekday, yearday, -1)

在 MicroPython 的 API 文档中,我们可以明确看到 RTC.datetime() 方法和 time.localtime() 方法返回的时间日期数据格式并不相同:

对比如下表所示:

rtc_to_localtime() 函数主要是通过解包获取 RTC.datetime() 返回的 8 个值后,将其时间元组转换为 9 元组格式,然后使用 time.localtime() 将时间戳转换回 time.localtime() 格式的时间元组。

之后,再通过 convert_to_beijing_time() 函数得到北京时间并打印到终端。

烧录代码,打开远程终端:

可以看到循环输出当前时间,我们选择一个网络在线时钟进行对比,其在线时钟网址为:

onlinealarmkur.com/clock/zh-cn...

可以看到 RTC 中时间和在线时钟中时间相差几秒,误差不大。

相关推荐
Freak嵌入式2 小时前
一篇搞懂:MicroPython urequests库与 Pico HTTP 客户端实现
嵌入式
Freak嵌入式2 小时前
一篇搞懂:MicroPython 与 Pico DHCP 动态 IP 的实现与调试
嵌入式
Freak嵌入式2 小时前
保姆级教程:MicroPython+W5500 实现 HTTP 服务器,网页一键控灯
嵌入式
Freak嵌入式2 小时前
【Pico 以太网实战系列 4/6】UDP 协议实操:MicroPython 实现回显服务器
嵌入式
Freak嵌入式2 小时前
MicroPython 以太网实战:Pico+W5500 TCP 客户端详解
嵌入式
Freak嵌入式3 小时前
MicroPython W5500 以太网实验|ping 测试:原理 + 实操全解析
嵌入式
Freak嵌入式6 小时前
USB-CDC 通信实战应用:树莓派 Pico 虚拟串口 + select+MicroPython 全拆解
嵌入式
Freak嵌入式1 天前
PIO 入门:手把手实现板载 LED 闪烁,原理与代码全解析
嵌入式
Freak嵌入式1 天前
一文搞懂树莓派 Pico 任务调度器:基于定时器的 MicroPython 多任务实现
嵌入式