Наличие часов реального времени (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 путем замыкания ключа на землю.
Для доступа к определенным устройствам на шине используется 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 кГц
voidI2C_Init()
// На частоте 16 МГц, частота SCL будет 16/(16+2(TWBR)), если предположить
// отсутствие деления частоты в прескалере (при TWSR == 0).
// Таким образом, для 100 кГц SCL значение для TWBR равно:
Примечание: ниже встретятся некоторые термины, которые возможно будут новыми для Вас: 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_READY (TWCR & 0x80) // TWI в состоянии готовности (TWINT вернулся в состояние лог. 1)
#define TW_STATUS (TWSR & 0xF8) // возвращает значение статуса
byte I2C_Start()// Генерация start condition по шине I2C.
{
TWCR = TW_START; // отправка sendwhile (!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 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:
voidI2C_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. Короче говоря, получение времени заключается в простом чтении соответствующих регистров.
// Процедура возвратит месяцы, дни и год в формате BCD.
{
*months = I2C_ReadRegister(DS1307,MONTHS_REGISTER);
*days = I2C_ReadRegister(DS1307,DAYS_REGISTER);
*years = I2C_ReadRegister(DS1307,YEARS_REGISTER);
}
voidSetTimeDate()
// Простой пример жесткой установки часов на дату 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 несложно, потому что каждая цифра числа закодирована отдельными битами. Здесь показан пример вывода цифр на экран LCD с использованием подпрограммы LCD_Char():
voidTwoDigits(byte data)
// Вспомогательная функция для WriteDate() и WriteTime().
// На входе она получает 2 цифры в формате BCD, и выводит
// их на дисплей LCD в текущую позицию курсора.
{
byte temp = data>>4; // получить старшие 4 бита
LCD_Char(temp+'0'); // отобразить старшую цифру
data &=0x0F; // получить младшие 4 бита
LCD_Char(data+'0'); // отобразить младшую цифру
}
[Примеры конструкций с часами на микросхеме DS1307]
Можно собрать макет на плате беспаечного монтажа типа breadboard или на простейшей stripboard, но можно поступить проще, если использовать макетные платы наподобие Arduino, DC boardino, AVR-USB-MEGA16, metaboard и т. п. На платах типа AVR-USB-MEGA16 и metaboard есть даже макетное поле, где Вы можете спаять несложную схему подключения чипа DS1307 к микроконтроллеру.
На макетной плате AVR-USB-MEGA16 установлен микроконтроллер ATmega32A. Порт TWI у него привязан к ножкам PC0 (SCL) и PC1 (SDA). Если Вы используете макетную плату metaboard или Arduino, то на них обычно стоит микроконтроллер ATmega328. Порт TWI у этого микроконтроллера привязан к ножкам PC5 (SCL) и PC4 (SDA).
Важное замечание: не пытайтесь запустить DS1307 без батарейки! На выводе +VBAT обязательно должно быть напряжение в диапазоне от 2 до 3.5 вольт, иначе микросхема либо совсем работать не будет, либо будет работать ненадежно (проверил на практике).
Обратите внимание, что не обязательно также покупать чип DS1307 и отдельные детали для сборки схемы (нагрузочные резисторы, кварц, сокет для батарейки). Как вариант, можно купить готовые модули RTC на сайтах типа dx.com, aliexpress или ebay. Вот примеры таких модулей:
Как вариант, можно купить готовые модули RTC на сайтах типа dx.com, aliexpress или ebay. Вот примеры таких модулей (в заголовках спойлеров ключевые слова для поиска):
Модуль на основе DS1307 с интерфейсом I2C, плюс бонус в виде микросхемы памяти I2C EEPROM 24C32. На aliexpress.com можно купить за $1, доставка бесплатно.
Модуль на основе 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.
Шина I2C разведена белым (SDA) и голубым (SCL) проводами. Установлены pull-up резисторы на 4.7 кОм, которые частично закрыты красными проводами. В качестве устройства отображения использовался LCD на основе контроллера HD44780 (в статье [5] автор подробно описывает подключение этого дисплея). Подстроечный резистор на 10 кОм управляет яркостью дисплея.
// 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
voidPulseEnableLine ()
// Генерирует импульс лог. 1 на выводе разрешения шины индикатора LCD.
{
SetBit(PORTB,LCD_E); // LCD enable переводится в лог. 1
_delay_us(40); // ожидание 40 мкс
ClearBit(PORTB,LCD_E); // LCD enable переводится в лог. 0
}
voidSendNibble(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 бита в контроллер
}
voidSendByte (byte data)
// Подпрограмма посылает байт через шину данных.
{
SendNibble(data); // послать старшие 4 бита
SendNibble(data<<4); // послать младшие 4 бита
ClearBit(PORTB,5); // включить светодиод LED
}
voidLCD_Cmd (byte cmd)
// Подпрограмма посылает команду контроллеру LCD.
{
ClearBit(PORTB,LCD_RS); // сигнал R/S = 0, что означает данные команды
SendByte(cmd); // отправка команды
}
voidLCD_Char (byte ch)
// Подпрограмма посылает символ контроллеру LCD для отображения.
{
SetBit(PORTB,LCD_RS); // сигнал R/S = 1, что означает данные символа
SendByte(ch); // отправка символа
}
voidLCD_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
}
voidLCD_Clear()
// Подпрограмма очищает дисплей.
{
LCD_Cmd(CLEARDISPLAY);
msDelay(3); // ожидания завершения обработки команды LCD
}
voidLCD_Home()
// Подпрограмма перемещает курсор LCD в "домашнюю" позицию
// (без очистки дисплея).
{
LCD_Cmd(SETCURSOR);
}
voidLCD_Goto(byte x, byte y)
// Подпрограмма перемещает курсор LCD в указанную позицию экрана.
{
byte addr =0; // строка 0 начинается с адреса 0x00switch (y)
{
case1: addr =0x40; break; // строка 1 начинается с адреса 0x40case2: addr =0x14; break;
case3: addr =0x54; break;
}
LCD_Cmd(SETCURSOR+addr+x); // обновить позицию курсора
}
voidLCD_Line(byte row)
// Подпрограмма перемещает курсор LCD в начало указанной строки экрана.
{
LCD_Goto(0,row);
}
voidLCD_String(constchar*text)
// Подпрограмма отобразит строку на экране LCD в текущей позиции.
{
while (*text) // цикл, пока не появится символ /0
LCD_Char(*text++); // послать символ и обновить указатель
}
voidLCD_Hex(int data)
// Подпрограмма отобразит HEX-значение данных на экране LCD
// в текущей позиции курсора.
{
char st[8] =""; // массив для хранения результата
itoa(data,st,16); // конвертация числа в hex-строку ASCII//LCD_Message("0x"); // если нужно, можно добавить префикс "0x"
LCD_String(st); // отобразить строку на LCD
}
voidLCD_Integer(int data)
// Подпрограмма отобразит целое число на экране LCD
// в текущей позиции курсора.
{
char st[8] =""; // массив для хранения результата
itoa(data,st,10); // преобразование числа в строку ASCII
LCD_String(st); // отобразить строку на LCD
}
// Функция возвратит адрес первого найденного устройства, начиная
// с адреса start. Если ничего не найдено на шине I2C, то вернет 0.
{
//Поиск по всем 256 адресам:for (byte addr=start;addr<0xFF;addr++)
{
if (I2C_Detect(addr))
return addr; // возврат, как только что-то найдено
}
return0; // ничего не найдено, возврат 0.
}
// Функция посылает байт данных 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;
}
voidI2C_WriteByte(byte busAddr, byte data)
// Подпрограмма записывает байт в slave-устройство.
{
I2C_Start(busAddr); // отправка адреса шины I2C
I2C_Write(data); // затем отправка байта данных
I2C_Stop();
}
// Функция читает байт из внутреннего регистра slave-устройства.
{
byte data =0;
I2C_Start(busAddr); // отправка адреса шины I2C
I2C_Write(deviceRegister); // установка указателя на регистр
I2C_Start(busAddr+READ); // перезапуск для операции чтения
data = I2C_ReadNACK(); // чтение данных регистра
I2C_Stop();
return data;
}
// Подпрограмма вернет месяц, день, год в формате BCD.
{
*months = I2C_ReadRegister(DS1307,MONTHS_REGISTER);
*days = I2C_ReadRegister(DS1307,DAYS_REGISTER);
*years = I2C_ReadRegister(DS1307,YEARS_REGISTER);
}
voidSetTimeDate()
// Простой пример установки даты и времени.
{
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);
}
intmain(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):
Если выбран 12-часовой формат с битом AM/PM в регистре часов, то нужно иметь в виду, что значение часов меняется от 1 до 12 по кругу. Я почему-то ошибочно полагал, что значение часов меняется от 0 до 11. Чтобы извлечь значение часов, нужно наложить маску 00011111 (шестнадцатеричное значение 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):
Старшую и младшую тетрады числа 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). 2. I2C Master library site:github.com. 3. I2c 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).
А разве RTC DS1307 имеет фиксированный адрес 0xd0, а не 0х68, как в даташите написано?
microsin: из-за наличия младшего бита в адресе, который работает как признак чтения/записи, в адресации I2C всегда получается некая путаница. Попробуйте перевести значения 0xD0 и 0x68 в двоичный вид и увидите, что это одна и та же последовательность бит: 0xD0 = 11010000 и 0x68 = 01101000, просто в случае 0x68 младший бит "выкинут".
Теперь читайте внимательно, в статье написано, цитата: "... фиксированный адрес 0xd0 (адрес содержится в 7 старших битах, в самом младшем разряде содержится бит read/write)."
Комментарии
microsin: из-за наличия младшего бита в адресе, который работает как признак чтения/записи, в адресации I2C всегда получается некая путаница. Попробуйте перевести значения 0xD0 и 0x68 в двоичный вид и увидите, что это одна и та же последовательно сть бит: 0xD0 = 11010000 и 0x68 = 01101000, просто в случае 0x68 младший бит "выкинут".
Теперь читайте внимательно, в статье написано, цитата: "... фиксированный адрес 0xd0 (адрес содержится в 7 старших битах, в самом младшем разряде содержится бит read/write)."
RSS лента комментариев этой записи