Итак, продолжаем эксперименты с собранным ранее I2C-шлюзом (который, как вы помните, у нас реализован на ATTiny2313). В этой статье мы рассмотрим полностью программную реализацию режима I2C-Slave, который позволит нашему девайсу из терминальной программы персонального компьютера прикидываться любым Slave-устройством, а также просто подглядывать за обменом данными на шине I2C (то есть работать как сниффер).
Прога, как всегда, на асме, в конце статьи, как всегда, исходники (с комментариями) и прошивка.
Протокол обмена шлюза с компьютером состоит из однобайтных сообщений, в которых передаются служебные (команды шлюзу – что нужно делать или сообщения компьютеру о том, что сделано) или информационные данные (информация, которую нужно передать или которая получена по шине I2C).
Служебные сообщения мы закодируем следующим образом:
При передаче сообщения в направлении ПК->Шлюз:
20h – отправить байт по I2C (следующий переданный компьютером байт будет отправлен шлюзом по I2C)
21h – принять байт по I2C (следующий принятый компом байт – это тот байт, который шлюз считал с шины)
22h – не посылать Ack
23h – послать Ack
24h – запросить состояние входов порта D (например, для определения уровня на входе Chip Select), следующий принятый компом байт – это байт состояния входов порта D (PIND)
При передаче сообщения в направлении Шлюз->ПК:
20h – на шине произошло start-условие
21h – на шине произошло stop-условие
22h – не получили (не послали) Ack
23h – получили (послали) Ack
24h – ждём дальнейших указаний
FFh – от ПК получена неизвестная команда
02h – произошла реинициализация шлюза
Прежде чем рисовать алгоритм, – напишу немного текстухи, – чтобы было понятнее как это всё работает и как сделан режим I2C-Slave. Работа нашего девайса основана на том, что Slave устройство в протоколе I2C не совсем бесправно, – оно может растягивать обмен, удерживая на низком уровне линию Clock. Таким образом, в те моменты, когда мастер роняет линию clock в ноль, мы можем захватить эту линию и удерживать её на низком уровне до тех пор, пока не “поговорим” с компьютером.
Далее, для того, чтобы определять на какой стадии обмена данными находится шлюз, – мы, во-первых, используем флаг T регистра SREG, в котором сохраняем текущее состояние линии Clock (это позволяет в дальнейшем определить от какой линии произошло прерывание – от Clock или от Data) и, во-вторых, создали в программе свой собственный регистр флагов (I2C_flags). В нём мы юзаем 3 флага. Нулевой бит регистра I2C_flags установливается в 1 после обнаружения start-условия и используется при отправке первого Ack (после получения первого байта). Используется он следующим образом: если мы посылаем “Ack”, то флаг сбрасывается и мы продолжаем обмен, если же мы посылаем “No_Ack”, то шлюз реинициализируется и ждёт нового старт-условия на шине. Это для случая, когда на шине несколько slave-устройств, а мы хотим эмулировать только какое-то одно (первый байт после старт-условия – это адрес устройства, и, если обращаются не к нам, то мы последующий обмен игнорируем и ждём нового старт-условия). Первый бит регистра I2C_flags – это направление передачи данных, когда он установлен в 1 – шлюз будет посылать данные мастеру, когда он сброшен в ноль – шлюз будет читать данные от мастера. И, наконец, второй бит регистра I2C_flags сообщает о том, что мы читаем – данные или Ack (чтобы знать сколько бит нам с шины читать – 8 или 1).
Наша прога делает следующее. Сначала мы инициализируем шлюз для чтения байта, флаг T устанавливаем равным 1, настраиваем прерывание от Pin_Change, разрешаем прерывания и переходим к циклической проверке обоих линий. Если на обоих линиях высокий уровень, то разрешаем прерывание от линии Data, если нет – запрещаем прерывание от любой линии. Таким образом первое прерывание у нас в любом случае должно произойти от изменения уровня на линии Data с высокого уровня на низкий. Далее, в прерывании мы первоначально попадём в обработчик старт-условия, там мы разрешаем прерывание от Clock, настраиваемся на приём от мастера первого байта, выставляем нужные флаги и переходим к процедуре ожидания (снова устанавливаем глобальный флаг разрешения прерываний и нифига не делаем). В протоколе определено время фиксации старт-условия, оно зависит от скорости, но главное, что оно есть и в течении этого времени нельзя менять уровни на линиях Clock и Data. За это время мы должны успеть выполнить весь обработчик start-условия и снова разрешить прерывания. Далее, при прерывании мы (с помощью флага T) определяем от какой линии произошло прерывание и, соответственно, на какой стадии обмена мы находимся. Если помните описание протокола I2C – во время высокого уровня на линии clock приёмник читает данные, во время низкого уровня на линии clock – передатчик выставляет данные на шину, соответственно, когда мы определяем, что уровень на линии clock изменился с 1 на 0 – мы захватываем линию clock (сами роняем её в ноль), запрещаем прерывание от изменения уровня на линии Data и занимаемся своими делами, после чего отпускаем линию clock и ждём прерывания от её изменения. Пока clock не изменится с 0 на 1 – снова разрешать прерывания от линии Data нельзя, поскольку уровень на линии Data при низком уровне на линии clock может в любой момент измениться (в это время передатчик выставляет данные на шину). Когда мы определяем, что уровень на линии clock изменился с 0 на 1 – мы сначала делаем все свои дела (читаем если нужно байт или сигнал Ack), а потом снова разрешаем прерывание от линии Data (благо посылать старт- и стоп-условие запрещено сразу после изменения уровня на линии Clock, так что некоторый запас времени, на то, чтобы позаниматься своими делами, не рискуя пропустить старт или стоп условие, у нас есть). Ну вот, дальше смотрим алгоритм и прогу, в случае необходимости, – пишем вопросы на форум.
Алгоритм:
Итак, в аппаратной части мы имеем:
PB0 – линия Clock
PB2 – линия Data
PD5 – линия CS
PD0 – линия Rx
PD1 – линия Tx
Program:
;---------------------------------------------------------
.device ATtiny2313
.include "tn2313def.inc"
.list
;-- определяем свои переменные
.def w=r16 ; это будет наш аккумулятор
.def Bit_counter=r17 ; счётчик бит
.def I2C_flags=r18 ; флаги I2C
;-- флаг 0 - признак, что только что было старт-условие
;-- флаг 1 - направление (приём: 0, передача: 1)
;-- флаг 2 - признак чтения ack
.def BTS=r19 ; байт для передачи
.def RDB=r20 ; принятый байт
.def Hi_PCIE=r21 ; сюда запишем GIMSK с поднятым PCIE
.def Clr_reg=r22 ; здесь будет просто ноль
.def PCMSK_D_Set=r23 ; тут PCMSK c установленным флагом
; для прерывания от линии Data
.def PCMSK_C_Set=r24 ; тут PCMSK c установленным флагом
; для прерывания от линии Clock
.def PCMSK_CD_Set=r25; тут PCMSK c флагами для
; прерываний от линий Data и Clock
;-- определяем константы (и даём им имена)
.equ Clock=0 ; PortB0/PinB0 - clock
.equ Data=2 ; PortB2/PinB2 - data
.equ CS=5 ; PinD5 - вход Chip Select
; кроме того, мы используем линии Rx (PD0), Tx (PD1)
;-- начало программного кода
.cseg
.org 0
rjmp Init ; переход на начало программы (вектор сброса)
;-- дальше идут вектора прерываний
;-- если не используем - пишем reti, иначе - переход к обработчику
reti ; внешнее прерывание INT0
reti ; внешнее прерывание INT1
reti ; Input capture interrupt 1
reti ; Timer/Counter1 Compare Match A
reti ; Overflow1 Interrupt
reti ; Overflow0 Interrupt
reti ; USART0 RX Complete Interrupt
reti ; USART0 Data Register Empty Interrupt
reti ; USART0 TX Complete Interrupt
reti ; Analog Comparator Interrupt
rjmp PCInt ; Pin Change Interrupt
reti ; Timer/Counter1 Compare Match B
reti ; Timer/Counter0 Compare Match A
reti ; Timer/Counter0 Compare Match B
reti ; USI start interrupt
reti ; USI overflow interrupt
reti ; EEPROM write complete
reti ; Watchdog Timer Interrupt
;--- начало программы ---
Init: ldi w,RAMEND ; устанавливаем указатель вершины
out SPL,w ; стека на старший байт RAM
sbi ACSR,ACD ; выключаем компаратор
;-- инициализируем порты
ser w ; w=0xFF
out DDRA,w ; настраиваем порт A. все линии на выход
clr w ; w=0x00
out PORTA,w ; на всех линиях ноль
ldi w,0b11111010 ; настраиваем порт B.
out DDRB,w ; PB0, PB2 - входы, остальные - выходы
clr w ; определяем начальное состояние
out PORTB,w ; (выходы - нули, подтяжек нет)
ldi w,0b11010100 ; настраиваем порт D
out DDRD,w ; PD0,1,3,5 - входы, остальные - выходы
clr w ; определяем начальное состояние
out PORTD,w ; (выходы - нули, подтяжек нет)
;-- инициализируем UART
out UBRRH,w ; UBRRR для кварца 20МГц и скорости 115200
ldi w,10 ; равен 10, т.е. UBRRH=0, UBRRL=10
out UBRRL,w
ldi w,0b00001110 ; поднимаем биты USBS, UCSZ1:0
out UCSRC,w ; формат: 1 старт, 8 данные, 2 стоп
sbi UCSRB,RXEN ; включить приёмник
in w,UDR ; считать из него мусор
sbi UCSRB,TXEN ; включить передатчик
;--- инициализируем вспомогательные регистры
clr Clr_reg
ldi Hi_PCIE,0b00100000
ldi PCMSK_D_Set,0b00000100
ldi PCMSK_C_Set,0b00000001
ldi PCMSK_CD_Set,0b00000101
;-- включить прерывание от pin change
out GIMSK,Hi_PCIE
;--- Для реинициализации ---
Reset: out PCMSK,Clr_reg; выключить прерывания от clock и data
ldi w,0x02 ; сообщаем, что был reset
out UDR,w
;-- проверяем флаги от прерываний
in w,EIFR ; читаем регистр
sbrc w,5 ; пропустить команду, если PCIF=0
out EIFR,Hi_PCIE ; сбрасываем PCIF, если он установлен
;---
cbi DDRB,Data ; отпускаем Data (=1)
set ; инициализируем флаг T
cbi DDRB,Clock ; отпускаем Clock (=1)
;-- разрешаем глобальные прерывания
sei
;-- если обе линии свободны - разрешаем прерывание от Data ---
Wait_start:
in w,PINB ; читаем входы PINB
com w ; инвертируем
andi w,0b00000101; если инверсные clock и data=0 - результат 0
breq Free_bus ; если флаг Z=1, то прыгать
Not_free_bus:
out PCMSK,Clr_reg; выключить прерывания от всех линий
rjmp Wait_start
Free_bus:
out PCMSK,PCMSK_D_Set; включить прерывание от линии Data
rjmp Wait_start
;------------------------------
;-- циклы ожидания после выполнения программы в C01 и C10
Wait_C: pop w ; выгружаем из стека адрес возврата
pop w
sei
Wait: rjmp Wait
;-------------------------------------------
;--- Обработчик прерывания от pin change ---
PCInt:
sbis PINB,Clock ; если clock=1 - пропустить команду
rjmp Clock_Low
;-------------------------------------------
;--- Если на линии Clock высокий уровень ---
Clock_Hi:
brts C1_no_changes ; если флаг T=1, то прыгаем
;---------------------------------------------
;--- Уровень на линии clock изменился с 0 на 1
C01:
set ; сохраняем новое состояние линии clock
sbrc I2C_flags,1;если флаг 1=0 - пропускаем 1 команду
rjmp C01_Exit
C01_Read:
sbrc I2C_flags,2;если флаг 2=0 - пропускаем 1 команду
rjmp C01_Read_Ack
C01_Read_Byte:
lsl RDB ; сдвиг влево
sbic PINB,Data ; если Data=0 - пропускаем 1 команду
sbr RDB,0b00000001 ; поднять нулевой бит
dec Bit_counter
brne C01_Exit ; если не считали 8 бит - просто выходим,
out UDR,RDB ; если считали - шлём их на комп и выходим
C01_Exit:
out PCMSK,PCMSK_CD_Set; включить прерыв. от Data и Clock
rjmp Wait_C
;----------------------
C01_Read_Ack:
sbic PINB,Data ; если Data=0, пропускаем след-ю команду
rjmp C01_Read_Ack_NotOk
C01_Read_Ack_Ok:
ldi w,0x23 ; говорим компу, что на линии есть Ack
out UDR,w
rjmp C01_Exit
C01_Read_Ack_NotOk:
ldi w,0x22 ; говорим компу, что на линии нет Ack
out UDR,w
rjmp C01_Exit
;---------------------------------------
;--- Уровень на линии clock не изменился
C1_no_changes: ; линия clock=1 и прерывание не от неё
;-- значит это старт или стоп
sbic PINB,Data ; если data=0 - пропустить команду
rjmp Stop_uslovie
Start_uslovie:
ldi Bit_Counter,8; один пакет = 8 бит от передатчика
out PCMSK,PCMSK_CD_Set ; добавляем прерывание от Clock
ldi w,0x20 ; сообщим, что получили start-условие
out UDR,w
ldi I2C_flags,0b00000001 ; чтение/было старт-условие
rjmp Wait_C
Stop_uslovie:
ldi w,0x21 ; сообщаем компу что получили stop-усл.
out UDR,w
pop w ; выгружаем адрес возврата из стека
pop w
rjmp Reset
;--------------------------------------
;--- Если на линии Clock низкий уровень
Clock_Low:
brts C10 ; если флаг T=1, то прыгаем
;---------------------------------------
;--- Уровень на линии clock не изменился
C0_no_changes: ; теоретически такого не должно произойти,
; поскольку мы выключаем прерывания от Data
; при переключении Clock 1->0
ldi w,0xFF ; сообщаем компу об ошибке
out UDR,w
pop w ; выгружаем адрес возврата из стека
pop w
rjmp Reset ; реинициализация
;---------------------------------------------
;--- Уровень на линии clock изменился с 1 на 0
C10:
sbi DDRB,Clock ; зажимаем clock (чтоб всё успеть)
cbi DDRB,Data ; отпускаем Data (=1)
clt ; записываем новое состояние clock
out PCMSK,PCMSK_C_Set ; убираем прерывание от Data
in w,EIFR ; читаем регистр
sbrc w,5 ; пропустить команду, если PCIF=0
out EIFR,Hi_PCIE; сбрасываем PCIF
;--- проверяем - читаем или пишем? ---
sbrs I2C_flags,1;если флаг 1=1 - пропускаем
rjmp C10_Read
;--- Пишем ---
C10_Write:
tst Bit_counter ; если записали 8 бит,
; то установится флаг Z
brne C10_Write_Next ; если нет - выходим, иначе:
C10_End_Write:
ldi I2C_flags,0b00000100 ; чтение ack
rjmp C10_Exit
C10_Write_Next:
lsr BTS
sbrs BTS,0 ; если бит 0 в BTS=1 - проп. 1 команду
sbi DDRB,Data ; Data=0
dec Bit_counter ; уменьшаем счётчик
C10_Exit:
cbi DDRB,Clock ; отпускаем clock
rjmp Wait_C
;--- Читаем ---
C10_Read:
sbrs I2C_flags,2; если читали ack - пропускаем 1 команду
rjmp C10_Read_Byte
;-- стоит режим чтения Ack (значит мы его только что
;-- прочли и ждём команды от компа что делать дальше)
C10_Ready:
sbis UCSRA,UDRE ; если буфер передатч. пуст - пропуск.1 команду
rjmp C10_Ready ; дожидаемся, пока предыдущее сообщение уйдёт
ldi w,0x24 ; сообщаем компу, что готовы принимать команды
out UDR,w
Wait_Comp_Answer:
sbis UCSRA,RXC ; если пришли данные от компа - пропуск.1 команду
rjmp Wait_Comp_Answer
in w,UDR ; читаем - что пришло от компа
cpi w,0x20
breq Com_Send_Byte ; переходим к команде "послать байт"
cpi w,0x21
breq Com_Read_Byte ; переходим к команде "считать байт"
cpi w,0x22
breq Com_NoAck ; переходим к команде "не посылать Ack"
cpi w,0x23
breq Com_Ack ; переходим к команде "послать Ack"
cpi w,0x24
breq Com_PIND_Status ; переходим к команде "PIND Status?"
;--- если пришла какая-то другая команда - сообщ.компу и ресетимся
ldi w,0xFF
out UDR,w
Exit_Reset:
pop w
pop w
rjmp Reset
;--- Выполнение разных команд ---
;-- готовимся писать байт
Com_Send_Byte:
ldi I2C_flags,0b00000010 ; направление - передача
ldi Bit_Counter,7
Wait_BTS:
sbis UCSRA,RXC ; если есть данные от компа - пропускаем 1 команду
rjmp Wait_BTS
in BTS,UDR ; читаем байт для передачи
;-- посылаем первый бит
cbi DDRB,Data ; Data=1
sbrs BTS,0 ; если бит 0 в BTS=1 - пропускаем команду
sbi DDRB,Data ; Data=0
;-- и выходим
rjmp C10_Exit
;-- готовимся читать байт
Com_Read_Byte:
ldi Bit_Counter,8
ldi I2C_flags,0b00000000 ; направление - чтение
rjmp C10_Exit
;-- шлём в шину NoAck
Com_NoAck:
sbrc I2C_flags,0; если приняли первый байт после старта
rjmp Exit_Reset ; выходим до нового start-условия
ldi I2C_flags,0b00000100 ; направление - чтение/читаем Ack
rjmp C10_Exit
;-- шлём в шину Ack
Com_Ack:
sbi DDRB,Data ; Data=0 (Ack)
ldi I2C_flags,0b00000100 ; направление - чтение/читаем Ack
rjmp C10_Exit
;-- шлём на комп состояние входов
Com_PIND_Status:
in w,PIND ; читаем входы порта D
out UDR,w ; шлём на комп
rjmp C10_Ready
;-- Не стоит режим чтения ack (значит мы читаем байт) --
C10_Read_Byte:
tst Bit_counter ; если прочитали 8 бит - установится флаг Z
brne C10_Exit ; если нет - выходим, иначе:
rjmp C10_Ready
;---------------------------------------------------------
Для правильной работы шлюза в контроллере должны быть “запрограммированы” следующие фьюзы: SPIEN, SUT0
Скачать готовую прошивку и asm-файл
Небольшой пример работы этой проги.
Пусть у нас есть мастер, который хочет считать байт по адресу 12h из микросхемы памяти 24С02, у которой адресные пины A0, A1, A2 подключены на общий провод (т.е. её 7-ми битный адрес равен 1010000), а мы хотим этой самой микрухой прикинуться и сказать мастеру, что по этому адресу у нас записан байт AAh. Открываем даташит и смотрим как с этой микросхемой памяти общаться (то есть смотрим – что мы будем получать от мастера и как должны ему отвечать). В даташите написано, что для чтения по произвольному адресу мастер должен сначала, после подачи старт-условия, адресовать нас для записи, передать адрес, потом послать повторное старт-условие, адресовать нас для чтения и потом уже прочитать байт.
Итак, заходим в терминалку, выбираем порт и подключаемся на скорости 115200. Описанный выше сценарий будет выглядеть в терминалке следующим образом:
(принимаемые от шлюза данные: <—, отправляемые шлюзу данные: —>):
<— 02h | // шлюз сообщает, что инициализирован |
<— 20h | // мастер послал старт-условие |
<— A0h | // A0h=”1010000 0″ – мастер говорит, что хочет обратиться к микросхеме памяти для записи |
<— 24h | // шлюз спрашивает, что делать дальше |
—> 23h | // просим шлюз послать Ack |
<— 23h | // шлюз сообщает, что отправил Ack |
<— 24h | // шлюз спрашивает, что делать дальше |
—> 21h | // просим шлюз принять байт по I2C |
<— 12h | // мастер устанавливает адрес, равным 12h |
<— 24h | // шлюз спрашивает, что делать дальше |
—> 23h | // просим шлюз послать Ack |
<— 23h | // шлюз сообщает, что отправил Ack |
<— 20h | // мастер послал старт-условие |
<— A1h | // A1h=”1010000 1″ – мастер говорит, что хочет обратиться к микросхеме памяти для чтения |
<— 24h | // шлюз спрашивает, что делать дальше |
—> 23h | // просим шлюз послать Ack |
<— 23h | // шлюз сообщает, что отправил Ack |
<— 24h | // шлюз спрашивает, что делать дальше |
—> 20h | // просим шлюз отправить байт по I2C |
—> AAh | // этот байт будет отправлен |
<— 23h | // шлюз сообщает, что получил от мастера Ack |
... | ... |
Замечания.
1) Несмотря на то, что шина I2C позволяет slave-устройствам растягивать обмен, удерживая линию Clock на низком уровне, необходимо понимать, что для большинства master-устройств неприемлемо ждать ответа до бесконечности (хотя есть и такие), поскольку им, в большинстве случаев, необходимо решать ещё и другие задачи (помимо обмена данными со slave-устройствами). Для нас это выражается в том, что мы не можем три часа сидеть перед терминалкой и думать, – что же мы хотим отправить нашему мастеру. Время ожидания у всех мастер-устройств разное, но оно практически у всех есть (у тех, что я видел, оно было порядка 0,5-1,5 секунд). По истечении этого времени, мастер, не дождавшись ответа от slave-устройства, вне зависимости от того, продолжаем ли мы удерживать clock на низком уровне, решит, что с линией что-то не так и прекратит обмен. Единственная возможность избежать такого развития событий – это автоматизировать ответы компьютера, то есть написать на компьютере полноценный эмулятор slave-устройства (чтобы не вы вручную из терминалки посылали мастеру нужные байты и подтверждения, а это это автоматически делал сам компьютер).
2) Как сделать, чтобы наш девайс работал в качестве I2C-сниффера? Да очень просто. I2C-cниффер должен всегда читать байт c шины (и никогда не должен отправлять), а после чтения байта он всегда должен посылать на шину NoAck. Другими словами, он никогда и ни при каких условиях не должен трогать линию Data (ему можно только читать с неё данные, чтобы никому не мешать, но зато читать он должен всё, не зависимо от направления передачи данных). Ну и, естественно, ту часть программы, которая реинициализирует шлюз в случае посылки NoAck после принятия от мастера первого после старт-условия байта, придётся удалить, поскольку снифферу должно быть абсолютно пофиг на адрес устройства, к которому обращается мастер (то есть, опять же, читать он должен весь обмен по шине, не зависимо от адреса).
3) Да и ещё одно, чуть не забыл про скорость. С кварцем 20 МГц эта программа позволяет шлюзу общаться с мастер-устройствами, работающими на скорости до 575 кбит/с. Естественно, скорость работы со шлюзом будет меньше, поскольку шлюз будет растягивать обмен (удерживая clock на низком уровне, пока не сделает все свои дела), здесь речь не об этом, а о том, что если мастер будет пытаться работать на скорости более 575 кбит/с, то шлюз просто не будет успевать ничего удержать и принять.
Source: http://radiohlam.ru