Программа, рассмотренная в этой статье, разработана для контроллера I2C-шлюза (шлюз у нас реализован на ATTiny2313). Эта программа позволяет из терминальной программы персонального компьютера общаться с I2C устройствами в режиме Master.
Программа полностью написана на ассемблере, в конце статьи выложены исходники (с комментариями) и прошивка.
Для реализации обмена данными по I2C между контроллером и подключаемым устройством была использована стандартная, написанная нами ранее библиотека процедур (по этой ссылке можно посмотреть алгоритмы и исходники библиотеки).
Протокол обмена с компьютером был придуман на ходу, ориентируясь на то, что он должен быть максимально простым, удобным и быстрым, тут каждый может придумать что-то своё. Я рассуждал следующим образом:
При работе с терминальной программой нам удобно посылать, принимать и отображать информацию байтами (по 8 бит), следовательно размер сообщений, которыми обмениваются ПК и шлюз должен быть кратен одному байту. Если внимательно почитать описание протокола I2C, то можно заметить, что общение по шине I2C требует 9 бит, из которых 8 бит посылает передатчик и один бит — приёмник (Ack), кроме того нужно как-то кодировать посылку старт- и стоп-условия и управлять линией Chip Select. Исходя из этого было принято решение, что обмен данными между компьютером и контроллером шлюза будет состоять из сообщений, размером в 1 или 2 байта. Первый байт будет служебным, в нём комп будет сообщать шлюзу что ему делать (и в том числе нужно ли посылать Ack при приёме), а шлюз будет сообщать компу что он сделал (и в том числе был ли принят Ack при отправке байта). Второй байт будет использоваться только в сообщении шлюза о приёме данных и в сообщении компа о необходимости отправить данные и будет содержать эти самые данные (которые шлюз принял по I2C или которые ему надо по I2C отправить).
Служебные сообщения мы закодируем следующим образом:
- При передаче сообщения в направлении ПК->Шлюз:
- 10h — сформировать start-условие
- 11h — сформировать stop-условие
- 12h — отправить байт по I2C (следующий переданный компьютером байт будет отправлен шлюзом по I2C)
- 13h — принять байт по I2C, выдать Ack
- 14h — принять байт по I2C, не выдавать Ack
- 15h — переключить линию CS в низкий уровень
- 16h — переключить линию CS в высокий уровень
- При передаче сообщения в направлении Шлюз->ПК:
- 10h — сформировал start-условие
- 11h — сформировал stop-условие
- 12h — послал байт по I2C, не получил Ack (следующий принятый ПК байт — это байт, который шлюз послал по I2C)
- 13h — послал байт по I2C, получил Ack (следующий принятый ПК байт — это байт, который шлюз послал по I2C)
- 14h — принял байт по I2C (следующий принятый ПК байт — это байт, который шлюз принял по I2C)
- 15h — установил низкий уровень на линии CS
- 16h — установил высокий уровень на линии CS
- FFh — от ПК получена неизвестная команда
Вот пожалуй и всё описание (за дополнительными объяснениями — добро пожаловать на форум), а теперь перейдём к алгоритму и программе.
Алгоритм:
Итак, в аппаратной части мы имеем:
- 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 - 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, например, перед строкой «;— инициализируем порты». Прошивку, естественно, тоже нужно перекомпилировать.