Аппаратный I2C (TWI) в микроконтроллерах AVR

При наличии на борту AVR аппаратной реализации I2C почему-то многие предпочитают программные реализации. Хотя, на мой скромный взгляд — использование железного варианта проще, стабильнее и удобнее.

Применение встроенного интерфейса и подразумевает работу на прерываниях, но сегодня мы обойдемся без оных. Для понимания работы контроллера это несколько проще, а переложить код на использование прерываний не составит труда.

Описывать шину I2C не имеет смысла, исчерпывающие описание можно найти на википедии, Казусе и конечно у DI HALT’a. Последняя ссылка заслуживает особого внимания, там основное внимание уделяется именно AVR, но в качестве примеров используется RTOS, что несколько абстрагирует от последовательности работы. Именно для того, чтобы дополнить статью DI HALT’a (а так же, чтобы не забыть что и как самому) и была написана эта небольшая заметка.

Итака, шина I2C глазами микроконтроллера AVR как всегда представляет собой несколько регистров, а именно:

  • TWBR — TWI Bit Rate Register: В этом регистре настраивается частота (скорость) шины, так же на частоту влияет биты TWPS0..1 в регистре TWSR
  • TWCR — TWI Control Register: Через этот регистр происходит все управление шиной
  • TWSR — TWI Status Register: За исключением первых трех бит (TWPS0..1 и зарезервированного) — регистр отражает состояние шины
  • TWDR — TWI Data Register: Как не сложно догадаться — регистр данных. Именно из него данные уходят по шине, и именно в него контроллер помещает полученные байты.

Частота шины рассчитывается по формуле: FSCL = FCPU/(16+2(TWBR)*4TWPS). И это единственное, что нужно сделать при инициализации.

Вся работа TWI сводится к алгоритму:

  1. Записать значение в регистр TWCR (а при передачи данных предварительно поместить байтик в TWDR)
  2. Дождаться флага TWINT в том же регистре TWCR (при работе с прерываниями — этот флаг вызовет прерывание по-вектору TWI)
  3. Получить статус из регистра TWSR — в зависимости от статуса, что-то делать или не делать дальше.

К этой не хитрой последовательности сводится вся логика работы шины, формирование стартов-рестартов-стопов, передача байта, прием байта и формирование ответа ACK.

Для простоты понимания, что и когда надо записывать в TWCR оформим небольшую функцию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define TWI_START       0
#define TWI_RESTART         1
#define TWI_STOP            2
#define TWI_TRANSMIT        3
#define TWI_RECEIVE_ACK     4
#define TWI_RECEIVE_NACK    5
 
uint8_t twi(uint8_t action){
    switch(action){
        case TWI_START:
        case TWI_RESTART:
            TWCR = _BV(TWSTA) | _BV(TWEN) | _BV(TWINT);// Если нужно прерывание | _BV(TWIE);
            break;
        case TWI_STOP: 
            TWCR = _BV(TWSTO) | _BV(TWEN) | _BV(TWINT);// | _BV(TWIE);
            break;
        case TWI_TRANSMIT: 
            TWCR = _BV(TWEN) | _BV(TWINT);// | _BV(TWIE);
            break;
        case TWI_RECEIVE_ACK:
            TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);//| _BV(TWIE);
            break;
        case TWI_RECEIVE_NACK:
            TWCR = _BV(TWEN) | _BV(TWINT);// | _BV(TWIE);
            break;
        }
    if(action != TWI_STOP)while (!(TWCR & _BV(TWINT)));   
    return (TWSR & 0xF8);
}

Функция получает требуемое действие в качестве аргумента, дожидается его выполнения и возвращает результат выполнения. Коды возврата довольно непонятно описаны в Datasheet, и очень хорошо у DI HALT’a (ссылка выше по тексту). Данные для передачи необходимо заранее загрузить в регистр TWDR перед выполнением

twi(TW_TRANSMIT)

После успешного получения байта с помощью

twi(TW_RECEIVE_ACK)

или

twi(TW_RECEIVE_NACK)

так же следует напрямую прочитать из TWDR
Пример чтения (без всяких проверок и контроля выполнения) 128 байтов из EEPROM 24C01S:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TWSR &= ~(_BV(TWPS0)|_BV(TWPS1)); // биты предделителя
TWBR=114;                         //Настраиваем частоту шины 
                                  //(при 8MHz F_CPU получаем 8000000/(2*(16+114)) = 32kHz)
uint8_t arr[128];  
twi(TWI_START);    //формируем сигнал START
TWDR=0xA0;         //Загружаем адрес EEPROM+W
twi(TWI_TRANSMIT); //Передаем адрес
TWDR=0x00;         //Загружаем адрес байта 
twi(TWI_TRANSMIT); //Передаем
twi(TWI_RESTART);  //Формируем рестарт (RESTART)
TWDR=0xA1;         //Загружаем адрес EEPROM+R
twi(TWI_TRANSMIT); //Передаем
uint8_t i;         //Принимаем 127 байт из EEPROM, формируя после каждого ответ ACK
for(i=0;i<127;i++) 
{
    twi(TWI_RECEIVE_ACK);
    arr[i]=TWDR;
}
twi(TWI_RECEIVE_NACK);//Читаем последний байт, формируем NACK - больше данные не нужны
arr[127] = TWDR;
twi(TWI_STOP);     //Формируем сигнал STOP

Это простейший и неправильный пример. Неправильного в нем — после каждого выполнения функции twi() необходимо проверять код возврата.
После выполнения контроллером любого действия код выполнения записывается в регистр статуса: TWSR, из-за наличия в этом же регистре настроек предделителя необходимо замаскировать первые три бита:

uint8_t result = TWSR & 0b11111000;

или

uint8_t result = TWSR & 0xF8;

Для удобства (или неудобства, как посмотреть), в Datasheet (и соответственно во всех остальных статьях и документах) коды возврата указаны с замаскированными 3 битами. (т.е. можно сравнивать TWSR & 0xF8 со значениями в Datasheet, сдвигать ничего никуда не нужно)
Каждое действие может вернуть несколько кода:

START/RESTART:

  • 0x08 — сигнал START передан
  • 0x10 — сигнал REPEATED START передан

Передача адреса (первого байта после START/RESTART):

  • 0x18 — Адрес для записи передан, ответ (ACK) получен
  • 0x20 — Адрес для записи передан, устройство не откликнулось
  • 0x38 — Контроллер потерял шину (вылез еще один контроллер)
  • 0x40 — Адрес для чтения передан, ответ (ACK) получен
  • 0x48 — Адрес для чтения передан, устройство не откликнулось

Передача данных (второго и последующих байтов) возвращает:

  • 0x28 — Байт отправлен, ACK получен
  • 0x30 — Байт отправлен, ACK не получен
  • 0x38 — Потеря шины

Прием данных:

  • 0x38 — Потеря шины
  • 0x50 — Данные получены, ACK передан
  • 0x58 — Данные получены, ACK не передан<

Следует оговориться насчет ACK: В протоколе I2C после передачи каждого байта информации, предусмотрено окно в 1 такт, для того чтобы устройство, которое выполняет прием данных могло откликнуться. Этот сигнал назван ACK и для его передачи принимающее устройство должно прижать линию SDA к земле в этом такте. Иногда, так же рассматривают сигнал NACK: NOT ACK (принимающее устройство не прижало SDA к земле на 9м такте передачи). Но с моей скромной точки зрения, как сигнал NACK рассматривать нельзя: во-первых это путает, во-вторых если на шине нет никаких устройств из-за подтяжки SDA к питанию передающий, прочитав 9й такт примет его за сигнал NACK — хотя сигнала никакого не было. Проще ACK считать за отклик, а NACK — за отсутствие отклика. Мастер передал адрес в шину, если адресуемое устройство присутствует на линии — получили ACK (отклик), нет устройства — нет отклика.
Так же ACK очень часто используется в передачи данных: например в случае для многобайтного чтения из 24CXX — хотим получить следующий байт формируем ACK, не хотим — не формируем.

P.S.
все примеры рассчитаны на ATmega16A, но с небольшими изменениями (а возможно и без) заработают на остальных

P.P.S.
Немного о граблях:
Самые частонаступаемые грабли в TWI на AVR — это работа с регистром TWCR. И большинство ошибок состоит в том, что это не совсем регистр (как и многие другие в AVR). Это не просто именованная ячейка памяти, а скорее именованный 8-и битный интерфейс для работы с периферией. И работать с ним надо как с интерфейсом. При записи каких-то битов не нужно выполнять присваивание с логическим ИЛИ (|=), для правильной работы необходимо именно перезаписывать его значение.