Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

Программа для контроллера I2C-шлюза (режим I2C-slave из терминалки ПК)

Итак, продолжаем эксперименты с собранным ранее I2C-шлюзом (который, как вы помните, у нас реализован на ATTiny2313). В этой статье мы рассмотрим полностью программную реализацию режима I2C-Slave, который позволит нашему девайсу из терминальной программы персонального компьютера прикидываться любым Slave-устройством, а также просто подглядывать за обменом данными на шине I2C (то есть работать как сниффер).

Прога, как всегда, на асме, в конце статьи, как всегда, исходники (с комментариями) и прошивка.

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

Служебные сообщения мы закодируем следующим образом:

  1. При передаче сообщения в направлении ПК->Шлюз:
  2. 20h — отправить байт по I2C (следующий переданный компьютером байт будет отправлен шлюзом по I2C)
  3. 21h — принять байт по I2C (следующий принятый компом байт — это тот байт, который шлюз считал с шины)
  4. 22h — не посылать Ack
  5. 23h — послать Ack
  6. 24h — запросить состояние входов порта D (например, для определения уровня на входе Chip Select), следующий принятый компом байт — это байт состояния входов порта D (PIND)
  1. При передаче сообщения в направлении Шлюз->ПК:
  2. 20h — на шине произошло start-условие
  3. 21h — на шине произошло stop-условие
  4. 22h — не получили (не послали) Ack
  5. 23h — получили (послали) Ack
  6. 24h — ждём дальнейших указаний
  7. FFh — от ПК получена неизвестная команда
  8. 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, так что некоторый запас времени, на то, чтобы позаниматься своими делами, не рискуя пропустить старт или стоп условие, у нас есть).

Вот такая концепция, дальше смотрим алгоритм и прогу, в случае необходимости, — пишем вопросы на форум.

Алгоритм:

Алгоритм работы I2C-шлюза (режим I2C-slave)

Итак, в аппаратной части мы имеем:

  1. PB0 — линия Clock
  2. PB2 — линия Data
  3. PD5 — линия CS
  4. PD0 — линия Rx
  5. 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 кбит/с, то шлюз просто не будет успевать ничего удержать и принять.

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