Kernel: Add generic DS1202/DS1302 Serial Real Time Clock driver
authorWill Sowerbutts <will@sowerbutts.com>
Thu, 1 Jan 2015 13:58:49 +0000 (13:58 +0000)
committerWill Sowerbutts <will@sowerbutts.com>
Thu, 1 Jan 2015 14:49:54 +0000 (14:49 +0000)
Kernel/dev/ds1302.c [new file with mode: 0644]
Kernel/dev/ds1302.h [new file with mode: 0644]

diff --git a/Kernel/dev/ds1302.c b/Kernel/dev/ds1302.c
new file mode 100644 (file)
index 0000000..065b080
--- /dev/null
@@ -0,0 +1,214 @@
+/*-----------------------------------------------------------------------*/
+/* DS1202 and DS1302 Serial Real Time Clock driver                       */
+/* 2014-12-30 Will Sowerbutts                                            */
+/*-----------------------------------------------------------------------*/
+
+#include <kernel.h>
+#include <kdata.h>
+#include <stdbool.h>
+#include <printf.h>
+#include <ds1302.h>
+
+static void ds1302_send_byte(uint8_t byte)
+{
+    uint8_t i;
+
+#ifdef DS1302_DEBUG
+    kprintf("ds1302: send byte 0x%x\n", byte);
+#endif
+    /* drive the data pin */
+    ds1302_set_pin_data_driven(true);
+
+    /* clock out one byte, LSB first */
+    for(i=0; i<8; i++){
+        ds1302_set_pin_clk(false);
+        /* for data input to the chip the data must be valid on the rising edge of the clock */
+        ds1302_set_pin_data(byte & 1);
+        byte >>= 1;
+        ds1302_set_pin_clk(true);
+    }
+}
+
+static uint8_t ds1302_receive_byte(void)
+{
+    uint8_t i, b;
+
+    /* tri-state the data pin */
+    ds1302_set_pin_data_driven(false);
+
+    /* clock in one byte, LSB first */
+    b = 0;
+    for(i=0; i<8; i++){
+        ds1302_set_pin_clk(false);
+        b >>= 1;
+        /* data output from the chip is presented on the falling edge of each clock */
+        /* note that output pin goes high-impedance on the rising edge of each clock */
+        if(ds1302_get_pin_data())
+            b |= 0x80;
+        ds1302_set_pin_clk(true);
+    }
+
+    return b;
+}
+
+static uint8_t from_bcd(uint8_t value)
+{
+    return (value & 0x0F) + (10 * (value >> 4));
+}
+
+void ds1302_read_clock(uint8_t *buffer, uint8_t length)
+{
+    uint8_t i;
+    irqflags_t irq = di();
+
+    ds1302_set_pin_ce(true);
+    ds1302_send_byte(0x81 | 0x3E); /* burst read all calendar data */
+    for(i=0; i<length; i++){
+        buffer[i] = ds1302_receive_byte();
+#ifdef DS1302_DEBUG
+        kprintf("ds1302: received byte 0x%x index %d\n", buffer[i], i);
+#endif
+    }
+    ds1302_set_pin_ce(false);
+    irqrestore(irq);
+}
+
+/* define CONFIG_RTC in platform's config.h to hook this into timer.c */
+uint8_t rtc_secs(void)
+{
+    uint8_t buffer;
+    ds1302_read_clock(&buffer, 1);   /* read out only the seconds value */
+    return from_bcd(buffer & 0x7F); /* mask off top bit (clock-halt) */
+}
+
+/****************************************************************************/
+/* Code below this point used only once, at startup, so we want it to live  */
+/* in the DISCARD segment. sdcc only allows us to specify one segment for   */
+/* each source file. This "solution" is a bit (well, very) hacky ...        */
+/****************************************************************************/
+static void DISCARDSEG(void) __naked { __asm .area _DISCARD __endasm; }
+
+/* we avoid using 32x32 bit multiply ds1302_read_rtc, because it causes sdcc to pull 
+   in a huge __mullong helper -- which is nearly 500 bytes! */
+static uint32_t multiply_8x32(uint8_t a, uint32_t b) __naked /* return a * b */
+{
+    __asm
+        ; WRS -- simple 8x32 multiply routine
+        ; low 16 bits in main registers, high 16 bits in alternate registers
+        ; DE contains b, shifts left, HL contains accumulated result, A contains a, shifts right.
+        ld iy, #0       ; load parameters from stack
+        add iy, sp
+        ld a, 2(iy)     ; load a
+        ld e, 3(iy)     ; load low 16 bits of b
+        ld d, 4(iy)
+        and a           ; clear carry flag
+        sbc hl, hl      ; low result = 0
+        exx
+        sbc hl, hl      ; high result = 0
+        ld e, 5(iy)     ; load high 16 bits of b
+        ld d, 6(iy)
+        exx
+mulgo:                  ; loop executes at most 8 times (depends on highest bit set in a)
+        or a            ; test value of A
+        jr z, muldone   ; if A is now zero we are done
+        rra             ; shift A right one bit, rotate low bit into carry flag
+        jr nc, mulnext  ; if low bit was zero, skip the accumulate
+        add hl, de      ; add low 16 bits
+        exx
+        adc hl, de      ; add high 16 bits
+        exx
+mulnext:
+        and a           ; clear carry flag
+        rl e            ; double low 16 bits
+        rl d
+        exx
+        rl e            ; double high 16 bits
+        rl d
+        exx
+        jr mulgo        ; go again
+muldone:
+        exx             ; get the high 16 bits into DE
+        push hl
+        exx
+        pop de
+        ret             ; return with 32-bit result in DEHL, as per sdcc callng convention
+    __endasm;
+    a; b;  /* squelch compiler warning */
+}
+
+static const uint16_t mktime_moffset[12] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
+
+uint32_t ds1302_read_rtc(void)
+{
+    uint32_t ret;
+    uint8_t buffer[7];
+    uint8_t year, month, day, hour, minute, second;
+
+    ds1302_read_clock(buffer, 7); /* read all calendar data */
+
+    year   = from_bcd(buffer[6]);
+    month  = from_bcd(buffer[4] & 0x1F);
+    day    = from_bcd(buffer[3] & 0x3F);
+    if(buffer[2] & 0x80) /* AM/PM 12-hour mode */
+        hour   = from_bcd(buffer[2] & 0x1F) + (buffer[2] & 0x20 ? 12 : 0);
+    else /* sensible 24-hour mode */
+        hour   = from_bcd(buffer[2] & 0x3F);
+    minute = from_bcd(buffer[1]);
+    second = from_bcd(buffer[0] & 0x7F);
+
+    if(year < 70)
+        year += 100;
+
+    /* following code is based on utc_mktime() from ELKS 
+       https://github.com/jbruchon/elks/blob/master/elkscmd/sh_utils/date.c */
+
+    /* uses zero-based month index */
+    month--;
+
+    /* calculate days from years */
+    ret = multiply_8x32(year - 70, 365);
+
+    /* count leap days in preceding years */
+    ret += ((year - 69) >> 2);
+
+    /* calculate days from months */
+    ret += mktime_moffset[month];
+
+    /* add in this year's leap day, if any */
+    if (((year & 3) == 0) && (month > 1)) {
+        ret ++;
+    }
+
+    /* add in days in this month */
+    ret += (day - 1);
+
+    /* convert to hours */
+    ret = multiply_8x32(24, ret);
+    ret += hour;
+
+    /* convert to minutes */
+    ret = multiply_8x32(60, ret);
+    ret += minute;
+
+    /* convert to seconds */
+    ret = multiply_8x32(60, ret);
+    ret += second;
+
+    /* return the result */
+    return ret;
+}
+
+void ds1302_init(void)
+{
+    time_t tod;
+
+    /* initialise the hardware into a sensible state */
+    ds1302_set_pin_data_driven(true);
+    ds1302_set_pin_data(false);
+    ds1302_set_pin_ce(false);
+    ds1302_set_pin_clk(false);
+
+    tod.high = 0;                   /* until 2106 */
+    tod.low = ds1302_read_rtc();
+    wrtime(&tod);
+}
diff --git a/Kernel/dev/ds1302.h b/Kernel/dev/ds1302.h
new file mode 100644 (file)
index 0000000..0c488e5
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef __DS1302_DOT_H__
+#define __DS1302_DOT_H__
+
+/* public interface */
+void ds1302_init(void);
+uint8_t rtc_secs(void);
+/* consult the DS1302 datasheet for data format;
+   http://datasheets.maximintegrated.com/en/ds/DS1302.pdf table 3 */
+void ds1302_read_clock(uint8_t *buffer, uint8_t length);
+
+/* platform code must provide these functions */
+void ds1302_set_pin_clk(bool state);
+void ds1302_set_pin_ce(bool state);
+void ds1302_set_pin_data(bool state);
+bool ds1302_get_pin_data(void);
+void ds1302_set_pin_data_driven(bool state); /* 0=tristate for input, 1=driven for output */
+
+#endif