Пример использования аппаратного TWI (I2C) ATmega Печать
Добавил(а) microsin   

Некоторые современные микроконтроллеры серии ATmega аппаратно поддерживают подключение к стандартной двухпроводной шине I2C, которая часто используется в приборах и бытовой технике. Atmel называет эту шину two-wire interface, сокращенно TWI. Изначально эта шина получила название I2C от фирмы Philips, но это название избегают использовать в документации Atmel из-за патентных проблем. Поэтому I2C и TWI это одно и то же.

Примечание: полное описание протокола I2C см. в документе UM10204.pdf [4].

[Введение в TWI]

Двухпроводный интерфейс TWI состоит из двух сигнальных проводов SDA (serial data, последовательные данные) и SCL (serial clock, такты последовательных данных), и конечно провод земли GND. Все устройства на шине соединены друг с другом параллельно, используя драйверы с открытым стоком для формирования на шине логических уровней сигналов 0 и 1. По этой причине на шине обязательно присутствуют два верхних нагрузочных подтягивающих резистора (pull-up, на рисунке это резисторы RPU, подключенные к плюсу питания VDD).

TWI I2C bus

Нагрузочные резисторы должны иметь достаточно малый номинал для используемой тактовой частоты, чтобы быстро заряжать емкость сигналов SDA и SCL и давать короткие фронты нарастания уровня лог. 1. Однако эти резисторы должны также быть достаточно большие, чтобы не перегружать выходные стоки драйверов подключенных устройств. Рекомендации по выбору номиналов pull-up резисторов и соответствующие формулы можно найти в даташите на используемые устройства I2C/TWI (типовое значение pull-up около 4.7 кОм).

Устройства на шине могут работать либо как мастер (master, т. е. главное устройство, оно инициирует передачи данных по шине), либо как подчиненное устройство (slave, оно работает с шиной только по вызову от master). Шина может работать с несколькими главными устройствами (multi-master), когда в каждый любой момент времени на шине активно только одно master-устройство. Однако чаще всего используют простую конфигурацию шины только с одним master-устройством, все остальные устройства работают как slave (подчиненных устройств может быть одно или несколько).

Для того, чтобы отличать подчиненные устройства на шине, используется 7-битный адрес (так изначально определила компания Philips), передаваемый как первый байт после специального сигнала, который называется start condition. Биты адреса передаются в старших семи битах байта, старшим битом (MSB) вперед. Самый младший бит (LSB) байта адреса имеет специальное значение R/~W, т. е. он определяет дальнейшее направление запроса передачи данных - чтение (read) или запись (write). Бывают также устройства с 10-битным или 16-битным адресом на шине, но этот случай в нашем примере не рассматривается.

[Проект примера программирования EEPROM через TWI]

Аппаратура TWI микроконтроллера ATmega поддерживает оба режима работы на шине TWI/I2C: master и slave. В этом примере демонстрируется только использование микроконтроллера AVR как TWI master. Реализацию примера постарались сделать как можно проще, концентрируясь на пошаговом общении с TWI slave, чтобы код легко можно было адаптировать для работы с разными устройствами. По той же причине не используется режим прерываний, вся обработка обмена осуществляется с опросом ожидания готовности (polled-mode), когда перед переходом к следующему шагу протокола используются циклы ожидания, пока не установится бит TWINT. Если необходимо, чтобы коммуникации TWI происходили в фоновом режиме, то это следует реализовать на основе прерываний, когда только запуск start condition нужно осуществить вне обработчика прерывания TWI.

К шине TWI можно подключить множество различных подчиненных устройств. В этом примере используется устройство памяти EEPROM из стандартной индустриальной серии 24Cxx (здесь xx может быть цифрами 01, 02, 04, 08 или 16). Такие микросхемы энергонезависимой памяти поставляются разными производителями. Выбор микросхемы не критичен, нужно главным образом обеспечить возможность работы в устройством EEPROM в обоих направлениях (чтение и запись), потому что это используется в коде примера.

Примечание: чаще всего нет необходимости подключать к AVR ATmega отдельную внешнюю микросхему EEPROM через TWI, потому что даже такой младший микроконтроллер, как ATmega8, имеет встроенные 512 байт EEPROM, что эквивалентно емкости устройства 24C04. ATmega128 имеет встроенный EEPROM, по размеру эквивалентный 24C16. Исключение может быть, когда снаружи по отношению к AVR используется отключаемое устройство EEPROM; например модуль памяти SDRAM PC поставляется с памятью TWI EEPROM, где записана конфигурационная информация и параметры SDRAM.

Ниже во врезке приведен исходный код примера и примечания к нему. Другие примеры реализации I2C на микроконтроллерах AVR см. по ссылкам [2, 3].

/*
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * <joerg@FreeBSD.ORG> написал этот модуль кода. Пока Вы сохраняете этот
 * текст, можете делать все что хотите с этим кодом. Если мы когда-нибудь
 * встретимся, и если Вы думаете, что этот код достаточно хорош, то можете
 * угостить меня пивом в знак благодарности.        Joerg Wunsch
 * ----------------------------------------------------------------------------
 */
/*
 * ----------------------------------------------------------------------------
 * Обновлено для поддержки более мощных устройств памяти с 16-битным адресным
 * пространством.                                  (2007-09-05) Ruwan Jayanetti
 * ----------------------------------------------------------------------------
 */

/* $Id: twitest.c 2244 2011-05-12 03:33:42Z arcanum $ */

/*
 * Простая демонстрационная программа, которая общается с микросхемой памяти
 * 24Cxx IІC EEPROM, используя встроенный аппаратный интерфейс TWI микроконтроллера
 * ATmega.
 */

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

#include <avr/io.h>
#include <util/twi.h>     /* Примечание 1 */

#define DEBUG 1

/*
 * Системная тактовая частота, Гц.
 */
#define F_CPU 14745600UL   /* Примечание 2 */

/*
 * Определения совместимости. Они должны работать с ATmega8, ATmega16,
 * ATmega163, ATmega323 и ATmega128 (IOW: на всех устройствах, в которых
 * имеется встроенный аппаратный интерфейс TWI (I2C).
 *
 * На ATmega128 по умолчанию используется USART 1.
 */
#ifndef UCSRB
# ifdef UCSR1A    /* ATmega128 */
#  define UCSRA UCSR1A
#  define UCSRB UCSR1B
#  define UBRR UBRR1L
#  define UDR UDR1
# else /* ATmega8 */
#  define UCSRA USR
#  define UCSRB UCR
# endif
#endif
#ifndef UBRR
#  define UBRR UBRRL
#endif

/*
 * Примечание 3
 * Адрес TWI для 24Cxx EEPROM:
 *
 * 1 0 1 0 E2 E1 E0 R/~W   24C01/24C02
 * 1 0 1 0 E2 E1 A8 R/~W   24C04
 * 1 0 1 0 E2 A9 A8 R/~W   24C08
 * 1 0 1 0 A10 A9 A8 R/~W  24C16
 */
#define TWI_SLA_24CXX   0xa0  /* E2 E1 E0 = 0 0 0 */

/*
 * Примечание 3a
 * Длина слова адреса устройства для 24Cxx EEPROM
 * Устройства EEPROM, у которых размер больше (начиная с 24C32),
 * имеют 16-битный адрес. Определите или закомментируйте это
 * макроопределение в зависимости от используемого устройства.
 */
//#define WORD_ADDRESS_16BIT

/*
 * Максимальное количество итераций для ожидания устройства, когда
 * оно должно ответить на его выбор. Должно быть достаточно большим,
 * чтобы позволить завершение ожидаемой записи, но достаточно малым,
 * чтобы оборвать бесконечный цикл, если slave-устройство не исправно,
 * либо просто отсутствует на шине. На частоте обмена 100 кГц TWI
 * передача старта (start condition) и пакета SLA+R/W занимает около
 * 10 мкс. Ожидается, что самый долгий период записи не превышает
 * примерно 10 мс. Таким образом, нормальное функционирование
 * не требует больше 100 итераций, чтобы получить от устройства
 * ответ на его выбор.
 */
#define MAX_ITER  200

/*
 * Количество байт, которое можно записать в строке, см. ниже
 * комментарии для ee24xx_write_page(). Некоторые производители устройств
 * могли бы принять 16, но 8 выглядит наименьшим общим знаменателем.
 *
 * Обратите внимание, что размер страницы должен быть равен степени двойки,
 * что упрощает вычисления границ страницы.
 */
#define PAGE_SIZE 8

/*
 * Сохраненный регистр состояния TWI (регистр TWSR), только для сообщений
 * об ошибках. Нам нужно сохранить его в переменную, поскольку даташит
 * гарантирует достоверное содержимое этого регистра только когда
 * установлен бит TWINT в регистре TWCR.
 */
uint8_t twst;

/*
 * Выполнение всех начальных инициализаций периферии: UART (для нашего
 * отладочного вывода и тестов) и тактов TWI.
 */
void ioinit(void)
{

#if F_CPU < 1000001UL
  /*
   * Примечание 4
   * На этой низкой тактовой частоте применяется двойная скорость UART
   * (double Baud), чтобы улучшить функционирование последовательного порта.
   */
   UCSRA = _BV(U2X);
   UBRR = (F_CPU / (8 * 9600UL)) - 1; /* 9600 бод */
#else
   UBRR = (F_CPU / (16 * 9600UL)) - 1; /* 9600 бод */
#endif
   UCSRB = _BV(TXEN);   /* Разрешение TX */

   /* Инициализация тактов TWI: частота тактов 100 кГц,
   TWPS = 0 => прескалер = 1 */
#if defined(TWPS0)
   /* Есть прескалер (ATmega128 и более новые микроконтроллеры) */
   TWSR = 0;
#endif

#if F_CPU < 3600000UL
  TWBR = 10;   /* Самое малое значение TWBR, см. примечание 5 */
#else
  TWBR = (F_CPU / 100000UL - 16) / 2;
#endif
}

/*
 * Примечание 6
 * Отправка символа через UART Tx, ожидание опустошения регистра
 * передачи.
 */
int uart_putchar(char c, FILE *unused)
{

   if (c == '\n')
      uart_putchar('\r', 0);
   loop_until_bit_is_set(UCSRA, UDRE);
   UDR = c;
   return 0;
}

/*
 * Примечание 7
 *
 * Функция читает len байт из EEPROM, начиная с адреса eeaddr,
 * и помещает их в буфер buf.
 *
 * Эта операция требует двух циклов шины: во время первого цикла устройство
 * будет выбрано (master transmitter mode) и будет передан адрес.
 * Биты адреса, превышающего 256, передаются в битах E2/E1/E0 (биты
 * субадреса) селектора устройства. Для устройств с 16-битным адресом
 * адрес передается в двух выделенных 8-битных передачах (относится
 * к достаточно большим устройствам EEPROM).
 *
 * Второй цикл шины будет заново выбирать устройство (повторится start
 * condition с переходом в master receiver mode), и будут переданы данные
 * из устройства в TWI master. Могут быть переданы несколько байт друг за
 * другом, если сигналом ACK подтверждается клиентская передача. 
 * последний байт получает сигнал NACK, что дает индикацию клиенту,
 * что передача чтения завершается.
 */
int ee24xx_read_bytes(uint16_t eeaddr, int len, uint8_t *buf)
{
   uint8_t sla, twcr, n = 0;
   int rv = 0;

#ifndef WORD_ADDRESS_16BIT
   /* Путь старших бит адреса EEPROM в SLA */
   sla = TWI_SLA_24CXX | (((eeaddr >> 8) & 0x07) << 1);
#else
   /* Устройства с 16-битным адресом нуждаются только в адресе TWI */
   sla = TWI_SLA_24CXX;
#endif

   /*
    * Примечание 8
    * Первый цикл: master transmitter mode
    */
restart:
   if (n++ >= MAX_ITER)
      return -1;
begin:

   TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN); /* передача start condition */
   while ((TWCR & _BV(TWINT)) == 0); /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_REP_START:   /* OK, но ничего не должно произойти */
   case TW_START:
      break;

   case TW_MT_ARB_LOST: /* Примечание 9 */
      goto begin;

   default:
      return -1;  /* Ошибка: не в состоянии start condition */
                   /* NB: не нужно отправлять stop condition */
    }

   /* Примечание 10 */
   /* Отправка SLA+W */
   TWDR = sla | TW_WRITE;
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0); /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MT_SLA_ACK:
      break;

   case TW_MT_SLA_NACK: /* NACK при выборе: устройство занято записью */
                        /* Примечание 11 */
      goto restart;

   case TW_MT_ARB_LOST: /* повторный арбитраж */
      goto begin;

   default:
      goto error;       /* нужно отправить stop condition */
    }

#ifdef WORD_ADDRESS_16BIT
   TWDR = (eeaddr >> 8);   /* Устройство с 16-битным словом адреса,
                              отправка старших 8 бит адреса */
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MT_DATA_ACK:
      break;

   case TW_MT_DATA_NACK:
      goto quit;

   case TW_MT_ARB_LOST:
      goto begin;

   default:
      goto error; /* нужно отправить stop condition */
   }
#endif

   TWDR = eeaddr;  /* младшие 8 бит адреса */
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MT_DATA_ACK:
      break;

   case TW_MT_DATA_NACK:
      goto quit;

   case TW_MT_ARB_LOST:
      goto begin;

   default:
      goto error; /* нужно отправить stop condition */
    }

   /*
    * Примечание 12
    * Следующий цикл (или циклы): master receiver mode
    */
   TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN); /* send (rep.) start condition */
   while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_START: /* OK, но ничего не должно произойти */
   case TW_REP_START:
      break;

   case TW_MT_ARB_LOST:
      goto begin;

   default:
      goto error;
   }

   /* Отправка SLA+R */
   TWDR = sla | TW_READ;
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0); /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MR_SLA_ACK:
      break;

   case TW_MR_SLA_NACK:
      goto quit;

   case TW_MR_ARB_LOST:
      goto begin;

   default:
      goto error;
   }

   for (twcr = _BV(TWINT) | _BV(TWEN) | _BV(TWEA) /* Примечание 13 */;
        len > 0;
        len--)
   {
      if (len == 1)
         twcr = _BV(TWINT) | _BV(TWEN); /* на этот раз отправка NACK */
      TWCR = twcr;   /* очистка прерывания для запуска передачи */
      while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
      switch ((twst = TW_STATUS))
      {
      case TW_MR_DATA_NACK:
         len = 0;    /* принудительное завершение цикла */
         /* ИДЕМ ДАЛЬШЕ */
      case TW_MR_DATA_ACK:
         *buf++ = TWDR;
         rv++;
         if(twst == TW_MR_DATA_NACK) goto quit;
         break;

      default:
         goto error;
      }
   }
quit:
   /* Примечание 14 */
   TWCR = _BV(TWINT) | _BV(TWSTO) | _BV(TWEN); /* отправка stop condition */

   return rv;

error:
   rv = -1;
   goto quit;
}

/*
 * Функция записывает len байт в EEPROM, начиная с адреса eeaddr,
 * байты для записи берутся из буфера buf.
 *
 * Эта функция немного проще, чем предыдущая, потому что обе операции
 * передачи адреса и передачи байт данных являются операциями записи,
 * данные передаются в master transmitter mode, так что не требуется
 * повторный выбор устройства. Однако микросхемы EEPROM могут записать
 * за один раз только одну страницу, поэтому нужно заботиться о том,
 * чтобы за одну операцию записи не пересечь границу страницы. Количество
 * данных на странице зависит от производителя устройства памяти EEPROM.
 * Некоторые производители используют только 8-байтные страницы для
 * устройств с малым объемом памяти, и 16-байтные страницы для устройств
 * большего объема, в то время как другие производители используют только
 * 16-байтные страницы. Таким образом, в этом коде мы используем самую
 * маленькую порцию данных из расчета 8 байт на страницу. Это значение
 * декларируется выше макросом PAGE_SIZE.
 *
 * Эта функция просто делает возврат после записи одной страницы,
 * возвращая количество реально записанных данных. Вызывающий код
 * должен позаботиться о том, чтобы делать повторные вызовы этой функции,
 * чтобы записать остальные данные.
 */
int ee24xx_write_page(uint16_t eeaddr, int len, uint8_t *buf)
{
   uint8_t sla, n = 0;
   int rv = 0;
   uint16_t endaddr;

   if (eeaddr + len <= (eeaddr | (PAGE_SIZE - 1)))
      endaddr = eeaddr + len;
   else
      endaddr = (eeaddr | (PAGE_SIZE - 1)) + 1;
   len = endaddr - eeaddr;

#ifndef WORD_ADDRESS_16BIT
   /* путь старших бит адреса EEPROM в SLA */
   sla = TWI_SLA_24CXX | (((eeaddr >> 8) & 0x07) << 1);
#else
   /* Устройства с 16-битным адресом требуют только адрес TWI-устройства */
   sla = TWI_SLA_24CXX;
#endif

restart:
   if (n++ >= MAX_ITER)
      return -1;
begin:

   /* Примечание 15 */
   TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN); /* Передача start */
   while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_REP_START:   /* OK, но ничего не должно произойти */
   case TW_START:
      break;

   case TW_MT_ARB_LOST:
      goto begin;

   default:
      return -1;  /* ошибка: не в состоянии start condition */
                  /* NB: не нужно посылать stop condition */
   }

   /* отправка SLA+W */
   TWDR = sla | TW_WRITE;
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MT_SLA_ACK:
      break;

   case TW_MT_SLA_NACK: /* NACK во время выбора: устройство занято записью */
      goto restart;

   case TW_MT_ARB_LOST: /* повторный арбитраж */
      goto begin;

   default:
      goto error;    /* нужно отправить stop condition */
   }

#ifdef WORD_ADDRESS_16BIT
   TWDR = (eeaddr>>8);  /* устройство с 16-битным словом адреса, отправка
                           старших 8 бит адреса */
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0); /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MT_DATA_ACK:
      break;

   case TW_MT_DATA_NACK:
      goto quit;

   case TW_MT_ARB_LOST:
      goto begin;

   default:
      goto error; /* нужно отправить stop condition */
   }
#endif

   TWDR = eeaddr; /* младшие 8 бит адреса */
   TWCR = _BV(TWINT) | _BV(TWEN); /* очистка прерывания для запуска передачи */
   while ((TWCR & _BV(TWINT)) == 0); /* ожидание передачи */
   switch ((twst = TW_STATUS))
   {
   case TW_MT_DATA_ACK:
      break;

   case TW_MT_DATA_NACK:
      goto quit;

   case TW_MT_ARB_LOST:
      goto begin;

   default:
      goto error; /* нужно отправить stop condition */
   }

   for (; len > 0; len--)
   {
      TWDR = *buf++;
      TWCR = _BV(TWINT) | _BV(TWEN); /* запуск передачи */
      while ((TWCR & _BV(TWINT)) == 0) ; /* ожидание передачи */
      switch ((twst = TW_STATUS))
      {
      case TW_MT_DATA_NACK:
         goto error;    /* устройство защищено от записи - Примечание 16 */

      case TW_MT_DATA_ACK:
         rv++;
         break;

      default:
         goto error;
      }
   }
quit:
   TWCR = _BV(TWINT) | _BV(TWSTO) | _BV(TWEN); /* отправка stop condition */

   return rv;

error:
   rv = -1;
   goto quit;
}

/*
 * Обертка вокруг ee24xx_write_page(), которая делает повторные вызовы
 * этой функции либо пока не возникнет ошибка, либо не будут записаны
 * все байты.
 */
int ee24xx_write_bytes(uint16_t eeaddr, int len, uint8_t *buf)
{
   int rv, total;

   total = 0;
   do
   {
#if DEBUG
      printf("Calling ee24xx_write_page(%d, %d, %p)",
             eeaddr, len, buf);
#endif
      rv = ee24xx_write_page(eeaddr, len, buf);
#if DEBUG
      printf(" => %d\n", rv);
#endif
      if (rv == -1)
         return -1;
      eeaddr += rv;
      len -= rv;
      buf += rv;
      total += rv;
   }
   while (len > 0);

   return total;
}

void error(void)
{
   printf("error: TWI status %#x\n", twst);
   exit(0);
}

FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

void main(void)
{
   uint16_t a;
   int rv;
   uint8_t b[16];
   uint8_t x;

   ioinit();

   stdout = &mystdout;

   for (a = 0; a < 256;)
   {
      printf("%#04x: ", a);
      rv = ee24xx_read_bytes(a, 16, b);
      if (rv <= 0)
         error();
      if (rv < 16)
         printf("warning: short read %d\n", rv);
      a += rv;
      for (x = 0; x < rv; x++)
         printf("%02x ", b[x]);
      putchar('\n');
   }
#define EE_WRITE(addr, str) ee24xx_write_bytes(addr, sizeof(str)-1, str)
   rv = EE_WRITE(55, "The quick brown fox jumps over the lazy dog.");
   if (rv < 0)
      error();
   printf("Wrote %d bytes.\n", rv);
   for (a = 0; a < 256;)
   {
      printf("%#04x: ", a);
      rv = ee24xx_read_bytes(a, 16, b);
      if (rv <= 0)
         error();
      if (rv < 16)
         printf("warning: short read %d\n", rv);
      a += rv;
      for (x = 0; x < rv; x++)
         printf("%02x ", b[x]);
      putchar('\n');
   }
   printf("done.\n");
}

Примечание 1. Заголовочный файл util/twi.h содержит некоторые макроопределения символических констант, используемых для обозначение регистров TWI (таких как TWI status register, регистр состояния TWI). Эти определения соответствуют именам регистров из даташита Atmel за исключением того, что все имена получают префикс TW_.

Примечание 2. Тактовая частота, используемая в вычислениях параметров таймера, осуществляется компилятором. Это используется для настройки скорости передачи UART (baud rate) и тактовой частоты TWI.

Примечание 3. Адрес, назначаемый для 24Cxx EEPROM, состоит из 1010 в старших 4 битах (разряды 6, 5, 4, 3 адреса). Значение следующих трех бит (разряды 2, 1, 0 адреса) определяют sub-адрес slave-устройства, что задается уровнями A2, A1, A0 на выводах микросхемы EEPROM. Это сделано для того, чтобы к одной шине можно было параллельно подключить несколько микросхем, различая их по sub-адресу. Однако следует иметь в виду, что для обращения к устройствам используется только 7-битный адрес, за которым сразу идет пакет данных, но может быть ситуация, когда к шине подключена микросхема EEPROM, требующая 10-битного адреса (такая как 24C04 и выше). Такая микросхема с 10-битным адресом может "украсть" этот байт данных, считая его байтом, где находятся дополнительные бит. Этот пример подразумевает, что используется маленькая микросхема с 7-битным адресом, у которой на ножки A2, A1, A0 подключены к земле (полный 7-битный адрес будет 1010000).

Примечание 3a. Микросхемы EEPROM типа 24C32 и выше не могут быть адресованы с помощью бит sub-адреса. Таким образом, они требуют, чтобы старшие биты адреса были посланы по шине отдельно от младших. Когда раскомментировано определение WORD_ADDRESS_16BIT, алгоритм реализует передачу дополнительного байта адреса.

Примечание 4. Для низкой частоты тактирования AVR используется удвоение скорости UART (разрешается 2 x U[S]ART clock multiplier). Это позволит осуществить обмен данными на скорости 9600 бод на частоте 1 МГц, которую предоставляет стандартный встроенный генератор RC AVR (обычно это настройка по умолчанию, определяемая фьюзами микроконтроллера AVR). Подробности см. в таблицах Baud rate порта USART даташита AVR.

Примечание 5. В даташите объясняется, почему должно сохраняться минимальное значение 10 для регистра TWBR, когда используется master mode. Таким образом, когда тактовая частота ниже 3.6 МГц, мы не можем запустить шину TWI на желаемой частоте обмена 100 кГц, в результате работа шины будет происходить медленнее.

Примечание 6. Эта функция используется для стандартного вывода, что удобно для отладки и демонстрации работы кода.

Примечание 7. Чтобы ускорить передачу данных по шине TWI, микросхемы 24Cxx EEPROM поддерживают пакетный режим, когда в ответ на один запрос по шине передается друг за другом несколько байт данных. Это происходит за счет автоматического инкремента внутреннего счетчика адреса байта микросхемы (не путайте этот адрес с TWI-адресом!) после успешной передачи каждого байта. При чтении данных за один запрос, если это необходимо, можно передать все содержимое памяти микросхемы (после достижения конца памяти счетчик адреса переполнится, и вернется к адресу 0).

Примечание 8. При чтении EEPROM сначала выполняется запись (R/~W установлен в 0, что показывает операцию записи), чтобы передать адрес EEPROM, откуда должно начаться чтение. Эта фаза называется master transmitter mode. Завершение каждой отдельной фазы обмена TWI показывается установкой бита TWINT в регистре TWCR (при этом может сгенерироваться прерывание, если оно разрешено. В нашем примере прерывания не используются). После выполнения действий, которые нужны для перехода к следующей фазе коммуникации, флаг прерывания должен быть очищен вручную путем установки бита TWINT (не удивляйтесь, бит TWINT сбрасывается записью в него лог. 1). В отличие от других источников прерывания, это нужно выполнить даже в коде реального обработчика прерывания, поскольку следующая фаза обмена не запустится, пока бит TWINT не будет очищен.

Примечание 9. Поскольку шина TWI может работать в режиме multi-master, может быть ситуация, когда может быть конкуренция (одновременный доступ к шине нескольких master-устройств) при попытке доступа к шине со стороны устройства master. Обычно аппаратный блок интерфейса шины TWI определит эту ситуацию, и не будет инициировать запуск start condition, пока шина занята. Однако в случае, когда два устройства master стартовали одновременно, сработает арбитраж шины, при этом всегда один из master-устройств проиграет арбитраж шины во время любой операции передачи. Master, который проиграл арбитраж, по протоколу должен сразу прекратить свою активность на шине; в частности, он не должен инициировать сигнал stop condition, чтобы не повредить исходящую передачу от другого активного master. В этом примере при детектировании события проигрыша арбитража, вся передача будет перезапущена заново. При этом будет инициирован новый сигнал start condition, который будет нормально задерживаться, пока текущий активный мастер не освободит шину.

Примечание 10. Далее slave-устройство выбирается повторно (с использованием так называемого повторного start condition, который предназначается для того, чтобы арбитраж шины остался у текущего master), используя тот же самый адрес подчиненного устройства (SLA), но на этот раз с целью чтения (бит R/~W установлен в лог. 1). В результате получится запрос к slave-устройству, чтобы оно в следующем пакете начало передавать данные для master.

Примечание 11. Если устройство EEPROM все еще занято записью одной или большего количества ячеек, что было инициировано предыдущим запросом на запись, то оно просто оставляет свои драйверы отключенными от шины, и никак не отвечает на выбор (запрос) со стороны master. Устройство master, которое попыталось выбрать устройство slave, в ответ на свой запрос увидит логический уровень единицы на SDA после передачи пакета SLA+R/W как сигнал NACK. Таким образом, процесс выбора устройства просто перезапустится (что эффективно приведет к повторам сигнала start condition), пока устройство не начнет реально отвечать. Эта процедура опроса рекомендуется в даташите 24Cxx, чтобы минимизировать время ожидания при записи. Обратите внимание, что в случае, когда slave-устройство повреждено и не отвечает по шине (или просто не присутствует на шине), это приведет к бесконечному зацикливанию (зависанию). Для устранения этой проблемы введен параметр MAX_ITER максимального количества итераций, после которых будет возвращена ошибка.

Примечание 12. Это так называемый режим приема главного устройства (master receiver mode): мастер шины будет просто выдавать импульсы тактов SCL, и slave-устройство в ответ будет управлять сигналом SDA, передавая туда соответствующие данные. После передачи 8 бит данных master-устройство ответит сигналом ACK (переведя SDA в лог. 0), чтобы запросить другую передачу данных от slave-устройства, или может оставить сигнал SDA в лог. 1 (передавая тем самым сигнал NACK), что покажет для slave-устройства, что нужно остановить передачу. Выдача сигнала ACK обрабатывается установкой бита TWEA в регистре TWCR, когда запускается текущая передача.

Примечание 13. Отправляется управляющее слово, чтобы инициировать передачу следующего пакета данных, когда предварительно установлен бит TWEA. Во время последней итерации цикла бит TWEA сбрасывается, чтобы slave-устройство было оповещено, что больше не нужны передачи данных.

Примечание 14. Кроме случая потери арбитража, все транзакции по шине должны завершаться устройством master путем выдачи сигнала stop condition.

Примечание 15. Запись в устройство EEPROM осуществляется проще, чем чтение, потому что нужно использовать только передачу master transmitter mode. Обратите внимание, что первый пакет после пакета выбора SLA+W всегда считается TWI-адресом для микросхемы EEPROM, который будет использоваться для следующей фазы обмена (этот пакет точно такой же, как был послан на фазе инициации операции чтения). В случае, когда остается в действии master transmitter mode, следующие отправляемые пакеты считаются записываемыми в EEPROM байтами данных. Внутренний счетчик адреса будет автоматически инкрементироваться после каждой операции записи.

Примечание 16. Микросхемы памяти 24Cxx могут стать защищенными от записи (write-protected) путем перевода их внешнего вывода ~WC в лог. 1 (если оставить этот вывод не подключенным, то на выводе ~WC получится сигнал лог. 0, что означает отсутствие защиты от записи). Когда устройство защищено от записи, на все запросы передачи данных на запись будут посылаться сигналы NACK. Имейте в виду, что в некоторых устройствах это может быть не реализовано.

[Пример программирования Si570 через TWI]

Это простой пример консольного приложения, работающего через виртуальный COM-порт. Синтезатор Si570CBC000121G [5] подключен к микроконтроллеру ATmega32U4 (использовалась макетная плата AVR-USB32U4 [6]), который через TWI программирует регистры Si570 для установки нужной частоты синтезатора. Показанная на рисунке схема использовалась в качестве гетеродина приемника [8].

Si570 programming SCH

Устанавливаемая частота вводится в консоли текстовой командой "f частота", где частота указывается в МГц, в виде числа с плавающей запятой. Пример ввода команды для установки частоты 42.667 МГц:

f 42.667

Модуль исходного кода Si570.c был портирован из даташита AN334 [7] на аппаратуру TWI микроконтроллера AVR.

Заголовочный файл Si570.h:

#ifndef __SI570__
#define __SI570__

// Частота запуска. Её нужно знать при заказе микросхемы, однако если
// частота не известна, то её можно изменить. Это нужно сделать точно,
// чтобы гарантировать точность всех других частот.
#define FOUT_START_UP 100     // МГц

// Диапазон Si57x/598/599's FDCO
// Эти значения менять не следует, однако они могут зависеть от приложения.
#define FDCO_MIN 4850.0    // float, МГц
#define FDCO_MAX 5670.0    // float, МГц

void ReadStartUpConfiguration(void);
void RunFreqProg (float freq);

#endif

Исходный код Si570.c:

#include <math.h>
#include "Si570.h"
#include "i2c.h"

// Здесь только допустимые значения для HSDIV. Подробности см. в даташите.
static const HSDIV[6] = {11, 9, 7, 6, 5, 4};
static float FOUT0 = FOUT_START_UP;

//-----------------------------------------------------------------------------
// Глобальные переменные
//-----------------------------------------------------------------------------
u8 INITIAL_HSDIV;       // Значение HSDIV, прочитанное из Si57x/598/599,
                        // когда включается питание
u8 INITIAL_N1;          // Значение N1, прочитанное из Si57x/598/599,
                        // когда включается питание
u8 REG[6];              // Массив бит, хранящий начальные значения,
                        // прочитанные из Si57x/598/599
u32 INITIAL_RFREQ_LONG; // Значение RFREQ, прочитанное из Si57x/598/599,
                        // когда включается питание
float RFREQ;            // Дробный множитель, используемый для достижения
                        // корректной выходной частоты
float FXTAL;            // Хранит значение частоты внутреннего
                        // кварца
u8* pTWI_DATA_IN;       // Глобальный указатель на данные TWI,
                        // сюда записываются все принимаемые данные
u8 TWI_SINGLEBYTE_OUT;  // Глобальное хранилище для одного записываемого байта.
u8* pTWI_DATA_OUT;      // Глобальный указатель на данные TWI.
                        // Отсюда берутся все передаваемые данные
u8 TWI_DATA_LEN;        // Глобальная переменная для хранения количества
                        // отправляемых или принимаемых байт
                        // текущей передачи TWI.
u8 WORD_ADDR;           // Глобальная переменная для слова адреса ячейки
                        // к которой будет осуществлен доступ в следующей передаче
//u32 FRAC_BITS;          // Метод вычисления с плавающей запятой двойной точности.

//-----------------------------------------------------------------------------
// SetBits
//-----------------------------------------------------------------------------
// Возвращаемое значение: unsigned char
// Параметры:
// 1) unsigned char original - оригинальное значение переменной, которая будет
//    изменена
//
// 2) unsigned char reset_mask - содержи маску бит, которые должны быть сброшены
//    в переменной original.
//
// 3) unsigned char new_val - содержит маску бит, которые должны быть установлены
//    в переменной original.
//
// Эта функция установит нужные биты в переменной unsigned char.
static u8 SetBits(u8 original, u8 reset_mask, u8 new_val)
{
   return (( original & reset_mask ) | new_val );
}

//-----------------------------------------------------------------------------
// ReadStartUpConfig
//-----------------------------------------------------------------------------
// Возвращаемое значение: нет
// Параметры: нет
//
// Читает стартовое содержимое регистров RFREQ, HSDIV и N1, и вычисляет
// частоту внутреннего кварцевого генератора (FXTAL).
void ReadStartUpConfiguration (void)
{
   u8 regval = 0x01;
   twi_write_bytes(135, 1, &regval);// Запись 0x01 в регистр 135. Это приведет
                                    // к копированию бит из NVM в RAM. Подробности
                                    // по регистру 135 см. в даташите Si57x/598/599.
   // Чтение регистров 7..12 синтезаторов Si57x/598/599.
   // REG[0] соответствует регистру 7 устройства
   // REG[5] соответствует регистру 12 устройства
   twi_read_bytes(7, 6, REG);
   // Получение значения fo INITIAL_HSDIV из REG[0]:
   INITIAL_HSDIV = ((REG[0] & 0xE0) >> 5) + 4;
   // Добавляется 4, потому что биты "000" соответствуют HSDIV 4, т. е. имеется
   // смещение 4. См. описание регистра 7 в даташите Si570.
   INITIAL_N1 = (( REG[0] & 0x1F ) << 2 ) + (( REG[1] & 0xC0 ) >> 6 );
   // Получение корректного значения INITIAL_N1 путем добавления
   //частей REG[0] и REG[1]:
   if(INITIAL_N1 == 0)
   {
      INITIAL_N1 = 1; // Это граничный случай для N1
   }
   else if((INITIAL_N1 & 1) != 0)
   {
      INITIAL_N1 = INITIAL_N1 + 1;  // По даташиту недопустимые нечетные значения
                                    // должны быть округлены вверх до получения
                                    // ближайшего четного значения.
   }
   // Метод вычисления с плавающей запятой двойной точности.
   // Преобразование RFREQ: реконструкция из регистров дробной части (биты 0..28)
   // (для необходимой точности этот метод требует чисел с плавающей запятой
   // двойной точности)
   /* FRAC_BITS = (( REG[2] & 0xF ) * POW_2_24 );
   FRAC_BITS = FRAC_BITS + (REG[3] * POW_2_16);
   FRAC_BITS = FRAC_BITS + (REG[4] * 256);
   FRAC_BITS = FRAC_BITS + REG[5];
   RFREQ = FRAC_BITS;
   RFREQ = RFREQ / POW_2_28;
   */
   // Чтение изначального значения RFREQ. 34-битное число помещается в 32 битах
   // путем игнорирования младших 2 бит.
   INITIAL_RFREQ_LONG = ( REG[1] & 0x3F );
   INITIAL_RFREQ_LONG = (INITIAL_RFREQ_LONG << 8) + ( REG[2] );
   INITIAL_RFREQ_LONG = (INITIAL_RFREQ_LONG << 8) + ( REG[3] );
   INITIAL_RFREQ_LONG = (INITIAL_RFREQ_LONG << 8) + ( REG[4] );
   INITIAL_RFREQ_LONG = (INITIAL_RFREQ_LONG << 6) + ( REG[5] >> 2 );
   // Преобразование RFREQ: реконструкция из регистров целой части.
   RFREQ = RFREQ + ( (( REG[1] & 0x3F ) << 4 ) + (( REG[2] & 0xF0 ) >> 4 ) );
   // Вычисление частоты внутреннего кварцевого генератора (FXTAL):
   FXTAL = (FOUT0 * INITIAL_N1 * INITIAL_HSDIV) / RFREQ; //MHz
}

//-----------------------------------------------------------------------------
// RunFreqProg
//-----------------------------------------------------------------------------
// Возвращаемое значение: нет
// Параметры: частота в мегагерцах
//
// Программирование Si570 для вывода нужной частоты.
void RunFreqProg (float freq)
{
   u8 i;             // Временная переменная счетчика, используемая в циклах for.
   u8 n1;            // Выходной делитель, который модифицируется и используется
                     // в вычислении нового коэффициента RFREQ
   u8 hsdiv;         // Выходной делитель, который модифицируется и используется
                     // в вычислении нового коэффициента RFREQ
   bool validCombo;  // Флаг, который устанавливается в 1, если допустимая
                     // комбинация N1 и HSDIV найдена
   u8 tmpreg;
   u16 divider_max;  // Максимальный делитель для комбинации HSDIV и N1
   u16 curr_div;     // Минимальный делитель для комбинации HSDIV и N1
   // u16 whole;       // Используется в методе чисел с плавающей запятой
                       //двойной точности
   u32 final_rfreq_long;// Конечный REFREQ, который отправляется в Si57x/598/599
   float curr_n1;    // Используется для вычисления конечного значения N1
   float n1_tmp;     // для отправки в Si570
   float ratio = 0;  // Будет хранить конечное соотношение для умножения
                     // начального REFREQ в процессе достижения конечного RFREQ
   // Поиск делителей (получение max и min диапазона делителя для
   // комбинации HSDIV и N1):
   divider_max = floorf(FDCO_MAX / freq);
   curr_div = ceil(FDCO_MIN / freq);
   validCombo = 0;
   while (curr_div <= divider_max)
   {
      // проверка всех значений HSDIV со следующим curr_div
      for(i=0; i < 6; i++)
      {
         // получение следующего возможного значения n1
         hsdiv = HSDIV[i];
         curr_n1 = (float)(curr_div) / (float)(hsdiv);
         // Проверка: если curr_n1 целое и четное число, или единица,
         // то это будет допустимым вариантом делителя для новой частоты
         n1_tmp = floorf(curr_n1);
         n1_tmp = curr_n1 - n1_tmp;
         if(n1_tmp == 0.0) // то curr_n1 является целым числом
         {
            n1 = (u8) curr_n1;
            if( (n1 == 1) || ((n1 & 1) == 0) ) // то вычисленный N1 является
            { // либо 1, либо четным числом
               validCombo = 1;
            }
         }
         if(validCombo == 1) break; // Делитель был найден, выход из цикла
      }
      if(validCombo == 1) break; // Делитель был найден, выход из цикла
      curr_div = curr_div + 1; // Если допустимый делитель не найден,
      // то инкремент curr_div и повтор тела цикла
   }
   // Если в этом месте validCombo == 0, то ошибка в вычислениях
   // Проверьте, предоставляют ли FOUT0 и FOUT1 допустимые
   // частоты.
   // Вычисление нового RFREQ
   RFREQ = (freq * n1 * hsdiv) / FXTAL;
   // RFREQ удерживается как unsigned long, в этом формате
   // доступно только 32 бита. В устройстве синтезатора
   // RFREQ имеет точность 34 бита. Нужны только 34 бита из 38,
   // потому что RFREQ находится между 42.0 и 50.0 для
   // частоты fxtal = 114.285MHz (номинал)
   ratio = freq / FOUT0;   // Стараемся удержать соотношение около 1
                           // для сохранения точности
   ratio = ratio * (((float)n1)/((float)INITIAL_N1));
   ratio = ratio * (((float)hsdiv)/((float)INITIAL_HSDIV));
   final_rfreq_long = ratio * INITIAL_RFREQ_LONG; // Вычисление конечного RFREQ
   // Значение используемого отношения вычислено выше
   for(i = 0; i < 6; i++)
   {
      REG[i] = 0; // очистка регистров
   }
   hsdiv = hsdiv - 4; // Вычтем 4 из-за смещения HSDIV.
   // Например: "000" отображается на 4, "001" отображается на 5
   // Установка верхних 3 бит REG[0], которые будут соответствовать
   // регистру 7 синтезаторов Si57x/598/599
   REG[0] = (hsdiv << 5);
   // Преобразование нового N1 в двоичное представление
   if(n1 == 1)
   {
      n1 = 0; // Крайний случай для N1. Если N1=1, то это представляется
              // как "00000000".
   }
   else if((n1 & 1) == 0)
   {
      n1 = n1 - 1; // Если n1 четное, округление вниз до ближайшего
                   // нечетного числа. Подробности см. в даташите
                   // синтезатора Si57x/598/599.
   }
   // Запись новых корректных значений в REG[0] .. REG[6].
   // Они будут отправлены в Si57x/598/599, и будет обновлена
   // выходная частота.
   REG[0] = SetBits(REG[0], 0xE0, (n1 >> 2)); // Установка части N1 в REG[0]
   REG[1] = (n1 & 3) << 6; // Установка части N1 в REG[1]
   // Запись новой версии RFREQ в соответствующие регистры
   REG[1] = REG[1] | (final_rfreq_long >> 30);
   REG[2] = final_rfreq_long >> 22;
   REG[3] = final_rfreq_long >> 14;
   REG[4] = final_rfreq_long >> 6;
   REG[5] = final_rfreq_long << 2;
   /*
   // Метод двойной точности числе с плавающей запятой.
   // Преобразование нового RFREQ в двоичное представление.
   // Отделение целой части.
   whole = FLOORF(RFREQ);
   // Получение двоичного представления дробной части.
   FRAC_BITS = FLOORF((RFREQ - whole) * POW_2_28);
   // Установка регистров 12..10,  делая frac_bits меньше
   // путем сдвига каждый раз последних 8 бит
   for(i=5; i >=3; i--)
   {
      REG[i] = FRAC_BITS & 0xFF;
      FRAC_BITS = FRAC_BITS >> 8;
   }
   // Установка последних 4 бит дробной части регистра 9.
   REG[2] = SetBits(REG[2], 0xF0, (FRAC_BITS & 0xF));
   // Установка целой части RFREQ в регистрах 8 и 9
   REG[2] = SetBits(REG[2], 0x0F, (whole & 0xF) << 4);
   REG[1] = SetBits(REG[1], 0xC0, (whole >> 4) & 0x3F);
   */
   twi_read_bytes(137, 1, &tmpreg); // Чтение текущего состояния регистра 137
   tmpreg |= 0x10;                  //Установка в этом регистре бита DCO.
   twi_write_bytes(137, 1, &tmpreg);// Это должно быть сделано, чтобы обновить
                                    // регистры 7..12 синтезаторов Si57x/598/599.
   twi_write_bytes(7, 6, REG);      // Запись новых значений в регистры 7..12.
   twi_read_bytes(137, 1, &tmpreg); // Чтение текущего состояния регистра 137
   tmpreg &= 0xEF;                  // Очистка бита DCO.
   twi_write_bytes(137, 1, &tmpreg);//
   tmpreg = 0x40;                   // Установка бита NewFreq, чтобы оповестить
   twi_write_bytes(135, 1, &tmpreg);// систему DPSLL, что применена новая
                                    // конфигурация частоты
}

[Словарик]

LSB Least Significant Bit, самый младший значащий бит.

MSB Most Significant Bit, самый старший значащий бит.

SLA SLave Address, TWI-адрес подчиненного устройства на шине.

TWI Two Wire Interface, двухпроводный интерфейс. То же самое, что и I2C.

[Ссылки]

1. avr-libc: Example using the two-wire interface (TWI) site:nongnu.org.
2. Подключение RTC DS1307 к микроконтроллеру AVR.
3. AVR245: кодовый замок с клавиатурой 4x4 и I2C LCD.
4. UM10204 I2C-bus specification and user manual site:nxp.com.
5Si570, Si571: программируемый через I2C генератор XO/VCXO.
6. Макетная плата AVR-USB32U4.
7AN334: пример программирования синтезаторов Si57X, Si598, Si599.
8. Любительский приемник эталонной частоты.