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

Интерфейс USB. Часть 6. Программная реализация LS устройства USB. Физика и приём пакетов

Сегодня мы начинаем писать программу для микроконтроллера, реализующую низкоскоростную версию интерфейса USB. И писать её мы начнём с самого низкого уровня — с физики и приёма пакетов.

Ранее (в предыдущей части, когда схему делали) мы решили:

  • что информационные линии у нас будут заводиться на ноги PB0 (D+) и PB1 (D-),

  • что определять начало передачи мы будем по переднему фронту на линии D+ (в состоянии IDLE у нас на D+ ноль, а на D- — единица), которую для максимально быстрой реакции мы дополнительно завели на ногу INT0 (для которой возможно установить прерывание по переднему фронту),
  • что длительность одного информационного бита у нас будет составлять 8 тактов контроллера (кварц на 12 МГц).
  • Кроме того, мы решили, что наше устройство будет управлять тремя светодиодами, которые подключены к ногам PB2, PB3, PB4.

Теперь давайте думать, как мы будем определять значения отдельных битов и принимать пакеты. Для этого давайте вспомним, что мы вообще об этом знаем.

Знаем мы следующее:

  • все пакеты начинаются с передачи синхропоследовательности SYNC=0b10000000,
  • все данные, кроме CRC передаются младшим битом вперёд (то есть SYNC передаётся в виде: 0b00000001),
  • данные передаются в NRZI-кодированном виде, то есть при передачи нуля состояние шины (уровни на линиях D+/D-) меняется на противоположное, а при передаче единицы — остаётся неизменным.

Исходя из этих знаний, а также начального уровня на линии D+ в состоянии IDLE (ноль), можно сделать вывод о том, какие данные на линии D+ мы должны видеть в начале каждого пакета. Закодировав SYNC в NRZI относительно D+, получаем: 0b10101011.

Вспомнив, что по спецификации некоторая часть первоначальных бит синхропоследовательности может потеряться (но мы не знаем сколько именно бит потеряно), остаётся только один способ определения начала пакета, — первые две подряд единицы на линии D+ после выхода из состояния IDLE.

Таким образом, задача определения начала пакета сводится к обнаружению в прерывании INT0 первых двух последовательно передаваемых единиц на линии D+. Следующий за этими двумя единицами бит — это уже первый информационный бит пакета. Плюс нужно учесть, что до того, как мы встретим две подряд единицы, — не должно встречаться два подряд нуля.

Причём, для этого первого информационного бита предыдущее состояние линии D+ будет 1, то есть для него ноль на линии D+ будет соответствовать передаче нулевого бита (состояние линии меняется на противоположное), а единица на D+ — передаче единичного бита (состояние линии не меняется). То есть, мы можем просто записывать через битовые интервалы состояние линии D+, а потом уже декодировать эту последовательность, исходя из правил NRZI (все данные у нас для этого есть, — набор состояний линии и знание, что первое состояние соответствует значению передаваемого бита).

Осталось определить конец пакета. Определить его мы можем по сигналу SE0 (в течении двух битовых интервалов на обоих информационных линиях ноль).

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

Далее, обдумаем сразу ещё один момент, — нужно ли принимать весь пакет целиком или, увидев неправильный PID или адрес, остаток пакета можно игнорировать? Рассуждаем так, — если мы выйдем из прерывания не дождавшись конца пакета, то мы сразу же опять попадём в прерывание, поскольку положительные фронты будут случаться на D+ всё то время, пока идут какие-то данные. Но теперь уже мы попадём не в начало пакета, а в середину. И не факт, что из данных, которые мы получим из середины, не сложится какой-то пакет, который будет похож на адресованный нам и даже такой, у которого окажется правильный CRC. Да и вообще, зачем рисковать, если прерывание в любом случае будет срабатывать и нас всё равно будут отвлекать. Поэтому, придётся нам в любом случае принимать все пакеты целиком, независимо от того, можем ли мы сразу опознать, что пакет предназначен не нам или нет, а потом уже решать, что с этим делать.

Следом сразу всплывает ещё одна проблема. «Сырые» данные мы будем складывать в буфер в оперативе, но поскольку контроллер у нас мелкий, то и оператива не резиновая. Тут можно схитрить. Давайте договоримся, что наши полезные данные могут быть размером максимум 8 байт (они по-моему на LS и так могут быть максимум 8 байт, не помню, надо спеки смотреть). Это значит, что, без учёта поля Sync, максимальный размер предназначенного для нас пакета может быть 1(PID+CHECK)+8+2(CRC16) байт. Сколько в таком сообщении может быть битовых вставок? С учётом того, что вставка делается каждые 6 подряд единичных бит, получаем максимум 11*8/6=14 вставок. Это при условии, что абсолютно все биты будут единицами (реально нужно было бы хотя бы PID+СHECK отсюда исключить). То есть нам нужен буфер для входящих данных максимум на 13 байт (маркер-пакеты и пакеты подтверждения в любом случае короче). А если входящих данных будет больше? Не будем записывать лишнее, да и дело с концом. Это точно не нам, так чего напрягаться, — будем просто дожидаться конца пакета.

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

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

.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
;---------------------------------
;--- распределение памяти --------
.equ  MaxUSBBytes = 13         ; максимальный размер буфера для "сырых" данных
.equ  StackTop    = RAMEND     ; вершина стека
.equ  InputBuffer = RAMEND-127 ; начало буфера "сырых" входных данных USB (0)
;------------------------------------------------------
.def  temp0       = r16 ; temporary register
.def  temp1       = r17 ; temporary register
.def  InputReg    = r18 ; входной регистр (сюда читаем значения линий)
.def  ShiftReg    = r19 ; сдвиговый регистр (сюда копим принимаемые биты)
.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&lt;&lt;LED0)+(1&lt;&lt;LED1)+(1&lt;&lt;LED2)
   out   Direction,temp0  ; линии светодиодов - выходы
   ldi   temp0,0b11111011
   out   PORTD,temp0      ; включаем подтяжки на PORTD, кроме PD2(INT0)
 
   rcall USBReset         ; обнуление адресов, сброс состояний и т.д.
 
   ldi   temp0,0x0F       ; INT0 - прерывание по переднему фронту
   out   MCUCR,temp0
   ldi   temp0,1&lt;&lt;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:
   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:
   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 информационные биты ---
USBLoop27:
   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  USBLoop27           ; если нет - читаем (7,8-й такты)
   nop                       ; 8-й такт
   ;--- читаем последний бит байта ---
USBLoop8: 
   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-й такт
   ;--- читаем первый бит очередного байта ---
USBLoop1:
   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-й такт, если не ноль
   nop                       ; 4-й такт
   rjmp  BufferOverrange     ; 5,6-й такт
   ;--- закончили принимать пакет и начинаем его анализировать ---
   ;--- попадаем сюда на 5-м такте первого или второго битового ---
   ;--- интервала SE0, теперь нужно как можно быстрее понять чего --- 
   ;--- от нас хотят и ответить (или не ответить, если хотят не от нас) ---
EndPacket:
   cpi   USBBufPtrXL,InputBuffer+3 ; приняли хотя бы 3 байта?
   brcs  ExitFromIRQ               ; если нет - просто выходим
   ;*********************************************
   ;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   ;--- а вот если да - начинаем разбирать пакет
   reti

[свернуть]

Ну вот пока и всё. Приведённый выше код позволяет нам считывать с шины USB все low speed пакеты (целиком или первые 13 байт), сохранять принятые данные в SRAM по адресам 0x60-0x6C, а так же определять концы принимаемых пакетов. В следующей части нужно будет решить ещё больше интересных вопросов: как быстро отправить хосту подтверждение, как из «сырых» данных восстановить передаваемую информацию и что с этой информацией делать дальше.

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

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