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

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

Программа, рассмотренная в этой статье, разработана для контроллера I2C-шлюза (шлюз у нас реализован на ATTiny2313). Эта программа позволяет из терминальной программы персонального компьютера общаться с I2C устройствами в режиме Master.

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

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

Протокол обмена с компьютером был придуман на ходу, ориентируясь на то, что он должен быть максимально простым, удобным и быстрым, тут каждый может придумать что-то своё. Я рассуждал следующим образом:

При работе с терминальной программой нам удобно посылать, принимать и отображать информацию байтами (по 8 бит), следовательно размер сообщений, которыми обмениваются ПК и шлюз должен быть кратен одному байту. Если внимательно почитать описание протокола I2C, то можно заметить, что общение по шине I2C требует 9 бит, из которых 8 бит посылает передатчик и один бит — приёмник (Ack), кроме того нужно как-то кодировать посылку старт- и стоп-условия и управлять линией Chip Select. Исходя из этого было принято решение, что обмен данными между компьютером и контроллером шлюза будет состоять из сообщений, размером в 1 или 2 байта. Первый байт будет служебным, в нём комп будет сообщать шлюзу что ему делать (и в том числе нужно ли посылать Ack при приёме), а шлюз будет сообщать компу что он сделал (и в том числе был ли принят Ack при отправке байта). Второй байт будет использоваться только в сообщении шлюза о приёме данных и в сообщении компа о необходимости отправить данные и будет содержать эти самые данные (которые шлюз принял по I2C или которые ему надо по I2C отправить).

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

  1. При передаче сообщения в направлении ПК->Шлюз:
  2. 10h — сформировать start-условие
  3. 11h — сформировать stop-условие
  4. 12h — отправить байт по I2C (следующий переданный компьютером байт будет отправлен шлюзом по I2C)
  5. 13h — принять байт по I2C, выдать Ack
  6. 14h — принять байт по I2C, не выдавать Ack
  7. 15h — переключить линию CS в низкий уровень
  8. 16h — переключить линию CS в высокий уровень
  1. При передаче сообщения в направлении Шлюз->ПК:
  2. 10h — сформировал start-условие
  3. 11h — сформировал stop-условие
  4. 12h — послал байт по I2C, не получил Ack (следующий принятый ПК байт — это байт, который шлюз послал по I2C)
  5. 13h — послал байт по I2C, получил Ack (следующий принятый ПК байт — это байт, который шлюз послал по I2C)
  6. 14h — принял байт по I2C (следующий принятый ПК байт — это байт, который шлюз принял по I2C)
  7. 15h — установил низкий уровень на линии CS
  8. 16h — установил высокий уровень на линии CS
  9. FFh — от ПК получена неизвестная команда

Вот пожалуй и всё описание (за дополнительными объяснениями — добро пожаловать на форум), а теперь перейдём к алгоритму и программе.

Алгоритм:

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

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

  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 - ack, при сброшенном флаге - посылаем ack
;-- флаг 1 - rcv, поднятый флаг - признак ожидания второго байта от компа
;-- (если флаг поднят - значит комп передаёт шлюзу
;-- двухбайтное сообщение и мы ждём второй байт)
.def   BTS=r19         ; байт для передачи
.def   RDB=r20         ; принятый байт
 
;-- определяем константы (и даём им имена)
.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         ; внешнее прерывание INT0
	reti         ; внешнее прерывание INT1
	reti         ; Input capture interrupt 1
	reti         ; Timer/Counter1 Compare Match A
	reti         ; Overflow1 Interrupt
	reti         ; Overflow0 Interrupt
	rjmp RX_INT  ; USART0 RX Complete Interrupt
	reti         ; USART0 Data Register Empty Interrupt
	reti         ; USART0 TX Complete Interrupt
	reti         ; Analog Comparator Interrupt
	reti         ; 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,0b11110100 ; настраиваем порт D
      out DDRD,w       ; PD0, PD1, PD3 - входы, остальные - выходы
      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,TXEN   ; включить передатчик
      nop
      sbi UCSRB,RXEN   ; включить приёмник
     ;-- разрешаем прерывание от приёмника
      sbi UCSRB,RXCIE  ; включить прерывания от приёмника
     ;-- разрешаем глобальные прерывания
      sei
     ;-- ждём прерывания ---
Wait_data:
      rjmp Wait_data
;---------------------------------------------------------
;--- Обработчик прерывания от приёмника ---
RX_INT:
   sbrs I2C_flags,1  ; если флаг поднят - приняли ожидаемый для отправки байт
   rjmp RX_one_int   ; если не ждали байт для отправки - прыгаем
   cbr  I2C_flags,0b00000010  ; сбрасываем флаг ожидания байта
   in   XL,UDR       ; читаем байт из приёмника в XL
  ;-- теперь у нас в ZH - команда отправки (0x12), в ZL - байт I2C
Com_0x12_2:
   mov   BTS,XL        ; посылаем байт
   rcall Send_Byte
   ldi   w,0x12
   sbrs  I2C_flags,0   ; если подтв-я не было, - пропустить следующую команду
   ldi   w,0x13
   out   UDR,w         ; сообщаем компу о результатах
wait_transmit1:
   sbis UCSRA,UDRE
   rjmp wait_transmit1 ; ждём, когда контроллер закончит передачу
   out  UDR,XL         ; и посылаем копию переданного байта
   reti
RX_one_int:
   in   XH,UDR       ; читаем байт из приёмника в XH
   cpi  XH,0x10      ; команда 0x10?
   breq Com_0x10
   cpi  XH,0x11      ; команда 0x11?
   breq Com_0x11
   cpi  XH,0x12
   breq Com_0x12_1
   cpi  XH,0x13
   breq Com_0x13
   cpi  XH,0x14
   breq Com_0x14
   cpi  XH,0x15
   breq Com_0x15
   cpi  XH,0x16
   breq Com_0x16
Com_err:
   ser  w
   out  UDR,w          ; посылаем сигнал об ошибочной команде
   reti
  ;-- действия для разных команд --
Com_0x12_1:    
   sbr  I2C_flags,0b00000010  ; поднимаем флаг, что ждём байт для отправки
   reti              ; выходим и разрешаем прерывания
  ;-- обработчики команд --
Com_0x10:
   rcall Start_uslovie ; посылаем старт-условие
   ldi   w,0x10
   out   UDR,w         ; сообщаем компу, что послали старт-условие
   reti
Com_0x11:
   rcall Stop_uslovie  ; посылаем стоп-условие
   ldi   w,0x11
   out   UDR,w         ; сообщаем компу, что послали стоп-условие
   reti
Com_0x13:
   sbr   I2C_flags,0b00000001   ; ack - не нужен
   rjmp  read_next
Com_0x14:
   cbr   I2C_flags,0b00000001   ; ack - нужен
read_next:
   rcall Recieve_Byte
   ldi   w,0x14
   out   UDR,w         ; сообщаем компу, что считали байт
wait_transmit2:
   sbis UCSRA,UDRE
   rjmp wait_transmit2 ; ждём, когда контроллер закончит передачу
   out  UDR,RDB        ; и посылаем считанный байт
   reti
Com_0x15:
   cbi   PORTD,CS      ; сбрасываем Chip Select
   out   UDR,XH        ; сообщаем об этом компу
   reti
Com_0x16:
   sbi   PORTD,CS      ; поднимаем Chip Select
   out   UDR,XH        ; сообщаем об этом компу
   reti
;--- Процедуры I2C ---------------------------------------
;-- сигнал ack - инвертированный, т.е. если он -----------
;-- ноль - есть ack, если 1 - нет ack --------------------
Clock_null:                ; установка нуля на линии Clock
  sbi  DDRB,Clock      ; переключаем ногу на выход
  ret                  ; (ноль в защёлку мы записали ещё при старте)
Clock_one:             ; установка единицы на линии Clock
  cbi  DDRB,Clock    ; переключаем ногу в Z-состояние
  ret
Data_null:             ; установка нуля на линии Data
  sbi  DDRB,Data     ; переключаем ногу на выход
  ret                  ; (ноль в защёлку мы записали ещё при старте)
Data_one:              ; установка единицы на линии Data
  cbi  DDRB,Data     ; переключаем ногу в Z-состояние
  ret
  ;--- start ----------
Start_uslovie:            ; формирование старт-условия
  rcall Clock_one
  rcall Pause_tbuf     ; "свободная шина" (4,7 мкс)
  rcall Data_null
  rcall Pause_thdsta   ; "фиксация старт-условия" (4 мкс)
  rcall Clock_null
  ret
  ;--- stop -----------
Stop_uslovie:             ; формирование стоп-условия
  rcall Data_null
  rcall Clock_one
wait_clock_p:
  sbis  PINB,Clock     ; проверяем шину Clock
  rjmp  wait_clock_p   ; ждём, пока отпустится шина Clock
  rcall Pause_tsusto   ; "готовность стоп-условия" (4 мкс)
  rcall Data_one
  ret
  ;--- transmit -------
Send_Byte:
  cbr   I2C_flags,0b00000001  ; сбрасываем флаг подтверждения
  ldi   Bit_counter,8         ; устанавливаем счётчик бит
next_bit_s:
  sbrc  BTS,7
  rcall Data_one              ; если передаваемый бит = 1
  sbrs  BTS,7
  rcall Data_null             ; если передаваемый бит = 0
  rcall Pause_tsudat          ; "готовность данных" (250 нс)
  rcall Clock_one
wait_clock_s1:
  sbis  PINB,Clock
  rjmp  wait_clock_s1
  rcall Pause_thigh           ; длительность полупериода считывания (4 мкс)
  rcall Clock_null
  lsl   BTS                   ; сдвиг влево
  dec   Bit_counter           ; уменьшаем счётчик
  brne  next_bit_s            ; если счётчик не равен нулю - шлём ещё
  rcall Data_one              ; если всё - отпускаем линию Data
  rcall Pause_tlow            ; длительность полупериода установки (4 мкс)
  rcall Clock_one
wait_clock_s2:
  sbis  PINB,Clock            ; проверяем - отпустилась ли линия Clock
  rjmp  wait_clock_s2
  sbic  PINB,Data             ; проверяем "ack"
  sbr   I2C_flags,0b00000001  ; если нет - поднимаем флаг
  rcall Pause_thigh           ; длительность полупериода считывания (4 мкс)
  rcall Clock_null            ; снова занимаем шину
  ret
  ;--- recieve --------
Recieve_Byte:
  clr   RDB                   ; очищаем приёмник
  ldi   Bit_counter,8         ; устанавливаем счётчик
next_bit_r:
  lsl   RDB                   ; сдвигаем влево приёмный регистр
  rcall Data_one              ; отпускаем линию Data
  rcall Pause_tlow            ; длительность полупериода установки (4 мкс)
  rcall Clock_one             ; отпускаем clock
wait_clock_r1:
  sbis  PINB,Clock            ; ждём когда отпустится clock
  rjmp  wait_clock_r1
  sbic  PINB, Data            ; если Data=1 - пишем в младший бит приёмника 1,
  sbr   RDB,0b00000001        ; если нулю - пропускаем эту команду
  rcall Pause_thigh           ; длительность полупериода считывания (4 мкс)
  rcall Clock_null            ; роняем clock
  dec   Bit_counter           ; уменьшаем счётчик
  brne  next_bit_r            ; если результат не равен нулю - читаем дальше
  sbrs  I2C_flags,0
  rcall Data_null             ; если надо посылать ack - роняем Data
  sbrc  I2C_flags,0
  rcall Data_one              ; если не надо посылать ack - отпускаем Data
  rcall Pause_tlow            ; длительность полупериода установки (4 мкс)
  rcall Clock_one             ; отпускаем Clock
wait_clock_r2:
  sbis  PINB,Clock            ; ждём, пока установится Clock
  rjmp  wait_clock_r2
  rcall Pause_thigh           ; даём время slav-у увидеть наш ack
                              ; (или его отсутствие)
  rcall Clock_null            ; роняем Clock
  ret
;--- Конец процедур I2C -------------------------------
;--- Задержки для частоты шины 100 кГц ------
  ;-- свободная шина (4,7 мкс) --
Pause_tbuf:
  ldi w,27
wait_tbuf:
  dec w
  brne wait_tbuf
  ret
  ;-- фиксация старт-условия (4 мкс) --
Pause_thdsta:
  ldi w,23
wait_thdsta:
  dec w
  brne wait_thdsta
  ret
  ;-- готовность стоп-условия (4 мкс) --
Pause_tsusto:
 ldi w,23
wait_tsusto:
  dec w
  brne wait_tsusto
  ret
  ;-- готовность данных (250 нс) --
Pause_tsudat:
  nop
  ret
  ;-- длительность полупериода считывания (4 мкс) --
Pause_thigh:
  ldi w,23
wait_thigh:
  dec w
  brne wait_thigh
  ret
  ;-- длительность полупериода установки (4 мкс) --
Pause_tlow:
  ldi w,23
wait_tlow:
  dec w
  brne wait_tlow
  ret
;---------------------------------------------------------

[свернуть]

Для правильной работы шлюза в контроллере должны быть «запрограммированы» следующие фьюзы: SPIEN, SUT0

Скачать готовую прошивку и asm-файл

Приведу небольшой пример работы со шлюзом:

Пусть мы хотим записать AAh в микруху SDA2546 по адресу 00h. Открываем даташит и смотрим как с этой микрухой общаться. В даташите написано, что для записи в микруху надо сначала послать сообщение CS/E: «1 0 1 0 0 A8 CS 0 0» (которое при A8=0 и CS=0 в шестнадцатиричном виде выглядит как A0h), после получения Ack послать адрес, по которому мы хотим писать (т.е. 00h), и далее после получения Ack послать сам байт, который должен быть записан по этому адресу (т.е. AAh). Непосредственно запись произойдёт после формирования стоп-условия.

Итак, заходим в терминалку, выбираем порт и скорость обмена (скорость у нас 115200), подключаемся и начинаем общаться со шлюзом:

— отправляем шлюзу 15h // (установить CS=0)
— получаем от шлюза 15h // он сообщает, что установил CS=0
— отправляем шлюзу 10h // просим шлюз сформировать старт-условие на шине I2C
— получаем от шлюза 10h // отчёт о выполнении нашей просьбы
— отправляем шлюзу 12h A0h // просим шлюз отправить по I2C байт A0h
— получаем от щлюза 13h A0h // шлюз сообщает, что отправил байт A0h и получил подтверждение
— отправляем шлюзу 12h 00h // посылаем по I2C адрес, по которому хотим писать
— получаем от шлюза 13h 00h
— отправляем шлюзу 12h AAh // посылаем по I2C байт, который хотим записать
— получаем от шлюза 13h AAh
— отправляем шлюзу 11h // просим шлюз сформировать стоп-условие
— получаем от шлюза 11h // отчёт, что стоп-условие сформировано

Вот и всё, в результате этих действий в нашей SDA2546 по адресу 00h окажется записано AAh.

Update. После небольших исследований, один из форумчан нашёл в данной программе ошибку, которая заключается в том, что при старте случайным образом может всё работать нормально, а может улететь по I2C первый же принятый байт. Для правильной работы программы нужно добавить в секцию Init команду clr I2C_flags, например, перед строкой «;— инициализируем порты». Прошивку, естественно, тоже нужно перекомпилировать.

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