Итак, продолжаем эксперименты с собранным ранее 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
Программа:
;--------------------------------------------------------- .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 кбит/с, то шлюз просто не будет успевать ничего удержать и принять.
Источник: http://radiohlam.ru