DS12C887时钟模块, STC89和STC12的代码实现

DS12C887是时钟芯片DS12C885集成了电池和晶振的版本. 如果拆掉DS12C887的外壳, 能看到里面就是DS12C885.

功能特性

  • 能输出世纪、年、月、日、时、分、秒等时间信息
  • 集成电池, 外部掉电时, 时间不会丢失.
  • 有12小时和24小时两种模式. 在12小时制模式中, 用AM和PM区分上午和下午
  • 时间的存储方式有两种: 一种用二进制数表示, 另一种是用BCD码表示
  • 带有128 byte RAM, 其中
    • 11 byte用来存储时间信息
    • 4 byte用来存储DS12C887的控制信息, 称为控制寄存器
    • 113 byte 通用RAM, 供用户使用.
  • 可以对DS12C887进行编程, 实现多种方波输出
  • 可对其内部的三路中断通过软件进行屏蔽
  • 芯片内部有一个精密的温度补偿电路用来监视Vee的状态, 如果检测到主电源故障, 该器件可以自动切换到备用电源供电
  • VBAeKUP引脚用于支持可充电电池或超级电容, 内部包括一个始终有效的涓流充电器.
  • 可以通过一个读写复用的单字节并行接口访问, 该接口支持Intel和Motorola模式

引脚功能

GND、 VCC
VCC接+5V输入, 当VCC输入为+5V时, 用户可以访问DS12C887内RAM中的数据, 并可对其进行读写操作. 当VCC的输入小于+4.25V时, 禁止用户对内部RAM进行读写操作, 此时用户不能正确获取芯片内的时间信息. 当VCC的输入小于+3V时, DS12C887会自动将电源发换到内部自带的锂电池上.

MOT
模式选择脚. DA12C887有两种工作模式: Motorola模式和Intel模式,

  • MOT接VCC时是Motorola模式,
  • MOT接GND或者浮空时, 是Intel模式. 一般使用Intel模式.

SQW
方波输出脚. 当供电电压VCC大于4.25V时, SQW脚可进行方波输出, 此时用户可以通过对控制寄存器编程来得到13种方波信号的输出

AD0-AD7
地址和数据复用的双向传输总线,

  • 地址信息出现在总线周期的前半部分,然后在AS的下降沿被锁存.
  • 写入数据在Motorola模式时在DS的下降沿被锁存, 在Intel模式时在R/W的上升沿被锁存
  • 在读周期, 输出的数据在DS(Motorola模式时为DS和R/W高电平, Intel模式时为DS低电平R/W高电平)的后沿
  • 在Motorola模式下DS变低电平, 或Intel模式下DS变高电平, 会结束读周期回到高阻态

简化一下, 对于Intel模式

  • 地址信息在AS的下降沿被锁存.
  • 写入数据在R/W的上升沿被锁存
  • 在读周期, 输出的数据在DS低电平, R/W高电平的后沿
  • DS变高电平, 会结束读周期回到高阻态

AS
Address Strobe, 地址闸(选通输入)脚, 在进行读写操作时, AS的下降沿将AD0-AD7上出现的地址信息锁存到DS12C887上, 而下一个上升沿清除AD0-AD7上的地址信息, 不论片选CS是否有效, DS12C887都将执行该操作.
Address Strobe Input. A positive-going address-strobe pulse serves to demultiplex the bus. The falling edge of AS causes the address to be latched within the device. The next rising edge that occurs on the AS bus clears the address regardless of whether CS is asserted. An address strobe must immediately precede each write or read access. If a write or read is performed with CS deasserted, another address strobe must be performed prior to a read or write access with CS asserted.

DS/RD
Data Strobe or Read Input, 数据闸(选通)或读输入脚, 该引脚有两种工作模式

  • MOT接VCC时, 为Motorola工作模式, 在这种工作模式中, 每个总线周期的后一部分的DS为高电平, 被称为数据选通, 在读操作中, DS的上升沿使DS12C887将内部数据送往总线AD0-AD7上, 以供外部读取, 在写操作中, DS的下降沿将使总线 AD0-AD7上的数据锁存在DS12C887中.
  • MOT接地时, 为Intel模式, 这时DS脚的功能为输出使能(Output Enable)信号, 定义读取的周期

R/W
读/写输入端. 该管脚也有2种工作模式

  • 当MOT接VCC时, 工作在Motorola模式, 此时该引脚的作用是区分进行的是读操作还是写操作, 当R/W为高电平时为读操作, R/W 为低电平时为写操作.
  • 当MOT接GND时, 工作在Intle模式, 此时该引脚仅作为RAM上的一个"可写"信号, 作为写允许输入, 数据在信号上升沿被锁存. the R/W signal is an active-low signal. In this mode, the R/W pin operates in a similar fashion as the write-enable signal (WE) on generic RAMs. Data are latched on the rising edge of the signal.

RESET
低电平有效

IRQ
中断请求输出, 低电平有效. 当对应的中断开启且中断对应的bit有效时输出低电平. 读取C寄存器可以清除中断. IRQ脚是一个开漏输出, 需要外部的上拉电阻接至VCC
Active-Low Interrupt Request Output. The IRQ pin is an active-low output of the device that can be used as an interrupt input to a processor. The IRQ output remains low as long as the status bit causing the interrupt is present and the corresponding interrupt-enable bit is set.
The processor program normally reads the C register to clear the IRQ pin. The RESET pin also clears pending
interrupts. When no interrupt conditions are present, the IRQ level is in the high-impedance state. Multiple interrupting devices can be connected to an IRQ bus, provided that they are all open drain. The IRQ pin is an open-drain output and requires an external pullup resistor to V CC.

CS
片选, 低电平有效, CS必须在以下状态中保持低电平:

  • Motorola模式: DS和AS
  • Intel模式: DS和RW

片内数据的地址和范围

控制地址

  • 0AH
  • 0BH
  • 0CH
  • 0DH

RAM地址

  • 0EH - 31H
  • 33H - 7FH

时钟数值地址 Binary Mode (DM = 1)

  • 00H: 低六位, 秒, [0, 3B]
  • 01H: 低六位, 秒, [0, 3B] 闹钟
  • 02H: 低六位, 分, [0, 3B]
  • 03H: 低六位, 分, [0, 3B] 闹钟
  • 04H: 低五位, 时, [0, 17] (24小时制)
  • 05H: 低五位, 时, [0, 17] (24小时制) 闹钟
  • 06H: 低二位, 日, [0, 07] 周中日
  • 07H: 低五位, 日, [0, 1F] 月中日
  • 08H: 低四位, 月, [0, 0C] 月
  • 09H: 低七位, 年, [0, 63] 00至99

Binary模式下, 没有世纪信息

时钟数值地址 BCD Mode (DM = 0)

BCD模式下, 因为是用于直接显示, 因此在存储中, 每个值都根据实际的显示位数被拆成一至二个数字

  • 32H, 值[00, 99], BCD模式下有世纪信息

STC89C52 驱动 DS12C877

连接

  • 操作DS12C887时钟芯片共需要13条信号线, 分别是并行数据地址复用线ADO~AD7, CS, AS, R/W, DS 和 IRQ
  • MOD接GND
  • RESET接VCC
  • DS, AS, R/W, CS分别连接P1.0 - P1.4
  • IRQ是DS12C887的中断请求, 需要连接STC89C52外部中断, 这里连接到P3.2
左侧
MOT => GND
AD0 => P0.0
...
AD7 => P0.7
GND => GND

右侧
VCC => VCC
SQW
IRQ => P3.2
RESET => VCC
DS  => P1.0
RW  => P1.1
AS  => P1.2
CS  => P1.4

USB2TTL连线

TX  => P3.0
RX  => P3.1

Keil 51 代码

读取时间信息的测试代码

#include <reg52.h>
#include <stdio.h>

typedef unsigned int u16;
typedef unsigned char u8;

u8 year, month, date, hour, minute, second, week_day;
u8 alarm_hour, alarm_minute, alarm_second;
u8 reg_a, reg_b, reg_c;

void delay(u16 z) {
  u16 x,y;
  for(x=z;x>0;x--)
    for(y=110;y>0;y--);
}

sbit DS12_DS  = P1^0;
sbit DS12_RW  = P1^1;
sbit DS12_AS  = P1^2;
sbit DS12_CS  = P1^4;
sbit DS12_IRQ = P3^2;

void DS12C887_write(u8 addr, u8 dat)
{
  delay(1);
  DS12_CS = 0; //  ds12c887de 使能端 cs=0
  delay(1);
  DS12_AS=1;
  DS12_DS=1;
  DS12_RW=1;
  delay(1);
  P0 = addr;
  delay(1);
  DS12_AS=0;
  DS12_RW=0;
  P0 = dat;
  DS12_RW=1;
  DS12_AS=1;
  delay(1);
  DS12_CS = 1; //  ds12c887de 使能端 cs=1
  delay(1); 
}

u8 DS12C887_read(u8 addr)
{
  u8 ds_date; 
  DS12_AS=1;
  DS12_DS=1;
  DS12_RW=1;
  delay(1);
  DS12_CS = 0;
  delay(1);
  P0 = addr;
  delay(1);
  DS12_AS=0;
  DS12_DS=0;
  P0 = 0XFF;
  ds_date=P0;
  DS12_DS=1;
  DS12_AS=1;
  DS12_CS = 1;
  return ds_date;
}

void DS12C887_init()
{ 
    DS12C887_write(0x0b, 0x26);
    DS12C887_write(0x0a, 0x20);
}

void main()
{
  P0 = 0x00; // P0口清零
  P1 = 0xff; // P1口全1

  DS12C887_init();//ds12c887 初始化

  EA  =1; // 开启中断, 开启TIM1中断, 开启外部中断0
  IT0 =1;
  EX0 =1;

  // 初始化UART
  TMOD = 0x20;
  SCON = 0x40;
  TH1 = 256 - 11.0592 * 1000 * 1000 / 12 / 32 / 9600 + 0.5;
  TCON |= 0x40;
  SCON |= 0x02;
  // 初始化UART结束

  /* 初始化写入
  DS12C887_write(9,11);     // Year
  DS12C887_write(8,8);      // Month
  DS12C887_write(7,7);      // Date
  DS12C887_write(6,7);      // Week Day
  DS12C887_write(4,9);      // Hour
  DS12C887_write(2,27);     // Minute
  DS12C887_write(0,25);     // Second
  */ 

  while(1) {
    second       = DS12C887_read(0x00);
    alarm_second = DS12C887_read(0x01);
    minute       = DS12C887_read(0x02);
    alarm_minute = DS12C887_read(0x03);
    hour         = DS12C887_read(0x04);
    alarm_hour   = DS12C887_read(0x05);
    date         = DS12C887_read(0x07);
    month        = DS12C887_read(0x08);
    year         = DS12C887_read(0x09);

    reg_a        = DS12C887_read(0x0a);
    reg_b        = DS12C887_read(0x0b);
    reg_c        = DS12C887_read(0x0c);

    printf("%bX-%bX-%bX ", reg_a, reg_b, reg_c);
    printf("%bX-%bX-%bX ", year, month, date);
    printf("%bX:%bX:%bX
", hour, minute, second);
  }      
}

void exter() interrupt 0
{   
     // 这里处理中断
}

SDCC代码(基于HML_FwLib_STC89)

下面的代码, 都是通过IRQ脚的中断来实现的, 启用中断后, DS12C887每秒都会将IRQ脚电压拉低, 此时0x0C的IRQF位会被置1, 如果不清除这个标志位, IRQ下次就不会被触发, 清除标志位根据手册Any time the IRQF bit is 1, the IRQ pin is driven low. This bit can be cleared by reading Register C or with a RESET.,

  • 读取寄存器0x0C
  • 使用RESET

如果需要基于下面的代码做改动, 读取寄存器0x0C这行reg_c = DS12C887_read(0x0c);要留意不能随便删, 不然只显示一次就再没新数据了.

#include "hml/hml.h"

#define DS12_DS  P1_0
#define DS12_RW  P1_1
#define DS12_AS  P1_2
#define DS12_CS  P1_4
#define DS12_IRQ P3_2

uint8_t reg_a, reg_b, reg_c;
uint8_t year, month, date, week_day, hour, minute, second;
uint8_t alarm_hour, alarm_minute, alarm_second;

void DS12C887_write(uint8_t addr, uint8_t w_data)
{
    DS12_CS = 0;
    NOP();
    DS12_AS = 1;
    DS12_DS = 1;
    DS12_RW = 1;
    NOP();
    P0 = addr;
    NOP();
    DS12_AS = 0;
    DS12_RW = 0;
    P0 = w_data;
    DS12_RW = 1;
    DS12_AS = 1;
    NOP();
    DS12_CS = 1;
}

uint8_t DS12C887_read(uint8_t addr)
{
    uint8_t ds_date;
    DS12_AS = 1;
    DS12_DS = 1;
    DS12_RW = 1;
    DS12_CS = 0;
    P0 = addr;
    DS12_AS = 0;
    DS12_DS = 0;
    P0 = 0XFF;
    ds_date = P0;
    DS12_DS = 1;
    DS12_AS = 1;
    DS12_CS = 1;
    return ds_date;
}

void print_time(void)
{
    reg_a  = DS12C887_read(0x0a);
    reg_b  = DS12C887_read(0x0b);
    reg_c  = DS12C887_read(0x0c);

    second = DS12C887_read(0x00);
    minute = DS12C887_read(0x02);
    hour   = DS12C887_read(0x04);
    week_day = DS12C887_read(0x06);
    date   = DS12C887_read(0x07);
    month  = DS12C887_read(0x08);
    year   = DS12C887_read(0x09);

    alarm_second = DS12C887_read(0x01);
    alarm_minute = DS12C887_read(0x03);
    alarm_hour   = DS12C887_read(0x05);

    UART_sendHex(reg_a);
    UART_sendByte('-');
    UART_sendHex(reg_b);
    UART_sendByte('-');
    UART_sendHex(reg_c);
    UART_sendByte(' ');
    UART_sendHex(year);
    UART_sendByte('-');
    UART_sendHex(month);
    UART_sendByte('-');
    UART_sendHex(date);
    UART_sendByte(' ');
    UART_sendHex(hour);
    UART_sendByte(':');
    UART_sendHex(minute);
    UART_sendByte(':');
    UART_sendHex(second);
    UART_sendByte('
');
}

void DS12C887_init()
{ 
    // 0B: SET PIE AIE UIE SQWE DM 24/12 DSE
    DS12C887_write(0x0b, 0x36);
    // 0A: UIP DV2 DV1 DV0 RS3 RS2 RS1 RS0
    DS12C887_write(0x0a, 0x20);  
    alarm_second = DS12C887_read(0x01);
    alarm_minute = DS12C887_read(0x03);
    alarm_hour   = DS12C887_read(0x05);
}

void sys_init(void)
{
    UART_configTypeDef uc;

    uc.baudrate          = 115200;
    uc.baudGenerator     = PERIPH_TIM_2;
    uc.interruptState    = DISABLE;
    uc.interruptPriority = UTIL_interruptPriority_0;
    uc.mode              = UART_mode_1;
    uc.multiBaudrate     = DISABLE;
    uc.receiveState      = ENABLE;

    UART_config(&uc);
    enableAllInterrupts();
}

void main(void)
{
    DS12C887_init();
    sys_init();

    EA = 1;
    IT0 = 1;
    EX0 = 1;

    /*
    DS12C887_write(0x00, 50); //sec
    DS12C887_write(0x01, 0);
    DS12C887_write(0x02, 58); //min
    DS12C887_write(0x03, 0);
    DS12C887_write(0x04, 17); //hour
    DS12C887_write(0x05, 0);
    DS12C887_write(0x06, 4);  //week
    DS12C887_write(0x07, 21); //day
    DS12C887_write(0x08, 8);  //moth
    DS12C887_write(0x09, 14); //year
    */
    while (1);
}

void Timer0IRQ(void) __interrupt (0)
{
    print_time();
}

观察时序

这是手册上的时序说明(Intel mode, read)

用逻辑分析仪进行观察, 采样率12MHz, 单次读取(AD0-AD3)

用逻辑分析仪进行观察, 采样率12MHz, 单次读取(AD4-AD7)

连续读取, 依次读取了0x0A, 0x0B, 0x0C, 0x00(AD0-AD3)

连续读取, 依次读取了0x0A, 0x0B, 0x0C, 0x00(AD4-AD7)

观察记录

  • 单次读取之间, 间隔27us
  • CS下拉后2.16us, 往AD口输入地址
  • DS下拉后, AD口的地址数据立即被清零
  • DS下拉后2.16us, AD出现数据, 此时可以读出数据
  • DS上拉后, AD数据立即上拉(全1)
  • CS -> AS -> DS, 下拉和上拉之间的间隔都是1us

STC12操作DS12C887

在STC12C5AxxS2系列上, 使用STC89的代码无法正常读取, 因为STC12的GPIO准双向模式较STC89加强了电流能力, 在DS12C887返回数据时, 有一定概率会被P0=0xFF;干扰. 解决的方案是在给完地址, DS下拉后, 立即将P0口的状态变为高阻态, 在读取完返回数据后, 再将P0口设置回准双向口. 代码如下, 根据DS12C887手册, 几个下拉时间点之间的间隔要求都是几十到一百个ns, 所以不需要延迟.

void DS12C887_write(uint8_t addr, uint8_t w_data)
{
    DS12_CS = 0;
    DS12_AS = 1;
    DS12_DS = 1;
    DS12_RW = 1;
    P0 = addr;
    DS12_AS = 0;
    DS12_RW = 0;
    P0 = w_data;
    DS12_RW = 1;
    DS12_AS = 1;
    DS12_CS = 1;
}

uint8_t DS12C887_read(uint8_t addr)
{
    uint8_t ds_date;
    DS12_AS = 1;
    DS12_DS = 1;
    DS12_RW = 1;
    DS12_CS = 0;
    P0 = addr;
    DS12_AS = 0;
    DS12_DS = 0;
    /**
     * Set P0 to high impedance. This is the tricky part, if the mode remains quasi-bidirectional,
     * you need to set P0 = 0xFF in order to get output data, but this will also interfere with the output.
     * This is quite different from STC89.
     */
    P0M0 = 0x00;
    P0M1 = 0xFF;
    ds_date = P0;
    DS12_DS = 1;
    DS12_AS = 1;
    DS12_CS = 1;
    /** Restore P0 to default quasi-bidirectional */
    P0M0 = 0xFF;
    P0M1 = 0x00;
    return ds_date;
}

参考

原文地址:https://www.cnblogs.com/milton/p/15168730.html