В этой статье рассматривается пример реализации на микроконтроллере AVR мастер-абонента шины I2C в режиме single-master (когда микроконтроллер выступает в роли единственного мастер-абонента на шине). Для понимания механизма функционирования интерфейса I2C, рекомендую сначала ознакомиться с теорией. Если же с теорией вы уже разобрались, тогда можно приступать к практической реализации.
Итак, рассматриваемый режим single-master является самым простейшим случаем, поскольку в этом случае наш контроллер является единственным устройством, которое может управлять линией Clock и формировать старт- и стоп-условия. Соответственно, функции арбитража реализовывать не нужно, никаких коллизий и так возникнуть не может. Всё, что нужно нашему мастер-абоненту для обмена данными со слэйв-устройствами — это уметь делать следующие вещи:
- формировать на шине "Старт"-условие;
- формировать на шине "Стоп"-условие;
- формировать и посылать пакет данных из 8 бит с обработкой ответа (подтверждения) от "Slave"-устройства;
- принимать пакет данных из 8 бит с посылкой бита подтверждения и без посылки бита подтверждения.
Каждую из перечисленных "обязанностей" удобно реализовать в виде отдельной процедуры. Для удобства восприятия назовём их, например так: Start_uslovie, Stop_uslovie, Send_byte, Recieve_byte.
Поскольку подключаемые к I2C устройства должны имитировать выходы "с открытым коллектором", то для реализации этих процедур нам понадобятся ещё 4 процедуры, которые будут непосредственно управлять соответствующими линиями портов, имитируя на них выходы "с открытым коллектором" (по две на каждую линию — Clock и Data). Их назовём так: Clock_null, Clock_one, Data_null, Data_one.
Кроме того, наше "Master"-устройство должно отслеживать такое событие, как "удержание" "Slave"-устройством линии Clock (когда оно по каким-либо причинам "растягивает" передачу данных).
В таблице ниже представлены алгоритмы и программные коды на ассемблере, реализующие каждую из указанных выше процедур. Перечисленные процедуры — это самый низкий уровень протокола, включив их в свой проект и выстроив обращения к ним определённым образом (в соответствии с логикой шины I2C), можно, как из конструктора, собрать весь процесс обмена с любым "Slave"-устройством на шине.
Для удобства нам понадобятся 4 пользовательских регистра: BTS (byte to send) — сюда будет записываться байт, который мы хотим послать, RDB (recieved byte) — сюда будет записываться принятый байт, Bit_counter — будет использоваться в качестве счётчика принятых и посланных бит и последний регистр — I2C_Flags — здесь будут храниться наши служебные флаги.
Собственно, в этом примере, мы будем использовать только один флаг и, соответственно, займём для этого только один нулевой бит. Переживать, что из целого регистра мы используем всего один бит не стоит, ведь мы рассматриваем простейший пример, но вдруг вы захотите его усложнить или добавите эти процедуры в какую-нибудь сложную программу, тут-то нам целый регистр собственных флагов наверняка пригодится.
Так вот, бит 0 регистра I2C_flags — флаг подтверждения (ACK) и работать он будет следующим образом. Когда мастер принимает данные: если этот бит равен 0, то мастеру нужно выдать подтверждение приёма байта, а если он установлен в 1, то подтверждение выдавать не нужно. Когда мастер посылает данные: если этот бит установился в 0 — подтверждение получено, если в 1 — подтверждение не получено.
Алгоритмы: | Программные коды: | Комментарии: | ||
Установка "0" на линии CLOCK
|
Clock_null: sbi DDR_reg,Clock_line ret |
Для установки линии в ноль — необходимо переключить её на "выход" и записать в защёлку 0. Ноль в защёлку можно записать один раз при инициализации и в дальнейшем менять только направление работы порта. | ||
Установка "1" на линии CLOCK
|
Clock_one: cbi DDR_reg,Clock_line ret |
Для установки линии в Z-состояние — достаточно переключить её на вход (в защёлке у нас ноль, так что подтягивающий резистор отключен). | ||
Установка "0" на линии DATA алгоритм аналогичен установке нуля на линии Clock |
Data_null: sbi DDR_reg,Data_line ret |
Для установки линии в ноль — необходимо переключить её на "выход" и записать в защёлку 0. Ноль в защёлку можно записать один раз при инициализации и в дальнейшем менять только направление работы порта. | ||
Установка "1" на линии DATA алгоритм аналогичен установке единицы на линии Clock |
Data_one: cbi DDR_reg,Data_line ret |
Для установки линии в Z-состояние — достаточно переключить её на вход (в защёлке у нас ноль, так что подтягивающий резистор отключен). | ||
В шапке программы необходимо директивой .equ указать — к каким именно выводам подключены линии Clock и Data. Для этого нужно включить в шапку следующий код:
Компилятор просто заменит все записи Clock_line и Data_line на соответствующие числа (номера каналов выводов). Кроме того, этой же директивой в шапке надо указать адреса регистра данных порта, регистра выбора направления каналов порта и виртуального регистра, из которого можно считать значения сигналов на входах:
|
||||
Формирование Старт-условия
|
Start_uslovie: rcall Clock_one rcall Pause_tbuf rcall Data_null rcall Pause_thdsta rcall Clock_null ret |
Для посылки старт-условия достаточно на свободной шине (когда обе линии притянуты к 1) уронить в ноль линию Data. Первая команда (Clock_one) нужна на тот случай, если мы подаём повторное старт-условие без подачи стоп-условия. |
||
Формирование Стоп-условия
|
Stop_uslovie: rcall Data_null rcall Clock_one wait_clock_p: sbis Pin_reg,Clock_line rjmp wait_clock_p rcall Pause_tsusto rcall Data_one ret |
Для посылки стоп-условия необходимо при отпущенной линии Clock перевести линию Data из нуля в единицу. Для этого надо в первой половине тактового импульса (когда линия Clock притянута к нулю) притянуть к нулю линию Data (чтобы во второй половине тактового импульса было что отпускать). |
||
Пересылка байта
|
Send_Byte: cbr I2C_flags,0b00000001 ldi Bit_counter,8 next_bit_s: sbrc BTS,7 rcall Data_one sbrs BTS,7 rcall Data_null rcall Pause_tsudat rcall Clock_one wait_clock_s1: sbis Pin_reg,Clock_line rjmp wait_clock_s1 rcall Pause_thigh rcall Clock_null lsl BTS dec Bit_counter,1 brne next_bit_s rcall Data_one rcall Pause_tlow rcall Clock_one wait_clock_s2: sbis Pin_reg,Clock_line rjmp wait_clock_s2 sbic Pin_reg,Data_line sbr I2C_flags,0b00000001 rcall Pause_thigh rcall Clock_null ret |
В девятом такте (18-я строка) процедура Data_one вызывается для того, чтобы slave мог выставить бит подтверждения). Если линию не отпустить, то будет непонятно, кто установил на линии ноль — вы в предыдущем такте или slave-устройство в текущем такте. |
||
Приём байта
|
Recieve_Byte: clr RDB ldi Bit_counter,8 next_bit_r: lsl RDB rcall Data_one rcall Pause_tlow rcall Clock_one wait_clock_r1: sbis Pin_reg,Clock_line rjmp wait_clock_r1 sbic Pin_reg, Data_line sbr RDB,0b00000001 rcall Pause_thigh rcall Clock_null dec Bit_counter brne next_bit_r sbrs I2C_flags,0 rcall Data_null sbrc I2C_flags,0 rcall Data_one rcall Pause_tlow rcall Clock_one wait_clock_r2: sbis Pin_reg,Clock_line rjmp wait_clock_r2 rcall Pause_thigh rcall Clock_null ret |
Процедура принимает 8 бит данных от slave-устройства, а в девятом такте посылает или не посылает бит подтверждения (в зависимости от значения специального флага в нашем регистре флагов). |
Замечание 1. Поскольку устройства I2C имеют выходы с открытым коллектором, то для линий шины I2C выражения "отпустить линию" и "установить линию в 1" означают одно и тоже, так что не пугайтесь, если встретите в тексте разные варианты (и не надо путать установку линии в "1" с установкой "1" на выходе контроллера).
Замечание 2. Процедурами Pause_txxx можно задавать желаемые значения таймингов, определяющих скорость обмена по интерфейсу.
Примеры использования описанных выше процедур: