Микроконтроллеры AVR: пример работы с часами реального времени DS1302

14 мая 2018

Часы реального времени (Real Time Clock, RTC) — это интегральная схема, предназначенная для отслеживания хода времени. RTC питаются от батарейки, что позволяет им продолжать работу даже тогда, когда все остальные части устройства обесточены. Именно благодаря часам реального времени ваш ноутбук всегда знает текущие время и дату, даже если он год пролежал выключенным. В этой заметке мы познакомимся с часами реального времени DS1302 и рассмотрим пример их использования микроконтроллерами AVR.

Fun fact! В микроконтроллерах STM32 часы реального времени, как правило, встроены, в связи с чем для них использовать внешние RTC довольно бессмысленно.

Откроем даташит [PDF] и посмотрим на распиновку часов:

Распиновка часов реального времени DS1302

Часы могут питаться напряжением от 2 В до 5.5 В. Батарейка (типа CR2032) подключается к VCC1 и GND. Основное питание подается на VCC2 и GND. Пины X1 и X2 подключаются к часовому кварцевому резонатору на 32768 Гц. Пины SCLK, IO и CE используются для общения с микроконтроллером по протоколу, напоминающему SPI. К сожалению, протокол совершенно не совместим с SPI, что является большим минусом этих часов. Причины несовместимости следующие:

  • Данные передаются в формате lsb-first и в режиме active-high, против типичных для SPI msb-first и active-low. Но это, как правило, еще можно настроить на стороне МК;
  • Запись данных в часы всегда производится по переднему фронту SCLK, а чтение из часов — по заднему фронту;
  • Данные передаются через один пин IO, тогда как протокол SPI типично использует два пина MOSI и MISO. При этом, в отличие от других микроконтроллеров, AVR’ки не умеют работать с SPI в полудуплексном режиме;

Сказать по правде, выяснив эти моменты, я серьезно задумался над тем, чтобы выкинуть DS1302 и взять часы с нормальным интерфейсом. Но поскольку половина пути уже была пройдена, я решил разобраться до конца.

Железная сторона у меня получилась следующей:

DS1302 подключенный к Arduino

Под Arduino существует готовая библиотека для часов DS1302, и она даже оказалась вполне рабочей. Но готовые Arduino-библиотеки не эффективны в смысле используемой ими памяти. Что, если я когда-нибудь захочу использовать DS1302 в проекте на базе ATtiny13 с 1 Кб flash-памяти? А еще, постоянно полагаясь на готовые библиотеки, трудно научиться чему-то новому. Поэтому я решил воспользоваться возможностью и лишний раз поупражняться в программировании под AVR на чистом C.

В итоге софтверная часть у меня получилась такой:

/* vim: set ai et ts=4 sw=4: */

#include <avr/io.h>
#include <util/delay.h>

#ifndef BAUD
#define BAUD 9600
#endif

#include <util/setbaud.h>

#define DS1302_CLK        PD6 // arduino pin 6
#define DS1302_CLK_DDR    DDRD
#define DS1302_CLK_PORT   PORTD

#define DS1302_DIO        PD5 // arduino pin 5
#define DS1302_DIO_DDR    DDRD
#define DS1302_DIO_PORT   PORTD
#define DS1302_DIO_PIN    PIND

#define DS1302_CE         PD4 // arduino pin 4
#define DS1302_CE_DDR     DDRD
#define DS1302_CE_PORT    PORTD

#define DS1302_DELAY_USEC 3

void UART_Init(void) {
    UBRR0H = UBRRH_VALUE;
    UBRR0L = UBRRL_VALUE;
    #if USE_2X
    UCSR0A |= (1 << U2X0);
    #else
    UCSR0A &= ~(1 << U2X0);
    #endif

    /* Enable UART transmitter/receiver */
    UCSR0B = (1 << TXEN0) | (1 << RXEN0);
    /* 8 data bits, 1 stop bit */
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void UART_TransmitByte(uint8_t data) {
    /* Wait for empty transmit buffer */
    loop_until_bit_is_set(UCSR0A, UDRE0);
    /* Send data */
    UDR0 = data;
}

uint8_t UART_ReceiveByte(void) {
    /* Wait for incoming data */
    loop_until_bit_is_set(UCSR0A, RXC0);
    /* Return register value */
    return UDR0;
}

void UART_TransmitString(const char* str) {
    while(*str) {
        UART_TransmitByte((uint8_t)*str);
        str++;
    }
}

/* Converts 4 bits into hexadecimal */
char nibbleToHexCharacter(uint8_t nibble) {
    if (nibble < 10) {
        return ('0' + nibble);
    } else {
        return ('A' + nibble - 10);
    }
}

/* Prints a byte as its hexadecimal equivalent */
void UART_TransmitByteHex(uint8_t byte) {
  uint8_t nibble;
  nibble = (byte & 0xF0) >> 4;
  UART_TransmitByte(nibbleToHexCharacter(nibble));
  nibble = byte & 0x0F;
  UART_TransmitByte(nibbleToHexCharacter(nibble));
}

void DS1302_Init(void) {
    // CE - output, set low
    DS1302_CE_DDR |= (1 << DS1302_CE);
    DS1302_CE_PORT &= ~(1 << DS1302_CE);

    // CLK - output, set low
    DS1302_CLK_DDR |= (1 << DS1302_CLK);
    DS1302_CLK_PORT &= ~(1 << DS1302_CLK);

    // DIO - output, set low (for now)
    DS1302_DIO_DDR |= (1 << DS1302_DIO);
    DS1302_DIO_PORT &= ~(1 << DS1302_DIO);
}

void DS1302_Select(void) {
    // set CE high
    DS1302_CE_PORT |= (1 << DS1302_CE);
}

void DS1302_Deselect(void) {
    // set CE low
    DS1302_CE_PORT &= ~(1 << DS1302_CE);
}

void DS1302_TransmitByte(uint8_t byte) {
    // DIO - output, set low
    DS1302_DIO_DDR |= (1 << DS1302_DIO);
    DS1302_DIO_PORT &= ~(1 << DS1302_DIO);

    // transmit byte, lsb-first
    for(uint8_t i = 0; i < 8; i++) {
        if((byte >> i) & 0x01) {
            // set high
            DS1302_DIO_PORT |= (1 << DS1302_DIO);
        } else {
            // set low
            DS1302_DIO_PORT &= ~(1 << DS1302_DIO);
        }

        // send CLK signal
        DS1302_CLK_PORT |= (1 << DS1302_CLK);
        _delay_us(DS1302_DELAY_USEC);
        DS1302_CLK_PORT &= ~(1 << DS1302_CLK);
    }
}

uint8_t DS1302_ReceiveByte(void) {
    // DIO - input
    DS1302_DIO_DDR &= ~(1 << DS1302_DIO);

    // NB: receive is always done after transmit, thus
    // falling edge of CLK signal was already sent
    // see "Figure 4. Data Transfer Summary" for more details

    // receive byte, lsb-first
    uint8_t byte = 0;
    for(uint8_t i = 0; i < 8; i++) {
        if(DS1302_DIO_PIN & (1 << DS1302_DIO)) {
            byte |= (1 << i);
        }

        // send CLK signal
        DS1302_CLK_PORT |= (1 << DS1302_CLK);
        _delay_us(DS1302_DELAY_USEC);
        DS1302_CLK_PORT &= ~(1 << DS1302_CLK);
    }

    return byte;
}

void set_time() {
    // see "Table 3. Register Address/Definition"
    const uint8_t bytes[8] =
      // sec   min  hour   day   mon  dow(1-7) year  wp (in BCD!)
      { 0x00, 0x11, 0x21, 0x26, 0x04, 0x04,    0x18  0x00 };

    DS1302_Select();
    // 0xBE = clock burst write
    DS1302_TransmitByte(0xBE);
    for(uint8_t i = 0; i < sizeof(bytes); i++) {
        DS1302_TransmitByte(bytes[i]);
    }
    DS1302_Deselect();
}

int main(int argc, char *argv[]) {
    UART_Init();
    DS1302_Init();

    // set current time
    // set_time();

    for (;;) {
        _delay_ms(1000);

        // read current time
        uint8_t bytes[8];

        DS1302_Select();
        // 0xBF = clock burst read
        DS1302_TransmitByte(0xBF);
        for(uint8_t i = 0; i < sizeof(bytes); i++) {
            bytes[i] = DS1302_ReceiveByte();
        }
        DS1302_Deselect();

        // send the time over UART
        UART_TransmitString("Time = ");
        for(uint8_t i = 0; i < sizeof(bytes); i++) {
            UART_TransmitByteHex(bytes[i]);
        }
        UART_TransmitString("\r\n");
    }
}

Здесь используются две команды. Команда clock burst read с кодом 0xBF считывает текущее состояние часов. В ответ RTC посылают 8 байт, в которых хранятся соответственно секунды, минуты, часы, день, месяц, день недели, год, и кое-какие флаги, все это в BCD. Установка времени производится аналогично с помощью команды clock burst write, имеющей код 0xBE.

На самом деле, формат байт чуть сложнее. В частности, можно переключаться между 12-и и 24-х часовым форматом. Отмечу также, что часы поддерживают и другие команды. Например, можно читать и писать отдельно секунды, минуты, и так далее. Кроме того, DS1302 предоставляет дополнительную RAM-память объемом 31 байт, в которых можно хранить произвольную информацию, например, какие-то настройки. Подробности можно найти в даташите.

Если все было сделано правильно, по UART будут приходить примерно такие данные:

Time = 0614212604041800

В данном случае часы показывают (читаем слева направо) «6 секунд, 14 минут, 21 час, 26 число, апрель, четверг, 2018 год, никому неинтересные флаги».

Работа с аппаратным UART в микроконтроллерах AVR на чистом C в данном блоге ранее не рассматривалась. Но, как видите, тут все довольно просто и сводится к манипуляции парой специальных регистров. Больше технических подробностей можно узнать, например, из 16-ой главы книги Make: AVR Programming (исходники к книге лежат здесь), а также из статьи The USART of the AVR в блоге maxembedded.com. А вот работу с GPIO мы уже изучали в рамках заметки Как я спаял электронные игральные кости на базе ATtiny85.

Fun fact! Используя наработки из этого поста, можно без труда спаять ночные часы. Особенно если взять за основу плату, описанную в статье Паяем таймер и матрицу из УФ-светодиодов для быстрой засветки фоторезиста. Кстати, там же можно найти пример работы с EEPROM, встраиваемым в AVR’ки, на языке C.

Как уже отмечалось, часы DS1302 мне не понравились из-за самопального протокола. Для него не получится задействовать аппаратный SPI и придется использовать лишние пины (потому что не I2C). Плюс к этому, из-за того, что чтение и запись происходит по разным фронтам SCLK, протокол рвет башню логическим анализаторам, что усложняет отладку. Лучше попробуйте часы, работающие по I2C, такие, как DS1307, PCF8563, M41T81 или DS3231.

Полная версия исходников к посту лежит на GitHub. Вопросы и дополнения, как обычно, всячески приветствуются.

Дополнение: Пример использования упомянутых выше часов DS3231 вы найдете в статье Знакомимся с HydraBus и пытаемся понять, зачем он нужен.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.