Программная реализация интерфейса 1-wire («Мастер») (библиотека процедур для AVR)

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

Первое, что нам понадобится — это имитация физического порта 1-wire.

Физическое устройство порта 1-wire

То есть нам нужно имитировать высокоомный вход и выход с открытым стоком, подключенные к одной линии (как на рисунке справа). Поскольку большинство портов микроконтроллера двунаправленные, то необходимость в имитации высокоомного входа сразу отпадает (он уже есть на каждой ноге контроллера). Остаётся имитировать только выход с открытым стоком. Такой имитации легко добиться, записав в соответствующую защёлку ноль и далее просто переключая ногу контроллера на вход или на выход. При переключении на вход — получаем на выводе Z-состояние, при переключении на выход — получаем на выводе ноль (поскольку в защёлке у нас записан ноль).

Ниже приведён алгоритм и код на ассемблере, реализующие описанный метод имитации выхода с открытым стоком (в виде отдельных процедур для установки на выходе высокого или низкого уровня).

Алгоритмы: Программные коды: Комментарии:

Установка "нуля" на выходе порта 1-wire.

Алгоритм установки нуля на выходе порта 1-wire

_1_wire_null:
  sbi  DDR_reg, line_1_wire
  ret

Для установки нуля на выходе порта 1-wire — необходимо переключить ногу микроконтроллера на "выход" и записать в защёлку 0. Ноль в защёлку можно записать один раз при инициализации и в дальнейшем менять только направление работы порта.

Установка "Z-состояния" на выходе порта 1-wire.

Алгоритм установки Z-состояния на выходе порта 1-wire

_1_wire_Z:
  cbi  DDR_reg,line_1_wire
  ret

Для установки линии в Z-состояние — достаточно переключить её на вход (в защёлке у нас ноль, так что встроенный подтягивающий резистор отключен).

В шапке программы необходимо директивой .equ указать какой вывод микроконтроллера используется в качестве порта 1-wire. Для этого нужно включить в шапку следующий код:

 .equ line_1_wire = номер канала вывода

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

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

 .equ Port_reg = адрес регистра данных порта (для Tiny13 - адрес регистра PORTB)
 .equ DDR_reg = адрес регистра направления каналов порта (для Tiny13 - адрес DDRB)
 .equ Pin_reg = адрес для считывания входов (для Tiny13 - адрес PINB)

Кроме физической реализации порта 1-wire, нам понадобится ещё инструмент для отсчёта различных временнЫх интервалов. В качестве такого инструмента мы будем использовать встроенный практически во все контроллеры AVR таймер — Timer0. Таймер будем использовать в режиме «Normal» с отключенными от ног контроллера выходами. Этот таймер позволяет измерить одновременно до трёх разных промежутков времени, при отсчёте каждого из которых устанавливается специальный флаг (также может быть сгенерировано прерывание, но мы пока не будем их использовать). В начале программы таймер надо будет проинициализировать:

          clr w
          out TCCR0A,w  ; таймер отключен от выводов OC0A(B)
          out TCNT0,w   ; сбрасываем таймер
          out TIMSK,w   ; запрещаем все прерывания от него
          ser w
          out TIFR,w    ; сбрасываем флаги таймера

Кроме всего описанного выше, отведём один из пользовательских регистров под флаги, позволяющие определить, какое действие мы в данный момент выполняем. Назовём этот регистр Flags. Флаги в нём будут такие: SEND_BIT — означает, что мы передаём бит, READ_BIT — означает, что мы принимаем бит, SEND_RESET — означает, что мы посылаем сигнал «Reset» и ждём сигнал «Presence», FREE_BUS — означает, что никаких действий на шине не производится и, наконец, SLAVE_IS — означает, что был получен сигнал «Presence».

Всё, теперь, когда мы знаем, как физически имитировать порт 1-wire, и определились со средством отсчёта различных временных интервалов — можно переходить к реализации протокола низкого уровня.

Прежде, чем приступать к реализации протокола, — давайте составим списки элементарных действий, которые должно уметь выполнять наше мастер-устройство, а уже потом посмотрим как эти элементарные действия можно реализовать.

Что должен уметь «Мастер»:

  1. формировать на шине сигнал «Reset» и принимать ответ («Presence») на этот сигнал
  2. посылать бит данных на шину;
  3. считывать бит данных с шины.

Вместо привычных алгоритмов, для каждого из перечисленных выше пунктов, я нарисую рисуночек, на котором отмечу моменты времени, в которые «Мастер» будет выполнять те или иные действия (указаны на рисунке синим цветом). Моменты времени выберем так, чтобы соответствовать спецификации, а определять их будем с помощью нашего таймера. Мне кажется, в данном случае рисунок будет выглядеть нагляднее алгоритма.

Итак, что и когда должен делать «Мастер», чтобы сформировать «Reset» и определить, посылают ли ему в ответ «Presence»:

Формирование сигнала Reset и определение Presence 1-wire-мастером

Что и когда должен делать «Мастер», чтобы отправить бит по шине 1-wire:

Передача бита Мастером по шине 1-wire

Что и когда должен делать «Мастер», чтобы считать бит с шины 1-wire:

Чтение бита Мастером с шины 1-wire

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

Если взять предделитель 1:64 и использовать внутренний генератор на 8 МГц, то (с учётом дополнительного времени на выполнение команд, которые нужно выполнить после срабатывания таймера) 505 мкс будут соответствовать 62 тикам, (505+64) мкс — 70 тикам, а 505+498 мкс — 124 тикам.

С учётом приведённых выше дополнений, код к первому рисунку будет таким:

Send_Reset_proc:
       sbr Flags, 1<<SEND_RESET
       cbr Flags, 1<<SLAVE_IS
     ;--- настраиваем таймер ---
       ldi w,131             ; начальное значение таймера (255-124)
       out TCNT0,w
       ldi w,193             ; +62 тика
       out OCR0A,w
       ldi w, 201            ; +8 тиков
       out OCR0B,w
     ;--- конец настройки таймера ---
       ldi w,0b00000011
       out TCCR0B,w          ; запускаем таймер с предделителем 1:64
       rcall _1_wire_null     ; формируем низкий уровень на шине
     ;--- ждём отсчёта первого интервала ---
M1:    in  w,TIFR
       sbrs w,OCF0A       ; если флаг установлен - пропустить команду
       rjmp M1
       rcall Occured_OCF0A   ; первое действие
     ;--- ждём отсчёта второго интервала ---
M2:    in w,TIFR
       sbrs w,OCF0B       ; если флаг установлен - пропустить команду
       rjmp M2
       rcall Occured_OCF0B   ; второе действие
     ;--- ждём отсчёта третьего интервала ---
M3:    in w,TIFR
       sbrs w,TOV0        ; если флаг установлен - пропустить команду
       rjmp M3
       rcall Occured_TOV0    ; третье действие
       ser w
       out TIFR,w            ; сбрасываем флаги таймера,
       cbr Flags, 1<<SEND_RESET ; флаг передачи сигнала "Reset"
       ret                   ; и выходим

В процедурах Occured_OCF0A, Occured_OCF0B, Occured_TOV0 нужно будет по установленному флагу SEND_RESET определить, какие именно действия следует выполнить:

Occured_OCF0A:
          ...
          sbrc Flags, SEND_RESET
          rjmp Send_Reset_OCR0A
          ...
Send_Reset_OCR0A:   ; первое действие для Send_Reset - отпустить шину
          rcall _1_wire_Z
          ret
Occured_OCF0B:
          ...
          sbrc Flags, SEND_RESET
          rjmp Send_Reset_OCR0B
          ...
Send_Reset_OCR0B:   ; второе действие для Send_Reset - прочитать "Presence"
          sbis Pin_reg,line_1_wire  ; если 1 - пропускаем команду
          sbr Flags, 1<<SLAVE_IS
          ret
Occured_TOV0:
          ...
          sbrc Flags, SEND_RESET
          rjmp Send_Reset_TOV0
          ...
Send_Reset_TOV0:   ; третье действие для Send_Reset - конец процедуры
          clr w
          out TCCR0B,w          ; останавливаем таймер
          ret

Для второго рисунка будем использовать предделитель 1:8, тогда 3,5 мкс = 1 тик, 62,5 мкс = 60 тиков, 68 мкс = 65 тиков. Передавать будем младший бит из регистра Transmit_R. Код ко второму рисунку будет таким:

Send_Bit_proc:
       sbr Flags, 1<<SEND_BIT
     ;--- настраиваем таймер ---
       ldi w,190             ; начальное значение таймера (255-65)
       out TCNT0,w
       ldi w,191             ; +1 тик
       out OCR0A,w
       ldi w, 250            ; +58 тиков
       out OCR0B,w
     ;--- конец настройки таймера ---
       ldi w,0b00000010
       out TCCR0B,w          ; запускаем таймер с предделителем 1:8
       rcall _1_wire_null     ; формируем низкий уровень на шине
     ;--- ждём отсчёта первого интервала ---
M4:    in w,TIFR
       sbrs w,OCF0A       ; если флаг установлен - пропустить команду
       rjmp M4
       rcall Occured_OCF0A   ; первое действие
     ;--- ждём отсчёта второго интервала ---
M5:    in w,TIFR
       sbrs w,OCF0B       ; если флаг установлен - пропустить команду
       rjmp M5
       rcall Occured_OCF0B   ; второе действие
      ;--- ждём отсчёта третьего интервала ---
M6:    in w,TIFR
       sbrs w,TOV0        ; если флаг установлен - пропустить команду
       rjmp M6
       rcall Occured_TOV0    ; третье действие
       ser w
       out TIFR,w            ; сбрасываем флаги таймера,
       cbr Flags, 1<<SEND_BIT  ; флаг передачи бита
       ret                   ; и выходим

В процедурах Occured_OCF0A, Occured_OCF0B, Occured_TOV0 нужно будет по установленному флагу SEND_BIT определить, какие именно действия следует выполнить:

Occured_OCF0A:
          ...
          sbrc Flags, SEND_BIT
          rjmp Send_Bit_OCR0A
          ...
Send_Bit_OCR0A:   ; первое действие для Send_Bit - установить бит
          sbrc Transmit_R,0 ; если передаём ноль - ничего делать не нужно
          rcall _1_wire_Z   ; если передаём единицу - отпускаем шину
          ret
Occured_OCF0B:
          ...
          sbrc Flags, SEND_BIT
          rjmp Send_Bit_OCR0B
          ...
Send_Bit_OCR0B:   ; второе действие для Send_Bit - отпустить шину
          rcall _1_wire_Z
          ret
Occured_TOV0:
          ...
          sbrc Flags, SEND_BIT
          rjmp Send_Bit_TOV0
          ...
Send_Bit_TOV0:   ; третье действие для Send_Bit - конец тайм-слота
          clr w
          out TCCR0B,w          ; останавливаем таймер
          ret

Для третьего рисунка будем использовать тот же предделитель, что и для второго, тогда все тайминги останутся прежними, за исключением тайминга на 14 мкс. Он будет соответствовать 12-ти тикам. Принятый бит будем записывать в старший бит регистра Recieve_R. Код к третьему рисунку будет таким:

Read_Bit_proc:
       sbr Flags, 1<<READ_BIT
     ;--- настраиваем таймер ---
       ldi w,190             ; начальное значение таймера (255-65)
       out TCNT0,w
       ldi w,191             ; +1 тик
       out OCR0A,w
       ldi w, 202            ; +11 тиков
       out OCR0B,w
     ;--- конец настройки таймера ---
       ldi w,0b00000010
       out TCCR0B,w            ; запускаем таймер с предделителем 1:8
       rcall _1_wire_null     ; формируем низкий уровень на шине
     ;--- ждём отсчёта первого интервала ---
M7:    in w,TIFR
       sbrs w,OCF0A       ; если флаг установлен - пропустить команду
       rjmp M7
       rcall Occured_OCF0A   ; первое действие
     ;--- ждём отсчёта второго интервала ---
M8:    in w,TIFR
       sbis w,OCF0B       ; если флаг установлен - пропустить команду
       rjmp M8
       rcall Occured_OCF0B   ; второе действие
     ;--- ждём отсчёта третьего интервала ---
M9:    in w,TIFR
       sbis w,TOV0        ; если флаг установлен - пропустить команду
       rjmp M9
       rcall Occured_TOV0    ; третье действие
       ser w
       out TIFR,w            ; сбрасываем флаги таймера,
       cbr Flags, 1<<READ_BIT ; флаг чтения бита
       ret                   ; и выходим

В процедурах Occured_OCF0A, Occured_OCF0B, Occured_TOV0 нужно будет по установленному флагу READ_BIT определить, какие именно действия следует выполнить:

Occured_OCF0A:
          ...
          sbrc Flags, READ_BIT
          rjmp Read_Bit_OCR0A
          ...
Read_Bit_OCR0A:   ; первое действие для Read_Bit - отпустить шину
          rcall _1_wire_Z
          ret
Occured_OCF0B:
          ...
          sbrc Flags, READ_BIT
          rjmp Read_Bit_OCR0B
          ...
Read_Bit_OCR0B:   ; второе действие для Read_Bit - прочитать бит с шины
          lsr Recieve_R ; сдвигаем регистр вправо, а в старший пишем ноль
          sbiс Pin_reg, 1-wire_line ; если 0 - пропускаем команду
          sbr Recieve_R,1<<7
          ret
Occured_TOV0:
          ...
          sbrc Flags, READ_BIT
          rjmp Read_Bit_TOV0
          ...
Read_Bit_TOV0:   ; третье действие для Read_Bit - конец тайм-слота
          clr w
          out TCCR0B,w          ; останавливаем таймер
          ret

Вот, собственно, и все дела. Добавлю только, что мы не случайно записывали принятый бит в старший бит регистра Recieve_R, а сам регистр перед этим сдвигали вправо. Это сделано потому, что данные по шине 1-wire передаются младшим битом вперёд и выбранный нами способ записи принятых бит позволяет восстановить нормальный порядок бит в передаваемом байте. По тем же причинам при передаче мы передаём младший бит регистра Transmit_R.

Ну и последнее замечание. В принципе, приведённые здесь алгоритмы по смыслу и функционалу идентичны алгоритмам, приведённым в апноте «AVR318: Dallas 1-Wire Master» (хотя моя реализация, как мне кажется, — лучше, по крайней мере оригинальнее точно), однако, и в решении от Atmel, и в моём варианте есть один интересный момент. В обоих вариантах описывается определение сигнала «Presence» только в ответ на посланный «Reset», в то время, как «Слэйвы» могут сформировать этот сигнал просто через 15 мкс после подключения на шину (когда на ней установлен высокий уровень сигнала) без всяких «Ресетов» от «Мастера». Так что, когда контроллер ничем не занят — он должен находиться в режиме ожидания сигнала «Presence» (я думаю, это и так понятно, просто на всякий случай отметил). Например, в ATTiny2313, для этих целей можно использовать внешние прерывания INT0, INT1 по заднему фронту сигнала или просто прерывание от изменения уровня на ноге контроллера, на которой реализован порт 1-wire.

Ссылки по теме:

  1. Как устроен однопроводный интерфейс 1-wire
  2. Эмуляция электронного ключа 1-wire на ATTiny2313
  3. Поиск устройств на шине 1-wire
  4. Шлюз «RS232<--->1-wire» на микроконтроллере ATTiny2313 (режим Master)

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