3 горизонтальные линии, бургер
3 горизонтальные линии, бургер
3 горизонтальные линии, бургер
3 горизонтальные линии, бургер

3 горизонтальные линии, бургер
Удалить все
ЗАГРУЗКА ...

Содержание



    Интерфейс I2C для ATmega328(Без библиотек, только Си и регистры). На примере подключения акселерометра MPU6050

    Часы
    11.12.2025
    /
    Часы
    11.12.2025
    /
    Часы
    21 минут
    Глазик
    214
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0

    О чём будет и для кого

    В данной статье я собираюсь написать о таком интерфейсе общения между чипами и датчиками на платах как I2C(IIC / 2Wire). 
    I2C (IIC - Inter-Integrated Circuit) - двухпроводной последовательный интерфейс, который позволяет общаться датчикам, сенсорам, микроконтроллерам между собой.
    Он, является очень простым в реализации. При разработке схем необходимо лишь два провода(линии), линия для синхронизации(SCL) и линия для передачи данных(SDA). Хотя ко-во допустимых "Рабов" у него ограничено в 127, да и скорость передачи довольно низкая в сравнении с тем же SPI.
    Максимальная скорость передачи зависит конкретно от устройств, которые используют данный интерфейс. Но стандарт это 100 и 400kHz. Некоторые устройства поддерживают и большие скорости, Fast Mode Plus (Fm+): до 1 МГц (1000 кГц) и High-Speed Mode (Hs-Mode): до 3.4 МГц (3400 кГц)
    Я бы хотел разобраться в том, как работает данный интерфейс ибо надо для моего проекта. Пока ничего рассказать о нём не могу, потому что может даже не смогу его реализовать. Очень много переменных предстоит учесть и многое предстоит изучить.
    Но в чём суть проблемы. Есть нужный мне чип - LIS2DH12TR(акселерометр). Туториалов по нему не то чтобы много, их нет. Есть только Datasheet, и карта регистров для него. Вернее сказать, есть одна библиотека, но она для плюсов. Мой проект на чистом Си поэтому нет. А переписывать библиотеку я не собираюсь.
    Да и при том всём, возможно я захочу использовать какой-нибудь другой чип, если этот не подойдёт, и под него может точно не быть ничего. Поэтому было решено, во что бы то ни стало, разобраться с интерфейсом I2C и без каких либо библиотек пообщаться с целевым чипом. Пока на примере общения MPU6050 с ATmega328P.
    Данная статья не о Wire.h или I2Cdev.h библиотеках. Эта статья о регистрах, простой побитовой математики и логики общения с периферией через I2C интерфейс.
    Задача разобраться в том, как всё это работает и применить на практике прочитав и записав в парочку регистров, пару байт информации. Давай начнём.

    Про I2C в целом, теория

    А начну я, конечно же, с теории. Я разберу формат отправляемых пакетов. Как эти пакеты перемещаются между устройствами, как эти устройства вообще понимают, что и куда отправлять, а главное как эти пакеты вообще читать, обо всём этом и будет в этой главе.

    Роли в I2C, процесс общения

    Для того чтобы общаться с другими датчиками, необходимо понять как I2C вообще работает. И как происходит обмен пакетами. Да именно пакетами. С этого в принципе и можно начать. 
    Каждый пакет длиною в 1 байт отправляется поочерёдно по SDA линии. Причём, он может путешествовать в двух направлениях. От мастера к рабу и от раба к мастеру. Чуть ниже я предоставлю используемую терминологию:
    Так принято называть устройства, где есть некоторая контролирующая схема. Тот кто контролирует, называется Мастером, а тот кто подчиняется Рабом.
    Мастер/Ведущий — устройство которое начинает и заканчивает передачу. Так же генерирует синхронизирующий сигнал.
    Раб/Ведомый — устройство к которому обращается ведущий.
    Передатчик — устройство передающее данные на шину.
    Приёмник — устройство читающее данные с шины.
    Из имеющихся ролей, можно сделать такой вывод, что общение возможно в двух вариантах. Первый, есть ведущий-передатчик и он передаёт данные рабу-приёмнику. Второй, есть ведущий-приёмник и он получает данные с раба-передатчика.
    На практике роли постоянно меняются. Я конечно немного забегаю вперёд, но чтобы мастеру хоть что-нибудь прочитать, ему сначала придётся указать(т.е. записать соответствующий регистр), а потом перейти в состояние приёмника и начать читать передаваемые данные от раба. Но давай по порядку.
    В этой статье я затрону только два режима для мастера (передатчик и приёмник), так как датчики я ещё не в силах сделать и тем более запрограммировать. Но, думаю поняв суть работы I2C протокола можно легко разобраться с тем как работать и в режиме раба приёмника и раба передатчика.

    Настройка битрейта

    Перед тем, как вообще что-либо делать, необходимо задать тактовую частоту на которой будет происходить общение между мастером и рабом. Битрейт сохраняется в специальном регистре TWBR (Two Wired Bitrate Register). 
    Битрейт не сохраняется в абсолютных числах, а в специальных значениях. Так, для МК(ATmega328P) с тактовой частотой в 16MHz, при необходимости достижения тактовой частоты передачи в 400kHz, необходимо задать TWBR в значение 12. Откуда 12 взялось? Из формулы, которую можно найти в документации.
    В коде это будет выглядеть вот так:
    // Устанавливаем бит масштаба в 1. Мне не нужно слишком малая частота общения TWSR = 0x00; // Это формула (стр. 180 стр 21.5.2) для просчёта бит-рейта между Master и Slave 400kHz TWBR = (F_CPU - TARGET_FREQUENCY * 16)/TARGET_FREQUENCY*2*pow(4,TWSR);
    Кроме того, что TWSR используется для выяснения появившихся ошибок и результата выполнения определённых шагов в коммуникации через I2C, он ещё и устанавливает бит масштаба.
    Завершив с инициализацией частоты передачи битов. Можно приступать к настройке и подготовке к реальному общению между устройствами.

    Режим мастера передатчика

    В этом режиме, ведущее устройство, записывает данные на регистр, который указал ранее. Как выглядит типичный обмен данных между мастером и рабом в таком режиме? Давай взглянем.
    Вот условные обозначения:
    1. Start - Условие начала передачи
    2. Stop - Условие конца передачи
    3. Adr  - Адрес раба
    4. RAdr - Адрес регистра
    5. +W - Бит записи
    6. Ack - Сигнал принят
    7. Data - Данные
    Последовательность записи одного бита.
    ВедущийStartAdr+W
    RAdr(Data)
    Data
    Stop
    Ведомый

    Ack
    Ack
    Ack
    А вот последовательность записи нескольких битов.
    ВедущийStartAdr+W
    RAdr(Data)
    Data
    Data
    Stop
    Ведомый

    Ack
    Ack
    Ack
    Ack
    Дальше будет очень много регистров и побитовой математики. Описание всех необходимых регистров можно прочитать в главе по ссылке, а побитовая математика будет объяснена в этой главе постепенно.
    Так же после каждого шага коммуникации мастера с рабом возможно проверять регистр TWSR на код состояния передачи. Все коды и то как это можно сделать описано в этой главе.
    В начале происходит генерация сигнала Start. Это делается путём записи в регистр TWCR битов TWINT, TWSTA, TWEN. Вот так: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)).
    В этом выражении (1 << TWINT) мы делаем побитовый сдвиг 1 (то есть 0000 0001) на  7 шагов влево. Почему на 7? Просто TWINT = 7. В результате получится 1000 0000. То же самое происходит и с другими выражениями (1 << TWSTA) и (1 << TWEN). Во втором случае передвигаем 1 на 5‑й бит, а в третьем на 2‑й бит. Потом мы просто объединяем (побитовое ИЛИ) все получившиеся бинарные числа. Вот так:
    1000 0000
    |
    0010 0000
    |
    0000 0100
    Побитовое ИЛИ проверяет два операнда. Если хотя бы один из них равен 1, то результатом побитового ИЛИ будет 1
    Должно получится следующее число - 1010 0100
    Дальше, согласно табличке необходимо отправить адрес устройства (Раба) и бит чтения/записи. В этом случае  записи.
    Помнишь я говорил, что общение между рабом и мастером происходит пакетами по 8 бит каждый? Так вот, следующий пакет, согласно интерфейсу I2C, это адрес раба + режим чтения/записи. Так как режим чтения займёт 1 бит, для адреса останется 7 битов, или 127 возможных адресов для рабов. Ну это на тот случай, если ты гадал, почему именно 127 адресов поддерживает I2C.
    Как это сделать? Для этого в регистр TWDR нужно сохранить адрес раба и добавить в конец бит записи. Вот так: TWDR = (0x68 << 1) | 0. Здесь 0x68 это адрес MPU6050 указанный в документации. Мы побитово сдвигаем его влево на один бит. То есть, было 0110 1000  стало 1101 0000 . После чего добавляем бит записи - 0.
    После сохранения адреса и бита записи в TWDR, нужно отправить его на шину по SDA (об этом в главе про подключение I2C). Для этого мы записываем в TWCR следующие биты, вот так: TWCR = ((1 << TWINT) | (1 << TWEN)). Получится 1000 0100.
    В случае если ведомое устройство с указанным адресом существует, он перехватывает SDA линию и понижает её ( то есть отправляет 0, то есть Ack сигнал). Если нет, линия SDA будет не тронута и сигнал NAck будет получен, то есть 1.
    После подтверждения, что ведомое устройство с таким адресом существует, отправляется байт информации, 1й отправляемый байт это не всегда адрес регистра.  Для большинства датчиков и других сложных устройств это так. Сначала отправляется адрес регистра, а потом уже данные для записи. Но если это что-то простое, например некоторые АЦП, они сразу принимаю данные. Чтобы сказать наверняка, необходимо уточнять это в соответствующей документации.
    Так, работая с MPU6050, сначала необходимо указать адрес регистра в который мы собираемся записывать. И лишь потом данные, которые хотим записать.
    Делается это так. 
    // Сохраняем данные в специальный регистр TWDR = data; // Обновляем контрольный регистр TWCR = ((1 << TWINT) | (1 << TWEN));
    Запись и отправка данных рабу
    Мы повторяем запись (то что в блоке кода) до тех пор, пока не будет отправлен сигнал Stop или Repeated Start. Делается это следующим образом. 
    Для генерации Stop сигнала в контрольный регистр записываем следующее: TWCR = ((1 << TWINT) | (1 << TWSTO) | (1 << TWEN)). Должно получится следующее - 1001 0100.
    Для генерации Repeated Start сигнала в контрольный регистр записываем следующее: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)). Должно получится следующее - 1010 0100.
    Сигнал Repeated Start позволяет переключиться между режимами или между рабами, которых может быть до 127, не отпуская при этом SDA линию.

    Режим мастера/ведущего приёмника

    В этом режиме ведущее устройств принимает данные от раба. Как он это делает? И как он указывает, что конкретно нужно прочитать? Для этого ему сначала придётся указать адрес нужного регистра (Режим мастера передатчика), указывает он его путём записи в этот регистр. После чего происходит смена режима на мастера приёмника. Вот как выглядит такая последовательность на схеме:
    Вот условные обозначения:
    1. Start - Условие начала передачи
    2. Stop - Условие конца передачи
    3. Adr - Адрес раба
    4. RAdr - Адрес регистра
    5. +W - Бит записи
    6. +R - Бит чтения
    7. Ack - Сигнал принят
    8. NAck - Сигнал не принят
    9. Data - Данные
    Последовательность чтения одного бита.
    ВедущийStartAdr+W
    RAdr
    StartAdr+R

    NAckStop
    Ведомый

    Ack
    Ack

    AckData

    Последовательность чтения нескольких битов
    ВедущийStartAdr+W
    RAdr
    StartAdr+R

    Ack
    NackStop
    Ведомый

    Ack
    Ack

    AckData
    Data

    Дальше будет очень много регистров и побитовой математики. Описание всех необходимых регистров можно прочитать в этой главе, а побитовая математика будет объяснена в этой главе.
    Так же после каждого шага коммуникации мастера с рабом возможно проверять регистр TWSR на код состояния передачи. Все коды и то как это можно сделать описано в этой главе.
    В начале, как и в режиме передачи, происходит генерация сигнала Start. Это делается путём записи в регистр TWCR битов TWINT, TWSTA, TWEN. Вот так: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)).
    В этом выражении (1 << TWINT) мы делаем побитовый сдвиг 1 (то есть 0000 0001) на 7 шагов влево. Почему на 7? Просто TWINT = 7. То же самое происходит и с другими выражениями (1 << TWSTA) и (1 << TWEN). Во втором случае передвигаем 1 на 5‑й бит, а в третьем на 2‑й бит. Потом мы просто объединяем (побитовое или) все получившиеся бинарные числа. Должно получится следующее число - 1010 0100
    Дальше, согласно табличке необходимо отправить адрес устройства (Раба) и бит чтения/записи. Так как я работаю с MPU6050, адресом будет 0x68 + 0 бит для записи.
    Как это сделать? Для этого в регистр TWDR нужно сохранить адрес раба и добавить в конец бит записи. Вот так: TWDR = (0x68 << 1) | 0. Здесь 0x68 это адрес MPU6050 указанный в документации. Мы побитово сдвигаем его влево на один бит. То есть было 0110 1000  стало 1101 0000 . После чего добавляем бит записи - 0.
    После сохранения адреса и бита чтения/записи в TWDR, нужно отправить его на шину по SDA (об этом в главе про подключение I2C). Для этого мы записываем в TWCR следующие биты, вот так: TWCR = ((1 << TWINT) | (1 << TWEN)). Получится 1000 0100.
    В случае если ведомое устройство с указанным адресом существует, он перехватывает SDA линию и понижает её ( то есть отправляет 0, то есть Ack сигнал). Если нет, линия SDA будет не тронута и сигнал NAck будет получен, то есть 1.
    После подтверждения, что ведомое устройство с таким адресом существует, отправляется байт информации, 1й отправляемый байт это не всегда адрес регистра.  Для большинства датчиков и других сложных устройств это так. Сначала отправляется адрес регистра, а потом уже данные для записи. Но если это что-то простое, например некоторые АЦП они сразу принимаю данные. Чтобы сказать наверняка, необходимо уточнять это в соответствующей документации.
    Так работая с MPU6050, сначала необходимо указать адрес регистра который мы собираемся читать. Отправить и подождать ответа.
    Вышеописанная последовательность выглядит вот так:
    // Сохраняем адрес целевого регистра у раба TWDR = addres_of_register; // Обновляем контрольный регистр, отправляем байт с адресом регистра TWCR = ((1 << TWINT) | (1 << TWEN)); // Ждем Ack сигнал while (!(TWCR & (1 << TWINT))); // 0x28 это успешный статус отправки данных с подтверждением от Slave (бит ACK в конце) if ((TWSR & 0xF8) != 0x28){ // Обрабатываем ошибку отправки данных }
    Если данные с адресом регистра были отправлены успешно (получен Ack сигнал) то, после этого нужно перевести мастера в режим приёмника. Делается это при помощи Repeates Start сигнала. Делается ровно так же как и обычный Start сигнал:  TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN))
    Дальше, отправляем адрес устройства (Раба) и бит чтения. Так как я работаю с MPU6050, адресом будет 0x68 + 1 ,бит для чтения. Делается вот так: TWDR = (0x68 << 1) | 1.
    TWDR = (0x68 << 1) | 1; // Говорим МК, чтобы он отправил данные из TWI Data Register т.е. из TWDR TWCR = (1 << TWINT) | (1 << TWEN);
    Отправляем адрес Раба на чтение
    Получив Ack сигнал в ответ, можно начать читать передаваемые данные, при этом уже ведущее устройство отправляет Ack или NAck сигнал. Какой конкретно сигнал отправить выбирает ведущий, то есть ты, мой дорогой читатель. И здесь очень важно всё не напутать. Ведь если неправильно ответить, линия передачи данных от Раба к Мастеру зависнет в бесконечном цикле ожидания.
    Я, опять же, немного забегаю вперёд, но проверка на то, были ли отправленные данные происходит путём чтения регистра TWCR. А если быть более конкретным, когда бит TWINT убирается (1 -> 0), внутренней логикой схемы TWI(I2C). Именно поэтому мы всегда его должны проставлять, когда делаем какой-нибудь шаг в процессе общения по I2C. Что схема могла его потом убрать.
    Смотри, когда происходит чтение последнего байта информации, необходимо отправить NAck сигнал. Иначе только Ack. Если нужно прочитать первый и последний байт из регистра Раба, сразу отправляем NAck сигнал.
    Если это первый и последний байт то, чтобы получить данные и ответить сигналом NAck, делаем это так: TWCR = (1<<TWINT) | (1<<TWEN).
    Если это первый или любой другой байт, но точно не последний и мы хотим прочитать ещё после него - то запрашиваем данные и отправляем Ack, вот так: TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA). То есть целенаправленно задаём бит TWEA, о нём подробнее в главе о регистрах.
    После, необходимо подождать пока данные не появятся в регистре TWDR. Делается это вот так: 
    // Если это последний байт который ведущий хочет получить // То настраиваем контрольный регистр соответственно TWCR = (1<<TWINT) | (1<<TWEN); // Ожидаем пока не произойдёт сдвиг в регистре данных while (!(TWCR & (1<<TWINT))); //Теперь можно получить данные и, например, сохранить их в SRAM uint8_t byte = TWDR;
    Когда мы закончили читать и отправили соответствующий NAck сигнал, необходимо прекратить передачу данных, либо Stop, либо Repeated Start сигналом. Делается это так же как и в случае с записью:
    Для генерации Stop сигнала в контрольный регистр записываем следующее: TWCR = ((1 << TWINT) | (1 << TWSTO) | (1 << TWEN)). Должно получится следующее - 1001 0100.
    Для генерации Repeated Start сигнала в контрольный регистр записываем следующее: TWCR = ((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)). Должно получится следующее - 1010 0100.
    Вот такая вот последовательность действий необходима при чтении данных с подключённого устройства по I2C. Да, это гораздо сложнее и запутаннее чем запись, но что есть, то есть.

    Коды ответов

    В этой главе я собрал все возможные коды ответов и их значения. Так как каждый из них уникален и есть некоторые общие коды для разных режимов, все они будут под одной главой. 
    Данные коды соответствуют только семейству микроконтроллеров от Arduino. То есть ATmega8/32/328 например. По этому, если у тебя другой микроконтроллер посмотри документацию к нему и там должны быть указаны уже кода для тебя.
    0x8TW_STARTУсловие старта общения (Start) было успешно выполнено
    0x10TW_REP_STARTУсловие повторного старта (Repeated Start) было успешно выполнено
    0x18TW_MT_SLA_ACKAdr+W байт был отправлен и получен сигнал Ack
    0x20TW_MT_SLA_NACKAdr+W байт был отправлен и получен сигнал NAck
    0x28TW_MT_DATA_ACKДанные в 1 байт были отправлены и получен сигнал Ack
    0x30TW_MT_DATA_NACKДанные в 1 байт были отправлены и получен сигнал NAck
    0x38TW_MT_ARB_LOST или TW_MR_ARB_LOSTВедомое устройство по указанному адресу Adr+W/+R сейчас занят общением с другим Мастером
    0x40TW_MR_SLA_ACK Adr+R байт был отправлен и получен сигнал Ack
    0x48TW_MR_SLA_NACKAdr+R байт был отправлен и получен сигнал NAck
    0x50TW_MR_DATA_ACKДанные в 1 байт были получены и отправлен сигнал Ack
    0x58TW_MR_DATA_NACKДанные в 1 байт были получены и отправлен сигнал NAck
    Код ответаМакросОбъяснение кода ответа
    Что бы узнать и проверить код ответа после каждого шага, необходимо прочитать регистр TWSR. Здесь, под каждым шагом, я подразумеваю действие сразу после очистки бита  TWINT в TWCR. В коде это выглядит вот так: while (!(TWCR & (1 << TWINT)));. Что, как бы говорит, состояние регистра TWSR изменилось, можно его проверить и узнать.
    Например, вот так: (TWSR & 0xF8) != 0x18
    В таком случае если вернётся какой-нибудь не тот код, мы войдём в процедуру и выполним, например, предупреждение по UART протоколу в консоль, на компьютер разработчика.
    Можно так же подключить заголовок util/twi.h для того, чтобы вместо статусов кодов можно было писать более дружелюбные к человеку названия (макросы). В практическом примере ниже, я этого не делал, ибо хотел чтобы ты, мой читатель, понимал и видел их явное использование.

    Используемы регистры на стороне Мастера (ATmega328P)

    TWCR (Two Wired Control Register) - регистр контроля хода коммуникации. То есть начало, конец коммуникации. Контроля и удержания шины занятой, пока идёт передача. Для генерации Ack сигнала.
    TWDR (Two Wired Data Register) - регистр, где хранятся данные. В этот регистр попадают данные отправленные рабом при чтении, туда же ведущее устройство записывает свои данные, которые хочет отправить. В том числе и адрес раба.
    TWSR (Two Wired Status Register) - регистр отвечающий за наблюдение состояния общения между рабом и мастером. От туда же можно узнать обо всех ошибках произошедших во время передачи или приёмки данных.
    TWBR (Two Wired Bit Rate Register) - регистр в котором хранится число(делитель) тактовой частоты мастера, что бы можно было узнать частоту передачи данных.
    Более подробно о флагах в каждом регистре можно ознакомиться в документации. Что я и настоятельно рекомендую. Стр 198. глава 21.9.

    Про подключение рабов через I2C

    Теперь, зная как работает передача данных по данному протоколу, необходимо понять, как подключать датчики, сенсоры и прочие устройства к микроконтроллеру в качестве рабов, чтобы можно было коммуницировать и обмениваться данными с ними.
    Основное преимущество перед тем же SPI интерфейсом является ко-во используемых проводов в схеме. Их необходимо только 2. Это конечно же не считая линию для напряжения и земли. В общем, необходима линия синхронизации(SCL) и линия данных(SDA). Ниже я предоставлю схему:
    У устройств семейства AVR, то есть всякие ардуино, пины для работы с другими устройствами через I2C, обозначены как А5 и А4.  А именно, для SCL это A5, для SDA это А4. 
    Кстати, эта схема валидна если ты работаешь с уже распаянным на отдельной плате чипом. Но если ты работаешь с голым чипом, то нужно ещё подключить подтягивающий резистор ну хотя бы на 4.7 кОм к 5V, SCL и SDA линиям. Если этого не сделать, ничего не заработает.

    Давай пообщаемся, или то как работать с периферией через I2C

    Подготовка (Заголовки и структуры)

    Вот такая вот небольшая теория. Если понять суть данного протокола, то он не такой уж и сложный. Давай покажу на примере. Мы будем общаться с датчиком ускорения и углового поворота. Короче с акселерометром MPU-6050.
    Так же, нам потребуется выводить куда-нибудь данные, чтобы понимать, работает ли код вообще. Можно использовать встроенную библиотеку и класс Serial от Ардуино, но в примерах кода я буду использовать свою реализацию работы UART интерфейса (когда-нибудь напишу и про этот интерфейс).
    Наша задача в этой главе, просто прочитать данные с гироскопа и акселерометра. Вообще, данный чип имеет ещё и температурный датчик, но мы проигнорируем его и отключим. Итак, с чего начнём? С подключения заголовков.
    #include <avr/io.h> #include <stdio.h> #include <stdbool.h> #include <math.h> #include "UARTMessanger.h"
    Дальше определим структуру для хранения данных с датчика. Назовём его MPU6050.
     
    typedef struct { int16_t accel_x; int16_t accel_y; int16_t accel_z; int16_t temp; int16_t gyro_x; int16_t gyro_y; int16_t gyro_z; } MPU6050; MPU6050 getData(){ MPU6050 data; return data; }
    Функция для того, чтобы получить данные, пока пуста и ничего не делает. Я заполню её чуть позже, а пока просто подготавливаем почву. Заметь мы храним данные датчиков в int16_t, ибо в документации к регистрам данного датчика видно, что для каждой оси по два байта.
    С подготовительной частью мы закончили, давай теперь перейдём к части где мы настраиваем работу самого I2C протокола.

    Настраиваем протокол I2C + немного абстракций

    Сейчас я собираюсь настроить работу протокола и датчика так, чтобы была вообще возможность обмениваться данными и чтобы можно было бы понять, подключились ли мы вообще к датчику MPU6050. Для этого в функции setup пишем:
    void setup(){ // Настраиваем скорость общения с приёмником (т.е. на машине, на которой пишется код) uart_init(57600); // Инициализируем I2C интерфейс i2c_init(); // MODE: Запись одного байта в регистр 0x6B (У MPU6500 это Power Manager 1 регистр) i2c_start(MPU_6050_W_ADDR); i2c_write(0x6B); // Это означает что мы передвинем один бит в лево на 3 позиции. Т.е Было 0000 0001 стало 0000 1000 i2c_write(0x00 | 1 << 3); // Под 3 подразумевается бит управления температурным датчиком. Допустим нам он не нужен, поэтому проставим 1 на соответ. место // MODE: Чтение одного байта из регистра 0x75 (У MPU6500 это WHO_AM_i регистр) i2c_start(MPU_6050_W_ADDR); i2c_write(0x75); i2c_start(MPU_6050_R_ADDR); uint8_t byte = i2c_read(true); sprintf(uart_buffer, "WHO_AM_I(0x75) - 0x%x\n", byte); uart_transmit_string(uart_buffer); }
    Первую строку с инициализацией интерфейса обмена данными по  UART мы игнорируем сегодня, переходим дальше. Ты наверняка мог заметить, что я позволил себе немного абстрагировать I2C интерфейс в 4 функции: i2c_init, i2c_start, i2c_read, i2c_write.

    Про небольшую надстройку над I2C интерфейсом (абстракция)

    И конечно же, я пройдусь по всем им. С твоего разрешения я уже не буду описывать работу самого интерфейса, ибо сделал это в начале, но полезные подсказки будут втречаться то тут, то там по ходу чтения кода, которые должны будут помочь в понимании процесса.
    i2c_init делает всё то, что описано в главе про настройку битрейта, плюс ещё задаёт флаг TWEN в TWCR для активации интерфейса.
    void i2c_init() { // Устанавливаем бит масштаба в 1. Мне не нужно слишком малая частота общения TWSR = 0x00; // Это формула (стр. 180 стр 21.5.2) для просчёта бит-рейта между Master и Slave 400kHz TWBR = (F_CPU - TARGET_FREQUENCY * 16)/TARGET_FREQUENCY*2*pow(4,TWSR); // Активируем TWI TWCR = (1<<TWEN); }
    i2c_start задаёт условие начала общения (START,REPEATED START) и отправляет адрес устройства с которым необходимо настроить связь.
    uint8_t i2c_start(uint8_t address) { // 1. Посылаем сигнал о том, что готовы начать общение с периферией, START сигнал // (1000 0000 | 0010 0000 | 0000 0100) -> Это означает что мы начинаем передачу данных START. TW сам уберёт флаг TWINT и в итоге будет 0010 0100 (0х24) TWCR = ((1<<TWINT)|(1<<TWSTA)|(1<<TWEN)); // !(0010 0100 & 1000 0000) -> Это означает что TW будет посылать сигнал на SCL до тех пор пока влаг TWINT не проставлен в регистре TWCR while (!(TWCR & (1<<TWINT))); // Происходит проверка кода статуса отправки байта (данных) // 0х08 Означает что ведущее устройство успешно послал сигнал START и готов продолжать общение // 0х10 Означает что ведущее устр. успешно послал сигнал REPEATED START и готов продолжать общение if ((TWSR & 0xF8) != 0x08 && (TWSR & 0xF8) != 0x10){ sprintf(uart_buffer, "Init START or REPEATED START error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } // 2. Записываем адрес устройства в соответствующий регистр TWDR // Так как адреса 7-битные, необходимо произвести сдвиг на один бит и записать в конец 0 // То есть было 0110 1000 после шифта стало 1101 0000 // 0 говорит о том, что мы будем записывать (write mode) TWDR = address; // Говорим МК, чтобы он отправил данные из TWI Data Register т.е. из TWDR TWCR = (1 << TWINT) | (1 << TWEN); while (!(TWCR & (1 << TWINT))); // Происходит проверка кода статуса отправки байта (данных). Так как это первая отправка, отправляется адрес Slave // 0x18 Означает, что адрес с битом записи были отправлены успешно и Slave добавил ACK бит в конец // 0x40 Означает, что адрес с битом чтения были отправлены успешно и Slave добавил ACK бит в конец if ((TWSR & 0xF8) != 0x18 && (TWSR & 0xF8) != 0x40){// 0x18 - SLA+W | 0x40 - SLA+R ACK sprintf(uart_buffer, "Send address error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } return TWSR & 0xF8; }
    i2c_write отправляет байт данных по шине и соответственно, ожидает возврата сигнала ( Ack или NAck).
    uint8_t i2c_write(uint8_t data){ // Сохраняем данные в специальный регистр TWDR = data; // Отправляем данные, поставив соответствующие флаги TWINT и TWEN TWCR = (1<<TWINT) | (1<<TWEN); // Ждём пока не CPU не сбросит флаг TWINT, что значит данные отправлены while (!(TWCR & (1<<TWINT))); // 0x28 это успешный статус отправки данных с подтверждением от Slave (бит ACK в конце) if ((TWSR & 0xF8) != 0x28){ sprintf(uart_buffer, "Data write error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } return 1; }
    i2c_read читает данные с шины и возвращает их если всё прошло успешно и ни один флаг в TWSR регистре не появился.
    uint8_t i2c_read(bool isLast) { // Указываем, что мы хотим читать данные дальше if (!isLast) TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); // Иначе, в последнем чтении необходимо убрать бит TWEA, чтобы сказать (сообщить Slave Transmitter) что мы закончили чтение else TWCR = (1<<TWINT) | (1<<TWEN); // Ожидаем пока МК сбросит наш TWINT флаг, что значит данные были получены while (!(TWCR & (1<<TWINT))); // 0x50 это успешный статус полученных данных с подтверждением от Slave (бит ACK в конце) // 0x58 это успешный статус полученных данных без подтверждения от Slave (бит NACK в конце) if (((TWSR & 0xF8) != 0x50) && ((TWSR & 0xF8) != 0x58)){ sprintf(uart_buffer, "Data read error: 0x%x\n", TWSR & 0xF8); uart_transmit_string(uart_buffer); } // Возвращаем полученные данные return TWDR; }
    Так же данная функция позволяет указать будет ли это последнее прочтение или нет. Если да (чтение будет последним), то убираем флаг TWEA из TWCR.
    Заметь как я использую статусы ответов. По сути все эти проверки как бы говорят, что если кроме указанных статусов будет ещё что-то, то возникла какая-то ошибка и мы входим в процедуру, где и выводим по UART интерфейсу сообщение об ошибке.

    Разберём подробнее инициализирующий код

    На вход функции i2c_start мы подаём адрес на чтение, который мы заранее вычислили и сохранили в макросе. Вот так:
    #define MPU_6050_W_ADDR 0xD0 // (0x68 << 1) | 0 (Write) #define MPU_6050_R_ADDR 0xD1 // (0x68 << 1) | 1 (Read) #define TARGET_FREQUENCY 400000L // Максимальная скорость обмена данными может быть 400kHz
    Дальше, мы отправляем адрес регистра в который хотим, что-нибудь записать. В нашем случае это 0x6B регистр: [i2c_write(0x6B);] Это регистр управления подачей питания на датчик. А после, записываем данные, которые должны быть записаны в указанный ранее адрес.
    Я записываю следующее: [i2c_write(0x00 | 1 << 3);] Где 3 это позиция бита в указанном адресе, что это вообще значит?
    Смотри мы по факту отправляем 0000 1000. Бит сна, поставленный в 0 пробуждает датчик, а поставив 1 в 3-й бит мы отключаем тепловой сенсор.
    И последнее что я делаю при инициализации это читаю ID самого датчика, просто на всякий случай.
    // MODE: Чтение одного байта из регистра 0x75 (У MPU6500 это WHO_AM_i регистр) i2c_start(MPU_6050_W_ADDR); i2c_write(0x75); i2c_start(MPU_6050_R_ADDR); uint8_t byte = i2c_read(true); sprintf(uart_buffer, "WHO_AM_I(0x75) - 0x%x\n", byte); uart_transmit_string(uart_buffer);
    Мы должны получить в ответ 0x68, то есть адрес датчика. Если у тебя, конечно же, именно этот датчик.
    Ты, скорее всего, получишь не 0x68, но 0x70. Почему? Потому что у тебя не MPU6050 а MPU6500, немного улучшенная версия датчика, у которого минимум отличий в регистрах с MPU6050. Просто знай, что это нормально и всё с твоим I2C в порядке.
    Мы закончили предварительную настройку и проверку протокола I2C. Заметь я не вызвал i2c_stop, ибо это не нужно. Нам и дальше нужно читать данные с шины, по этому нет смысла её отпускать. Ровно так же, как и в случае с повторным стартом для чтения ID датчика. Мы просто отправили REPEATED START сигнал и коммуникация продолжилась.

    Прочтём данные с датчика

    Нам нужно получать свежие данные с датчика, по этому одним чтением всё не обойдётся. Будем постоянно его читать и обновлять. Вот как это можно делать в цикле:
    void loop(){ // MODE: Чтение последовательности байтов, начиная с 0x3B регистра и заканчивая 0x48 (Итого 14 байтов) // 0x3B это регистр акселерометра по X оси. Дальше идут accel_y(H,L), accel_z(H,L), temperature(H,L), gyro_x(H,L), gyro_y(H,L), gyro_z(H,L) // NOTE: Значения хранятся в 2 байтах, старший - H и младший - L // Итого 7 датчиков по два байта каждый i2c_start(MPU_6050_W_ADDR); // <- START условие i2c_write(0x3B); // Собираем данные с 7 датчиков i2c_start(MPU_6050_R_ADDR); // <- REPEATED START условие MPU6050 data = getData(); // Просто вывожу данные sprintf(uart_buffer, "accel_x=%d\taccel_y=%d\taccel_z=%d\t", data.accel_x, data.accel_y, data.accel_z); uart_transmit_string(uart_buffer); sprintf(uart_buffer, "temp=%d\t", data.temp); uart_transmit_string(uart_buffer); sprintf(uart_buffer, "gyro_x=%d\tgyro_y=%d\tgyro_z=%d\n", data.gyro_x, data.gyro_y, data.gyro_z); uart_transmit_string(uart_buffer); // Отпускаю шину. После этого данные с датчиков обновятся, и на следующем цикле будут новые. // Не отпустив шину, чип MPU6500 не будет обновлять данные в своих регистрах i2c_stop(); }
    Мы, задав изначальный адрес(0x3B) в режиме записи сразу после этого переходим в режим чтения. Первый байт который мы прочтём, будет байт находящийся в 0x3B регистре. С каждым чтением внутренний счётчик датчика будет увеличиваться на 1. По этому, после прочтения адреса 0x3B мы прочтём адрес 0x3C и так далее пока мы не ответим сигналом NAck.
    После успешного прочтения всей последовательности мы завершаем передачу и отпускаем шину функцией i2c_stop. Где просто ставим соответствующий флаг TWSTO: TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO);.
    А почему бы не отправить сигнал REPEATED START и не повторить чтение? Так можно сделать, но данные на датчиках не обновятся и будут висеть старые значения.
     Запишем полную реализацию функции getData для получения данных с датчика.
    MPU6050 getData(){ MPU6050 data; // NOTE: Для того чтобы правильно сохранить данные с регисров MPU6500 // необходимо их поместить в 2 байта. // Для этого прочитав 1 байт с шины, мы помещаем его в int16_t используя побитовый сдвиг в лево // То есть было [0000 0000 1001 1111] а стало [1001 1111 0000 0000] // Accel X data.accel_x = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Accel Y data.accel_y = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Accel Z data.accel_z = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // temp data.temp = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Gyro X data.gyro_x = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Gyro Y data.gyro_y = (int16_t)((i2c_read(false) << 8) | i2c_read(false)); // Gyro Z // Заметь, читая последний байт мастеру необходимо отправить NACK сигнал, чтобы уведомить // раба о завершении чтения. Иначе мы не сможем инициировать сигналы START или в моём случае REPEATED START data.gyro_z = (int16_t)((i2c_read(false) << 8) | i2c_read(true)); return data; }
    Начну с замечания. Так как адреса для каждого значения ускорения, температуры и гироскопа находятся последовательно то код гораздо проще и нам не приходится постоянно менять адрес чтения. Что удобно.
    Так же, данные датчиков хранятся в 2 байтах, по этому, необходимо сделать 2 чтения подряд и объединить их в одну переменную с целочисленным значением int16_t. Для этого, первый байт мы сдвигаем вперёд на половину, то есть 8 битов, чтобы данные со второго чтения влезли.
    Под конец, просто читаем с отправкой NAck сигнала подключённому датчику.
    И вот таким вот образом мы успешно прочитали данные с датчика используя только встроенные регистры, документацию и внутреннюю логику работы устройств. Твои выходные данные в консоли могут выглядеть примерно так:
    Заметь, что температура не изменилась, хотя я трогал) Это значит, что мы успешно отключили температурный датчик. Так же взгляни на WHO_AM_I. Он равен 0x70. Это нормально, как было описано выше.

    В заключение

    В заключении я бы хотел сказать, что вот так вот программировать и работать с устройствами гораздо интереснее и "надёжнее" я бы сказал. Ведь в своей реализации не запутаешься) Плюс приходит более глубокое понимание интерфейсов и сути работы микроконтроллеров и прочих устройств.
    Я надеюсь, что помог тебе разобраться в устройстве I2C интерфейса, и теперь ты сможешь подключить любое устройство и прочитать его данные за минуты.


    Не забудь поделиться, лайкнуть и оставить комментарий)

    Комментарии

    (0)

    captcha
    Отправить
    ЗАГРУЗКА ...
    Сейчас тут пусто. Буть первым (o゚v゚)ノ