В этой статье рассматривается пример реализации на микроконтроллере PIC мастер-абонента шины 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 bcf Port_reg,Clock_line bsf Status,5 bcf Tris_reg,Clock_line bcf Status,5 return |
Для установки линии в ноль — необходимо переключить её на "выход" и записать в защёлку 0. | ||
Установка "1" на линии CLOCK
|
Clock_one bsf Status,5 bsf Tris_reg,Clock_line bcf Status,5 return |
Для установки линии в Z-состояние — достаточно переключить её на вход. | ||
Установка "0" на линии DATA алгоритм аналогичен установке нуля на линии Clock |
Data_null bcf Port_reg,Data_line bsf Status,5 bcf Tris_reg,Data_line bcf Status,5 return |
Для установки линии в ноль — необходимо переключить её на "выход" и записать в защёлку 0. | ||
Установка "1" на линии DATA алгоритм аналогичен установке единицы на линии Clock |
Data_one bsf Status,5 bsf Tris_reg,Data_line bcf Status,5 return |
Для установки линии в Z-состояние — достаточно переключить её на вход. | ||
В шапке программы необходимо директивой equ указать — к каким именно выводам подключены линии Clock и Data. Для этого нужно включить в шапку следующий код:
Компилятор просто заменит все записи Clock_line и Data_line на соответствующие числа (номера каналов выводов). Кроме того, этой же директивой в шапке надо указать адреса регистра порта и регистра выбора направления каналов порта:
|
||||
Формирование Старт-условия
|
Start_uslovie Call Clock_one Call Data_null Call Clock_null return |
Для посылки старт-условия достаточно на свободной шине (когда обе линии притянуты к 1) уронить в ноль линию Data. Первая команда (Clock_one) нужна на тот случай, если мы подаём повторное старт-условие без подачи стоп-условия. |
||
Формирование Стоп-условия
|
Stop_uslovie call Data_null call Clock_one wait_clock_p btfss Port_reg,Clock_line goto wait_clock_p call Data_one return |
Для посылки стоп-условия необходимо при отпущенной линии Clock перевести линию Data из нуля в единицу. Для этого надо в первой половине тактового импульса (когда линия Clock притянута к нулю) притянуть к нулю линию Data (чтобы во второй половине тактового импульса было что отпускать). |
||
Пересылка байта
|
Send_Byte bcf I2C_flags,0 movlw .8 movwf Bit_counter next_bit_s btfsc BTS,7 Call Data_one btfss BTS,7 call Data_null Call Clock_one wait_clock_s1 btfss Port_reg,Clock_line goto wait_clock_s1 call Clock_null rlf BTS,1 decfsz Bit_counter,1 goto next_bit_s call Data_one call Clock_one wait_clock_s2 btfss Port_reg,Clock_line goto wait_clock_s2 btfsc Port_reg,Data_line bsf I2C_flags,0 call Clock_null return |
В девятом такте (18-я строка) процедура Data_one вызывается для того, чтобы slave мог выставить бит подтверждения). Если линию не отпустить, то будет непонятно, кто установил на линии ноль — вы в предыдущем такте или slave-устройство в текущем такте. |
||
Приём байта
|
Recieve_Byte clrf RDB movlw .8 movwf Bit_counter next_bit_r bcf Status,0 rlf RDB,1 call Data_one call Clock_one wait_clock_r1 btfss Port_reg,Clock_line goto wait_clock_r1 btfsc Port_reg, Data_line bsf RDB,0 call Clock_null decfsz Bit_counter goto next_bit_r btfss I2C_flags,0 call Data_null btfsc I2C_flags,0 call Data_one call Clock_one wait_clock_r2 btfss Port_reg,Clock_line goto wait_clock_r2 call Clock_null return |
Процедура принимает 8 бит данных от slave-устройства, а в девятом такте посылает или не посылает бит подтверждения (в зависимости от значения специального флага в нашем регистре флагов). |
Замечание 1. Поскольку устройства I2C имеют выходы с открытым коллектором, то для линий шины I2C выражения "отпустить линию" и "установить линию в 1" означают одно и тоже, так что не пугайтесь, если встретите в тексте разные варианты (и не надо путать установку линии в "1" с установкой "1" на выходе контроллера).
Замечание 2. При тактовой частоте внутреннего генератора PIC-контроллеров (4 МГц) эти процедуры укладываются в тайминги (в некоторых местах — впритык) и отлично работают, но если тактовая частота будет выше или если вы захотите уменьшить скорость обмена, то в процедуры придётся вносить дополнительные задержки.