使用 PCF8583 实时时钟和 AVR 保持时间





5.00/5 (6投票s)
实时时钟开发板。
实时时钟开发扩展板
这是我系列开发扩展板 (DEB) 文章中的第二篇,如果您错过了第一篇 AVR LED/电平转换板,请务必查看。这些 DEB 板是为了将常用组件安装在 MPU 外部的板上,易于在开发周期中使用,并且能够插入面包板中以便于接线。在像 RTC 这样的情况下,我们将使用带有 256 X 8 位 RAM 的 PCF8583 时钟/日历设备,通过 I2C 总线协议与设备进行通信。我之前没有使用过此设备,也没有使用过任何 I2C 总线设备,但我想学习,所以就有了这个项目。尽管我使用了 AVR 处理器,但此板可与任何支持 GPIO 和中断(如果需要通知)的 MPU 配合使用。
鸣谢:此库使用 Peter Fleury 的 I2C 库 进行 I2C 通信。Peter Fleury 的其他库是极好的资源,并且在 GNU GPL 许可下开源。
我一直推迟学习 I2C 总线,因为我认为它会很难学,而且确实有点棘手,但使用 Peter Fleury 的 I2C 库使其变得相当容易,并提供了与设备通信所需的基本构建块。借助我最近从 Saleae Logic 购买的逻辑分析仪,我很快就完成了通信的设置和运行。这个出色的设备为我节省了数小时的调试时间,并且可以实际查看发送的数据。过去,由于没有此类工具,我一直没有尝试学习 I2C。下图是配置为解释 I2C 数据的逻辑分析仪的快照。

分析仪正在监测时钟线和数据线,正在发送的数据包用绿色圆圈标记起始字节,红色方块是停止字节。我们将在后面的章节中更详细地介绍数据包结构。
本文将介绍大量信息,因此内容分为硬件和软件两个主要部分,其中软件部分又进一步分为描述芯片功能、如何配置、设置和读取寄存器以及控制其使用的高级软件的章节。
硬件
完成的电路板安装在面包板上的图像如下所示,以及我们在上一篇文章中创建的 DEB 板 AVR LED/电平转换板。我通过将当前时间设置为 00:00:00.00,并将闹钟时间设置为 00:00:2.50 来设置了一个定时闹钟,当闹钟触发中断时,它会激活 LED 板上的一个灯。我意识到这非常简单,但我们将来会开发接受输入和显示结果的 DEB 板,但我们还没有达到那个阶段,所以我们只是想确保我们的板子正常工作,并且我们了解如何控制它。

从下面的原理图可以看出,这个板子实际上没什么特别的,大部分功能都内置在芯片中,我们只是在控制它。我已将设备的资料表包含在本文的源代码中,它很好地解释了大部分内部工作原理,但在描述不同的定时器模式和闹钟时变得非常模糊。至少对我来说很难理解,因此我将尝试填补本文中的一些空白,并提供与设备进行高级通信所需的工具。

该板需要 5V 供电,并需要 2/3 个 GPIO 端口进行接口,如果不需要通过闹钟接收通知,则只需要 2 个。I2C 通信需要两个 4.7K 的上拉电阻,并假设总线上没有引入其他上拉电阻。
此外,芯片上的闹钟中断引脚连接到板上的一个引脚,以便可以配置闹钟通知来执行某些操作。在这个简单的示例中,我在代码中设置当前时间和闹钟时间,并使用引脚更改中断来拦截闹钟,然后在闹钟触发时打开 LED DEB 板上的一个灯。将来,当我完成输入和输出板时,我可能会在此主题上进行扩展。我一直在开发 EADOGm126 显示板,并取得了良好的进展,并打算在不久的将来完成,但我前几天收到了 32.768 Khz 晶振,并希望让 RTC 工作。我拥有 RTC 芯片已经很长时间了,但由于某种原因直到最近才订购晶振。因此,在整理我的工作时,我将完成显示板。
开发此板的主要设计标准是:板应提供电源和控制引脚,这些引脚位于板上,当插入面包板时,提供对板通信总线和中断端口的访问。这种极简主义方法模仿了即插即用架构,虽然存在明显的差异和局限性,但在我看来,它将使原型设计和设计/概念验证阶段变得更容易、更快,因为大部分接线已经完成。这种即插即用架构也扩展到软件,因此如果设备需要特定要求,可以添加适当的板,连接线路,并将软件模块包含在您的源代码中。
PCF8583 RTC 芯片描述
该设备具有 256 X 8 位的 RAM,带有一个自动增量地址寄存器,允许数据沿任一方向顺序传输。地址空间的前 8 个字节或寄存器包含与时钟相关的信息。从 0x08 到 0x0f 的第二个 8 个字节仅在启用闹钟时有效,如果未启用闹钟,则可作为空闲空间使用。我已包含该芯片的数据表,其中显示了小时、年/日期和星期/月份寄存器的进一步细分。这些字段也可以通过控制/状态寄存器中的掩码标志位访问,该位允许读取寄存器,除了实际值之外的所有内容都被掩码。

内存地址 0x00 是控制/状态寄存器,用于定义芯片的整体行为。控制/状态寄存器和寄存器中每个位的简要描述如下图所示。
控制/状态寄存器

计时器和闹钟标志根据您当前运行的模式规则设置,并且必须清除它们才能识别后续闹钟。
当控制/状态寄存器中的闹钟使能位设置时,从 0x08 到 0x0f 的寄存器变为活动状态,闹钟功能由寄存器位置 0x08 处的闹钟控制字节决定。
功能模式定义了 4 种操作模式,但本文中我们只关注时钟模式 32.768 kHz 模式,因为我们为此目的在板上包含了 32.768 晶振。
如果控制/状态寄存器的闹钟使能位设置,则闹钟控制寄存器变为活动状态,并用于定义闹钟的行为。
闹钟寄存器

我们将在这里查看的定时器模式是时钟、定时器和闹钟定时器模式,其中两种定时器模式非常相似,定时器从零或预定义值开始计数到 99,然后重置为零,闹钟定时器模式从零开始计数,直到达到预定义值,此时如果配置了中断,则会触发中断。以下部分描述了如何配置定时器、涉及哪些寄存器以及演示用法的伪代码。
时钟模式
使用时钟模式基本上就是你对闹钟的理解,即你设置当前时间,设置闹钟时间,启用闹钟,当闹钟响起时禁用闹钟。此模式使用闹钟寄存器 0x09-0x0e 作为目标闹钟时间,如果闹钟寄存器中的闹钟中断使能位设置,则当当前时间等于闹钟时间时,将以闹钟控制寄存器中时钟闹钟功能位 4-5 指定的频率触发中断。
This mode mode requires that; Control/Status Register; bit 2 set to enable alarms. Alarm Register; bits 4-5 set to one of the clock alarm modes. bit 7 alarm interrupt enable. Example pseudocode: Setting the chip for a dated alarm Stop counter - Control/Status register bit 7 = 0 Set Clock Time - Clock time/date/ registers 0x01 - 0x06 Set Alarm time - Alarm time/date registers 0x09 - 0x0e Enable Alarm - Control/Status register bit 2 = 1 Start counter Control/Status register bit 7 = 0
定时器模式
这种模式让我想起了秒表,寄存器 0x07 用作计数器,从 0x00 或预定义值开始计数到 99,此时计数器返回零,如果配置了,则触发中断。
This mode mode requires that; Control/Status Register; bit 2 enable alarms. Alarm Register; bits 0-3 count interval the timer uses. bit 3 timer interrupt enable bit 7 alarm interrupt enable Register 0x07 - Start value for timer as needed. Example pseudocode: Setting timer to run Stop counter Control/Status register bit 7 = 1 Set Alarm Time - register 0x07 [optional] Set count interval - Alarm register bit3 0-2 Enable Alarm - Alarm register bit 3 Start counter Control/Status register bit 7 = 0
定时器闹钟模式
将此模式视为类似于鸡蛋计时器,其中设置了一个时间间隔,启用计时器,当计数器和间隔时间匹配时,如果配置了中断,则会触发中断。
This mode mode requires that; Control/Status Register; bit 2 be set to enable alarms. Alarm Register; bits 0-3 count interval the timer uses. bit 6 alarm timer interrupt enable bit 7 alarm interrupt enable Register 0x0f - Compare timer value. Example pseudocode: Setting timer to run Stop counter Control/Status register bit 7 = 1 Set Alarm Timer register (0x0f) Set count interval - Alarm register bit3 0-2 Enable Alarm Timer Alarm register bit 6 Start counter Control/Status register bit 7 = 0
您使用的定时器模式取决于您的需求,但源代码中提供的库应该能够满足您的大部分需求,如果不能,也可以轻松进行修改以适应您的需求。
软件
为了更好地理解本节,我们将把它分解成更容易管理的块,因为有很多内容。在以下章节中,我们将涵盖此处列出的主题。该软件是为 ATMega328p 处理器编写的,但可以通过修改 Clock.h 模块中的适当定义进行调整。
- 使用 I2C 总线 - 与设备通信有关。
- 与芯片交互
- 高级控制 - 时钟库
使用 I2C 总线
正如章节名称所暗示的,我们将探讨如何使用 I2C 总线,而无需深入探讨其许多机制。有关该主题的精彩阅读,此处有一个不错的 I2C 教程。
简而言之,I2C 通信使用两根线;一根用于时钟,一根用于数据。幸运的是,ATMega328p 芯片在硬件中处理大部分 I2C 总线协议,而 Peter Fleury 库处理与芯片的所有低级交互。对通信协议中使用的包方案的非常简单的描述是,对于每次传输,一个起始字节(包括从机地址),后跟任何所需数据,重新启动,最后是一个停止字节以表示包的结束。此协议和 I2C 库假设这是一个只有一个主节点网络。尽管芯片说它可以处理多个主节点,但实现起来却是一项相当艰巨的任务,因为您必须开始考虑冲突检测以及如何处理它们。我将其比作异步处理,您必须具备线程意识。对于我的大部分工作,单个主节点网络就可以正常工作。
图 17-19 展示了与设备交换数据的各种数据包格式。在此阶段,我们正在构建通信堆栈中的另一个层,其方法调用 I2C 库的基本方法。此抽象级别用于以数据包的形式向/从设备发送/接收信息,而不考虑内容。
与芯片交互
芯片识别三种数据包格式,它们在以下章节中描述。
主设备向从设备接收器(写入)模式传输。图 17 显示了一个数据包,其中数据从主设备发送到从设备,最常用于设置寄存器值或一组连续的寄存器值。这是数据从主设备到从设备的单向传输。
用于设置定时器寄存器的伪代码
Send Start character (includes slave address) Send Word Address (Address to set or start at) Send Data(s) Send Stop character

代码中设置时间的示例
//send start byte with device address and write mode status = i2c_start(DeviceAddress+I2C_WRITE); //If device is responding if ( !status ) { //Write the register address to start writting. //For clock time wa = 0x01, for alarm time wa = 0x09 i2c_write(wa); //Write the values sequentially to the appropriate registers. i2c_write(time->hundreds); i2c_write(time->seconds); i2c_write(time->minutes); i2c_write(time->hours); } //Send the stop byte i2c_stop();主设备设置工作地址后读取(带字地址,读取数据)。
图 18 显示了主设备发送请求以从从设备读取给定起始寄存器数量的预定字节数据。
用于读取时间寄存器的伪代码
Send Start character (includes slave address) Send Word Address (Register address to start at) Send a Restart character Read Data with ACK Read Data with NAK (last item) Send Stop character

代码中读取时间的示例
//send start byte with device address and write mode status = i2c_start(DeviceAddress+I2C_WRITE); if ( !status ) { i2c_write(wa); status = i2c_rep_start(DeviceAddress+I2C_READ); if (!status) { _current_time.hundreds = i2c_readAck(); _current_time.seconds = i2c_readAck(); _current_time.minutes = i2c_readAck(); _current_time.hours = i2c_readNak(); } } i2c_stop();主设备在第一个字节后立即读取从设备(读取模式)。
图 19 显示了主设备发送请求,要求从设备从地址 0x00 开始接收数据,并一直读取直到发出 ReadNAK。利用芯片的自动增量功能来读取连续寄存器。

库如何使用的示例
在此示例中,我们定义了一个时钟闹钟,它在启用闹钟后 2.5 秒触发中断。
/* * RealTimeClock.c * * Created: 2/10/2012 11:17:26 PM * Author: Mike */ #ifndef FCPU #define F_CPU 8000000L #endif #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> #include "i2cmaster.h" #include "clock.h" void pci_init(); void io_init(); void SetClockModeAlarm(); void SetTimeModeAlarm(); void SetAlarmTimeNodeAlarm(); ISR(PCINT1_vect) { PORTB = (1<<PB5); } int main(void) { unsigned char dummy; bool line = false; i2c_init(); // init I2C interface io_init(); StopCounter(); //Pin change interrupt init pci_init(); //uncomment the one of the following SetClockModeAlarm(); StartCounter(0x04); sei(); while(1) { ReadTime(false); _delay_ms(100); } } void io_init() { DDRB = (1<<PB5); DDRC |= (1<<PC4) | (1<<PC5); } void pci_init() { PCIFR = (1<<PCIF1); PCMSK1 = (1<<PCINT8); PCICR = (1<<PCIE1); } void SetClockModeAlarm() { Time now; Date date; WriteRegister(ALARM_REG, 0xb0); date.year = 0x12; date.month = 0x02; SetDate(&date, false); SetDate(&date, true); now.hundreds = 0x00; now.seconds = 0x00; now.minutes = 0x13; now.hours = 0x08; now.r4.h_1224 = 1; SetTime(&now, false); now.hundreds = 0x32; now.seconds = 0x02; SetTime(&now, true); }
摘要
创建 PCF8583 设备库和本文让我对设备的内部工作原理有了很多了解。在新购买的逻辑分析仪的帮助下,工作进展得更快,坦率地说,如果我没有它,我可能还会一直推迟学习 I2C 总线协议,因为我脑子里认为它会很难学。
我将在不久的将来创建更多的 DEB 板,并打算在完成输入和显示板后重新使用此板。也许我们会尝试一个可编程的台式计时器?