Итак, в прошлой части мы написали код, обеспечивающий приём пакета 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. Аналогично можно вычислить как будут выглядеть первые байты для других пакетов.
Едем дальше. Поскольку в прерывании мы принимаем по одному пакету, а транзакция состоит из нескольких пакетов, то нам нужно будет их «склеивать», а для этого придётся выделить регистр, в котором будет храниться информация о текущей стадии обмена данными. Зная, какую часть транзакции мы только что приняли, мы будем понимать, какую часть мы теперь ожидаем.
В общем-то теперь можно начинать писать код, однако, поскольку в этой части алгоритм у нас более сложный, чем в предыдущей, то давайте всё-таки прежде нарисуем сам алгоритм:
Теперь, с учётом этого алгоритма, наш код будет выглядеть так:
.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. Основы.
- Часть 2. Как происходит передача данных по шине.
- Часть 3. Что должно уметь любое USB-устройство.
- Часть 4. Дескрипторы и классы.
- Часть 5. Программная реализация LS устройства USB. Схема.
- Часть 6. Программная реализация LS устройства USB. Физика и приём пакетов.
- Часть 7. Программная реализация LS устройства USB. Разбираем пакеты по типам.
- Часть 8. Программная реализация LS устройства USB. Передача по USB произвольного буфера и пакетов подтверждения.
- Часть 9. Программная реализация LS устройства USB. Продолжаем разбираться с принятыми пакетами.
Исправил ошибку.
Было: .equ NRZI_rDATA1PID = 0b11001010
Стало: .equ NRZI_rDATA1PID = 0b11001001