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

Интерфейс USB. Часть 7. Программная реализация LS устройства USB. Разбираем пакеты по типам

Итак, в прошлой части мы написали код, обеспечивающий приём пакета USB в буфер SRAM. Теперь пришло время определить, что за пакеты мы приняли. Именно этим мы сегодня и займёмся, а потом уже будем решать, что нам с этим дальше делать

Сначала давайте подумаем, какие пакеты нас вообще будут интересовать. Из всего многообразия нас интересуют в первую очередь token-пакеты Setup, Out и In, отмечающие начало транзакций управления и передачи данных. Естественно, нас также будут интересовать пакеты данных Data0, Data1, содержащие сами передаваемые в транзакциях данные. Все остальные пакеты, включая пакеты подтверждения (handshake), пока оставим без внимания.

Теперь нужно придумать, как бы побыстрее идентифицировать эти пакеты, чтобы побыстрее, в случае необходимости, послать ответ хосту. Здесь поступим следующим образом, — вместо того, чтобы сначала декодировать из NRZI весь принятый буфер, а потом извлекать из полученных восстановленных данных PID и адрес, мы, наоборот, — будем хранить в программе все нужные идентификаторы сразу в закодированном виде (в NRZI) и точно также (в закодированном виде) будем хранить адрес устройства. При этом мы точно знаем, что ни в одном первом байте пакета битовые вставки не используются (просто потому, что там первые 4 бита — это сам идентификатор, а вторые 4 бита — его инверсная копия). Про адрес мы ничего подобного не знаем, но точно знаем, что битовые вставки не используются для начального адреса (он у нас ноль и по определению не может содержать 6 подряд единиц). Кроме того, мы знаем, что все заголовки маркер-пакетов оканчиваются единицей, поэтому можем точно сказать, как будет выглядеть начальный (нулевой) адрес в формате NRZI.

Для примера покажу как должен выглядеть первый байт setup-пакета в нашем буфере. Такой пакет имеет PID 1101. Дополняем PID его инверсной копией: 1101 0010. Все поля пакетов (кроме CRC) передаются младшим битом вперёд. Не байты, а именно поля! При приёме, в нашем сдвиговом регистре первые принятые биты оказываются старшими. Поэтому, чтобы получить правильный порядок бит первого байта setup-пакета (такой же, как в нашем буфере) — меняем порядок бит в каждом полубайте: 1011 0100. Ну и теперь осталось только закодировать этот байт в NRZI, с учётом того, что данные мы читаем с линии D+, состояние которой перед передачей этого байта точно было 1 (об этом мы в прошлый раз говорили). Получается вот что: 1000 1101. Аналогично можно вычислить как будут выглядеть первые байты для других пакетов.

Едем дальше. Поскольку в прерывании мы принимаем по одному пакету, а транзакция состоит из нескольких пакетов, то нам нужно будет их «склеивать», а для этого придётся выделить регистр, в котором будет храниться информация о текущей стадии обмена данными. Зная, какую часть транзакции мы только что приняли, мы будем понимать, какую часть мы теперь ожидаем.

В общем-то теперь можно начинать писать код, однако, поскольку в этой части алгоритм у нас более сложный, чем в предыдущей, то давайте всё-таки прежде нарисуем сам алгоритм:

алгоритм идентификации различных USB-пакетов

Теперь, с учётом этого алгоритма, наш код будет выглядеть так:

Текст под катом

.device ATtiny2313
.include "tn2313def.inc"
.list
;--- определяем выводы портов ---
.equ  InputPort  = PINB  ; отсюда читаем
.equ  OutputPort = PORTB ; сюда пишем
.equ  Direction  = DDRB  ; выбор направления
.equ  USBDPlus   = 0     ; PB0 - DATA+
.equ  USBDMinus  = 1     ; PB1 - DATA-
.equ  LED0       = 2
.equ  LED1       = 3
.equ  LED2       = 4
;--- вспомогательные константы ---
.equ  USBPinMask = ~((1<<USBDMinus)|(1<<USBDPlus)) ; 0b11111100 - маска для D+/D-
;--- идентификаторы пакетов, которые мы будем принимать ---
;--- при приёме у нас байты получаются c обратным порядком бит ---
.equ  SETUPPID   = 0b11010010 ; PID+CHECK старшим битом вперёд
.equ  OUTPID     = 0b00011110
.equ  INPID      = 0b10010110
.equ  DATA0PID   = 0b00111100
.equ  DATA1PID   = 0b10110100
.equ  ADDRESS0   = 0b00000000 ; начальный адрес = 0
 
.equ  rSETUPPID  = 0b10110100 ; rPID+rCHECK - поля младшими битами вперёд
.equ  rOUTPID    = 0b10000111
.equ  rINPID     = 0b10010110
.equ  rDATA0PID  = 0b11000011
.equ  rDATA1PID  = 0b11010010
.equ  rADDRESS0  = 0b00000000 ; начальный адрес, обратный порядок бит
 
.equ  NRZI_rSETUPPID = 0b10001101 ; такой пакет мы принимаем (относительно D+)
.equ  NRZI_rOUTPID   = 0b10101111
.equ  NRZI_rINPID    = 0b10110001
.equ  NRZI_rDATA0PID = 0b11010111
.equ  NRZI_rDATA1PID = 0b11001001
.equ  NRZI_rADDRESS0 = 0b01010101 ; поскольку все заголовки маркер-пакетов оканчиваются единицей
;--- флаги различных стадий обработки данных
.equ  BaseStage  = 0 ; начальная стадия
.equ  SetupStage = 1 ; приняли маркер-пакет транзакции Setup
.equ  OutStage   = 2 ; приняли маркер-пакет транзакции Out
;---------------------------------
;--- распределение памяти --------
.equ  MaxUSBBytes = 13         ; максимальный размер буфера для "сырых" данных
.equ  StackTop    = RAMEND     ; вершина стека
.equ  InputBuffer = RAMEND-127 ; начало буфера "сырых" входных данных USB (0)
;------------------------------------------------------
;--- регистры -----------------------------------------
.def  MyOutAddress = r14 ; адрес для пакетов Out
.def  MyInAddress  = r15 ; адрес для пакетов In/Setup
 
.def  temp0    = r16 ; temporary register
.def  temp1    = r17 ; temporary register
.def  InputReg = r18 ; входной регистр (сюда читаем значения линий)
.def  ShiftReg = r19 ; сдвиговый регистр (сюда копим принимаемые биты)
.def  Stage    = r20 ; стадия обмена данными
 
.def  USBBufPtrXL = r26 ; XL - указатель на буфер USB
.def  USBBufPtrXH = r27 ; XH - указатель на буфер USB
;******************************************************
;-- начало программного кода
  .cseg
  .org 0
  rjmp  Init       ; переход на начало программы (вектор сброса)
;-- дальше идут вектора прерываний
  rjmp  IRQ_INT0   ; внешнее прерывание INT0
  .org  WDTaddr+1  ; программа начинается за таблицей векторов
;-----------------------------------------------------------
;-- начало программы (инициализация портов и переменных) ---
Init:
  ldi   temp0,StackTop
  out   SPL,temp0       ; инициализируем стек
 
  ldi   temp0,(1<<LED0)+(1<<LED1)+(1<<LED2)
  out   Direction,temp0 ; линии светодиодов - выходы
  ldi   temp0,0b11111011
  out   PORTD,temp0     ; включаем подтяжки на линиях порта D, кроме PD2(INT0)
 
  rcall USBReset        ; обнуление адресов, сброс состояний и т.д.
 
  ldi   temp0,0x0F      ; INT0 - прерывание по переднему фронту
  out   MCUCR,temp0
  ldi   temp0,1<<INT0 ; включаем внешнее прерывание INT0
  out   GIMSK,temp0
  sei                   ; разрешаем немаскированные прерывания
;--- Основной цикл ---
General_loop:
  sbis  InputPort,USBDminus ; если D- = 0, то возможно это Disconnect,
  rjmp  CheckUSBReset       ; в случае которого мы будем делать Reset
  rjmp  General_loop        ; если D- = 1, то считаем, что это IDLE
;--- Проверяем, не случился ли Disconnect ---
CheckUSBReset:
  ldi   temp0,255           ; будем отсчитывать 255 циклов
   ; если за это время не изменится состояние D- (так и останется 0),
   ; то считаем, что случился Disconnect и нужен Reset
WaitUSBReset:
  sbic  InputPort,USBDminus ; если D- всё ещё ноль - пропустить
  rjmp  General_loop
  dec   temp0               ; уменьшаем счётчик
  brne  WaitUSBReset        ; прыгаем, если не ноль
  rcall USBReset
  rjmp  General_loop
;--- Конец основного цикла ---
;**********************************************************
;--- Сброс USB (обнуление адресов, сброс состояний ...) ---
USBReset:
  clr   Stage                ; начальная стадия
  ldi   temp0,NRZI_rADDRESS0 ; нулевой
  mov   MyInAddress,temp0    ; начальный
  mov   MyOutAddress,temp0   ; адрес
  ret
;**********************************************************
;--- Внешнее прерывание INT0 (положительный фронт на D+) ---
IRQ_INT0:
  ldi   temp0,2      ; готовимся отсчитывать количество одинаковых битов
  ldi   temp1,2
   ; определяем момент смены бита
CheckDMOne:
  sbis  InputPort,USBDMinus
  rjmp  CheckDMOne
   ;--- теперь ждём единичный бит (в синхропоследовательности это в
   ;--- обязательном порядке будет состояние, когда D+ = 1)
   ;--- момент обнаружения этого бита будем использовать для синхронизации
   ;--- (от него начинаем отсчёт битовых интервалов, поэтому дальнейшие куски
   ;--- кода должны быть строго выверены по количеству тактов в отладчике)
CheckDPOne:
  sbis  InputPort,USBDPlus ; самое начало единичного бита
  rjmp  CheckDPOne         ; 2-й такт
DetectSyncEnd:
  sbis  InputPort,USBDPlus ; 3,4-й такты (D+=1) или  3-й такт (D+=0)
  rjmp  TestBit0           ; -/-                     4,5-й такты (D+=0)
TestBit1:
  ldi   temp0,2            ; 5-й такт (сбрасываем счётчик нулей)
  dec   temp1              ; 6-й такт
  nop                      ; 7-й такт (задержка, чтобы получить 8 тактов на бит)
  breq  USBBeginPacket     ; 8-й такт при temp1>0 или 8-й и 1-й при temp1=0
  rjmp  DetectSyncEnd      ; 1,2-й такты
TestBit0:
  ldi   temp1,2            ; 6-й такт (сбрасываем счётчик единиц)
  dec   temp0              ; 7-й такт
  nop                      ; 8-й такт
  brne	DetectSyncEnd      ; 1,2-й такты при temp0>0
   ;--- сюда попадаем, если два подряд бита равны нулю ---
   ;--- считаем, что это был глюк и просто выходим ---
ExitFromIRQ:
  ldi   ShiftReg,1<<INTF0
  out   GIFR,ShiftReg      ; сбрасываем флаг прерывания INT0
  reti
   ;--- начинаем принимать информационные биты ---
USBBeginPacket:	; сюда мы попадаем после 1-го такта первого информационного бита
   ;--- читаем первый информационный бит байта ---
  in    ShiftReg,InputPort ; и сразу пишем его в сдвиговый регистр
  nop                      ; 3-й такт
USBLoopBegin:
  ldi   temp0,6                 ; счётчик битов (для следующих 6 битов)
  ldi   temp1,MaxUSBBytes       ; счётчик байтов
  ldi   USBBufPtrXL,InputBuffer ; 6-й такт
  nop                           ; 7-й такт
USBLoopByte:
  nop                           ; 8-й такт
   ;--- читаем 2-7 информационные биты ---
USBLoop16:
  in    InputReg,InputPort  ; читаем значение битов D+/D-      | 1-й такт
  cbr   InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D-
  breq  EndPacket           ; если оба нули - конец пакета     | 3-й такт, если не ноль
  ror   InputReg            ; вытесняем D+ в CF                | 4-й такт
  rol   ShiftReg            ; и пишем его в сдвиговый регистр  | 5-й такт
  dec   temp0               ; прочитали 7 битов?               | 6-й такт
  brne  USBLoop16           ; если нет - читаем (7,8-й такты)
  nop                       ; 8-й такт
   ;--- читаем последний бит байта ---
USBLoop7: 
  in    InputReg,InputPort  ; 1-й такт
  cbr   InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D-
  breq  EndPacket           ; если оба нули - конец пакета     | 3-й такт, если не ноль
  ror   InputReg            ; вытесняем D+ в CF                | 4-й такт
  rol   ShiftReg            ; и пишем его в сдвиговый регистр  | 5-й такт
  ldi   temp0,7             ; настраиваем счётчик на приём 7 битов
  st    X+,ShiftReg         ; сохраняем в буфер принятый байт  | 7,8-й такт
   ;--- читаем первый бит очередного байта ---
USBLoop0:
  in    ShiftReg,InputPort  ; сразу пишем его в сдвиговый регистр
  cbr   InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D-
  breq  EndPacket           ; если оба нули - конец пакета     | 3-й такт, если не ноль
  dec   temp0               ; 4-й такт
  dec   temp1               ; 5-й такт
  brne  USBLoopByte         ; 6,7-й такт, если не 0            | 6-й такт, если 0
   ;--- если буфер переполнен - остальные биты просто не записываем,
   ;--- (наш пакет по-любому меньше, но конца пакета нужно дождаться)
BufferOverrange:
  nop                       ; 7-й такт
  nop                       ; 8-й такт
  in    InputReg,InputPort  ; 1-й такт
  cbr   InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D-
  breq  EndPacket           ; если оба нули - конец пакета  | 3-й такт, если не ноль, иначе 3,4
  nop                       ; 4-й такт
  rjmp  BufferOverrange     ; 5,6-й такт
   ;--- закончили принимать пакет и начинаем его анализировать ---
   ;--- попадаем сюда на 5-м такте первого или второго битового ---
   ;--- интервала SE0, теперь нужно как можно быстрее понять чего --- 
   ;--- от нас хотят и ответить (или не ответить, если хотят не от нас) ---
EndPacket:
  cpi   USBBufPtrXL,InputBuffer+3 ; приняли хотя бы 3 байта?
  brcs  ExitFromIRQ               ; если меньше - выходим
   ;*********************************************
   ;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   ;--- а вот если да - начинаем разбирать пакеты
  lds   temp0,InputBuffer   ; читаем из буфера PID
  lds   temp1,InputBuffer+1 ; читаем из буфера адрес
   ;--- обе предыдущие команды не меняют флаги
  brne  TestDataPacket      ; если не ровно 3 байта (т.е. больше трёх, ибо
   ;--- меньше мы уже обработали) - это может быть только пакет данных, - прыгаем
   ;--- если ровно три байта - сначала проверяем не setup ли это?
TestSetupPacket:
  cpi   temp0,NRZI_rSETUPPID
  brne  TestOutPacket     ; если это не Setup, - проверяем, может это Out
  cp    temp1,MyInAddress ; если Setup, то нам ли он?
  brne  NoMyPacket        ; если не нам - прыгаем
  ldi   Stage,SetupStage  ; ставим признак начала Setup-транзакции
  rjmp  ExitFromIRQ       ; выходим из прерывания
   ;--- проверяем, - не Out ли пакет мы получили
TestOutPacket:
  cpi   temp0,NRZI_rOUTPID
  brne  TestInPacket       ; если это не Out, - проверяем, может это In
  cp    temp1,MyOutAddress ; если Out, то нам ли?
  brne  NoMyPacket         ; если не нам - прыгаем
  ldi   Stage,OutStage     ; ставим признак начала Out-транзакции
  rjmp  ExitFromIRQ        ; выходим из прерывания
TestInPacket:
  cpi   temp0,NRZI_rINPID
  brne  TestDataPacket    ; если это не IN - прыгаем
  cp    temp1,MyInAddress ; если IN, то нам ли он?
  brne  NoMyPacket        ; если не нам - прыгаем
AnswerToInRequest:
   ; 1) приняли пакет IN (хост запрашивает данные), нужно что-то отвечать
TestDataPacket:
  cpi   temp0,NRZI_rDATA0PID  ; это DATA0?
  breq  Data0Packet           ; если да - прыгаем
  cpi   temp0,NRZI_rDATA1PID  ; это DATA1?
  brne  NoMyPacket            ; если нет (это не DATA0 и не DATA1) - прыгаем
Data0Packet:
  cpi   Stage,SetupStage   ; мы в Setup-транзакции?
  breq  RecieveSetupData   ; значит мы приняли управляющие данные от хоста (Setup)
  cpi   Stage,OutStage     ; мы в Out-транзакции?
  breq  RecieveOutData     ; значит мы приняли какие-то данные от хоста
NoMyPacket:
  ldi   Stage,BaseStage    ; возвращаемся в начальную стадию
  rjmp  ExitFromIRQ        ; выходим
RecieveSetupData:
   ; 2) приняли пакет данных транзакции Setup (команду управления)
RecieveOutData:
   ; 3) приняли пакет данных транзакции Out
  reti

[свернуть]

Вот и всё. Определять нам или не нам предназначены принятые пакеты, а также разделять «свои» пакеты по типам, мы теперь умеем. Осталось начиться правильно на «свои» пакеты отвечать. Это, как вы уже понимаете, ждёт нас в дальнейших статьях.

  1. Часть 1. Основы.
  2. Часть 2. Как происходит передача данных по шине.
  3. Часть 3. Что должно уметь любое USB-устройство.
  4. Часть 4. Дескрипторы и классы.
  5. Часть 5. Программная реализация LS устройства USB. Схема.
  6. Часть 6. Программная реализация LS устройства USB. Физика и приём пакетов.
  7. Часть 7. Программная реализация LS устройства USB. Разбираем пакеты по типам.
  8. Часть 8. Программная реализация LS устройства USB. Передача по USB произвольного буфера и пакетов подтверждения.
  9. Часть 9. Программная реализация LS устройства USB. Продолжаем разбираться с принятыми пакетами.

Комментарии 1

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