Подключение RTC DS1307 к микроконтроллеру AVR |
![]() |
Добавил(а) microsin | ||||||||||
[Что такое 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. 1. Introduction into TWI site:nongnu.org. Прежде чем продолжить изложение дальше, следует отметить, что есть очень простые, готовые библиотеки для 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. После каждой операции бит готовности (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. Для осуществления чтения нужно добавить только еще одну функцию, которая используется на шаге 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 несложно, потому что каждая цифра числа закодирована отдельными битами. Здесь показан пример вывода цифр на экран 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 к микроконтроллеру. На макетной плате 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. Вот примеры таких модулей (в заголовках спойлеров ключевые слова для поиска): Geeetech DS1307 RTC Real Time Clock Module I2C protocal For PLC ARM Arduino site:ebay.com
Модуль на основе DS1307 с интерфейсом I2C. На ebay.com можно купить за $4.5, доставка бесплатно. Arduino Real Time Clock RTC Module DS1302 Model for AVR ARM PIC SMD site:aliexpress.com
Модуль на основе DS1302 (в корпусе DIP8) с интерфейсом SPI. На aliexpress.com можно купить за $2.75, доставка бесплатно. DS1302 Real-time Clock Module / with Batteries CR2032 site:ebay.com
Модуль на основе DS1302 (в корпусе DIP8, с панелькой) с интерфейсом SPI. На ebay.com можно купить за $2.5, доставка бесплатно. Чтобы подключить такой модуль к микроконтроллеру, достаточно всего лишь 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 кОм управляет яркостью дисплея. Этот исходный код в виде проекта для 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). |