Программирование AVR Подключение RTC DS1307 к микроконтроллеру AVR Thu, November 21 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Подключение RTC DS1307 к микроконтроллеру AVR Печать
Добавил(а) microsin   

AvrDS1307Наличие часов реального времени (real-time clock, сокращенно RTC) в системе с микроконтроллером очень удобная опция. Особенно она полезна, когда нужно запоминать определенные события с привязкой ко времени (ведение лога системы). Микросхема часов DS1307 компании Maxim широко известна и получила популярность благодаря простоте и дешевизне. Для подключения этой микросхемы к микроконтроллеру достаточно всего лишь 2 линии ввода/вывода, используемых при обмене данными. Если Вы хотите добавить RTC к системе на основе микроконтроллера AVR, или если хотите узнать немного про интерфейс two-wire (сокращенно TWI, переводится как "два-провода"), или так называемый I2C (это два разных названия одного и того же интерфейса), то эта статья должна пригодиться (здесь приведен перевод статьи [1]).

[Что такое I2C или TWI]

Компания Atmel, выпускающая микроконтроллеры AVR, называет аппаратное устройство интерфейса I2C интерфейсом "two-wire", или сокращенно TWI. Этот аппаратный интерфейс предназначен для поддержки последовательного протокола обмена данными через 2 сигнальные линии: сигнал данных (data line, SDA) и сигнал тактов (clock line, SCL). Устройства, которые обмениваются данными по шине I2C (или TWI) могут в любой момент времени быть либо мастером шины (master), либо подчиненным устройством на шине (slave). В качестве мастера шины почти всегда выступает микроконтроллер, и именно так подключается микросхема DS1307 к микроконтроллеру AVR - микроконтроллер работает как master шины, а DS1307 как подчиненное устройство, slave. Мастер инициирует все передачи данных по шине, и микросхемы slave только лишь отвечают на запросы мастера. В этой статье приведен пример подключения DS1307 (подчиненное устройство) к микроконтроллеру AVR ATmega328 (мастер).

К мастеру шины может быть подключено не одно устройство slave, а несколько, параллельно через те же самые сигналы данных SDA и SCL. Буферные каскады интерфейса имеют схему с открытым коллектором (или открытым стоком), это означает, что в пассивном состоянии линии данных притянуты к уровню питания VCC через нагрузочные резисторы R1 и R2, так называемые резисторы pull-up (см. схему на рисунке). Т. е. когда на шине нет активности, оба сигнала SCL и SDA имеют уровень лог. 1. Когда какое-то устройство на шине получает к нему доступ, то оно генерирует логический уровень лог. 0 путем замыкания ключа на землю.

i2c bus

Для доступа к определенным устройствам на шине используется 7-битная адресация, плюс один бит чтения/записи (read/write bit). Таким способом всего к одной шине можно подключить 128 устройств на чтение и запись. Устройство на микросхеме RTC DS1307 имеет фиксированный адрес 0xd0 (адрес содержится в 7 старших битах, в самом младшем разряде содержится бит read/write).

Есть довольно много хорошего материала, посвященного программированию TWI/I2C для микроконтроллеров AVR. Ниже во врезке приведены соответствующие тематические ссылки. В этой статье сделана попытка максимально простого прикладного изложения работы с микросхемой RTC DS1307.

Прежде чем продолжить изложение дальше, следует отметить, что есть очень простые, готовые библиотеки для I2C для AVR, особенно если Вы используете систему программирования Arduino. Практически ничего не нужно делать самому, просто вбейте в строку поиска I2C master library, и сразу найдете несколько готовых альтернативных решений. Таким образом, если Вы не хотите изучить основы I2C, то можно не читать дальше эту статью.

Можно реализовать протокол I2C, просто программно управляя любыми двумя ножками микроконтроллера как портами ввода-вывода GPIO (так называемый принцип bit-bang). Готовых реализаций подобных реализаций довольно много еще со времен архитектуры MCS51 (см. например [2, 3]). Благодаря таким библиотекам можно довольно просто организовать обмен по шине I2C.

Однако в микроконтроллере ATmega328 (и также почти во всех микроконтроллерах серии ATmega, например ATmega16, ATmega32, ATmega256 и т. п.) имеется специальный, выделенный аппаратный узел I2C, который упрощает работу с интерфейсом. Код становится короче, экономятся рабочие такты процессора. Именно аппаратный интерфейс I2C/TWI будет рассмотрен в этой статье.

[Настройка I2C]

Инициализация аппаратуры TWI. Первое, что нужно сделать, это установить частоту тактов сигнала данных I2C. Обычно тактовая частота выбирается либо 10 кГц (slow mode, медленный режим), 100 кГц (standard mode, стандартный режим) или 400 кГц (fast mode, быстрый режим). Максимальная частота шины определяется самым медленным устройством на шине, а также емкостью и индуктивностью проводов шины (эти параметры связаны с длиной шины и её физическими характеристиками). Обычно на практике устройства I2C работают на частоте 100 кГц. Наша микросхема DS1307 также будет запущена на этой же частоте 100 кГц.

Примечание переводчика: несмотря на этот стандартный ряд частот, шина I2C является полностью синхронной и не привязанной жестко к интервалам времени. Т. е. шина I2C может работать на любой, даже самой низкой частоте, что весьма упрощает программирование шины в системах реального времени, когда применяется программное (bit-bang) управление сигналами SDA и SCL через порты GPIO.

Имеется 2 специальных регистра ATmega, которые управляют частотой SCL: регистры TWSR и TWBR. Регистр TWSR это регистр состояния TWI (TWI status register), и он содержит биты настройки прескалера (т. е. делителя тактовой частоты). Прескалер делит тактовую частоту CPU, и этим определяет тактовую частоту SCL (от коэффициента деления прескалера зависит рабочая частота TWI). В нашем случае микроконтроллер ATmega328 работает на частоте 16 МГц (возможно, это самая популярная тактовая частота, потому что она используется на платах Arduino). Однако прескалер не понадобится, про биты управления прескалером можно забыть, потому что еще регистр управления скоростью TWBR (TWI bit-rate register). Частота SCL является функцией от частоты frequency CPU и значения в этом регистре, и вычисляется по следующей формуле:

F_SCL (в МГц) = F_CPU/(16+2(TWBR))

В этой формуле, как Вы уже догадались, F_CPU это тактовая частота ядра CPU, в нашем случае 16 МГц. Из формулы можно вывести формулу вычисления значение для TWBR для нужной тактовой частоты SCL:

TWBR = ((F_CPU/F_SCL)-16)/2

По этой формуле для частоты F_SCL= 100 кГц (т. е. 0.1 МГц ) значение для TWBR должно быть:

((16/0.1)-16)/2 = (160-16)/2 = 72

Ниже приведен простой код процедуры настройки интерфейса TWI.

#define F_CPU 16000000L // Частота тактов CPU равна 16 МГц
#define F_SCL 100000L   // Частота I2C 100 кГц
void I2C_Init()
// На частоте 16 МГц, частота SCL будет 16/(16+2(TWBR)), если предположить
// отсутствие деления частоты в прескалере (при TWSR == 0).
// Таким образом, для 100 кГц SCL значение для TWBR равно:
// ((F_CPU/F_SCL)-16)/2 = ((16/0.1)-16)/2 = 144/2 = 72.
{
   // коэффициент деления прескалера 1 (нет деления частоты F_CPU):
   TWSR = 0;
   // настройка частоты SCL:
   TWBR = ((F_CPU/F_SCL)-16)/2;  // == 72
}

[Как работает протокол I2C на передачу]

Ниже приведено краткое описание последовательности работы протокола в режиме передачи master -> slave.

Примечание: ниже встретятся некоторые термины, которые возможно будут новыми для Вас: Start Condition, Stop Condition, ACK и т. п. Это просто некие логические сигналы на шине, задаваемыми определенными комбинациями и последовательностями смены уровней сигналов SDA и SCL. ИМХО нет смысла вдаваться в чрезмерные подробности, достаточно понимать основное назначение сигналов их применение. Кому нужны подробности, см. Википедию на английском [4] (на русском описание несколько хуже).

Start Condition: состояние начала транзакции по шине. Шина занимается парой master/slave на время транзакции по шине.

Stop Condition: состояние завершения транзакции по шине, шина освобождается.

ACK: положительное подтверждение.

NACK: отрицательное подтверждение (сигнал о том, что операция не завершена).

data transfer: состояние передачи данных.

1. Мастер генерирует Start Condition, после чего должен быть возвращен код статуса 0x08.
2. Мастер посылает адрес подчиненного устройства (slave address = 0xd0), DS1307 отвечает ACK, код статуса 0x18.
3. Мастер посылает один или большее количество байт данных, DS1307 отвечает ACK, код статуса 0x28.
4. Мастер генерирует Stop Condition, код статуса не возвращается.

После каждой операции бит готовности (ready bit) регистре TWCR перейдет в состояние лог. 0, и вернется в состояние лог. 1, когда операция завершится. Данные размером в байт посылаются/принимаются через специальный регистр TWDR. Условия start, stop и data transfer указываются через регистр управления TWCR. И коды статуса помещаются в регистр TWSR. Давайте посмотрим на код, и сравним его с протоколом. Вот так генерируется состояние start condition:

#define TW_START 0xA4            // отправка start condition (TWINT,TWSTA,TWEN)
#define TW_READY (TWCR & 0x80)   // TWI в состоянии готовности (TWINT вернулся в состояние лог. 1)
#define TW_STATUS (TWSR & 0xF8)  // возвращает значение статуса
 
byte I2C_Start()// Генерация start condition по шине I2C.
{
   TWCR = TW_START;           // отправка send
   while (!TW_READY);         // ожидание завершения
   return (TW_STATUS==0x08);  // возврат 1, если устройство найдено, иначе возврат 0
}

Для генерации start загрузите в TWCR значение 0xA4 и ждите. Почему именно 0xA4? В двоичном коде это будет 10100100. Три единички в этом коде соответствуют битам TWINT, TWSTA и TWEN в регистре управления. Эти биты разрешают прерывание TWI, start condition, и весь модуль TWI. Вы возможно увидите где-нибудь в библиотеках код наподобие такого: TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN). Возможно, что это "самодокументированный" код, используйте этот прием, если он Вам подходит. Для простоты здесь будет просто использоваться код 0xA4.

Далее нужно послать адрес для того устройства slave, с которым должен произойти обмен. Как уже упоминалось, для DS1307 этот адрес всегда 0xd0. Вот код, который посылает адрес, здесь передается адрес DS1307 в регистре TWDR, дается команда послать адрес через регистр TWCR, и запускается цикл ожидания:

#define DS1307 0xD0           // адрес по шине I2C для DS1307 RTC
#define TW_SEND 0x84          // команда отправки данных (TWINT,TWEN)
 
byte I2C_SendAddr(addr)// Послать адрес шины для подчиненного устройства.
{
   TWDR = addr;               // загрузка адреса устройства
   TWCR = TW_SEND;            // и отправка его
   while (!TW_READY);         // ожидание завершения
   return (TW_STATUS==0x18);  // возврат 1, если устройство найдено, иначе возврат 0
}

Следующее действие состоит в отправке байта данных, что будет выглядеть практически так же, как и отправка адреса. Однако обратите внимание, что возвращаемый код статуса должен быть другим:

byte I2C_Write (byte data)
// Отправка данных подчиненному устройству.
{
   TWDR = data;               // загрузка данных для отправки
   TWCR = TW_SEND;            // и передача их
   while (!TW_READY);         // ожидание завершения
   return (TW_STATUS!=0x28);  // возврат 1, если устройство найдено, иначе возврат 0
}

Как видите, для передачи данных в DS1307 мы дважды выполнили операцию записи: один раз для адреса RTC, и второй раз для данных.

Последний шаг - отправить Stop condition. Здесь нужно просто записать в регистр команд значение 0x94. И снова это значение установит биты разрешения модуля TWI, прерывания, и бит stop. Вместо шестнадцатеричного значения Вы можете использовать (1 << TWINT) | (1 << TWEN) | (1 << TWSTO). Для отправки команды останова не надо ждать и проверять код статуса, поэтому можно использовать простой встраиваемый (inline) макрос:

#define TW_STOP 0x94                // послать stop condition (TWINT,TWSTO,TWEN)
#define I2C_Stop() TWCR = TW_STOP   // inline-макрос для stop condition

Краткое замечание по проверке кодов статуса: для простейшего случая, когда гарантируется присутствие ожидаемой микросхемы на шине, можно игнорировать результаты возврата подпрограмм.

[Как работает протокол I2C на прием]

Чтение данных несколько сложнее: сначала требуется сделать запись в устройство, чтобы установить внутренний указатель адреса, и затем прочитать данные по установленному адресу (внимание, это не тот же адрес, что и адрес по шине I2C). Вот краткое содержание протокола приема данных от slave-устройства шины.

1. Мастер генерирует Start Condition, после чего должен быть возвращен код статуса 0x08.
2. Мастер посылает адрес подчиненного устройства (slave address = 0xd0), DS1307 отвечает ACK, код статуса 0x18.
3. Мастер посылает внутренний адрес микросхемы (указатель на внутренние регистры RTC), DS1307 ответит ACK, код статуса 0x28.
4. Мастер генерирует второй Start Condition, что означает рестарт, будет возвращен код статуса 0x10.
5. Мастер посылает адрес по шине для подчиненного устройства плюс установленный бит чтения (0xd1), DS1307 ответит ACK, код статуса 0x40.
6. Мастер запросит байт данных с NACK, DS1307 вернет байт, код статуса 0x58.
7. Мастер генерирует Stop Condition, код статуса не возвращается.

Для осуществления чтения нужно добавить только еще одну функцию, которая используется на шаге 6. Она выглядит примерно так же, как и операция записи. NACK используется для запроса одного (или последнего) байта данных.

#define TW_NACK 0x84    // чтение данных с NACK (что означает, что это последний байт)
#define READ 1
 
byte I2C_ReadNACK ()    // чтение байта данных от подчиненного устройства
{
   TWCR = TW_NACK;      // NACK означает, что будет прочитан последний байт,
                        //  больше байт прочитано не будет.
   while (!TW_READY);   // ожидание завершения
   return TWDR;         // возврат прочитанного байта
}

Если соединить все вместе, получатся две подпрограммы для записи и чтения регистров микросхемы часов DS1307:

void I2C_WriteRegister(byte deviceRegister, byte data)
{
   I2C_Start();
   I2C_SendAddr(DS1307);      // послать адрес шины
   I2C_Write(deviceRegister); // послать 1 байт - адрес внутреннего регистра микросхемы
   I2C_Write(data);           // послать 2 байт - данные, которые надо записать
   I2C_Stop();
}
 
byte I2C_ReadRegister(byte busAddr, byte deviceRegister)
{
   byte data = 0;
   I2C_Start();
   I2C_SendAddr(busAddr);      // послать адрес шины
   I2C_Write(deviceRegister); // установка указателя на регистр
   I2C_Start();
   I2C_SendAddr(busAddr+READ); // перезапуск в качестве операции чтения
   data = I2C_ReadNACK();     // чтение данных регистра
   I2C_Stop();
   return data;
}

[Работа с микросхемой DS1307]

Программирование часов реального времени (RTC) на основе DS1307 очень прямолинейное и простое. Микросхема содержит регистры данных, из которых можно прочитать значения секунд (seconds), минут (minutes), часов (hours), дней (days), месяцев (months) и лет (years). Эти регистры Вы записываете, когда хотите установить текущее время часов, и читаете, чтобы получить текущее время. Ниже дан список адресов этих регистров.

#define SECONDS_REGISTER   0x00
#define MINUTES_REGISTER   0x01
#define HOURS_REGISTER     0x02
#define DAYOFWK_REGISTER   0x03
#define DAYS_REGISTER      0x04
#define MONTHS_REGISTER    0x05
#define YEARS_REGISTER     0x06

Есть несколько специальных случаев. Регистр секунд содержит флаг для запуска/остановки часов. И регистр часов имеет флаг для настройки используемого формата времени суток: 12/24 часовой формат и AM/PM. Короче говоря, получение времени заключается в простом чтении соответствующих регистров.

void DS1307_GetTime(byte *hours, byte *minutes, byte *seconds)
// Процедура возвратит часы, минуты и секунды в формате BCD.
{
   *hours = I2C_ReadRegister(DS1307,HOURS_REGISTER);
   *minutes = I2C_ReadRegister(DS1307,MINUTES_REGISTER);
   *seconds = I2C_ReadRegister(DS1307,SECONDS_REGISTER);
   if (*hours & 0x40) // режим 12 часов:
      *hours &= 0x1F; // используются младшие 5 бит (бит pm = temp & 0x20)
   else
      *hours &= 0x3F; // режим 24 часов: используются младшие 6 бит
}
 
void DS1307_GetDate(byte *months, byte *days, byte *years)
// Процедура возвратит месяцы, дни и год в формате BCD.
{
   *months = I2C_ReadRegister(DS1307,MONTHS_REGISTER);
   *days = I2C_ReadRegister(DS1307,DAYS_REGISTER);
   *years = I2C_ReadRegister(DS1307,YEARS_REGISTER);
}
 
void SetTimeDate()
// Простой пример жесткой установки часов на дату 8/13/21013, время 8:51 PM
{
   I2C_WriteRegister(DS1307,MONTHS_REGISTER, 0x08);
   I2C_WriteRegister(DS1307,DAYS_REGISTER, 0x31);
   I2C_WriteRegister(DS1307,YEARS_REGISTER, 0x13);
   I2C_WriteRegister(DS1307,HOURS_REGISTER, 0x08+0x40); // добавление 0x40 для PM
   I2C_WriteRegister(DS1307,MINUTES_REGISTER, 0x51);
   I2C_WriteRegister(DS1307,SECONDS_REGISTER, 0x00);
}

Примечание: также есть более эффективный метод чтения и записи времени - с помощью режима последовательного доступа к массиву регистров. Для этого в подпрограмме чтения нужно выполнять чтение не с сигналом NACK, а с сигналом ACK, читая друг за другом все 7 регистров последовательно, и только на последнем регистре нужно сделать чтение с NACK и выдать stop.

Например, можно начать чтение I2C с регистра секунд с внутренним адресом 0x00. После каждой операции чтения внутренний указатель адреса регистра в микросхеме DS1307 автоматически инкрементируется, и следующая операция чтения без установки указателя позволит прочитать следующий регистр. Таким образом, можно сразу прочитать все 7 регистров без остановки, что ускорит доступ и экономит место под код. Ниже приведен пример блочного чтения, который читает сразу 8 регистров подряд - от регистра с номером 0 (регистр секунд) до регистра с номером 7 (регистр управления) включительно.

   I2C_Start(DS1307);
   I2C_Write(SECONDS_REGISTER);  // установка указателя на первый регистр
   I2C_Start(DS1307+READ);       // перезапуск для операции чтения
   u8 regcnt;
   for (regcnt=SECONDS_REGISTER;regcnt<CONTROL_REGISTER;regcnt++)
      rtc.data[regcnt] = I2C_ReadACK();
   rtc.data[regcnt] = I2C_ReadNACK();
   I2C_Stop();

Для сравнения: вся эта блочная операция занимает 1 мс, тогда как если бы мы попробовали прочитать все эти регистры по отдельности (восьмикратным вызовом I2C_ReadRegister), то нам потребовалось бы около 13 мс.

[Что такое формат BCD]

В каждом регистре данные хранятся в так называемом двоично-десятичном формате (Binary Coded Decimal, BCD). Это означает, что в каждом байте хранится сразу 2 готовые для отображения цифры, по одной в каждой тетраде. В старшей тетраде хранится старшая цифра, и в младшей младшая цифра.

Например, рассмотрим десятичное число 36. Если закодировать это число в обычном двоичном формате, то получится шестнадцатеричное число 0x24, или в двоичном представлении 0010.0100. Но в формате BCD число 36 будет закодировано как 0011.0110. Обратите внимание, что в старших 4 битах (старшая тетрада) хранится число 0011 (десятичная цифра 3), и в младших 4 битах (младшая тетрада) находится число 0110 (десятичная цифра 6).

BCD format

Отображение значений BCD несложно, потому что каждая цифра числа закодирована отдельными битами. Здесь показан пример вывода цифр на экран LCD с использованием подпрограммы LCD_Char():

void TwoDigits(byte data)
// Вспомогательная функция для WriteDate() и WriteTime().
// На входе она получает 2 цифры в формате BCD, и выводит
// их на дисплей LCD в текущую позицию курсора.
{
   byte temp = data>>4;    // получить старшие 4 бита
   LCD_Char(temp+'0');     // отобразить старшую цифру
   data &= 0x0F;           // получить младшие 4 бита
   LCD_Char(data+'0');     // отобразить младшую цифру
}
 
void WriteDate()
{
   byte months, days, years;
   DS1307_GetDate(&months,&days,&years);
   TwoDigits(months);
   LCD_Char('/');
   TwoDigits(days);
   LCD_Char('/');
   TwoDigits(years);
}
 
void WriteTime()
{
   byte hours, minutes, seconds;
   DS1307_GetTime(&hours,&minutes,&seconds);
   TwoDigits(hours);
   LCD_Char(':');
   TwoDigits(minutes);
   LCD_Char(':');
   TwoDigits(seconds);
}

[Примеры конструкций с часами на микросхеме DS1307]

Можно собрать макет на плате беспаечного монтажа типа breadboard или на простейшей stripboard, но можно поступить проще, если использовать макетные платы наподобие Arduino, DC boardino, AVR-USB-MEGA16, metaboard и т. п. На платах типа AVR-USB-MEGA16 и metaboard есть даже макетное поле, где Вы можете спаять несложную схему подключения чипа DS1307 к микроконтроллеру.

DS1307 AVR USB MEGA16

На макетной плате AVR-USB-MEGA16 установлен микроконтроллер ATmega32A. Порт TWI у него привязан к ножкам PC0 (SCL) и PC1 (SDA). Если Вы используете макетную плату metaboard или Arduino, то на них обычно стоит микроконтроллер ATmega328. Порт TWI у этого микроконтроллера привязан к ножкам PC5 (SCL) и PC4 (SDA).

DS1307 sch conn

Важное замечание: не пытайтесь запустить DS1307 без батарейки! На выводе +VBAT обязательно должно быть напряжение в диапазоне от 2 до 3.5 вольт, иначе микросхема либо совсем работать не будет, либо будет работать ненадежно (проверил на практике).

Обратите внимание, что не обязательно также покупать чип DS1307 и отдельные детали для сборки схемы (нагрузочные резисторы, кварц, сокет для батарейки). Как вариант, можно купить готовые модули RTC на сайтах типа dx.com, aliexpress или ebay. Вот примеры таких модулей:

Как вариант, можно купить готовые модули RTC на сайтах типа dx.com, aliexpress или ebay. Вот примеры таких модулей (в заголовках спойлеров ключевые слова для поиска):

I2C RTC DS1307 AT24C32 Real Time Clock Module for Arduino 51 AVR ARM PIC for Arduino site:aliexpress.com

DS1307 24C32 aliexpress pic01 DS1307 24C32 aliexpress pic02

Модуль на основе DS1307 с интерфейсом I2C, плюс бонус в виде микросхемы памяти I2C EEPROM 24C32. На aliexpress.com можно купить за $1, доставка бесплатно.

Geeetech DS1307 RTC Real Time Clock Module I2C protocal For PLC ARM Arduino site:ebay.com

DS1307 Geeetech pic01 DS1307 Geeetech pic02 DS1307 Geeetech pic03 DS1307 Geeetech pic04 DS1307 Geeetech pic05

Модуль на основе DS1307 с интерфейсом I2C. На ebay.com можно купить за $4.5, доставка бесплатно.

RTC DS1302 Real Time Clock RTC Module With CR1220 Battery For arduino AVR ARM PIC site:aliexpress.com

DS1302 aliexpress pic01 DS1302 aliexpress pic02

Модуль на основе DS1302 (в корпусе SOIC8) с интерфейсом SPI. На ebay.com можно купить за $6.99, доставка бесплатно.

Arduino Real Time Clock RTC Module DS1302 Model for AVR ARM PIC SMD site:aliexpress.com

DS1302 DIP8 aliexpress pic01

Модуль на основе DS1302 (в корпусе DIP8) с интерфейсом SPI. На aliexpress.com можно купить за $2.75, доставка бесплатно.

DS1302 Real-time Clock Module / with Batteries CR2032 site:ebay.com

DS1302 ebay pic01 DS1302 ebay pic02 DS1302 ebay pic03 DS1302 ebay pic04

Модуль на основе DS1302 (в корпусе DIP8, с панелькой) с интерфейсом SPI. На ebay.com можно купить за $2.5, доставка бесплатно.

50129 Free shipping DS3231 AT24C32 IIC Module Precision Real Time Clock Memory Module site:aliexpress.com

DS3231 AT24C32 aliexpress pic01

Модуль на основе DS3231 (высокоточная микросхема без кварца!) плюс память EEPROM AT24C32, все с интерфейсом I2C. На aliexpress.com можно купить за $0.86, доставка бесплатно.

Чтобы подключить такой модуль к микроконтроллеру, достаточно всего лишь 4 провода - GND, +5V, SCL и SDA. В таблице и на фото ниже показано такое подключение к макетной плате AVR-USB162MU (AVR-микроконтроллер типа AT90USB162).

При таком простом подключении помните, что каждый сигнал данных SDA и SCL требует наличия верхних нагрузочных резисторов (pull-up), подтягивающих лог. уровень к напряжению питания (лог. 1). Проверьте Ваш модуль RTC на наличие таких резисторов. Если таких резисторов нет, то их необходимо подключить. Подойдут резисторы на 2.2 кОм (с тремя красными полосками), но номинал не критичен, можно установить любые в диапазоне 1.5 кОм .. 6.2 кОм. На фото показана конструкция автора [1], собранная на плате breadboard на основе DC boardino.

AvrDS1307 with comments

Шина I2C разведена белым (SDA) и голубым (SCL) проводами. Установлены pull-up резисторы на 4.7 кОм, которые частично закрыты красными проводами. В качестве устройства отображения использовался LCD на основе контроллера HD44780 (в статье [5] автор подробно описывает подключение этого дисплея). Подстроечный резистор на 10 кОм управляет яркостью дисплея.

Этот исходный код в виде проекта для Atmel Studio 6 можно скачать в архиве по ссылке [6].

//-----------------------------------------------------------------------------
// i2c01: Эксперименты с подключением к ATmega328 часов DS1307 RTC
//
// Автор: Bruce E. Hall < bhall66@gmail.com >
// Сайт: w8bh.net
// Версия: 1.1
// Дата: 7 Sep 2013
// Целевой процессор: микроконтроллер ATmega328P
// Язык программы: C, использовалась среда разработки Atmel Studio 6
// Размер полученной прошивки: 1386 байта при использовании оптимизации -O1
//
// Настройки фьюзов: 8 МГц тактовая частота с 65 мс задержкой, разрешен SPI,
// отключено деление тактовой частоты на 8.
 
// ---------------------------------------------------------------------------
// Глобальные определения:
#define F_CPU 16000000L             // AVR CPU работает на частоте 16 МГц
#define LED 5                       // На плате Boarduino светодиод LED подключен к PB5
#define ClearBit(x,y) x &= ~_BV(y)  // Макрос, эквивалентный cbi(x,y)
#define SetBit(x,y) x |= _BV(y)     // Макрос, эквивалентный sbi(x,y)
 
// ---------------------------------------------------------------------------
// Подключаемые файлы
#include < avr/io.h >               // Определения для регистров AVR
#include < util/delay.h >           // Для функции задержки _delay_ms
#include < string.h >               // Подпрограммы обработки строк
#include < stdlib.h >
 
// ---------------------------------------------------------------------------
// Определения типов
typedef uint8_t byte;               // Просто более удобные определения для
typedef int8_t sbyte;               // байтовых чисел без знака и со знаком.
 
// ---------------------------------------------------------------------------
// Дополнительные подпрограммы
void InitAVR()
{
   DDRB = 0x3F;   // 0011.1111; настройка B0-B5 как выходов
   DDRC = 0x00;   // 0000.0000; порт PORTC работает как входы
}
 
void msDelay(int delay)
// Вызов задержки помещен в отдельную подпрограмму,
// чтобы избежать встраивания функции (inlining);
// это иногда удобно для отладки.
{
   for (int i=0;i<delay;i++)
   _delay_ms(1);
}
 
void FlashLED()
// Подпрограмма для мерцания светодиода.
{
   SetBit(PORTB,LED);
   msDelay(250);
   ClearBit(PORTB,LED);
   msDelay(250);
}
 
// ---------------------------------------------------------------------------
// Функции для управления индикатором LCD HD44780
//
// LCD_Init инициализирует контроллер LCD
// LCD_Cmd посылает команду контроллеру
// LCD_Char посылает один символ ASCII на экран LCD
// LCD_Clear очищает экран LCD и возвращает курсор в исходную позицию
// LCD_Home возвращает курсор на экране LCD в исходную позицию
// LCD_Goto помещает курсор в позицию (x,y)
// LCD_Line помещает курсор в начало строки (x)
// LCD_Hex отображает шестнадцатеричное значение
// LCD_Integer отображает целочисленное значение
// LCD_String отображает строку
//
// Для подключения модуля LCD требуется 6 выводов портов GPIO: 2 для сигналов
// управления и 4 для передачи данных.
// Порт B используется для обмена данными с контроллером HD44780 LCD.
// Следующие определения указывают, к каким выводам портов микроконтроллера AVR
// подключен индикатор LCD:
#define LCD_RS 0  // ножка для LCD R/S (PB0)
#define LCD_E 1   // ножка для сигнала разрешения LCD
#define DAT4 2    // ножка для d4
#define DAT5 3    // ножка для d5
#define DAT6 4    // ножка для d6
#define DAT7 5    // ножка для d7
 
// Следующие определения задают команды для контроллера HD44780.
#define CLEARDISPLAY 0x01
#define SETCURSOR    0x80
 
void PulseEnableLine ()
// Генерирует импульс лог. 1 на выводе разрешения шины индикатора LCD.
{
   SetBit(PORTB,LCD_E);    // LCD enable переводится в лог. 1
   _delay_us(40);          // ожидание 40 мкс
   ClearBit(PORTB,LCD_E);  // LCD enable переводится в лог. 0
}
 
void SendNibble(byte data)
// Подпрограмма посылает ниббл через шину данных.
{
   PORTB &= 0xC3;       // 1100.0011 = очистка 4 линий данных
   if (data & _BV(4)) SetBit(PORTB,DAT4);
   if (data & _BV(5)) SetBit(PORTB,DAT5);
   if (data & _BV(6)) SetBit(PORTB,DAT6);
   if (data & _BV(7)) SetBit(PORTB,DAT7);
   PulseEnableLine();   // этот импульс передает 4 бита в контроллер
}
 
void SendByte (byte data)
// Подпрограмма посылает байт через шину данных.
{
   SendNibble(data);    // послать старшие 4 бита
   SendNibble(data<<4); // послать младшие 4 бита
   ClearBit(PORTB,5);   // включить светодиод LED
}
 
void LCD_Cmd (byte cmd)
// Подпрограмма посылает команду контроллеру LCD.
{
   ClearBit(PORTB,LCD_RS); // сигнал R/S = 0, что означает данные команды
   SendByte(cmd);          // отправка команды
}
 
void LCD_Char (byte ch)
// Подпрограмма посылает символ контроллеру LCD для отображения.
{
   SetBit(PORTB,LCD_RS);   // сигнал R/S = 1, что означает данные символа
   SendByte(ch);           // отправка символа
}
 
void LCD_Init()
// Подпрограмма инициализирует контролер дисплея.
{
   LCD_Cmd(0x33);    // инициализация контроллера LCD
   LCD_Cmd(0x32);    // перевод контроллера в 4-битный режим шины данных
   LCD_Cmd(0x28);    // 2 строки, матрица символа 5x7
   LCD_Cmd(0x0C);    // выключить курсор (0x0E чтобы разрешить)
   LCD_Cmd(0x06);    // направление курсора: вправо
   LCD_Cmd(0x01);    // очистка дисплея
   msDelay(3);       // ожидание завершения инициализации LCD
}
 
void LCD_Clear()
// Подпрограмма очищает дисплей.
{
   LCD_Cmd(CLEARDISPLAY);
   msDelay(3);       // ожидания завершения обработки команды LCD
}
 
void LCD_Home()
// Подпрограмма перемещает курсор LCD в "домашнюю" позицию
// (без очистки дисплея).
{
   LCD_Cmd(SETCURSOR);
}
 
void LCD_Goto(byte x, byte y)
// Подпрограмма перемещает курсор LCD в указанную позицию экрана.
{
   byte addr = 0;                // строка 0 начинается с адреса 0x00
   switch (y)
   {
   case 1: addr = 0x40; break;   // строка 1 начинается с адреса 0x40
   case 2: addr = 0x14; break;
   case 3: addr = 0x54; break;
   }
   LCD_Cmd(SETCURSOR+addr+x);    // обновить позицию курсора
}
 
void LCD_Line(byte row)
// Подпрограмма перемещает курсор LCD в начало указанной строки экрана.
{
   LCD_Goto(0,row);
}
 
void LCD_String(const char *text)
// Подпрограмма отобразит строку на экране LCD в текущей позиции.
{
   while (*text)           // цикл, пока не появится символ /0
      LCD_Char(*text++);   // послать символ и обновить указатель
}
 
void LCD_Hex(int data)
// Подпрограмма отобразит HEX-значение данных на экране LCD
// в текущей позиции курсора.
{
   char st[8] = "";        // массив для хранения результата
   itoa(data,st,16);       // конвертация числа в hex-строку ASCII
   //LCD_Message("0x");      // если нужно, можно добавить префикс "0x"
   LCD_String(st);         // отобразить строку на LCD
}
 
void LCD_Integer(int data)
// Подпрограмма отобразит целое число на экране LCD
// в текущей позиции курсора.
{
   char st[8] = "";        // массив для хранения результата
   itoa(data,st,10);       // преобразование числа в строку ASCII
   LCD_String(st);         // отобразить строку на LCD
}
 
// ---------------------------------------------------------------------------
// Подпрограммы I2C (TWI)
//
// На микроконтроллерах серии AVRmega часто (но не всегда) PA4 является
// сигналом данных (SDA) и PA5 является сигналом тактов (SCL).
// Стандартная частота тактов 100 кГц, что устанавливается подпрограммой
// I2C_Init. Тактовая частота I2C зависит от рабочей тактовой частоты AVR.
#define F_SCL 100000L   // тактовая частота I2C равна 100 кГц
#define READ 1#define TW_START 0xA4   // отправка start condition (TWINT,TWSTA,TWEN)
#define TW_STOP 0x94    // отправка stop condition (TWINT,TWSTO,TWEN)
#define TW_ACK 0xC4     // возврат ACK для подчиненного устройства (slave)
#define TW_NACK 0x84    // нет возврата ACK для slave
#define TW_SEND 0x84    // отправка данных (TWINT,TWEN)
#define TW_READY (TWCR & 0x80)      // готовность, когда TWINT вернется в лог. 1
#define TW_STATUS (TWSR & 0xF8)     // вернет значение из регистра статуса
#define I2C_Stop() TWCR = TW_STOP   // inline-макрос для отправки stop condition
 
void I2C_Init()
// На частоте 16 MHz частота SCL будет равна 16/(16+2(TWBR)), если предположить,
// что прескалер обнулен (не делит частоту). Тогда для 100 кГц SCL:
// TWBR = ((F_CPU/F_SCL)-16)/2 = ((16/0.1)-16)/2 = 144/2 = 72
{
   TWSR = 0;            // отключает прескалер
   TWBR = ((F_CPU/F_SCL)-16)/2;  // установка частоты SCL
}
 
byte I2C_Detect(byte addr)
// Функция ищет slave-устройство по указанному адресу, вернет 1 если
// устройство найдено, иначе вернет 0.
{
   TWCR = TW_START;           // отправка start condition
   while (!TW_READY);         // ожидание готовности
   TWDR = addr;               // передача устройству
   TWCR = TW_SEND;            //  адреса шины
   while (!TW_READY);         // ожидание готовности
   return (TW_STATUS==0x18);  // вернет 1, если устройство найдено
}
 
byte I2C_FindDevice(byte start)
// Функция возвратит адрес первого найденного устройства, начиная
// с адреса start. Если ничего не найдено на шине I2C, то вернет 0.
{
   //Поиск по всем 256 адресам:
   for (byte addr=start;addr<0xFF;addr++)
   {
      if (I2C_Detect(addr))
         return addr; // возврат, как только что-то найдено
   }
   return 0;          // ничего не найдено, возврат 0.
}
 
void I2C_Start (byte slaveAddr)
{
   I2C_Detect(slaveAddr);
}
 
byte I2C_Write (byte data)
// Функция посылает байт данных slave-устройству.
{
   TWDR = data;         // загрузка данных
   TWCR = TW_SEND;      //  и отправка их
   while (!TW_READY);   // ожидание готовности
   return (TW_STATUS!=0x28);
}
 
byte I2C_ReadACK ()
// Функция прочитает байт данных из slave-устройства в блочном режиме.
{
   TWCR = TW_ACK;       // ack означает, что далее 
                        // будут еще читаться данные
   while (!TW_READY);   // ожидание готовности
   return TWDR;
}
 
byte I2C_ReadNACK ()
// Функция прочитает байт данных из slave-устройства.
{
   TWCR = TW_NACK;      // nack означает, что будет прочитан только 1 байт
   while (!TW_READY);   // ожидание готовности
   return TWDR;
}
 
void I2C_WriteByte(byte busAddr, byte data)
// Подпрограмма записывает байт в slave-устройство.
{
   I2C_Start(busAddr);  // отправка адреса шины I2C
   I2C_Write(data);     // затем отправка байта данных
   I2C_Stop();
}
 
void I2C_WriteRegister(byte busAddr, byte deviceRegister, byte data)
// Подпрограмма записывает байт во внутренний регистр slave-устройства.
{
   I2C_Start(busAddr);        // отправка адреса шины I2C
   I2C_Write(deviceRegister); // первый байт = внутреннему адресу регистра
   I2C_Write(data);           // второй байт = данным для этого регистра
   I2C_Stop();
}
 
byte I2C_ReadRegister(byte busAddr, byte deviceRegister)
// Функция читает байт из внутреннего регистра slave-устройства.
{
   byte data = 0;
   I2C_Start(busAddr);        // отправка адреса шины I2C
   I2C_Write(deviceRegister); // установка указателя на регистр
   I2C_Start(busAddr+READ);   // перезапуск для операции чтения
   data = I2C_ReadNACK();     // чтение данных регистра
   I2C_Stop();
   return data;
}
 
// ---------------------------------------------------------------------------
// Подпрограммы, специфичные для доступа к DS1307 RTC
#define DS1307 0xD0           // адрес шины I2C для микросхемы DS1307 RTC
#define SECONDS_REGISTER   0x00
#define MINUTES_REGISTER   0x01
#define HOURS_REGISTER     0x02
#define DAYOFWK_REGISTER   0x03
#define DAYS_REGISTER      0x04
#define MONTHS_REGISTER    0x05
#define YEARS_REGISTER     0x06
#define CONTROL_REGISTER   0x07
#define RAM_BEGIN          0x08
#define RAM_END            0x3F
 
void DS1307_GetTime(byte *hours, byte *minutes, byte *seconds)
// Подпрограмма вернет часы, минуты, секунды в формате BCD.
{
   *hours = I2C_ReadRegister(DS1307,HOURS_REGISTER);
   *minutes = I2C_ReadRegister(DS1307,MINUTES_REGISTER);
   *seconds = I2C_ReadRegister(DS1307,SECONDS_REGISTER);
   if (*hours & 0x40) // 12-часовой режим:
      *hours &= 0x1F; // используются младшие 5 бит (бит pm выделяется маской 0x20)
   else
      *hours &= 0x3F; // 24-часовой режим: используются младшие 6 бит
}
 
void DS1307_GetDate(byte *months, byte *days, byte *years)
// Подпрограмма вернет месяц, день, год в формате BCD.
{
   *months = I2C_ReadRegister(DS1307,MONTHS_REGISTER);
   *days = I2C_ReadRegister(DS1307,DAYS_REGISTER);
   *years = I2C_ReadRegister(DS1307,YEARS_REGISTER);
}
 
void SetTimeDate()
// Простой пример установки даты и времени.
{
   I2C_WriteRegister(DS1307,MONTHS_REGISTER, 0x08);
   I2C_WriteRegister(DS1307,DAYS_REGISTER, 0x31);
   I2C_WriteRegister(DS1307,YEARS_REGISTER, 0x13);
   I2C_WriteRegister(DS1307,HOURS_REGISTER, 0x08+0x40); // добавление 0x40 для PM
   I2C_WriteRegister(DS1307,MINUTES_REGISTER, 0x51);
   I2C_WriteRegister(DS1307,SECONDS_REGISTER, 0x00);
}
 
// ---------------------------------------------------------------------------
// Подпрограммы приложения
void ShowDevices()
// Сканирует шину I2C и отображает адреса всех найденных устройств.
{
   LCD_Line(1); LCD_String("Found:");
   byte addr = 1;
   while (addr>0)
   {
      LCD_Char(' ');
      addr = I2C_FindDevice(addr);
      if (addr>0) LCD_Hex(addr++);
   }
}
 
void LCD_TwoDigits(byte data)
// Вспомогательная функция для WriteDate().
// На входе получает 2 цифры в формате BCD.
// Выводит эти цифры на дисплей LCD в текущую позицию курсора.
{
   byte temp = data>>4;
   LCD_Char(temp+'0');
   data &= 0x0F;
   LCD_Char(data+'0');
}
 
void WriteDate()
{
   byte months, days, years;
   DS1307_GetDate(&months,&days,&years);
   LCD_TwoDigits(months);
   LCD_Char('/');
   LCD_TwoDigits(days);
   LCD_Char('/');
   LCD_TwoDigits(years);
}
 
void WriteTime()
{
   byte hours, minutes, seconds;
   DS1307_GetTime(&hours,&minutes,&seconds);
   LCD_TwoDigits(hours);
   LCD_Char(':');
   LCD_TwoDigits(minutes);
   LCD_Char(':');
   LCD_TwoDigits(seconds);
}
 
void LCD_TimeDate()
{
   LCD_Line(0); WriteTime();
   LCD_Line(1); WriteDate();
}
 
// ---------------------------------------------------------------------------
// Главный цикл программы.
void MainLoop()
{
   while(1)
   {
      LCD_TimeDate();   // отображение на LCD времени и даты
      msDelay(1000);    // между обновлениями ожидание 1 секунда
   }
}
 
// ---------------------------------------------------------------------------
// Основная программа: тело функции main.
int main(void)
{
   InitAVR();     // настройка портов GPIO микроконтроллера
   LCD_Init();    // инициализация контроллера HD44780 дисплея LCD
   I2C_Init();    // настройка шины I2C (с установкой её тактовой частоты)
   LCD_String("Ready.");
   ShowDevices(); // показать, что шина I2C работает нормально
   msDelay(4000);
   LCD_Clear();
   MainLoop();    // отображает время
}

[На что нужно обратить внимание]

1. 12-часовой формат числа в регистре часов. Чтобы установить этот режим работы, в регистр часов нужно записать значение, в котором установлен бит D6 (01000000, шестнадцатеричное значение 0x40):

I2C_WriteRegister(DS1307, HOURS_REGISTER, 0x09+0x40);

Если выбран 12-часовой формат с битом AM/PM в регистре часов, то нужно иметь в виду, что значение часов меняется от 1 до 12 по кругу. Я почему-то ошибочно полагал, что значение часов меняется от 0 до 11. Чтобы извлечь значение часов, нужно наложить маску 00011111 (шестнадцатеричное значение 0x1F):

u8 hoursBCD = I2C_ReadRegister(DS1307, HOURS_REGISTER) & 0x1F;

Старшую и младшую тетрады числа hoursBCD можно сразу выводить на индикатор. Но если надо получить двоичное значение часов от 0 до 11, то нужно из BCD преобразовать в двоичное значение, и из результата вычесть 1:

u8 hoursBIN = BCDtoBIN(hoursBCD) - 1;

Бит AM/PM извлекается из разряда D5 наложением маски 00100000 (шестнадцатеричное значение 0x20):

bool AMPM = (hoursBCD & 0x20)?true:false;

2. 24-часовой формат числа в регистре часов. Чтобы установить этот режим работы, в регистр часов нужно записать значение, в котором сброшен бит D6:

I2C_WriteRegister(DS1307, HOURS_REGISTER, 0x09); 

Если выбран 24-часовой формат, то нужно иметь в виду, что значение часов меняется от 0 до 23 по кругу. Чтобы извлечь значение часов, нужно наложить маску 00111111 (шестнадцатеричное значение 0x3F):

u8 hoursBCD = I2C_ReadRegister(DS1307, HOURS_REGISTER) & 0x3F;

Старшую и младшую тетрады числа hoursBCD можно сразу выводить на индикатор. Но если надо получить двоичное значение часов от 0 до 23, то нужно из BCD преобразовать в двоичное значение:

u8 hoursBIN = BCDtoBIN(hoursBCD);

3. Запуск часов. Если Вы только первый раз подключили батарейку и включили питание, то часы можно запустить записью в регистр секунд любого числа, у которого бит D7 сброшен в 0:

I2C_WriteRegister(DS1307,SECONDS_REGISTER, 0x00);

Чтобы остановить ход часов, в бите D7 должна быть единица (не понимаю, зачем это может понадобиться, но все-таки):

I2C_WriteRegister(DS1307,SECONDS_REGISTER, 0x80);

4. Без батарейки часы не работают! Если батарейка села (напряжение ниже 2.5 вольт), то часы могут остановиться, и значения времени и данные в ячейках памяти перестанут быть актуальными. Нужно поменять батарейку, и снова запустить часы записью в регистр секунд любого числа, у которого бит D7 равен 0.

[Ссылки]

1. Add a DS1307 RTC clock to your AVR microcontroller site:w8bh.net (автор Bruce E. Hall, W8BH).
2I2C Master library site:github.com.
3I2c Master (Bit-Bang) site:procyonengineering.com - замечательная библиотека. Есть код для работы с I2C как через аппаратуру TWI, так и через порты GPIO. Также есть много других полезных подпрограмм для работы с различными устройствами.
4. I2C site:en.wikipedia.org.
5. Add an LCD to your AVR microcontroller site:w8bh.net (автор Bruce E. Hall, W8BH).
6. 151223AVR-DS1307.zip - исходный код описанного примера (проект для Atmel Studio 6).

 

Комментарии  

 
0 #3 Vladimir Petrov 16.04.2024 21:28
Большое спасибо! Все встало на свои места (в моей голове) :-)
Цитировать
 
 
+1 #2 Trykov 04.02.2022 16:19
А разве RTC DS1307 имеет фиксированный адрес 0xd0, а не 0х68, как в даташите написано?

microsin: из-за наличия младшего бита в адресе, который работает как признак чтения/записи, в адресации I2C всегда получается некая путаница. Попробуйте перевести значения 0xD0 и 0x68 в двоичный вид и увидите, что это одна и та же последовательно сть бит: 0xD0 = 11010000 и 0x68 = 01101000, просто в случае 0x68 младший бит "выкинут".

Теперь читайте внимательно, в статье написано, цитата: "... фиксированный адрес 0xd0 (адрес содержится в 7 старших битах, в самом младшем разряде содержится бит read/write)."
Цитировать
 
 
+6 #1 Matvey Rybalkin 02.02.2022 18:32
:-) :-) Огромное СПАСИБО!
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page