Библиотека Wire для Arduino для работы с шиной I2C. Копаем глубже...

Добавлено 11 октября 2017 в 14:41

Введение

Документация на библиотеку Wire довольно скромна. Нигде нет объяснения её связи с библиотекой twi из avr-lib, и, что еще хуже, с аппаратным модулем TWI в микроконтроллерах ATmega. Эта статья попытается заполнить пробелы и предоставить необходимую документацию по библиотеке Wire.

Для более подробной информации смотрите техническое описание на микроконтроллер, используемый в вашей плате Arduino. Это бесценная справочная информация для понимания аппаратного обеспечения TWI и способов связи контроллеров ATmega через I2C.

Внутри ATmega328

В основе данного подраздела лежит техническое описание на ATmega328 версии Rev. 8271C – 08/10. Приводимые мной страницы могут немного отличаться от текущей версии.

Для полного понимания кода библиотеке Wire, нам необходимо понимать, что ей нужно сделать с микроконтроллером, чтобы его настроить.

Просмотр технического описания на микроконтроллер показывает нам, как работает двухпроводная (Two Wire) система. Стоит отметить:

  • Микроконтроллер включает в себя аппаратный модуль TWI, который обрабатывает связь через шину I2C. ... Интересно, что это означает, что связь не обрабатывается библиотекой исключительно программно, как вы могли бы подумать! Другими словами, библиотека сама в программе не создает битовый поток. Библиотека взаимодействует с аппаратным компонентом, который выполняет тяжелую работу. Смотрите страницу 222 технического описания.
  • "AVR TWI работает с байтами и основывается на прерываниях..." (раздел 21.6 на странице 224). Это ключевой момент; это означает, что
    • вы настраиваете регистры;
    • вы позволяете TWI модулю осуществлять связь;
    • вы можете делать в это время что-то еще; ваш микроконтроллер с тактовой частотой 16 МГц не занят управлением последовательной связью на 100 кГц;
    • TWI модуль вызывает прерывание, когда заканчивает работу, чтобы уведомить процессор об изменениях состояния (включая успешность операций и/или ошибки).
  • Однако обратите внимание, что библиотека Wire блокирует ввод/вывод. Это означает, что он переходит в цикл ожидания и ждет завершения связи I2C. Ваше приложение не может ничего делать, пока модуль TWI общается по шине I2C. Обратите внимание, что это может быть не то, чего бы вы хотели: если ваша программа критична ко времени, то ваш 16-мегагерцовый процессор, застрявший в цикле ожидания и ждущий 100-килогерцового потока связи, будет не эффективен. Возможно, вам лучше написать собственный код I2C. В исходном коде avr-libc есть пример в ./doc/examples/twitest/twitest.c (смотрите http://www.nongnu.org/avr-libc/). Вы можете найти версию avr-libc, используемую вашей конкретной IDE Arduino, посмотрев файл versions.txt в каталоге установки Arduino IDE. Это будет где-то в Java/hardware/tools/avr. На Mac полный путь будет следующим /Applications/Arduino.app/Contents/Resources/Java/hardware/tools/avr/versions.txt; путь у вас будет другим, но схожим.

Регистры

Вот несколько регистров, задействованных аппаратным модулем TWI (Two Wire Interface, двухпроводный интерфейс):

РегистрНазваниеНазначение
TWCRРегистр управления двухпроводным интерфейсомУправляет действиями модуля TWI
TWSRРегистр состояния двухпроводного интерфейсаСообщает статус действий TWI
TWDRРегистр данных/адреса двухпроводного интерфейсаСодержит данные, которые вы хотите передать или приняли
TWBRРегистр битовой скорости двухпроводного интерфейсаУправляет частотой тактового сигнала (SCL)

Аппаратный модуль TWI

Смотрите следующую схему потока для записи на шину I2C:

Взаимодействие приложения с шиной TWI (I2C) во время типовой передачи
Взаимодействие приложения с шиной TWI (I2C) во время типовой передачи

Ссылаясь на таблицу регистров, приведенную выше, операция записи I2C с использованием аппаратного модуля TWI вкратце будет выглядеть следующим образом:

  • Библиотека Wire конфигурирует микросхему ATmega так, что внутренний аппаратный модуль TWI использует свои выводы, которые жестко привязаны к двум аналоговым выводам (4 и 5 на Duemilanove).
  • Библиотека Wire устанавливает значение регистра TWCR для создания состояния START.
    • Сначала она проверяет шину I2C, чтобы убедиться, что она свободна. Шина свободна, когда на обеих линиях и SDA, и SCL установлен высокий логический уровень (потому что устройство по умолчанию устанавливает выводы на шине в третье состояние «не подключено». Общий резистор подтягивает сигнал на шине к высокому уровню).
    • Что такое состояние START? Линия тактового сигнала SCL остается в состоянии логической 1, а мастер в это время меняет состояние на линии SDA на логический 0. Это уникальный момент, поскольку во время обычной передачи данных линии SDA изменяет состояние только тогда, когда на линии SCL установлен низкий логический уровень. Момент, когда сигнал на линии данных изменяется на логический 0, а на SCL логическая 1, является сигналом для всех устройств на шине I2C, с которыми мастер собирается начать взаимодействовать.
  • Аппаратный модуль TWI при завершении выполнения действия вызывает прерывание процессора.
  • Библиотека Wire проверяет состояние по регистру TWSR. Предположим, что всё хорошо.
  • Библиотека Wire загружает в регистр TWDR адрес ведомого устройства плюс бит «запись». Вместе это значение известно как "SLA+W" ("SLave Address plus Write").
  • Библиотека Wire устанавливает значение регистра TWCR для передачи SLA+W.
  • В случае успеха, библиотека Wire загружает данные в регистр TWDR, устанавливает TWCR, и данные передаются.
  • Библиотека Wire проверяет состояние по регистру TWSR. Предположим, что всё хорошо.
  • Библиотека Wire устанавливает значение регистра TWCR для передачи состояния STOP. В состоянии STOP линия SCL освобождается (переходит в высокий логический уровень), и затем линия SDA переходит в состояние логической 1. Обычно линия SDA должна оставаться неизменной, когда на линии SCL установлен высокий логический уровень.

Операция чтения выполняется похожим образом.

Проблемы при использовании с ATtiny

В библиотеке Wire для буферов используется большой объем памяти. Он включает в себя 3 разных байтовых буфера, каждый размером 32 байта. Это не большая проблема для ATmega, но ATtiny (например, ATtiny48) обладает меньшим количеством RAM (512/1K у ATmega, и 256/512 у ATtiny). Когда я попытался использовать библиотеку Wire на ATtiny, у меня появлялись случайные сбои, зависания и т.д. Как только я изменил размер буфера (в .\libraries\Wire\utility\twi.h) на 6, всё отлично заработало.

Не делайте ошибку, думая, что вы можете просто указать в своем скетче #define TWI_BUFFER_LENGTH 6 потому, что это не работает. Способ, которым Arduino IDE компилирует ваш код, не очевиден. Библиотека компилируется отдельно от вашего скетча, а затем линкуется с ним. Поэтому вам придется изменить размер буфера в заголовочном файле библиотеки, который также включается в исходном файле библиотеки (twi.c).

Библиотека Wire

Подробное описание библиотеки.

Обратите внимание, что библиотека Wire написана на C++. Таким образом, все перечисленные здесь вызовы являются настоящими методами класса C++. Если вы не знакомы с программированием на C++, вы можете думать о методе как о просто функции или подпрограмме, и будете правы.

Библиотека Wire блокирует ввод/вывод. Это означает, что при отправке или приеме по шине I2C ваше приложение не запускается до завершения связи. По правде говоря, работа оборудования TWI в вашем процессоре управляется прерываниями, поэтому приложение может работать на полной скорости, во время связи TWI, но эта возможность библиотекой Wire не используется.

В следующих подразделах в секциях «Под капотом» описывается действие, которое выполняет каждый метод с модулем TWI на ATmega и, следовательно, с шиной I2C.

begin()

Описание

Wire.begin() или Wire.begin(address) должен вызываться первой для запуска любой связи по I2C, использующей библиотеку Wire. Метод Wire.begin() устанавливает индексы внутренних буферов библиотеки для чтения и записи в значение 0, затем готовит аппаратный модуль TWI с помощью вызова функции twi.init().

twi.init() – это внутренний вызов библиотеки Wire. Он выполняет следующие задачи:

  • включает доступ аппаратного модуля TWI доступ к соответствующим выводам процессора ATmega. Если это ATmega328, то это аналоговые выводы: 4 для линии SDA, и 5 для линии SCL шины I2C;
  • устанавливает тактовую частоту, которую аппаратный модуль TWI будет использовать, если/когда он будет являться мастером на шине I2C. В исходниках она устанавливается на 100 кГц, но теоретически вы можете сбросить эту частоту, вызвав Wire.setClock(). Переданное значение – это битовая скорость от 400000L до 31000L. Wire.setClock(400000L); установит частоту на 400 кГц. Wire.setClock() должен вызываться после Wire.begin(). Wire.begin() устанавливает битрейт на 100 кГц.
Аргументы
Нет
Под капотом
  • Инициализирует внутренние переменные библиотеки Wire.
  • Вызывает twi.init(), который настраивает два порта ATmega на использование аппаратным модулем TWI:
    • если это ATmega168, ATmega8 или ATmega328P: PC4 для SDA, PC5 для SCL;
    • иначе PD0 для SCL, PD1 для SDA. Для более подробной информации смотрите техническое описание на свой процессор.
  • Устанавливает скорость передачи TWI на 100 кГц.
  • Включает аппаратный модуль TWI

begin(address)

Описание
  • Записывает TWAR (регистр адреса TWI) младшие 7 бит аргумента address. 8 бит аргумента сдвигаются влево, и добавляется бит 0. Этот 0 – это бит TWGCE.
  • Запускает функции twi_attachSlaveTxEvent(onRequestService); и twi_attachSlaveRxEvent(onReceiveService);. Эти функции назначают определенные пользователем функции, которые вызываются во время передачи и/или приема по I2C, когда процессор работает в качестве ведомого устройства I2C. Таким образом, onRequest() и onReceive() должны быть вызваны до вызова этого метода. Смотрите описание методов onRequest() и onReceive() ниже.
  • Запускает метод begin(), как и метод приведенный выше.
Аргументы
address: 8-битное целое число или значение int (по умолчанию размером 16 бит). Если ATmega будет в режиме ведомого, то это его адрес. Он преобразуется в 8-битное целое число. Если задано значение типа int (16 бит), у числа будет отсечено 8 старших бит.
Примечания
  • Если вы хотите, чтобы какая-либо функция автоматически вызывалась после каждой передачи по I2C, то для начала связи по I2C вы должны использовать эту функцию.
  • Вы должны использовать эту функцию, если хотите использовать Arduino как ведомое устройство на шине I2C.

Этот метод устанавливает значение регистра TWAR в аппаратном модуле TWI, который используется, когда вы хотите, чтобы Arduino работал как ведомое устройство I2C. Блок соответствия адресов TWI проверяет входящие сообщения I2C с содержимым TWAR. Когда есть совпадение, блок управления TWI оповещается, и модуль TWI будет выполнять действие в зависимости от настроек TWCR. Обратите внимание, что в библиотеке Wire бит TWGCE не устанавливается в 1. Это означает, что Arduino не будет отвечать на адрес общего вызова (адрес 0x00, который в основном используется для широковещательной рассылки по I2C).

twi_attachSlaveTxEvent() назначает функцию обратного вызова, которая будет использоваться в передаче I2C, если ATmega используется как ведомый I2C передатчик. Эта функция должна быть предварительно установлена с помощью метода onRequest(handler). Она вызывается после получения SLA+R, и она должна вызывать twi_transmit(), чтобы заполнить буфер передачи данными. Она также должна вызывать twi_reply(1), чтобы реально начать передачу по шине I2C (аргумент twi_reply() указывает, что ATmega отправит ACK после своего ответа). После вызова twi_reply() обработчик прерывания библиотеки Wire и аппаратный модуль TWI будут обрабатывать передачу данных.

twi_attachSlaveRxEvent() назначает функцию обратного вызова, которая будет использоваться при приеме по I2C, если ATmega используется как ведомое I2C устройство. Эта функция должна быть предварительно установлена с помощью метода onReceive(handler); она должна вызываться с двумя аргументами: приемный буфер и индекс к последним данным в буфере. Эта функция выполняется после принятия состояния STOP, сигнализирующего о завершении сеанса I2C. ATmega подтягивает линию тактового сигнала к нулю, тем самым заставляя ведущее устройство I2C дождаться завершения обработчика.

Под капотом
  • Устанавливает значение регистра TWAR в аппаратном модуле.
  • Назначает функцию обратного вызова, используемую при передаче по I2C. Эта функция вызывается. Эта функция после получения SLA+R, и она должна вызывать twi_transmit() и twi_reply(1).

requestFrom(address, count)

Описание

Используется, когда ATmega работает в качестве ведущего устройства I2C. Запрашивает count байт от ведомого устройства I2C, по заданному адресу. Каждый вызов этого метода сбрасывает индекс буфера чтения; то есть внутренний буфер считывается с начала, с нулевой ячейки массива. Обратите внимание, что адрес должен быть 7-битным; если передается 8-битный адрес, старший бит просто отсекается.

Метод requestFrom() вызывает функцию twi_readFrom(), которая фактически выполняет чтение I2C, отправляя состояние START и ожидая заполнения буфера в соответствии с запросом. Ведомое устройство I2C ответит NACK на запрос на чтение, если вы запросите больше байтов, чем доступно, и метод requestFrom() завершится.

Этот метод возвращает количество байтов, прочитанных с ведомого устройства.

Аргументы
  • address: 8-битное целое число, или значение int (обычно размером 16 бит). Адрес ведомого I2C устройства, с которым ATmega будет связываться. Он приводится к 8-разрядному беззнаковому целому числу. Если задано значение типа int (16 бит), у числа будет отсечено 8 старших бит. Затем address сдвигается влево на один бит, поэтому используются только младшие 7 бит. Если у вас 8-битный адрес, чтобы использовать его на Arduino, вы должны сдвинуть address вправо (потеряв самый младший бит).
  • count: целое число или 8-битное значение (тип uint8_t), задает количество байтов для чтения. Значение count ограничено значением BUFFER_LENGTH, которое определено в заголовочном файле Wire.h; на время написания оно было рано 32. Если count больше 32, то он всё равно будет установлен в значение 32.
Под капотом
  • Включает модуль TWI, который передает состояние START, таким образом, начиная сеанс связи по I2C.
  • Поэтому вызов функции SIGNAL будет использоваться при срабатывании прерывания процессора от модуля TWI, чтобы продолжить I2C сеанс и заполнить буфер чтения.

beginTransmission(address)

Описание

Используется, когда ATmega действует как мастер I2C. Устанавливает внутренние переменные библиотеки Wire для подготовки к передаче на указанный адрес.

Следующий вызов библиотеки Wire должен быть методом send().

Аргументы
address: адрес ведомого устройства I2C. Это 7-битный адрес; если передаются 8 бит, самый старший бит просто отсекается.
Под капотом
Никакой активности в TWI. Этот метод просто изменяет некоторые внутренние переменные библиотеки Wire.

endTransmission()

Описание

Вызывает функцию twi, которая выполняет реальную передачу TWI в режиме ведущего передатчика: он пытается стать мастером на шине I2C и записывает последовательность байтов в устройство на шине. Байты должны быть предварительно загружены в буфер передачи библиотеки Wire с помощью вызова send().

Результаты возвращаются в виде значения размером байт. Возможны следующие варианты:

  • 0: передача успешна;
  • 1: буфер передачи больше буфера twi. Этого не должно случиться, так как размер буфера TWI, установленный в twi.h, равен размеру буферу передачи, установленному в Wire.h;
  • 2: адрес был передан, но был получен NACK. Это проблема, и мастер должен отправить состояние STOP;
  • 3: данные были переданы, но был получен NACK. Это означает, что ведомому устройству нечего передавать. Мастер может послать состояние STOP или повторить START;
  • 4: произошла другая ошибка TWI (например, мастер потерял арбитраж шины).
Под капотом
  • Включает модуль TWI, который отправляет I2C условие START, что запускает сеанс связи I2C.
  • Поэтому вызов функции SIGNAL будет использоваться при срабатывании прерывания процессора от модуля TWI, чтобы продолжить I2C сеанс и передать содержимое выходного буфера данных по шине I2C.

send()

Описание

Заполняет буфер передачи данными, полученными из аргументов. Список возможных аргументов приведен ниже.

Термин "send" (отправить) для этого метода является чем-то неправильным. Этот метод на самом деле ничего не отправляет. Буфер данных (или массив) просто заполняется аргументом данных.

В режиме ведущего передатчика: в случае send(value) в буфер данных добавляется один байт. В случае send(string) или send(data, quantity) представленные данные добавляются в буфер данных с ограничением BUFFER_LENGTH. Любые данные, переданные для отправки, и которые могут переполнить буфер после превышения BUFFER_LENGTH (который на момент написания равен 32), просто игнорируются.

При работе в качестве ведомого передатчика I2C данные вставляются в буфер передачи ведомого I2C для доставки оборудованием TWI. Смотрите Wire.begin(address) и Wire.onRequest(handler).

Аргументы

Есть три варианта аргументов:

  • send(value): отправить байт, где значение типа uint8_t;
  • send(string): отправить строку символов char. Вызывает send(data, quantity), где quantity вычисляется как длина строки;
  • send(data, quantity): отправить массив байтов данных. Этот массив ограничен значением BUFFER_LENGTH, которое задано равным 32 в заголовочных файлах. Если quantity больше, чем BUFFER_LENGTH, дополнительные данные просто игнорируются.
Под капотом
Никакой активности в TWI. Этот метод просто изменяет некоторые внутренние переменные библиотеки Wire.

available()

Описание

Возвращает количество байтов доступных в приемном буфере библиотеки Wire. Этот буфер заполняется:

  • в методе requestFrom();
  • в методе onReceiveService(), который вызывается во время сессии ведомого I2C приемника после того, как мастер послал состояние STOP или повторил START, по сути, завершая передачу данных.
Аргументы

Нет.

Под капотом
Никакой активности в TWI. Этот метод просто изменяет некоторые внутренние переменные библиотеки Wire.

receive()

Описание

Возвращает следующий байт из приемного буфера библиотеки Wire. Смотрите метод available() выше для более подробной информации о приемном буфере библиотеки Wire.

Аргументы

Нет.

Под капотом
Никакой активности в TWI. Этот метод просто изменяет некоторые внутренние переменные библиотеки Wire.

onReceive(handler)

Устанавливает функцию обработчик handler, которая должна вызываться после того, как ATmega получит данные, работая в качестве ведомого устройства I2C. Обработчик вызывается с единственным аргументом, целым числом, которое является количеством полученных байтов. Он не должен возвращать каких-либо значений. Поэтому он должен выглядеть так:

void handler(int byteCount)

Шина I2C не освобождается до тех пор, пока не завершится выполнение обработчика, поэтому, вероятно, стоит убедиться, что обработчик завершается относительно быстро. Обратите внимание, что «относительно быстро» означает, что для ATmega это здесь предоставляется довольно много времени, так как на большинстве Arduino контроллер работает на частоте 16 МГц, а связь по I2C – лишь на частоте 100 кГц.

Обработчик может использовать метод receive() для чтения следующего байта из приемного буфера библиотеки Wire.

onRequest(handler)

Устанавливает функцию обработчик handler, которая должна вызываться, когда ATmega работает в качестве ведомого передатчика. Обработчик не принимает никаких аргументов и не возвращает каких-либо значений. Поэтому он должен выглядеть так:

void handler(void)

Обработчик вызывается после того, как ATmega принял SLA+R (SLave Address plus the Read bit, адрес ведомого плюс бит чтения). Он должен вызвать twi_transmit(), чтобы заполнить данными буфер передачи. Либо он должен вызвать метод send() из библиотеки Wire (смотрите выше), чтобы заполнить буфер передачи. Затем он должен вызвать twi_reply(1), чтобы на самом деле начать передачу по шине I2C (аргумент "1" в twi_reply() указывает ATmega, что после отклика необходимо послать ACK). Как только будет вызвана функция twi_reply(), будет вызвано прерывание, работа обработчика будет прервана, и модуль TWI обработает передачу данных.

Теги

ArduinoATmegaI2CTWIWireПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.