Программная реализация мастер-абонента шины I2C в режиме single-master (библиотека процедур для AVR)

В этой статье рассматривается пример реализации на микроконтроллере AVR мастер-абонента шины I2C в режиме single-master (когда микроконтроллер выступает в роли единственного мастер-абонента на шине). Для понимания механизма функционирования интерфейса I2C, рекомендую сначала ознакомиться с теорией. Если же с теорией вы уже разобрались, тогда можно приступать к практической реализации.

Итак, рассматриваемый режим single-master является самым простейшим случаем, поскольку в этом случае наш контроллер является единственным устройством, которое может управлять линией Clock и формировать старт- и стоп-условия. Соответственно, функции арбитража реализовывать не нужно, никаких коллизий и так возникнуть не может. Всё, что нужно нашему мастер-абоненту для обмена данными со слэйв-устройствами — это уметь делать следующие вещи:

  1. формировать на шине "Старт"-условие;
  2. формировать на шине "Стоп"-условие;
  3. формировать и посылать пакет данных из 8 бит с обработкой ответа (подтверждения) от "Slave"-устройства;
  4. принимать пакет данных из 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 (I2C)

Clock_null:
  sbi  DDR_reg,Clock_line
  ret
Для установки линии в ноль — необходимо переключить её на "выход" и записать в защёлку 0. Ноль в защёлку можно записать один раз при инициализации и в дальнейшем менять только направление работы порта.

Установка "1" на линии CLOCK

Алгоритм установки единицы на линии Clock (I2C)

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. Для этого нужно включить в шапку следующий код:

.equ Clock_line = <i>номер канала вывода</i>
.equ Data_line = <i>номер канала вывода</i>

Компилятор просто заменит все записи Clock_line и Data_line на соответствующие числа (номера каналов выводов).

Кроме того, этой же директивой в шапке надо указать адреса регистра данных порта, регистра выбора направления каналов порта и виртуального регистра, из которого можно считать значения сигналов на входах:

.equ Port_reg = <i>адрес регистра данных порта</i>; (для Tiny13 это будет адрес регистра PORTB)
.equ DDR_reg = <i>адрес регистра направления каналов порта</i>; (для Tiny13 - адрес DDRB)
.equ Pin_reg = <i>адрес для считывания входов</i>; (для Tiny13 это будет адрес PINB)

Формирование Старт-условия

Алгоритм посылки старт-условия (I2C)

Start_uslovie:
 rcall Clock_one
 rcall Pause_tbuf
 rcall Data_null
 rcall Pause_thdsta
 rcall Clock_null
 ret

Для посылки старт-условия достаточно на свободной шине (когда обе линии притянуты к 1) уронить в ноль линию Data.

Первая команда (Clock_one) нужна на тот случай, если мы подаём повторное старт-условие без подачи стоп-условия.

Формирование Стоп-условия

Алгоритм посылки стоп-условия (I2C)

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 (чтобы во второй половине тактового импульса было что отпускать).

Пересылка байта

Алгоритм посылки одного байта по I2C

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-устройство в текущем такте.

Приём байта

Алгоритм приёма одного байта по I2C

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 можно задавать желаемые значения таймингов, определяющих скорость обмена по интерфейсу.

Примеры использования описанных выше процедур:

Управляющая программа для контроллера I2C-шлюза.

Добавить комментарий