При наличии на борту 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 сводится к алгоритму:
- Записать значение в регистр TWCR (а при передачи данных предварительно поместить байтик в TWDR)
- Дождаться флага TWINT в том же регистре TWCR (при работе с прерываниями — этот флаг вызовет прерывание по-вектору TWI)
- Получить статус из регистра 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-и битный интерфейс для работы с периферией. И работать с ним надо как с интерфейсом. При записи каких-то битов не нужно выполнять присваивание с логическим ИЛИ (|=), для правильной работы необходимо именно перезаписывать его значение.