
1. 项目概述为MC68HC705J1A增添一颗“心跳”在嵌入式系统的世界里时间是一个既基础又关键的概念。无论是记录设备何时开机、何时发生故障还是实现一个简单的闹钟功能都离不开对时间的精确测量和保持。对于许多早期的8位微控制器比如我们今天要聊的MC68HC705J1A以下简称J1A它本身并没有集成实时时钟RTC功能。这意味着一旦系统断电时间信息就会丢失所有需要时间基准的功能都将失效。为了解决这个问题工程师们通常会选择外挂一颗专门的RTC芯片。这就像给你的单片机系统安装了一块独立的“电子手表”即使主系统断电这块“手表”也能依靠一颗小小的纽扣电池继续走时。DS1307就是这样一款在当年乃至现在都非常经典的RTC芯片。它不仅仅提供秒、分、时、日、月、年等完整的日历时间信息还自带56字节的非易失性RAM可以用来保存一些关键的掉电数据。更重要的是它通过I2C当时常被称为2线制总线与主控通信只需要两根IO口线这对于引脚资源极其宝贵的J1A总共才20个引脚其中14个是IO来说简直是天作之合。我手头正好有一个基于J1A的老项目需要增加时间戳记录功能翻出了这份1998年的Freescale应用笔记AN1759。虽然文档年代久远但其中关于硬件接口和底层时序驱动的设计思想至今依然适用。接下来我将结合这份笔记和我的实际调试经验为你完整拆解如何让J1A这颗“老将”与DS1307这颗“心脏”协同工作打造一个稳定可靠的嵌入式时钟系统。无论你是想复现经典设计还是学习I2C底层驱动原理这篇文章都能给你提供从电路到代码的完整参考。2. 核心芯片选型与特性解析在动手连接电路和编写代码之前我们必须先吃透两个核心器件作为主控的MC68HC705J1A和作为外设的DS1307。理解它们的特性和限制是成功设计接口的第一步。2.1 MC68HC705J1A资源有限的8位主控J1A是Motorola后为Freescale现为NXPHC05家族中引脚数最少的成员之一。它的资源以今天的眼光看非常精简内存1240字节的EPROM可擦除可编程只读存储器和64字节的RAM。这意味着我们的程序代码必须非常紧凑。IO14个双向IO口分布在Port A和Port B。我们需要从中分出两根来模拟I2C总线。无硬件I2C这是最关键的一点。J1A没有专用的I2C控制器因此所有的I2C时序——包括起始信号、停止信号、数据发送和接收、应答检测——都需要我们通过软件精确地“位操作”IO口来模拟实现也就是常说的“软件I2C”或“Bit-Banging I2C”。选择J1A来驱动DS1307是一个在有限资源下实现功能的典型案例。它迫使我们必须编写高效、可靠的底层驱动这也正是理解I2C协议精髓的好机会。2.2 DS1307功能全面的串行实时时钟DS1307是Dallas Semiconductor后并入Maxim Integrated现为ADI一部分推出的一款经典RTC。它的核心价值在于“集成”和“低功耗”。核心时间保持功能计时单元以BCD码格式提供秒、分、时、星期、日、月、年信息支持12小时制带AM/PM指示或24小时制。自动日历自动处理月末日期28, 29, 30, 31日和闰年修正有效期至2100年无需软件干预。时钟停止位寄存器0的第7位CH位。上电默认是1振荡器停止在初始化时必须将其写为0才能启动晶振开始计时。这是一个常见的“坑”如果忘了操作时钟永远不走。附加实用功能56字节非易失性RAM这是除了时钟功能外最大的亮点。当Vcc主电源掉电芯片切换到VBAT电池供电时这56字节的SRAM内容依然依靠电池保持。你可以用它来存储设备序列号、校准参数、上次关机状态、事件日志索引等关键信息。注意它只是靠电池维持数据的SRAM并非真正的EEPROM或Flash所以电池没电数据还是会丢。可编程方波输出SQW/OUT引脚可以输出1Hz, 4.096kHz, 8.192kHz, 32.768kHz四种频率的方波。1Hz信号特别有用可以连接到MCU的外部中断引脚让MCU每秒被中断一次去读取时间从而避免频繁轮询节省CPU资源。电源自动切换与低功耗当检测到主电源Vcc低于VBAT * 1.25时芯片自动切换到电池供电并禁止I2C总线访问防止数据冲突。在纯电池供电模式下典型电流小于500nA25°C一颗标准的CR2032纽扣电池可以轻松支撑数年。通信接口标准的I2C从设备设备地址固定为1101000二进制读写位构成完整的8位地址字节写地址0xD0读地址0xD1。它始终作为从机等待主机发起通信。3. 硬件接口电路设计详解硬件连接是通信的基础一个稳定可靠的电路能避免很多后续软件调试的麻烦。J1A与DS1307的接口非常简单但几个细节决定了系统的稳定性。3.1 最小系统电路连接根据应用笔记中的原理图我们可以整理出以下连接关系MC68HC705J1A 引脚连接至 DS1307 引脚功能说明注意事项PA0 (配置为输出)SCL (第6脚)I2C 串行时钟线由J1A完全控制用于产生时钟信号。PA1 (配置为开漏输出/输入)SDA (第5脚)I2C 串行数据线需软件模拟开漏输出。必须外接上拉电阻。(不连接)SQW/OUT (第7脚)方波输出如需使用1Hz中断可连接至J1A的IRQ引脚。本例未使用。系统 5VVcc (第8脚)主电源为DS1307提供正常工作电压。系统 GNDGND (第4脚)电源地与J1A共地。(不连接)VBAT (第3脚)备份电池正极接3V锂电池正极负极接GND。用于掉电保持。(不连接)X1 (第1脚), X2 (第2脚)晶振连接连接一个32.768kHz的晶振。无需外部负载电容芯片内部已集成。电路搭建核心要点上拉电阻I2C总线是开漏结构SDA和SCL线都必须通过上拉电阻接到正电源通常是Vcc。电阻值的选择需要在通信速度和功耗间折衷。笔记中使用了4.7kΩ这是一个在5V系统、标准模式100kHz下的常用值。如果总线较长或负载较多可以适当减小电阻值如2.2kΩ以提升边沿速度如果追求低功耗可以增大电阻值如10kΩ。备份电池VBAT引脚需要连接一个2.5V至3.5V的备份电源通常是一颗3V的CR2032纽扣电池。电池正极接VBAT负极接GND。务必注意极性反接可能导致芯片损坏或电池漏液。DS1307的电源切换电路非常智能你无需担心电池和主电源之间的电流倒灌问题。晶振选择DS1307内部振荡器电路是针对负载电容为12.5pF的32.768kHz晶振设计的。笔记中推荐了Epson C-001R晶振。选择其他晶振时需要关注其标称负载电容CL是否匹配。最重要的一点DS1307内部已经集成了必要的振荡电路X1和X2引脚之间只需要连接晶振本身不需要再外接两个负载电容这与许多MCU的晶振电路不同简化了设计。电源去耦良好的电源是数字电路稳定的基石。建议在DS1307的Vcc和GND引脚之间靠近芯片的位置放置一个0.1μF的陶瓷去耦电容用于滤除高频噪声。3.2 硬件设计中的避坑指南上拉电阻必不可少如果你调试时发现总线始终为低或者数据无法被正确读取首先检查上拉电阻是否焊接或连接好。电池电压监测虽然DS1307在电池供电下功耗极低但电池总会耗尽。在产品化设计中可以考虑通过MCU的ADC如果可用或简单的电压比较电路来监测VBAT电压在电池电压过低时提醒用户更换。布线考虑SCL和SDA是信号线应尽量避免与高频或大电流走线平行以减少干扰。如果条件允许可以在信号线周围布上地线进行屏蔽。4. 软件驱动用代码“模拟”I2C总线协议由于J1A没有硬件I2C我们必须用软件“模拟”出所有I2C时序。这要求我们对I2C协议的每一个状态、每一个时序要求都了如指掌。应用笔记提供了五个核心子程序构成了驱动DS1307的API。4.1 I2C总线协议状态精讲在深入代码前我们必须把I2C的几种基本状态刻在脑子里总线空闲SDA和SCL线都被上拉电阻拉高呈现高电平。起始条件S在SCL为高电平期间SDA线产生一个从高到低的下降沿。这个信号是唯一的由主机产生标志着一次传输的开始。停止条件P在SCL为高电平期间SDA线产生一个从低到高的上升沿。这个信号也是唯一的由主机产生标志着一次传输的终止。数据有效在SCL为高电平期间SDA线上的数据必须保持稳定。只有SCL为低电平时SDA线上的数据才允许改变。每个SCL高电平周期传输一位数据。应答ACK每成功传输一个字节8位后接收方需要在第9个时钟脉冲期间将SDA线拉低作为应答信号。如果接收方是DS1307从机它发送ACK如果接收方是J1A主机在读取数据时它也需要在读完前N-1个字节后发送ACK告诉从机继续发送只有在读取最后一个字节时发送“非应答NACK”即保持SDA高电平。4.2 底层驱动子程序逐行解析笔记中的代码是用68HC05汇编语言编写的。为了更清晰地理解逻辑我会用类似C语言的伪代码来描述其流程并附上关键汇编操作的意图。1. START_SER (产生起始条件)START_SER: bclr SDA, SER_PORT ; 将SDA线PA1输出低电平 bclr SCL, SER_PORT ; 将SCL线PA0输出低电平 rts ; 返回操作意图先拉低SDA再拉低SCL。注意起始条件要求SCL高时SDA产生下降沿。这个子程序执行前总线应处于空闲状态SDA和SCL均为高。因此正确的调用顺序是先确保SDA和SCL为高然后调用START_SER。在START_SER内部它直接输出SDA0, SCL0为后续的数据传输准备好“时钟线低”的初始状态。真正的“起始信号”产生于调用START_SER之前SDA从高变低的那一刻需要调用者保证。这是一个需要注意的实现细节。2. STOP_SER (产生停止条件)STOP_SER: bset SCL, SER_PORT ; 将SCL线PA0输出高电平 bset SDA, SER_PORT ; 将SDA线PA1输出高电平 rts ; 返回操作意图在数据传输结束后SCL已经是低电平。先拉高SCL然后在SCL高期间拉高SDA从而产生SDA的上升沿形成停止条件。执行完后总线回到空闲状态。3. TXD (发送一个字节并检查ACK)这是最复杂的子程序之一。流程如下设置计数器X8。将累加器A中的数据左移最高位MSB进入进位标志C。根据C的值将SDA线设置为1C1或0C0。产生一个SCL脉冲拉高再拉低将SDA上的数据位发送出去。计数器减1循环直到8位发送完毕。关键步骤检查从机应答。发送完8位后主机需要释放SDA线将PA1配置为输入然后产生第9个SCL高脉冲。在此期间主机读取SDA线如果DS1307正确接收它会将SDA拉低ACK。如果SDA为高NACK则跳转到错误处理循环。最后将SDA重新配置为输出返回。4. RXD (读取一个字节并发送ACK)将SDA线PA1配置为输入准备读取。设置计数器X8。拉高SCL在SCL高电平期间读取SDA线的电平将其值移入累加器A。拉低SCL为读取下一位做准备。计数器减1循环直到读完8位。发送ACK将SDA重新配置为输出并拉低SDA。然后产生一个SCL高脉冲。这个低电平就是主机发给从机的ACK意思是“我收到了请继续发送下一个字节”。拉低SCL返回。5. RXD_LAST (读取最后一个字节并发送NACK)流程与RXD几乎完全相同区别仅在第6步 6.发送NACK将SDA重新配置为输出并拉高SDA。然后产生一个SCL高脉冲。这个高电平就是NACK告诉DS1307“这是最后一个字节发送可以停止了。”4.3 软件驱动中的时序与稳定性技巧延时的重要性在真实的软件I2C驱动中在设置SDA/SCL电平后需要插入短暂的延时几个微秒以确保信号边沿稳定满足I2C协议对建立时间和保持时间的要求。原笔记代码为了简洁可能省略了显式的延时循环依赖于指令执行时间。在实际移植到不同主频的MCU时必须根据SCL目标频率如100kHz计算并添加必要的延时。开漏输出模拟J1A的IO口是准双向口要模拟开漏输出在输出0时直接写0在输出1时需要先将IO口配置为输入高阻态依靠外部上拉电阻拉到高电平。原代码中通过bclr SDA, DDRA将方向寄存器位清0设为输入来释放总线正是模拟了开漏输出的“高阻态1”。错误处理TXD子程序中有ACK_ERROR循环。在实际产品代码中不应死循环而应设置错误标志或尝试重传几次。5. 应用层编程读写DS1307的完整流程有了底层的五个驱动函数我们就可以像搭积木一样构建读写DS1307的完整操作序列。笔记中的测试程序清晰地展示了三个核心操作配置时钟、写入初始时间、读取当前时间。5.1 初始化与配置流程配置DS1307主要做两件事启动振荡器和设置方波输出如果需要。发送起始条件(START_SER)。发送DS1307写地址0xD0(TXD)。发送控制寄存器地址0x07(TXD)。发送控制字(TXD)。例如0x10二进制00010000的含义是BIT7 (OUT): 0 – 当方波输出禁用时SQW/OUT引脚输出低电平。BIT4 (SQWE): 1 –使能方波输出。BIT1, BIT0 (RS1, RS0): 00 – 选择方波频率为1Hz。 所以0x10配置了1Hz的方波输出。如果不需要方波控制字通常写0x00。发送停止条件(STOP_SER)。5.2 写入初始时间时间数据需要以BCD码格式写入地址0x00至0x06的连续寄存器。发送起始条件。发送DS1307写地址0xD0。发送起始寄存器地址0x00秒寄存器。连续发送7个字节的时间数据秒、分、时、星期、日、月、年。特别注意秒寄存器它的最高位BIT7是时钟停止位CH。写入0x00表示CH0启动振荡器。如果写入0x80则时钟停止。示例下午4点30分00秒星期六1998年6月20日。秒:0x00(CH0, 秒00)分:0x30(BCD码的30)时:0x64(BIT61为12小时制BIT51为PM低4位是4即0110 0100)星期:0x06(星期六)日:0x20(BCD码的20)月:0x06(BCD码的6)年:0x98(BCD码的98)发送停止条件。5.3 读取当前时间读取操作稍微复杂它由一个“伪写”操作来设置读起始地址然后才是真正的读操作。设置读指针伪写操作发送起始条件。发送DS1307写地址0xD0。发送要读取的起始寄存器地址例如0x00。发送停止条件。注意这个停止条件很重要它结束了写地址序列但没有写入数据。发起读操作并连续读取发送起始条件。发送DS1307读地址0xD1。调用RXD子程序读取第一个字节秒存入缓冲区。继续调用RXD读取后续字节分、时、星期、日、月。在读取最后一个字节年时调用RXD_LAST主机发送NACK。发送停止条件。5.4 应用层编程经验谈BCD码处理DS1307所有时间寄存器都是BCD码。MCU内部运算通常用二进制HEX所以需要进行转换。有专门的十进制调整指令如DAA或库函数来处理在资源紧张的J1A上可能需要对加减运算进行特殊处理或直接以BCD格式存储和显示。读取连续性一次读取多个连续地址时DS1307内部地址指针会自动递增。这保证了读取时间数据的一致性避免了在两次单独读取之间时钟寄存器发生变化而导致的时间错乱例如在23:59:59时读取读完秒后分进位导致读到“23:59:00”和“00:00:59”的混合体。初始化时机系统上电后应先读取一下秒寄存器的CH位。如果CH1说明时钟未运行可能是首次上电或电池耗尽需要进行初始化写入。如果CH0说明时钟正在运行通常不应重新初始化以免覆盖当前时间。6. 调试技巧与常见问题排查即使按照文档连接和编程在实际调试中也可能遇到各种问题。以下是我在项目中总结的一些排查思路和技巧。6.1 硬件层面排查时钟完全不走读取的秒数不变首要怀疑晶振未起振。用示波器探头高阻档测量X1或X2引脚应能看到32.768kHz的正弦波幅度较小约0.5Vpp。如果看不到检查晶振是否损坏、焊接是否良好。关键检查秒寄存器的CH位BIT7是否为0。如果为1振荡器被禁用。确认初始化代码中是否将0x00写入了秒寄存器。电源检查测量Vcc电压是否稳定在4.5V-5.5V之间。VBAT电池电压是否在2.5V-3.5V之间。I2C通信失败无法读写测量总线电压用万用表测量SCL和SDA线对地电压。在总线空闲时应为高电平接近Vcc。如果一直是低电平检查是否有引脚对地短路或上拉电阻是否未连接/损坏。用示波器看波形这是最直接的调试方法。触发起始条件观察SCL和SDA的波形。看起始、停止、数据位、ACK位是否清晰时序是否符合I2C规范如SCL高电平期间SDA是否稳定。检查地址确认发送的器件地址是0xD0写和0xD1读而不是0x687位地址左移一位后的值在某些库中常用。DS1307的7位地址是11010000x68加上读写位后写为110100000xD0读为110100010xD1。软件延时如果MCU主频较高而软件I2C驱动中没有足够延时可能导致SCL频率过快DS1307无法响应。尝试在SCL高低电平切换后增加NOP指令或短延时循环。6.2 软件与数据层面排查读取的时间数据乱码ACK检查确保TXD子程序中的ACK检查逻辑正确。如果DS1307没有应答后续读取的数据是无效的。时序问题在RXD子程序中读取SDA数据的时刻必须在SCL为高电平的稳定期。确保bset SCL和brclr SDA指令之间有足够的稳定时间。字节顺序确认你发送和接收的字节顺序与寄存器地址映射一致秒、分、时、星期、日、月、年。时间走不准晶振精度32.768kHz晶振本身有一定误差通常±20ppm。对于精度要求高的场合可以选择精度更高的温补晶振或者通过软件定期校准需要参考更精确的时钟源。负载电容虽然DS1307内部集成负载电容但PCB布线会引入杂散电容。如果走线很长可能会影响振荡频率。尽量让晶振靠近芯片X1、X2引脚。电池供电时间短检查VBAT引脚电流在电池供电模式下用万用表uA档串联测量VBAT引脚电流应小于1uA。如果电流过大检查PCB上是否有漏电路径或者Vcc引脚是否完全断电有些LPM关断不完全。电池选择使用质量可靠的锂锰电池如CR2032注意其容量和自放电率。6.3 利用SQW/OUT输出进行低功耗管理笔记中提到了一个高级应用将SQW/OUT的1Hz输出连接到J1A的IRQ外部中断引脚。你可以这样操作将DS1307控制寄存器配置为输出1Hz方波SQWE1, RS10, RS00。将J1A的IRQ引脚配置为下降沿或上升沿触发中断。在中断服务程序ISR中读取DS1307的时间。由于每秒只中断一次MCU在99.99%以上的时间可以处于休眠模式如果J1A支持极大地降低了系统功耗。这对于电池供电的便携设备非常有价值。7. 项目总结与扩展思考通过将MC68HC705J1A与DS1307配对我们成功为一个原本没有计时能力的8位微控制器系统赋予了精确的、掉电不丢失的日历时钟功能。这个项目虽然基于一个二十多年前的芯片组合但其蕴含的嵌入式系统设计思想——资源评估、外设扩展、协议模拟、低功耗设计——至今依然具有很高的学习价值。在实际操作中我最深的体会是时序就是生命线。软件模拟I2C时那几个微秒的延时差可能就是通信成功与失败的分水岭。务必用示波器验证波形。另一个关键是理解数据手册DS1307的时钟停止位CH、12/24小时模式位、控制寄存器配置每一个细节都写在手册里忽略任何一点都可能导致功能异常。这个设计可以很容易地移植到其他没有硬件I2C的MCU上只需要重写START_SER,STOP_SER,TXD,RXD这几个底层的位操作函数即可。对于资源更丰富的现代MCU你当然可以直接使用硬件I2C外设驱动会简单很多。但通过这个项目你真正理解了一根串行总线是如何通过两根线实现寻址、读写和握手的这种理解对于调试更复杂的I2C设备网络如连接多个传感器至关重要。最后DS1307的56字节NV RAM是一个宝藏区域。你可以用它来存储设备的唯一ID、运行时间计数器、关键配置参数甚至是简单的日志条目索引。合理利用这部分资源能让你的嵌入式系统更加智能和健壮。希望这篇详细的拆解能帮助你顺利实现自己的嵌入式时钟功能。