树莓派连接 DS3231 时钟模块
一、硬件准备
树莓派(任意型号,如 Raspberry Pi 4) DS3231 模块(带 I2C 接口,VCC、GND、SDA、SCL 引脚) 杜邦线若干 接线说明(自定义 SDA/SCL 引脚,此处以 GPIO17=SDA,GPIO27=SCL 为例。也可以接默认IIC引脚:SDA 接 GPIO2(SDA)、SCL 接 GPIO3(SCL)): DS3231 引脚 树莓派 GPIO 引脚 说明 VCC 3.3V 电源(勿接 5V,避免烧毁) GND GND 接地 SDA GPIO17(自定义) 数据信号线 SCL GPIO27(自定义) 时钟信号线
二、系统准备
关闭NTP服务,不再使用网络服务器时间。
执行命令终止正在运行的时间同步进程:
sudo systemctl disable systemd-timesyncd
防止系统重启后 NTP 服务自动恢复:
sudo systemctl disable systemd-timesyncd开启 I2C 功能:终端输入 sudo raspi-config,选 “Interface Options”→“I2C” 启用,重启树莓派。 安装依赖:终端执行sudo apt-get install python3-smbus python3-rpi.gpio。 测试连接:输入sudo i2cdetect -y 1,若显示 “68” 则连接成功。
三、python代码
1.DS3231读写代码 clock_readwrite.py,注意,周几的设置不一定要准确,但是一定要有。可以发送给树莓派时不发这一个值。
"""
一、硬件准备
树莓派(任意型号,如 Raspberry Pi 4)
DS3231 模块(带 I2C 接口,VCC、GND、SDA、SCL 引脚)
杜邦线若干
可选:面包板(方便接线)
接线说明(自定义 SDA/SCL 引脚,此处以 GPIO17=SDA,GPIO27=SCL 为例):
DS3231 引脚 树莓派 GPIO 引脚 说明
VCC 3.3V 电源(勿接 5V,避免烧毁)
GND GND 接地
SDA GPIO17(自定义) 数据信号线
SCL GPIO27(自定义) 时钟信号线
二、原理说明
DS3231 通过 I2C 协议通信,设备地址为0x68(7 位地址)。模拟 I2C(bit-bang)需通过软件手动控制 SDA 和 SCL 引脚的高低电平,模拟 I2C 的起始信号、数据传输、应答信号、停止信号等时序。
核心操作:
模拟 I2C 起始 / 停止信号
发送 / 接收字节数据
处理设备应答(ACK)
读写 DS3231 的时间寄存器(地址0x00~0x06存储秒、分、时、日、月、年等信息)
三、软件实现(Python)
使用树莓派的 GPIO 库(RPi.GPIO)模拟 I2C 时序,实现 DS3231 的时间读写。
步骤 1:安装依赖
确保已安装 GPIO 库
sudo apt-get update
sudo apt-get install python3-rpi.gpio--------代码说明--------
BCD 码转换:DS3231 的时间数据以 BCD 码存储(如十进制 25→BCD 码 0x25),需通过bcd_to_dec和dec_to_bcd转换。
I2C 时序:i2c_start、i2c_stop、i2c_send_byte、i2c_read_byte函数模拟 I2C 通信的核心时序。
寄存器操作:ds3231_write_reg和ds3231_read_reg通过模拟 I2C 读写 DS3231 的寄存器。
时间设置 / 读取:ds3231_set_time和ds3231_get_time封装了时间的设置和读取逻辑。
五、注意事项
引脚选择:SDA 和 SCL 可自定义为任意 GPIO 引脚(需与接线一致),但建议避开树莓派硬件 I2C 引脚(如 GPIO2=SDA,GPIO3=SCL)以避免冲突。
延时调整:代码中的time.sleep(0.0001)(100μs)确保时序稳定,若通信失败可适当增加延时(如0.001)。
星期定义:不同 DS3231 模块的星期定义可能不同(如 1 = 周一或 1 = 周日),需根据实际模块调整。
错误处理:代码中包含简单的应答检查,若提示 “未收到设备应答”,需检查接线、电源或模块是否正常。
通过代码手动设置星期值,再读取验证,即可确定对应关系:
修改代码中的设置函数,手动指定星期值并写入:
# 示例:设置星期为1,其他时间随意(确保格式正确)
ds3231_set_time(23, 10, 1, 1, 12, 34, 56) # 最后一个参数1是星期值
读取并记录结果,观察当前实际星期几,与读取到的week值对比:
time_data = ds3231_get_time()
print("读取到的星期值:", time_data['week']) # 假设实际今天是星期一,若读取到2,则说明2=星期一"""
import RPi.GPIO as GPIO
import time
from typing import Dict, Optionalclass DS3231BitBang:def __init__(self, sda_pin: int = 17, scl_pin: int = 27, timeout: float = 0.1):"""初始化DS3231模拟I2C通信"""self.SDA_PIN = sda_pinself.SCL_PIN = scl_pinself.TIMEOUT = timeoutself.DS3231_ADDR = 0x68 # 7位设备地址self.REG_SECONDS = 0x00 # 秒寄存器self.REG_MINUTES = 0x01 # 分寄存器self.REG_HOURS = 0x02 # 时寄存器self.REG_DAY = 0x03 # 日寄存器self.REG_MONTH = 0x04 # 月寄存器self.REG_WEEKDAY = 0x05 # 星期寄存器self.REG_YEAR = 0x06 # 年寄存器# 温度寄存器地址(DS3231温度存储在这两个寄存器)self.REG_TEMP_HIGH = 0x11 # 温度整数部分(二进制补码)self.REG_TEMP_LOW = 0x12 # 温度小数部分(仅高4位有效)# 初始化GPIO(推挽输出,默认高电平)GPIO.setmode(GPIO.BCM)GPIO.setwarnings(False)GPIO.setup(self.SDA_PIN, GPIO.OUT, initial=GPIO.HIGH)GPIO.setup(self.SCL_PIN, GPIO.OUT, initial=GPIO.HIGH)# 微秒级延时self._usleep = lambda x: time.sleep(x / 1_000_000)def _i2c_start(self) -> bool:"""发送I2C起始信号,返回是否成功"""start_time = time.time()GPIO.output(self.SDA_PIN, GPIO.HIGH)GPIO.output(self.SCL_PIN, GPIO.HIGH)self._usleep(20)while time.time() - start_time < self.TIMEOUT:GPIO.output(self.SDA_PIN, GPIO.LOW)self._usleep(20)GPIO.output(self.SCL_PIN, GPIO.LOW)return Truereturn Falsedef _i2c_stop(self) -> None:"""发送I2C停止信号"""GPIO.output(self.SCL_PIN, GPIO.LOW)GPIO.output(self.SDA_PIN, GPIO.LOW)self._usleep(20)GPIO.output(self.SCL_PIN, GPIO.HIGH)self._usleep(20)GPIO.output(self.SDA_PIN, GPIO.HIGH)self._usleep(20)def _i2c_send_byte(self, data: int) -> bool:"""发送1字节数据,返回从机应答状态"""for i in range(8):# 设置SDA引脚电平GPIO.output(self.SDA_PIN, (data >> (7 - i)) & 0x01)self._usleep(20) # <--- 增加延时,确保信号稳定# 拉高SCL,让从机采样GPIO.output(self.SCL_PIN, GPIO.HIGH)self._usleep(20)# 拉低SCL,准备下一位数据GPIO.output(self.SCL_PIN, GPIO.LOW)self._usleep(20) # <--- 也可以适当增加这里的延时# 读取应答GPIO.setup(self.SDA_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)self._usleep(20) # <--- 增加延时GPIO.output(self.SCL_PIN, GPIO.HIGH)ack = not GPIO.input(self.SDA_PIN)self._usleep(20)GPIO.output(self.SCL_PIN, GPIO.LOW)GPIO.setup(self.SDA_PIN, GPIO.OUT, initial=GPIO.HIGH) # 恢复输出return ackdef _i2c_read_byte(self, ack: bool = True) -> int:"""读取1字节数据,发送ACK/NACK"""data = 0GPIO.setup(self.SDA_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)for i in range(8):GPIO.output(self.SCL_PIN, GPIO.HIGH)self._usleep(20)data = (data << 1) | GPIO.input(self.SDA_PIN)GPIO.output(self.SCL_PIN, GPIO.LOW)self._usleep(20)# 发送应答GPIO.setup(self.SDA_PIN, GPIO.OUT, initial=GPIO.HIGH)GPIO.output(self.SDA_PIN, not ack)self._usleep(20)GPIO.output(self.SCL_PIN, GPIO.HIGH)self._usleep(20)GPIO.output(self.SCL_PIN, GPIO.LOW)self._usleep(20)return data@staticmethoddef _bcd_to_dec(bcd: int) -> int:"""BCD码转十进制"""return (bcd >> 4) * 10 + (bcd & 0x0F)# return ((bcd & 0xF0) >> 4) * 10 + (bcd & 0x0F)@staticmethoddef _dec_to_bcd(dec: int) -> int:"""十进制转BCD码(新增:用于设置时间)"""return ((dec // 10) << 4) | (dec % 10)def read_time(self) -> Optional[Dict]:"""读取DS3231时间,返回字典或None"""if not self._i2c_start():print("起始信号失败")return None# 发送设备写地址if not self._i2c_send_byte((self.DS3231_ADDR << 1) | 0x00):print("写地址无应答")self._i2c_stop()return None# 发送起始寄存器地址(从秒开始读)if not self._i2c_send_byte(self.REG_SECONDS):print("寄存器地址发送失败")self._i2c_stop()return None# 切换为读模式if not self._i2c_start():print("重新起始失败")self._i2c_stop()return Noneif not self._i2c_send_byte((self.DS3231_ADDR << 1) | 0x01):print("读地址无应答")self._i2c_stop()return None# 连续读取7个寄存器try:seconds = self._bcd_to_dec(self._i2c_read_byte(ack=True) & 0x7F)minutes = self._bcd_to_dec(self._i2c_read_byte(ack=True) & 0x7F)hours = self._bcd_to_dec(self._i2c_read_byte(ack=True) & 0x3F)weekday = self._bcd_to_dec(self._i2c_read_byte(ack=True) & 0x3F)day = self._bcd_to_dec(self._i2c_read_byte(ack=True) & 0x3F)month = self._bcd_to_dec(self._i2c_read_byte(ack=True) & 0x03F)year = 2000 + self._bcd_to_dec(self._i2c_read_byte(ack=False))except Exception as e:print(f"读取失败:{e}")self._i2c_stop()return Noneself._i2c_stop()# 数据校验print("year",year,month,day,weekday,hours,minutes,seconds)if not (0 <= seconds <= 59 and 0 <= minutes <= 59 and 0 <= hours <= 23 and1 <= day <= 31 and 1 <= month <= 12 and 1 <= weekday <= 7 and0 <= (year - 2000) <= 99):print("无效时间数据")return Nonereturn {"year": year, "month": month, "day": day,"weekday": weekday, "hour": hours, "minute": minutes, "second": seconds}def set_time(self, year: int, month: int, day: int, weekday: int, hour: int, minute: int, second: int) -> bool:"""设置DS3231时间:param year: 年份(4位,如2023):param month: 月份(1-12):param day: 日期(1-31):param weekday: 星期(1-7,需与模块定义匹配):param hour: 小时(0-23):param minute: 分钟(0-59):param second: 秒(0-59):return: 设置成功返回True,失败返回False"""# 输入参数校验if not (2000 <= year <= 2099 and 1 <= month <= 12 and 1 <= day <= 31 and1 <= weekday <= 7 and 0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):print("无效的时间参数")return False# 转换为BCD码(年取后两位)bcd_year = self._dec_to_bcd(year % 100)bcd_month = self._dec_to_bcd(month)bcd_day = self._dec_to_bcd(day)bcd_weekday = self._dec_to_bcd(weekday)bcd_hour = self._dec_to_bcd(hour)bcd_minute = self._dec_to_bcd(minute)bcd_second = self._dec_to_bcd(second)# 发送I2C起始信号if not self._i2c_start():print("起始信号失败")return False# 发送设备写地址if not self._i2c_send_byte((self.DS3231_ADDR << 1) | 0x00):print("写地址无应答")self._i2c_stop()return False# 发送起始寄存器地址(从秒开始写)if not self._i2c_send_byte(self.REG_SECONDS):print("寄存器地址发送失败")self._i2c_stop()return False# 依次写入秒、分、时、日、月、星期、年(连续写)try:if not self._i2c_send_byte(bcd_second):raise Exception("秒寄存器写入失败")if not self._i2c_send_byte(bcd_minute):raise Exception("分寄存器写入失败")if not self._i2c_send_byte(bcd_hour):raise Exception("时寄存器写入失败")if not self._i2c_send_byte(bcd_weekday):raise Exception("星期寄存器写入失败")if not self._i2c_send_byte(bcd_day):raise Exception("日寄存器写入失败")if not self._i2c_send_byte(bcd_month):raise Exception("月寄存器写入失败")if not self._i2c_send_byte(bcd_year):raise Exception("年寄存器写入失败")except Exception as e:print(f"写入失败:{e}")self._i2c_stop()return False# 发送停止信号self._i2c_stop()return True# ---------------------- 新增:温度读取函数 ----------------------def read_temperature(self) -> Optional[float]:"""读取DS3231内置温度传感器数据返回:温度值(单位°C,精度0.0625°C),失败返回None"""# 1. 发送I2C起始信号,指定设备写地址(用于设置温度寄存器地址)if not self._i2c_start():print("温度读取:起始信号失败")return None# 2. 发送设备写地址(0x68 << 1 | 0 = 0xD0)if not self._i2c_send_byte((self.DS3231_ADDR << 1) | 0x00):print("温度读取:设备无应答(写地址)")self._i2c_stop()return None# 3. 发送温度高位寄存器地址(从0x11开始读,后续可连续读0x12)if not self._i2c_send_byte(self.REG_TEMP_HIGH):print("温度读取:寄存器地址发送失败")self._i2c_stop()return None# 4. 重新发送起始信号,切换为读模式if not self._i2c_start():print("温度读取:重新起始失败")self._i2c_stop()return None# 5. 发送设备读地址(0x68 << 1 | 1 = 0xD1)if not self._i2c_send_byte((self.DS3231_ADDR << 1) | 0x01):print("温度读取:设备无应答(读地址)")self._i2c_stop()return None# 6. 连续读取温度高位(0x11)和低位(0x12)寄存器try:temp_high = self._i2c_read_byte(ack=True) # 读高位后发ACK,继续读低位temp_low = self._i2c_read_byte(ack=False) # 读低位后发NACK(结束读取)except Exception as e:print(f"温度读取:数据读取失败:{e}")self._i2c_stop()return None# 7. 发送I2C停止信号self._i2c_stop()# 8. 温度数据转换(关键:处理二进制补码,支持负温度)# - 高位寄存器(temp_high):8位二进制补码,代表温度整数部分# - 低位寄存器(temp_low):仅高4位有效,每一位代表0.0625°Cif temp_high & 0x80: # 最高位为1,说明是负温度(二进制补码)# 负温度转换:补码转原码(取反+1)integer_part = -((~temp_high + 1) & 0xFF)else: # 正温度,直接取原值integer_part = temp_high# 小数部分:取低位寄存器高4位,乘以0.0625°Cdecimal_part = (temp_low >> 4) * 0.0625# 总温度 = 整数部分 + 小数部分temperature = integer_part + decimal_part# 9. 温度范围校验(DS3231温度测量范围通常为-40°C ~ +85°C)if not (-40.0 <= temperature <= 85.0):print(f"温度读取:无效温度值({temperature}°C),可能通信错误")return Nonereturn round(temperature, 2) # 保留2位小数,提升可读性def cleanup(self) -> None:"""释放GPIO资源"""GPIO.cleanup([self.SDA_PIN, self.SCL_PIN])# ---------------------- 测试:同时读取时间和温度 ----------------------
if __name__ == "__main__":ds3231 = DS3231BitBang(sda_pin=2, scl_pin=3)try:# 可选:先设置一次时间(根据实际需求启用)# 示例:设置时间(xxxx年x月x日 周x xx:xx:xx)# 注意:星期值需与模块定义匹配(如1=周日,2=周一...)set_success = ds3231.set_time(2025, 11, 15, 3, 8, 54, 56)print("设置时间成功" if set_success else "设置时间失败")# 读取并打印时间time_data = ds3231.read_time()if time_data:print(f"当前时间:{time_data['year']}-{time_data['month']:02d}-{time_data['day']:02d} "f"{time_data['hour']:02d}:{time_data['minute']:02d}:{time_data['second']:02d} "f"星期{time_data['weekday']}")# 读取并打印温度temp = ds3231.read_temperature()if temp is not None:print(f"当前温度:{temp}°C")finally:ds3231.cleanup()2.写入树莓派设置时间代码
注意点:月份要使用字母缩写,所以做一个map,把ds3231的数字格式变成字母
def get_3231_datetime(time_data):if time_data:print(f"当前时间:{time_data['year']}-{time_data['month']:02d}-{time_data['day']} "f"{time_data['hour']:02d}:{time_data['minute']:02d}:{time_data['second']:02d} "f"星期{time_data['weekday']}")try:# 月份映射字典MONTHS_MAP = {'01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr','05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug','09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'}locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')day = f"{time_data['day']}"month = MONTHS_MAP.get(f"{time_data['month']:02d}")year = str(time_data['year'])HH = f"{time_data['hour']:02d}"MM = f"{time_data['minute']:02d}"SS = f"{time_data['second']:02d}"ltime = time.strptime(day + " " + month + " " + year + " " + HH + ":" + MM + ":" + SS, "%d %b %Y %H:%M:%S")ttime = time.localtime(time.mktime(ltime))dat = "%u-%02u-%02u" % (ttime.tm_year, ttime.tm_mon, ttime.tm_mday)tm = "%02u:%02u:%02u" % (ttime.tm_hour, ttime.tm_min, ttime.tm_sec)cur_time = dat + " " + tmcommand = "sudo date --s '{}'".format(cur_time)print(command)os.system(command)return 1except Exception as e:exec_type, exec_value, exec_traceback = sys.exc_info()print('time update Exception', str(exec_type), 'error', str(exec_value))return 1