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

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

Сейчас, когда мы научились отправлять хосту любые произвольные данные из буфера, вернёмся немного назад, к седьмой части этой бесконечной статьи. В седьмой части мы остановились на том, что научились определять три типа пакетов, которые предназначены нам и на которые нам нужно что-то отвечать. Теперь, когда мы умеем что-то отвечать в принципе, можно думать, что отвечать в каждом конкретном случае.

Идея здесь вот какая, — в самом прерывании будем отправлять только пакеты подтверждения и заранее подготовленные ответы, а всю остальную работу будем выполнять вне прерывания. В противном случае нам просто может не хватить времени на обработку данных и подготовку ответа до прихода следующего пакета. А «отмалчиваться» в USB нельзя, иначе хост решит, что мы просто «отвалились». Лучше уж мы будем в случае чего отвечать на запросы пакетом Nak («не готов разговаривать»).

При этом для того, чтобы определять, что именно мы должны делать вне прерывания, придётся завести ещё одну переменную (назовём её скажем Action). В состоянии, когда ничего вне прерывания делать не нужно, — значение переменной Action будет равно нулю. Для её объявления, инициализации и сброса допишем следующий код:

в шапку:

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

;--- флаги действий вне прерывания
.equ	NothingAction	= 0	; ничего не делать
 
.def	Action	= r23		; что нам делать вне прерывания

[свернуть]

в процедуру Reset:

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

  clr	Action			; после сброса ничего не нужно делать

[свернуть]

Всё, переходим к конкретным ответам. Начнём с ответа на пакет данных, полученный в транзакции Setup (в нашей программе это место обозначено меткой ReciveSetupData). Как мы уже решили выше, в самом прерывании просто отправим пакет Ack и запишем в переменную Action значение, которое подскажет что мы должны делать после выхода из прерывания.

Кроме того, давайте скопируем данные входного буфера в какой-нибудь другой буфер (назовём его, например, рабочим) и вне прерывания будем работать с ним (поскольку данные во входном буфере могут быть затёрты при следующем прерывании).

Ещё нужно учесть, что при переходе от приёма пакета к обработке может получиться так, что во входной буфер не запишется последний полный или неполный байт (просмотрите ещё раз внимательно код приема пакета из 6-й части). Здесь нам нужно будет его дописать. Сам этот байт у нас хранится в регистре ShiftReg, поэтому нужно проследить, чтобы этот регистр не был затёрт ни при разборе пакетов, ни при отправке пакета подтверждения. Кроме ShiftReg нужно также не затереть temp0 (счётчик битов в последнем байте).

Для того, чтобы не затереть temp0 при разборе пакетов — придётся добавить ещё одну переменную (temp3) и переписать код разбора пакетов, заменив в нём temp0 на temp3. Далее, для того, чтобы не затереть temp0 и ShiftReg при отправке пакета — будем просто сохранять эти значения в стеке. В коде это всё выглядит так:

Дописываем в шапку объявление флага:

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

.equ	SetupAction	= 1	; нужно обработать пакет данных транзакции Setup

[свернуть]

Дописываем код после метки RecieveSetupData:

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

  rcall	SendACK			; отвечаем ACK и завершаем транзакцию управления
  ldi	Action,SetupAction	; отмечаем, что нам нужно обработать пакет данных от хоста
  rcall	FinishRecieving		; готовим принятые данные к работе
  rjmp	ExitFromIRQ		; выходим из прерывания

[свернуть]

Дописываем код в секцию дополнительных процедур:

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

SendAck:
  ldi	ByteCount,2		; передаём 2 байта
  ldi	USBBufPtrYL, AckBuffer	; указатель на начало буфера
  clr	OutBitstuffLength	; нет битовых вставок
  rcall SendUSBBuffer		; отправляем по USB
  ret

[свернуть]

Дописываем после метки SendUSBBuffer:

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

  push	temp0
  push	ShiftReg

[свернуть]

И дописываем в конце процедуры отправки пакета (перед ret):

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

  pop	ShiftReg
  pop	temp0

[свернуть]

Осталось описать процедурку сохранения последнего байта:

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

FinishRecieving:
  cpi	temp0,7			; счётчик настроен на 7 бит?
  breq	NoRemainingBits	; нет незаписанных битов
  st	X+,ShiftReg		; сохраняем последний байт и увеличиваем указатель
NoRemainingBits:
  mov	temp1,USBBufPtrXL	; temp1 = текущий адрес для сохранения
  subi	temp1,InputBuffer	; temp1 = кол-во байт во входном буфере
  mov	InputBufferLength,temp1	; сохраняем это количество
  ldi	USBBufPtrYL,WorkBuffer	; указатель на начало рабочего буфера
  ldi	USBBufPtrXL,InputBuffer	; указатель на начало входного буфера
CopyInputDataToWorkBuffer:
  ld	temp0,X+		; читаем байт из входного буфера
  st	Y+,temp0		; и копируем в рабочий
  dec	temp1
  brne	CopyInputDataToWorkBuffer	; копируем
  ret

[свернуть]

Переходим к ответу на пакет данных, полученный в транзакции Out. Тут всё просто, — наш девайс очень простой и рулить им мы будем только через управляющие передачи. Поэтому, на транзакции Out будем отвечать только Ack или Nak (мол мы вас понимаем, но делать ничего не будем). Соответственно, остатки пакета нам тоже неинтересны и копировать пакет никуда не нужно. Nak будем отвечать в том случае, если установлен флаг обработки транзакции Setup, в противном случае — Ack. В коде это выглядит так:

После метки RecieveOutData добавляем следующий код:

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

  cpi	Action,SetupAction	; мы всё ещё обрабатываем Setup
  breq	WeAreNotReady	; тогда мы не готовы к разговору
WeAreReady:
  rcall	SendACK	; отвечаем ACK
  clr	Action	; указываем, что нам ничего не нужно делать
  rjmp	ExitFromIRQ	; выходим из прерывания
WeAreNotReady:
  rcall	SendNAK	; отвечаем NAK
  rjmp	ExitFromIRQ	; выходим из прерывания

[свернуть]

Процедура SendACK у нас уже есть, поэтому добавляем процедуру SendNAK:

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

SendNAK:
  ldi	ByteCount,2	; передаём 2 байта
  ldi	USBBufPtrYL, NakBuffer	; указатель на начало буфера
  clr	OutBitstuffLength	; нет битовых вставок
  rcall	SendUSBBuffer	; отправляем по USB
  ret

[свернуть]

Осталось написать код, отвечающий за отправку данных хосту в транзакции IN. Здесь нам придётся ввести ещё пару флагов для регистра Action: ReadySendAnswer (готовы слать данные хосту) и PrepareAnswerCicl (готовим новую порцию ответа). Ответ мы будем готовить вне обработчика прерывания, в буфере OutputBuffer, так что в обработчике нам останется его только отправить в подходящий момент.

В коде это выглядит следующим образом:

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

AnswerToInRequest:
  cpi	Action,ReadySendAnswer	; ответ готов?
  brne	WeAreNotReady	; не готов
  rcall	SendUSBAnswer	; готов, отправляем
  ldi	Stage,InStage	; мы в транзакции IN
  ldi	Action,PrepareAnswerCicl	; готовим новую порцию ответа в свободное время
  rjmp	ExitFromIRQ	; выходим из прерывания

[свернуть]

С учётом добавленного сегодня, наш код целиком выглядит так:

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

.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  SYNC     = 0b10000000 ; начало пакета
.equ  ACKPID   = 0b00101101
.equ  NAKPID   = 0b10100101
.equ  SETUPPID = 0b11010010 ; PID+CHECK старшим битом вперёд
.equ  OUTPID   = 0b00011110
.equ  INPID    = 0b10010110
.equ  DATA0PID = 0b00111100
.equ  DATA1PID = 0b10110100
.equ  ADDRESS0 = 0b00000000 ; начальный адрес = 0
 
.equ  rSYNC     = 0b00000001
.equ  rACKPID   = 0b01001011
.equ  rNAKPID   = 0b01011010
.equ  rSETUPPID = 0b10110100 ; rPID+rCHECK - поля младшими битами вперёд
.equ  rOUTPID   = 0b10000111
.equ  rINPID    = 0b10010110
.equ  rDATA0PID = 0b11000011
.equ  rDATA1PID = 0b11010010
.equ  rADDRESS0 = 0b00000000 ; начальный адрес в обратном порядке
 
   ;-- чтобы получить NRZI нужно учитывать, что перед началом передачи на линии D+
   ;-- висит ноль, а в конце последовательности SYNC - единица, т.е. начальным
   ;-- состоянием для SYNC будет ноль, а для других полей - единица
.equ  NRZI_rSYNC     = 0b10101011
.equ  NRZI_rACKPID   = 0b00100111
.equ  NRZI_rNAKPID   = 0b00111001
.equ  NRZI_rSETUPPID = 0b10001101 ; такой пакет мы принимаем (относительно D+)
.equ  NRZI_rOUTPID   = 0b10101111
.equ  NRZI_rINPID    = 0b10110001
.equ  NRZI_rDATA0PID = 0b11010111
.equ  NRZI_rDATA1PID = 0b11001001 ; здесь у меня была ошибка (0b11001010)
.equ  NRZI_rADDRESS0 = 0b01010101 ; поскольку все маркер-пакеты оканчиваются единицей
;--- флаги различных стадий обмена данными
.equ  BaseStage   = 0 ; начальная стадия
.equ  SetupStage  = 1 ; стадия приёма транзакции Setup
.equ  OutStage    = 2 ; стадия приёма транзакции Out (данные от хоста)
.equ  InStage     = 3 ; стадия приёма транзакции In  (данные к хосту)
;--- флаги действий вне прерывания
.equ  NothingAction     = 0 ; ничего не нужно делать
.equ  SetupAction       = 1 ; нужно обработать пакет данных транзакции Setup
.equ  ReadySendAnswer   = 2 ; готовы слать данные хосту
.equ  PrepareAnswerCicl = 3 ; готовим новую порцию ответа
;---------------------------------
;--- распределение памяти --------
.equ  MaxUSBBytes  = 13                       ; максимальный размер буфера для "сырых" данных
.equ  StackTop     = RAMEND                   ; вершина стека
.equ  InputBuffer  = RAMEND-127               ; начало буфера "сырых" входных данных USB (0)
.equ  OutputBuffer = InputBuffer+MaxUSBBytes  ; начало выходного буфера USB
.equ  AckBuffer    = OutputBuffer+MaxUSBBytes ; начало буфера Ack
.equ  NakBuffer    = AckBuffer+2              ; начало буфера Nak
.equ  WorkBuffer   = NakBuffer+2              ; начало рабочего буфера
;------------------------------------------------------
;--- регистры -----------------------------------------
.def  InputBufferLength  = r11 ; количество байт во входном буфере
.def  OutBitstuffLength  = r12 ; количество битовых вставок в выходном буфере
.def  OutputBufferLength = r13 ; длина ответа, подготовленного в выходном буфере
.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  ByteCount = r21 ; счётчик переданных байт
.def  BitCount  = r22 ; счётчик переданных бит
.def  Action    = r23 ; что нам делать вне прерывания
.def  temp3     = r24 ; temporary register
 
.def  USBBufPtrXL = r26 ; XL - указатель на буфер USB (сюда читаем)
.def  USBBufPtrXH = r27 ; XH - указатель на буфер USB
.def  USBBufPtrYL = r26 ; YL - указатель на буфер USB (отсюда отдаём)
.def  USBBufPtrYH = r27 ; YH - указатель на буфер 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
;--- Конец основного цикла ---
;**********************************************************
;--- Внешнее прерывание 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   temp3,InputBuffer   ; читаем из буфера PID
  lds   temp1,InputBuffer+1 ; читаем из буфера адрес
  ;--- обе предыдущие команды не меняют флаги
  brne  TestDataPacket      ; если не ровно 3 байта (т.е. больше трёх, ибо
  ;--- меньше мы уже обработали) - это может быть только пакет данных, - прыгаем
  ;--- если ровно три байта - сначала проверяем не setup ли это?
TestSetupPacket:
  cpi   temp3,NRZI_rSETUPPID
  brne  TestOutPacket     ; если это не Setup, - проверяем, может это Out
  cp    temp1,MyInAddress ; если Setup, то нам ли он?
  brne  NoMyPacket        ; если не нам - прыгаем
  ldi   Stage,SetupStage  ; ставим признак начала Setup-транзакции
  rjmp  ExitFromIRQ       ; выходим из прерывания
  ;--- проверяем, - не Out ли пакет мы получили
TestOutPacket:
  cpi   temp3,NRZI_rOUTPID
  brne  TestInPacket       ; если это не Out, - проверяем, может это In
  cp    temp1,MyOutAddress ; если Out, то нам ли?
  brne  NoMyPacket         ; если не нам - прыгаем
  ldi   Stage,OutStage     ; ставим признак начала Out-транзакции
  rjmp  ExitFromIRQ        ; выходим из прерывания
TestInPacket:
  cpi   temp3,NRZI_rINPID
  brne  TestDataPacket     ; если это не IN - прыгаем
  cp    temp1,MyInAddress  ; если IN, то нам ли он?
  brne  NoMyPacket         ; если не нам - прыгаем
  ;****************************
  ;--- приняли пакет IN (хост запрашивает данные), нужно что-то отвечать	
AnswerToInRequest:
  cpi   Action,ReadySendAnswer   ; ответ готов?
  brne  WeAreNotReady            ; не готов
  rcall SendUSBAnswer            ; шлём ответ
  ldi   Stage,InStage            ; мы в транзакции IN
  ldi   Action,PrepareAnswerCicl ; готовим новую порцию ответа в свободное время
  rjmp  ExitFromIRQ              ; выходим из прерывания
  ;****************************
TestDataPacket:
  cpi   temp3,NRZI_rDATA0PID ; это DATA0?
  breq  Data0Packet          ; если да - прыгаем
  cpi   temp3,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          ; выходим из прерывания
  ;****************************
  ;--- приняли пакет данных транзакции Setup (команду управления)
RecieveSetupData:
  rcall SendACK             ; отвечаем ACK и завершаем транзакцию управления
  ldi   Action,SetupAction  ; отмечаем, что нам нужно обработать пакет данных транзакции Setup
  rcall FinishRecieving     ; готовим принятые данные к работе
  rjmp  ExitFromIRQ         ; выходим из прерывания
  ;****************************
  ;--- приняли пакет данных транзакции Out (эти пакеты мы не обрабатываем, но отвечаем Ack)
RecieveOutData:
  cpi   Action,SetupAction  ; мы всё ещё обрабатываем Setup?
  breq  WeAreNotReady       ; тогда мы не готовы к разговору
WeAreReady:
  rcall SendACK             ; отвечаем ACK
  clr   Action              ; указываем, что нам ничего не нужно делать
  rjmp  ExitFromIRQ         ; выходим из прерывания
WeAreNotReady:
  rcall SendNAK             ; отвечаем NAK
  rjmp  ExitFromIRQ         ; выходим из прерывания
  ;****************************
;**********************************************************
;--- дополнительные процедуры -----------------------------
;--- Сброс USB (обнуление адресов, сброс состояний ...)
USBReset:
  clr   Stage                ; начальная стадия
  ldi   temp0,NRZI_rADDRESS0 ; начальный адрес
  mov   MyInAddress,temp0    ; нулевой адрес
  mov   MyOutAddress,temp0   ; нулевой адрес
  clr   USBBufPtrXH          ; обнуляем на случай, если там мусор после старта
  clr   USBBufPtrYH          ; обнуляем на случай, если там мусор после старта
  clr   Action               ; после сброса ничего не нужно делать
 
  rcall InitAckNakBuffers    ; заполняем буферы с ответами Ack и Nak
  ret
;--- передача в линию подготовленного ответа заданной длины
  ;-- подготовка
SendUSBAnswer:
  mov   ByteCount,OutputBufferLength ; длина ответа без вставок - в счётчик
  ldi   USBBufPtrYL,OutputBuffer     ; указатель - на начало буфера
  ;-- сама передача
SendUSBBuffer:
  push  temp0     ; сохраняем кол-во бит в последнем байте
  push  ShiftReg  ; сохраняем значения бит в последнем байте
 
  mov   temp0,ByteCount      ; загружаем в счётчик количество байт без учёта вставок
  ldi   temp1,~USBPinMask    ; mask for xoring ( 0b00000011 ), чтобы не менять остальные биты
  ld    ShiftReg,Y+          ; загружаем первый байт в регистр для передачи и увеличиваем указатель
  ; настраиваем линии USB на выход
  cbi   OutputPort,USBDPlus  ; DPlus=0  : idle state of USB
  sbi   OutputPort,USBDMinus ; DMinus=1 : idle state of USB
  sbi   Direction,USBDPlus   ; DPlus = output
  sbi   Direction,USBDMinus  ; DMinus = output
 
  in    InputReg,OutputPort  ; читаем Idle state, а также состояние остальных линий порта
SendUSBBufferLoop:
  ldi   BitCount,7           ; счётчик бит                      ; 4-й такт
SendUSBBufferByteLoop:
  nop                        ; для тайминга                     ; 5-й такт
  rol   ShiftReg             ; вытесняем передаваемый бит в CF  ; 6-й такт
                             ; (у нас уже реверсирован порядок,
                             ; поэтому передаём старшим вперёд)
  brcs  NoXORSend            ; если передаём 1 - не меняем состояние линий   ; 7-й такт при CF=0, 7,8-й такты при CF=1
  eor   InputReg,temp1       ; иначе меняем состояние линий USB на противоположное ; 8-й такт
NoXORSend:
  out   OutputPort,InputReg  ; выставляем передаваемый бит на линии USB      ; 1-й такт
  dec   BitCount             ; уменьшаем счётчик бит                         ; 2-й такт
  brne  SendUSBBufferByteLoop                                                ; 3-й, если ноль; 3-й и 4-й, если не ноль
  sbrs  ShiftReg,7           ; если последний передаваемый бит в байте = 1 - пропускаем следующую команду   ; 4-й, если 0, 4,5-й, если не 0
  eor   InputReg,temp1       ; иначе меняем состояние линий на противоположное (остальные линии порта не меняются)   ; 5-й такт
NoXORSendLastBit:
  dec   temp0                ; уменьшаем счётчик байт                        ; 6-й такт
  ld    ShiftReg,Y+          ; загружаем следующий байт для передачи         ; 7,8-й такт
  out   OutputPort,InputReg  ; выставляем передаваемый бит на линии USB      ; 1-й такт
  brne  SendUSBBufferLoop    ; если передали не все байты - прыгаем          ; 2-й, если все, 2,3-й, если не все
  ; теперь нужно передать ещё OutBitstuffLength бит, то есть столько,
  ; сколько мы добавили к исходному буферу при NRZI-кодировании
  mov   BitCount,OutBitstuffLength ; счётчик бит = количество добавочных бит ; 3-й такт
  cpi   BitCount,0                 ; вставки не нужны ?                      ; 4-й такт
  breq  NoBitstuffNeeded           ; если битовых вставок нет                ; 5-й если есть вставки, 5,6-й если нет
SendUSBBufferBitstuffLoop:
  rol   ShiftReg             ; вытесняем передаваемый бит в CF               ; 6-й такт
  brcs  NoXORBitstuffSend    ; если передаём 1 - не меняем состояние линий   ; 7-й такт, если CF = 0 или 7-й и 8-й, если CF=1
  eor   InputReg,temp1       ; иначе меняем состояние линий на противоположное (остальные линии порта не меняются)   ; 8-й такт
NoXORBitstuffSend:
  out   OutputPort,InputReg       ; выставляем передаваемый бит на линии USB ; 1-й такт
  nop                             ; для соблюдения тайминга                  ; 2-й такт
  dec   BitCount                  ; уменьшаем счётчик бит                    ; 3-й такт
  brne  SendUSBBufferBitstuffLoop ; если передали не все биты                ; 4-й такт, если все, 4,5-й, если не все
  nop                             ; для соблюдения тайминга                  ; 5-й такт
  nop                             ; для соблюдения тайминга                  ; 6-й такт
NoBitstuffNeeded:
  nop                             ; для соблюдения тайминга                  ; 7-й такт
  ; обе линии в 0 (формируем сигнал EOP, длительностью 2 бита)
  cbr   InputReg,(1<<USBDMinus)|(1<<USBDPlus)                                ; 8-й такт
  out   OutputPort,InputReg       ; передаём его в шину                      ; 1-й такт
  ldi   BitCount,4                ; готовимся передавать EOP длит-ю 2-х бита ; 2-й такт
SendUSBWaitEOP:                   ; 1 проход    2проход    3 проход     4 проход
  dec   BitCount                  ; 3-й такт,     6-й        9-й          12-й
  brne  SendUSBWaitEOP            ; 4,5-й такт,   7,8       10,11         13-й
  nop                             ; 14-й
  nop                             ; 15-й
  ; переходим в состояние IDLE (D- > 2V, D+ < 2B)
  sbr   InputReg,(1<<USBDMinus)   ; готовимся   16-й такт
  out   OutputPort,InputReg       ; переходим
  ; переводим линии на вход
  cbi   Direction,USBDPlus        ; D+ - на вход
  cbi   Direction,USBDMinus       ; D- - на вход
  cbi   OutputPort,USBDMinus      ; D- в Z-состояние
 
  pop   ShiftReg
  pop   temp0
  ret
;--- инициализация буферов, содержащих ответы ACK и NAK
InitAckNakBuffers:
  ; заполняем Ack-буфер
  ldi   temp0,rSYNC
  sts   AckBuffer+0,temp0
  ldi   temp0,rACKPID
  sts   AckBuffer+1,temp0
  ; заполняем Nak-буфер
  ldi   temp0,rSYNC
  sts   NakBuffer+0,temp0
  ldi   temp0,rNAKPID
  sts   NakBuffer+1,temp0
  ret
;--- отправка ответа ACK
SendACK:
  ldi   ByteCount,2           ; передаём 2 байта
  ldi   USBBufPtrYL,AckBuffer ; указатель на начало буфера
  clr   OutBitstuffLength     ; нет битовых вставок
  rcall SendUSBBuffer         ; отправляем по USB
  ret
;--- копируем принятые данные в рабочий буфер, сохраняя последний принятый байт
FinishRecieving:
  cpi   temp0,7               ; счетчик настроен на 7 бит?
  breq  NoRemainingBits       ; нет незаписанных битов
  st    X+,ShiftReg           ; сохраняем последний байт и увеличиваем указатель
NoRemainingBits:
  mov   temp1,USBBufPtrXL       ; temp1 = текущий адрес для сохранения
  subi  temp1,InputBuffer       ; temp1 = количество байт во входном буфере
  mov   InputBufferLength,temp1 ; сохраняем это количество
  ldi   USBBufPtrYL,WorkBuffer  ; указатель на начало рабочего буфера
  ldi   USBBufPtrXL,InputBuffer ; указатель на начало входного буфера
CopyInputDataToWorkBuffer:
  ld    temp0,X+                  ; читаем байт из входного буфера
  st    Y+,temp0                  ; и копируем в рабочий
  dec   temp1
  brne  CopyInputDataToWorkBuffer ; копируем
  ret
;--- отправка ответа NAK
SendNAK:
  ldi   ByteCount,2           ; передаём 2 байта
  ldi   USBBufPtrYL,NakBuffer ; указатель на начало буфера
  clr   OutBitstuffLength     ; нет битовых вставок
  rcall SendUSBBuffer         ; отправляем по USB
  ret

[свернуть]

Всё, что нужно делать в обработчике прерывания мы разобрали, далее будем разбираться с тем, что нужно делать вне этого обработчика (подготовка ответов, обработка данных управляющих передач…).

  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. Продолжаем разбираться с принятыми пакетами.

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

  • Здравствуйте из 2021). Спасибо вам за огромный проделанный труд, очень хочется разобраться с USB на низком уровне. Не могли бы вы выложить готовый проект, или хотя бы указать что делать дальше, так как продолжения статьи я к сожалению не нашёл. Материал очень полезный для начинающих.

    • Продолжение статьи пока не существует и, честно говоря, я сомневаюсь в его необходимости. Когда всё это затевалось — на рынке не было дешёвых контроллеров с USB. Сейчас проще взять STM32 или его клон с готовыми библиотеками под full-speed usb, чем париться с 8-битными low-speed самоделками.
      Если только из чисто академического интереса. Ну… может быть.

  • Спасибо больше за ответ! Да, как раз чисто академический интерес. Использование готовых библиотек не даёт понимания что там происходит внутри. А так можно было бы сопоставить чтение стандарта с практикой)

  • Добрый день из 2023. Статья хорошая. Кроме того ассемблер позволяет понять все по кирпичам. Я пытаюсь из академических соображений создать девайс, который эмитирует работу адаптера peak-usb. Чтобы своим девайсом общаться с приложением can-view.
    Т.е. подключив мой девайс к PC система определит его как родной и загрузит необходимые драйвера для работы. В оригинальном девайсе usb шина обслуживается через контроллер, который в свою очередь общается с mcu через параллельный канал. Мой девайс обслуживается через max4321e который стоит на Ардуино мега АДК. (Использую что есть под рукой). Все библиотеки для работы этого «агрегата» рассчитаны для работы как хост и не как девайс (даже чтоб поднять ногу 1.5 ом нужно делать это на низком уровне.)
    Среда разработки Arduino ide1.8 (все для дилетантов как я, которые не имеют мозгов работать с серьезными программами).
    Max4321e позволяет обрабатывать общение по USB т.е. прием, передача и т.д. уже есть нужно только читать и писать в регистры по spi. Это позволяет считывать и записывать буферы данных/команд, флаги, указывать отправку подтверждений ack, nack и т.д.
    В принципе я потихоньку карабкаясь в познаниях но с напрягом для мозгов.
    На сегодня:
    После поднятия 1.5 на d+(режим h speed). Просто пока жду прихода флага командного пакета. Он приходит. 80 6 0 1 0 0 40 0.
    Т.е. меня просят передать дескриптор устройства (Я его уже подготовил прочитав оригинальный) но тут вопрос — а почему меня просят 64 байта????? Веди стандартный размер 18 байт и это все знают.???????
    В ответ Я отправляю свой дескриптор. И мне приходит извещение о присвоении адреса.
    Я говорю хороше ack.
    В след за этим мне прилетает запрос дескриптора но уже длинной 18 байт и в этот момент я вижу в регистре адресса реально адресс 2.
    Я отвечаю снова посылкой дескриптора.
    И тут адрес пропадает и приходит запрос дескриптора с длинной 64 байта.

    Мне кажется что я ошибаюсь с подтверждениями. Или долго отвечаю.

    Если можете помогите разобраться.

  • За сегодня Я смог доползти до дескрипторов строк.
    Сначала получился с кусками пакетов т.к. дескриптор устройства определил максимальный размер пакета 16 байт. А сам дескриптор устройства 18 так что надо посылать двумя кусками.
    Всего в устройстве 3 конфигурации в каждой по одному интерфейсу. А в них по четыре конечных точки.
    В общем после того как послал правильно пакет дескриптора устройства пришел запрос дескриптора конфигурации. Один раз с размером в 256 байт и

  • После первого послания конфигурации где указывается только 46 байт пришел соответствующий запрос по нужной длине.
    Повторил отправку конфигурации
    За этим пришел запрос на дескриптор строки. Отправил нулевой индекс с парой английского языка.
    Пришел запрос по этой паре но дальше пока заключило. Кроме того в ПК появился девайс юсб pecan но пока с ошибками.

  • У меня есть вопрос в общем: могу я изменить размер пакета в дескриптора устройства с 16 на 64. Ведь Макс 4321е поддерживает его и проблем с кусками нет. Драйвер и приложения будут с этим согласны?

    • Ну, насколько я понимаю, в дескрипторе устройства указывается максимальный размер пакета и для HS-устройств он как раз 64 байта. Так что проблем вроде бы быть не должно. В первом вашем вопросе, с дескрипторами устройства, хост тоже видимо не 64 байта запрашивал, а предупреждал, что он ждёт максимум 64 байта. Как, кстати, этот вопрос в итоге решился?

      • connecting *
        **************************
        FNADDR 0
        80 6 0 1 0 0 40 0 EP0BC 0
        FNADDR 0
        0 5 2 0 0 0 0 0 EP0BC 0
        FNADDR 2
        80 6 0 1 0 0 12 0 EP0BC 0
        FNADDR 2
        80 6 0 2 0 0 FF 0 EP0BC 18
        FNADDR 2
        80 6 0 2 0 0 2E 0 EP0BC 9
        FNADDR 2
        80 6 0 3 0 0 FF 0 EP0BC 9
        FNADDR 2
        80 6 3 3 9 4 FF 0 EP0BC 10
        FNADDR 2
        80 6 0 1 0 0 12 0 EP0BC 18

      • Я думаю что по логике мне нужно отвечать на запрос?

      • И ещё… Дескриптор строки должен начинаться с длины строки плюс два. Но каждый символ состоит из двух байт, даже если символы английские?
        Формат: длина . тип=3 . FF . FE . P . 0 . R. 0 . ……..
        Или
        Формат: длина . Тип=3 . P . 0 . R. 0 . …….
        Или
        Формат: длина . Тип=3 . P . R . O .

        Нулевой индекс записан так:
        4 . 3 . 9 . 4

  • Я добрался до дескриптора строки.
    Посылая нулевой индекс с кодом англ. Языка приходит следу.щий запрос для дескриптора строки продукта. После отправки создаётся впечатление что ответ не принят. Так как выходит опять запрос на дескриптор устройства

  • Вопрос: как отвечать на set confuguration?

  • По идее смотря на работу оригинала должен быть запрос строки с индексом 3

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