Интерфейс USB. Реализация, часть 2.



Для того чтобы продолжить реализацию устройства, нам придется немного вернуться на физический уровень и уровень передачи данных. Задача периферии МК — абстрагировать нас от этих уровней интерфейса, но кое-какие моменты нам придется обрабатывать самостоятельно.

Возвращение на физический уровень

Итак, рассмотрим более детально физический уровень передачи данных. Если в двух словах, то для передачи данных используется кодирование NRZI. Нас интересуют некоторые особые состояния шины, а именно, «Сброс» (D+ и D- low >= 2.5 мкс) и «Состояние неактивности» (Idle State, кодируются по разному, в зависимости от типа устройства).

Детально, о состояниях шины можно почитать здесь.

При обнаружении на линии состояния «Сброс» устройство должно перейти в исходное состояние (Default state). На практике, нам нужно присвоить устройству «нулевой» адрес и подготовить «нулевую контрольную точку» к приему и обработке стандартных USB запросов от хоста. Хост всегда выставляет «Сброс» на шине сразу после того, как определит подключение устройства. До «сброса» устройство не должно передавать какие-либо данные в т. ч. в ответ на запрос по адресу 0 (благодаря этому, как я понимаю, решается проблема коллизии, возникающей в случае одновременного подключения к хосту нескольких новых устройств).

Теперь разберемся с «состоянием неактивности». На физическом уровне хост раз в 1 мс посылает устройству определенный сигнал («начало пакета» либо «конец пакета» в зависимости от типа устройства) для сигнализации о активности шины. Если хост хочет «усыпить» определенное устройство, он перестает слать указанный сигнал и переводит шину в особое состояние «состояние неактивности». Если на шине нет активности в течение 3 мс, устройство должно «уснуть», снизив свое энергопотребление. «Просыпается» устройство при обнаружении активности на шине (любое изменение состояния шины отличное от «состояния неактивности»). Также, стандартом предусмотрен режим «удаленного пробуждения» (remote wakeup) – устройство само может уведомить хост о необходимости пробуждения.

Наш МК автоматически распознает перечисленные состояния шины, при обнаружении одного из них, выставляется соответствующий бит в регистре статуса и инициируется аппаратное прерывание.

Возвращение на уровень передачи данных

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

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

Данный пакет содержит следующие поля:
— Тип пакета (PID), 8 бит
— Адрес получателя (ADDR), 7 бит
— Номер конечной точки получателя (ENDP), 4 бита
— Контрольная сумма TOKEN пакета (CRC5), 5 бит

Тип пакета определяет в каком направлении будет происходить обмен данными – от хоста к устройству или от устройства к хосту.

Передача данных от хоста к устройству.





Хост отравляет TOKEN пакет типа OUT, следовательно, в дальнейшем ожидается передача данных от хоста к устройству.

После этого хост посылает пакеты данных. Размер каждого пакета зависит от максимального размера пакета для конкретной конечной точки (эта информация есть в дескрипторе устройства, об этом позже). Для контрольных точек типа Control, Bulk и Interrupt каждый пакет данных содержит контрольную суму. Также, для перечисленных типов контрольных точек, устройство должно подтверждать получение каждого пакета.
Для подтверждения получения пакета (управления обменом) используются следующие «служебные» (Handshake) пакеты:

— ACK – пакет получен успешно.
— NAK – устройство не может принять пакет (например, буфер-приемник переполнен). В этом случае хост должен отправить пакет повторно.
— STALL – пакет получен, но не может быть обработан. Свидетельствует о логической ошибке, например конечная точка типа Bulk получает пакет, предназначенный для управления устройством (для конечной точки типа Control).

Для контрольных точек типа Isochronous пакеты данных идут «сплошным потоком» без подтверждения приема.

Передача данных от устройства к хосту.


Хост отравляет TOKEN пакет типа IN, следовательно, в дальнейшем ожидается передача данных от устройства к хосту.
Далее устройство предает пакеты данных, а хост подтверждает их получение (по аналогии с предыдущем случае, только хост и устройство меняются «ролями»). Если в устройстве нет данных для передачи хосту – устройство в ответ на TOKEN IN пакет посылает NAK пакет, и хост переходит к обмену данными со следующим устройством.

Передача «запросов управления».



Как мы уже знаем, «нулевая контрольная точка» должна обрабатывать специальные запросы, предназначенные для управления (инициализации) устройством. Но, данная контрольная точка также может использоваться для передачи «пользовательских» данных. Чтобы отличать «запросы управления» от пользовательских данных, для передачи «запросов управления» используется специальный TOKEN пакет — TOKEN SETUP.

Сами запросы мы будем рассматривать в следующей статье. Пока нам нужно знать, что после SETUP транзакции хост может начать OUT транзакцию, для того, чтобы передать дополнительные параметры запроса, и/или IN транзакцию, для того чтобы прочитать ответ устройства на «запрос управления».

Еще раз подчеркну, что данное описание упрощено и не учитывает некоторых «тонкостей», с подробностями можно ознакомиться в разделе спецификации USB 2.0

На практике, всю работу с транзакциями возьмет на себя периферия МК.

Как уже указывалось в предыдущей статье, для передачи данных нам достаточно заполнить буфер передатчика и выставить флаг готовности данных к отправке. Когда МК получит очередной TOKEN IN пакет, он самостоятельно сформирует пакет данных, посчитает, при необходимости, контрольную сумму и передаст пакет хосту. Получив ACK пакет от хоста, периферия МК выставит бит в статусном регистре «передача завершена» и инициирует аппаратное прерывание. Нам, по большому счету, нужно только «подливать» данные в буфер отправки.

Аналогично с приемной частью – периферия МК, получив TOKEN OUT пакет, самостоятельно скопирует последующий пакет данных в буфер приемника (в один из банков) и, при необходимости, проверит контрольную суму. После этого периферия инициализирует аппаратное прерывание для того, чтобы наша программа могла забрать данные из буфера. Вычитав данные из буфера, мы устанавливаем флаг «прием завершен» в регистре управления и периферия шлет хосту ACK –пакет.

В данном случае, обмен данными на уровне программы чем-то напоминает обмен данными через USART :)

Возвращение к реализации.

Дальше много кода. Если Вас не интересует реализация стека под данный МК – можете смело «перематывать» в конец статьи :).

Мы готовы писать обработчик прерывания (на этом мы остановились в предыдущей статье).

«Усыплять» МК мы не будем — просто сделаем «заглушку» для событий связанных с управлением энергопотреблением.


//--- Обработка аппаратного прерывания
static void USB_IrqHandler(void) {
	DWORD isr = AT91C_BASE_UDP->UDP_ISR;

	//обнаружен "Сброс" на шине
	if (isr & AT91C_UDP_ENDBUSRES) {

		//Сбрасываем все КТ
		EndpointReset(AT91C_EP_CFG, EP_TYPE_CFG, EP_CFG_BUFFER_SIZE);
		EndpointReset(AT91C_EP_OUT, EP_TYPE_BULK, EP_BUFFER_SIZE);
		EndpointReset(AT91C_EP_IN, EP_TYPE_BULK, EP_BUFFER_SIZE);

		//Устанавливаем "нулевой" адрес
		AT91F_UDP_SetAddress(AT91C_BASE_UDP, 0);

		//Подготавливаем "нулевую конечную точку" для обработки запросов
		*EndpointCfg->CSR = (AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_CTRL);

		//Сбрасываем номер текущей конфигурации
		CurrentConfiguration = 0;

		//Разрешаем прерывания для "нулевой конечной точки"
		EndpointEnableInterrupt(EndpointCfg);

		//Сбрасываем флаг прерывания
		AT91C_BASE_UDP->UDP_ICR = AT91C_UDP_ENDBUSRES;
	}

	//Прерывание на конечной точке 0 - вызываем обработчик и сбрасываем флаг
	if (isr & AT91C_UDP_EPINT0) {
		EndpointHandler(&Endpoint[0]);
		AT91C_BASE_UDP->UDP_ICR = AT91C_UDP_EPINT0;
	}

	//Прерывание на конечной точке 1 - вызываем обработчик и сбрасываем флаг
	if (isr & AT91C_UDP_EPINT1) {
		EndpointHandler(&Endpoint[1]);
		AT91C_BASE_UDP->UDP_ICR = AT91C_UDP_EPINT1;
	}

	//Прерывание на конечной точке 2 - вызываем обработчик и сбрасываем флаг
	if (isr & AT91C_UDP_EPINT2) {
		EndpointHandler(&Endpoint[2]);
		AT91C_BASE_UDP->UDP_ICR = AT91C_UDP_EPINT2;
	}

	//Прерывание на конечной точке 3 - вызываем обработчик и сбрасываем флаг
	if (isr & AT91C_UDP_EPINT3) {
		EndpointHandler(&Endpoint[3]);
		AT91C_BASE_UDP->UDP_ICR = AT91C_UDP_EPINT3;
	}

	//Заглушка для событий Sleep, Wakeup и т. д. - просто сбрасываем флаг прерывания
	if(isr & (AT91C_UDP_RXSUSP | AT91C_UDP_RXRSM | AT91C_UDP_EXTRSM | AT91C_UDP_SOFINT | AT91C_UDP_WAKEUP)) {
		AT91C_BASE_UDP->UDP_ICR = AT91C_UDP_RXSUSP | AT91C_UDP_RXRSM | AT91C_UDP_EXTRSM | AT91C_UDP_SOFINT | AT91C_UDP_WAKEUP;
	}

}



Теперь реализация EndpointHandler() — обработчика событий для конечной точки:


static void EndpointHandler(UsbEndpoint * endpoint) {
	
	//Передача предыдущего блока данных завершена
	if(*endpoint->CSR & AT91C_UDP_TXCOMP) {
		//Если мы назодимся в состоянии отправки данных
		if(endpoint->Status == EP_STATUS_WTITE) {
			//Если отправлены все данные 
			if(endpoint->Size == endpoint->ByteReady) {
				if(endpoint->Size && endpoint->Size % endpoint->MaxSize == 0) EndpointSetFlag(endpoint, AT91C_UDP_TXPKTRDY);
				//Вызываем callback для асинхронного режима
				EndpointEndOfTransfer(endpoint, TRANSFER_STATUS_SUCCESS);
			} else {
				//Отправляем следующий пакет данных
				DWORD cpt = MIN(endpoint->Size - endpoint->ByteReady, endpoint->MaxSize);
				while (cpt--) *endpoint->FDR = endpoint->Buffer[endpoint->ByteReady++];
				EndpointSetFlag(endpoint, AT91C_UDP_TXPKTRDY);
			}
		} else {
			//Передача данных завершена, можно запретить прерывания (для "нулевой" КТ прерывания запрещать нельзя)
			if(endpoint->Type != EP_TYPE_CFG) EndpointDisableInterrupt(endpoint);
		}
		//Сбрасываем флаг "передача завершена"
		EndpointClearFlag(endpoint, AT91C_UDP_TXCOMP);
	}

	//Получены данные от хоста
	if(*endpoint->CSR & (AT91C_UDP_RX_DATA_BK0 | AT91C_UDP_RX_DATA_BK1)) {
		//Если мы находимся в состоянии чтения данных
		if(endpoint->Status == EP_STATUS_READ) {
			//Копируем данные из аппаратного буфера
			DWORD cpt = MIN((*endpoint->CSR) >> 16, endpoint->Size);
			for(DWORD i = 0; i < cpt; i++) endpoint->Buffer[endpoint->ByteReady++] = *endpoint->FDR;
			//Сбрасываем флаг "данные получены"
			EndpointClearRxFlag(endpoint);
			//Если это последний блок данных - запрещаем прерывания и вызываем callback для асинхронного режима
			if(cpt < endpoint->MaxSize || endpoint->Size == endpoint->ByteReady) {
				//для "нулевой" КТ прерывания запрещать нельзя
				if(endpoint->Type != EP_TYPE_CFG) EndpointDisableInterrupt(endpoint);
				EndpointEndOfTransfer(endpoint, TRANSFER_STATUS_SUCCESS);	
			}
		} else {
			//Если мы не готовы получать данные - запрещаем прерывания 
			EndpointClearRxFlag(endpoint);
			//для "нулевой" КТ прерывания запрещать нельзя
			if(endpoint->Type != EP_TYPE_CFG) EndpointDisableInterrupt(endpoint);
		}
	}

	//Получили запрос управления - вычитываем запрос и вызываем обработчик
	if(*endpoint->CSR & AT91C_UDP_RXSETUP) {
		UsbRequest Request;
		for(BYTE i = 0; i < 8; i++) ((BYTE *) &Request)[i] = *endpoint->FDR;
		if(Request.bmRequestType & 0x80) EndpointSetFlag(endpoint, AT91C_UDP_DIR);
		EndpointClearFlag(endpoint, AT91C_UDP_RXSETUP);
		USB_Enumerate(&Request);
	}


	//Получили STALL пакет - просто сбрасываем соответствующие флаги
	if(*endpoint->CSR & AT91C_UDP_STALLSENT) {
		EndpointClearFlag(endpoint, AT91C_UDP_STALLSENT);
		EndpointClearFlag(endpoint, AT91C_UDP_FORCESTALL);
	}
}


Мы еще немного вернемся к этому коду в следующей статье.

Ну вот, нам осталось только реализовать обработку стандартных запросов USB и создать дескрипторы устройства. Этим мы займемся в следующей статье.

У меня вопрос – нужно ли «побитно» разбирать дескрипторы устройства?

Просто есть много источников, в которых структура дескрипторов и значение каждого байта/бита подробно описана. Плюс, дескриптор нашего устройства будет мало чем отличатся от дескриптора любого другого устройства CDC класса.
  • +6
  • 11 ноября 2011, 12:48
  • e_mc2

Комментарии (21)

RSS свернуть / развернуть
Детально, о состояниях шины модно почитать здесь.

Yeah, о состояних шины читать модно! :DDD

А так — спасибо за статью. +1.
0
  • avatar
  • _YS_
  • 11 ноября 2011, 18:56
:) Исправил. Спасибо за поддержку.
0
Это Вам спасибо за статьи.

Будем надеяться, что когда-нибудь читать про состояния USB и впрямь станет модно. ;D
0
Спасибо за статью.
Просто, доступно и моск на месте…
0
Спасибо за статью.
Если не трудно — сделайте пожалуйста ссылки в статье на все предыдущие статьи — так будет удобнее читать, а то тэги со временем забьются другими статьями и для ориентации будут плохо подходить
0
  • avatar
  • lit
  • 12 ноября 2011, 08:51
не пойму чё там на арме usb реализовывать, оно там есть изначально, статья ниочём.
-2
У тебя в арме есть только физический уровень, средства приёма и отправки пакетов. Всё остальное приходится делать самому.
0
не пойму чё там на арме usb реализовывать, оно там есть изначально, статья ниочём.
Я сначало подумал типа проекта V-USB будет разбираться по полочкам, а тут фигня какая-то.

Это тоже самое что велосипедисту рассказывать как устроен станок для изготовления спиц.
-1
Статья должна была содержать материал под девизом " USB на 155ла3"
Вот это було бы интересно…
А тут…
Ну полная бредятина.
-3
Ты так говоришь, как будто USB состоит только из физического уровня.
Иди вон еще отпишись что вместо ENC28J60 тоже надо было на 155ЛА3 эзернет делать, а статья ни о чем.
0
Если в двух словах, то для передачи данных используется кодирование NRZI, логическая «1» передается как D+ high, D- low, логический «0» как D+ low, D- high.
Кстати, ошибаешься. Нолик передается как смена логического уровня на линиях, а единица — как неизменный уровень.
0
  • avatar
  • Vga
  • 13 ноября 2011, 20:11
Хм… Насколько я знаю в USB используется именно NRZI —
Non-Return-to-Zero инвертированный, единица передается как изменение состояний, а ноль — как сохранение состояний.
Или Вы имеете ввиду, что фразу
логическая «1» передается как D+ high, D- low, логический «0» как D+ low, D- high
лучше заменить на «логическая «1» соответствует D+ high, D- low, логический «0» соответствует D+ low, D- high»
0
Вот честно говоря не уверен, единица или ноль передается как смена состояний, но то, что описываешь ты — это дифференциальный NRZL — т.е. то кодирование, которое используется в RS232. В NRZI нельзя сказать «логическая «1» соответствует D+ high, D- low, логический «0» соответствует D+ low, D- high», т.к. логическая единица кодируется неизменным логическим уровнем (и без разницы, D+=L D-=H это или D+=H D-=L), а ноль — сменой уровня (т.е. D+=H D-=L при передаче ноля переключается в D+=L D-=H и наоборот). Такое кодирование позволяет избежать неизменного уровня сигнала при передаче последовательности нолей, что важно, т.к. именно по изменениям уровней на шине приемник синхронизируется с передатчиком.
0
Вот честно говоря не уверен, единица или ноль передается как смена состояний
Точнее уже уверен, проверил, но исправить начало сообщения забыл. 1 передается постоянным уровнем, 0 сменой.
0
ОК, согласен, в контексте дифф. кодирования фразы « 0 соответствует … 1 соответствует… » некорректны.
Спасибо, поправил статью.
0
Здесь даже не в дифференциальной передаче дело, тот же NRZL (0=+5V, 1=-5V) можно передавать и по диффлинии. Здесь фишка именно в том, что данные кодируются не уровнями, а их изменением.
+1
а как тогда передается последовательность единиц? И про нули не совсем понял. Тоесть для передачи 0 мы сменили лог уровни на обеих линиях, потом чтоб приемник не успел слопать единицу опять сменили лог уровни?
0
В Wiki все неплохо описано (правда на английском). Сотрите подраздел Non-Return-to-Zero Inverted (NRZI).
0
На длинной последовательности единиц уровень неизменен и приемник может рассинхронизироваться (это грабля всех асинхронных интерфейсов). Поэтому каждые 6 единиц в передаваемые данные вставляется лишний нолик, это называется bit stuffing. Приемник, соответсвенно, приняв 6 единиц следующий за ними нолик дропает. Такое кодирование позволяет сделать самосинхронизирующийся код с относительно небольшим оверхедом.
Тоесть для передачи 0 мы сменили лог уровни на обеих линиях, потом чтоб приемник не успел слопать единицу опять сменили лог уровни?
Именно. Каждый битовый интервал (в зависимости от выбранной скорости — LS, FS, HS это будет 1.5, 12 или 480 МГц) при передаче нолика состояние линий инвертируется. При передаче единицы — остается неизменным.
0
всё именно так.
0
Посоветуйте программу для перехвата данных USB
0
  • avatar
  • VIC
  • 03 декабря 2011, 22:26
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.