Работаем с USB стеком nRF24LU1+. Часть 2

в 9:36, , рубрики: nRF24L01+, NRF24LE1, nRF24LU1+, usb, программирование микроконтроллеров, Электроника для начинающих, метки:

Продолжение, первая часть здесь.
Упрощенная структура USB. Видно что есть всего два прерывания USBIRQ и USBWU
Работаем с USB стеком nRF24LU1+. Часть 2 - 1

Инициализация USB

Ничего сложного, описание регистров смотрите даташите. Разрешаем endpoint 0-2, включаем прерывания, указывает буферы для хранения данных. (Endpoint 2 и 3 разрешены для работы с double buffer при отправке данных к хосту)

Инициализация USB
void usb_init(void)
{
	// Setup state information
	usb_state = DEFAULT;
	usb_bm_state = 0;

	// Setconfig configuration information
	usb_current_config = 0;
	usb_current_alt_interface = 0;
	
	// Disconnect from USB-bus since we are in this routine from a power on and not a soft reset:

	usbcs |= 0x08;
	delay_ms(50);
	usbcs &= ~0x08;

	/*intterrupt enable uresie,suspie,sutokie,sudavie */
	usbien = 0x1d; 
	/*Endpoint 0 to 5 IN interrupt enables (in_ien)*/
	in_ien = 0x01;
	/*Endpoints 0 to 5 IN interrupt request register (in_irq)  - clear interrupt*/
	in_irq = 0x1f;
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)*/
	out_ien = 0x01;
	/*Endpoints 0 to 5 OUT interrupt request register (out_irq) - clear in interrupt*/
	out_irq = 0x1f;

	// Setup the USB RAM with some OK default values:
	bout1addr = MAX_PACKET_SIZE_EP0/2;
	bout2addr = MAX_PACKET_SIZE_EP0/2 + USB_EP1_SIZE/2;
	bout3addr = MAX_PACKET_SIZE_EP0/2 + 2*USB_EP1_SIZE/2;
	bout4addr = MAX_PACKET_SIZE_EP0/2 + 3*USB_EP1_SIZE/2;
	bout5addr = MAX_PACKET_SIZE_EP0/2 + 4*USB_EP1_SIZE/2;
	binstaddr = 0xc0;
	bin1addr = MAX_PACKET_SIZE_EP0/2;
	bin2addr = MAX_PACKET_SIZE_EP0/2 + USB_EP1_SIZE/2;
	bin3addr = MAX_PACKET_SIZE_EP0/2 + 2*USB_EP1_SIZE/2;
	bin4addr = MAX_PACKET_SIZE_EP0/2 + 3*USB_EP1_SIZE/2;
	bin5addr = MAX_PACKET_SIZE_EP0/2 + 4*USB_EP1_SIZE/2;

	// Set all endpoints to not valid (except EP0IN and EP0OUT)
	/*Endpoints 0 to 5 IN valid bits (Inbulkval)*/
	inbulkval = 0x01;
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	outbulkval = 0x01;
	/*Isochronous IN endpoint valid bits (inisoval)*/
	inisoval = 0x00;
	/*Isochronous OUT endpoint valid bits (outisoval)*/
	outisoval = 0x00;

	/* Switch ON Endpoint 1 */
	
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)* - out1ien */
	in_ien |= 0x02; 
	
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	inbulkval |= 0x02;
	
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)*/
	out_ien |= 0x02;
	
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	outbulkval |= 0x02;
	/* Endpoint 0 to 5 OUT byte count registers (outxbc) ?Maybe 0xff is register clear*/
	out1bc = 0xff;
	
	/* Switch ON Endpoint 2 */
	
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)* - out1ien */
	in_ien |= 0x04; 
	
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	inbulkval |= 0x04;
	
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)*/
	out_ien |= 0x04;
	
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	outbulkval |= 0x04;
	/* Endpoint 0 to 5 OUT byte count registers (outxbc) ?Maybe 0xff is register clear*/
	out2bc = 0xff;		
	
	/* Switch ON Endpoint 3 */
	
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)* - out1ien */
	in_ien |= 0x08; 
	
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	inbulkval |= 0x08;
	
	/*Endpoint 0 to 5 OUT interrupt enables (out_ien)*/
	out_ien |= 0x08;
	
	/*Endpoints 0 to 5 OUT valid bits (outbulkval)*/
	outbulkval |= 0x08;
	
	/* Endpoint 0 to 5 OUT byte count registers (outxbc) ?Maybe 0xff is register clear*/
	out3bc = 0xff;		
	
}

Далее разрешаем перекрывание USBIRQ (дальше нужно анализировать флаги) и включаем прерывания глобально:

USB = 1; // USBIRQ is mapped to IEN1.4
EA = 1; // enable global interrupt 

Собственно это и вся инициализация. При каждом прерывании, будет вызываться обработчик который выглядит примерно вот так:

/* USB interrupt request */
void usb_irq_handler(void) interrupt INTERRUPT_USB_INT  {
		usb_irq();
}

Обработчик прерывания USB

Это основа нашей системы. Все действие будет крутиться здесь. Какое именно событие произошло будем понимать после анализа регистра IVEC.

void usb_irq(void)
{
	uint8_t i;
	uint8_t temp_irq;

	if (ivec == INT_USBRESET)
	{
		/*The USB interrupt request register (usbirq)  - clear USB reset interrupt request*/
		usbirq = 0x10;
		usb_state = DEFAULT;
		usb_current_config = 0;
		usb_current_alt_interface = 0;
		usb_bm_state = 0;
	}
	else
	{
		switch(ivec)
		{
		case INT_SUDAV: /*Setup data valid interrupt*/
			usbirq = 0x01;
			isr_sudav();
			break;
		case INT_SOF: /*Start of frame interrupt (sofir)*/
			usbirq = 0x02;
			break;
		case INT_SUTOK: /*Setup token interrupt*/
			usbirq = 0x04;
			packetizer_data_ptr = NULL;
			packetizer_data_size = 0;
			packetizer_pkt_size = 0;
			break;
		case INT_SUSPEND: /*Suspend interrupt (suspir)*/
			usbirq = 0x08;
			break;
		case INT_EP0IN:
			in_irq = 0x01;
			packetizer_isr_ep0_in();
			break;
		case INT_EP0OUT:
			out_irq = 0x01;
			packetizer_data_size = 0;
			USB_EP0_HSNAK();
			break;
		case INT_EP1IN:
			in_irq = 0x02;
			int_ep1in_handler();
			break;
		case INT_EP1OUT:
			out_irq = 0x02;     
			out1bc = 0xff;
			break;
		case INT_EP2IN:
			in_irq = 0x04;
			break;
		default:
			break;
		}
	}
}

Энумерация

Пожалуй, воспользуюсь цитатой из замечательного руководства USB in a NutShell — путеводитель по стандарту USB

Энумерация – процесс определения факта, что устройство действительно подключено к шине USB и каких параметров это требует – потребляемая мощность,
количество и тип конечной точки (или точек), класс устройства и т. д. В процессе энумерации хост назначает устройству адрес и разрешает конфигурацию,
позволяющую устройству передавать данные по шине. [...]
Общий процесс энумерации под операционной системой Windows включает в себя следующие шаги:

1. Хост или хаб детектирует подключение нового устройства с помощью pull-up резисторов, которое устройство подключает к паре сигнальных проводов данных (D+ и D-). Хост делает задержку как минимум 100 мс, что позволяет вставить коннектор полностью и застабилизировать питание устройства.
2. Хост выдает на шину сброс, который выводит устройство в состояние по умолчанию. Устройство может теперь ответить на заданный по умолчанию нулевой адрес.
3. Хост MS Windows запрашивает первые 64 байта дескриптора устройства (Device Descriptor).
4. После приема первых 8 байт дескриптора устройства, хост немедленно выдает новый сброс шины.
5. Теперь хост выдает команду Set Address, чем переводит устройство в адресуемое состояние.
6. Хост запрашивает все 18 байт дескриптора устройства.
7. Затем он запрашивает 9 байт дескриптора конфигурации (Configuration Descriptor), чтобы определить полный её размер.
8. Хост запрашивает 255 байт дескриптора конфигурации.
9. Хост запрашивает все строковые дескрипторы (String Descriptors), если они имеются.

Для поддержи WinUSB требуется дополнительная процедура:

  • На шаге 9 Windows будет запрашивать нестандартный строковый дескриптор 0xEE. Именно правильный ответ на него начинает дальнейшую процедуры обмена данными для WinUSB.
  • Далее идет запрос Extended Compat ID OS Descriptor.
  • Последний — Extended Properties OS Descriptor. Именно здесь девайс сообщает свой GUID.

Рекомендую вот этот мануал для понимая процесса.

Для анализа и отладки минимальный набор это — UART на стороне контроллера и программный анализатор USB на хосте. Рекомендую бесплатный Microsoft Message Analyzer.

Итак, вся энумерация (и последующие vendor requset и прочие запросы, если они нужны) будут обрабатываться в функции isr_sudav() (Setup data valid interrupt).

isr_sudav()

static void isr_sudav()
{
	bmRequestType = setupbuf[0];
	
	/* Host-to-device standart request */
	if((bmRequestType & 0x60 ) == 0x00)
	{
		switch(setupbuf[1])
		{
		case USB_REQ_GET_DESCRIPTOR:
			usb_process_get_descriptor();
			break;

		case USB_REQ_GET_STATUS:
			usb_process_get_status();
			break;

		case USB_REQ_SET_ADDRESS:
			usb_state = ADDRESSED;
			usb_current_config = 0x00;
			break;

		case USB_REQ_GET_CONFIGURATION:
			switch(usb_state)
			{
			case ADDRESSED:
				in0buf[0] = 0x00;
				in0bc = 0x01;
				break;
			case CONFIGURED:
				in0buf[0] = usb_current_config;
				in0bc = 0x01;
				break;
			case ATTACHED:
			case POWERED:
			case SUSPENDED:
			case DEFAULT:
			default:
				USB_EP0_STALL();
				break;
			}
			break;

		case USB_REQ_SET_CONFIGURATION:
			switch(setupbuf[2])
			{
			case 0x00:
				usb_state = ADDRESSED;
				usb_current_config = 0x00;
				USB_EP0_HSNAK();
				break;
			case 0x01:
				usb_state = CONFIGURED;
				usb_bm_state |= USB_BM_STATE_CONFIGURED;
				usb_current_config = 0x01;
				USB_EP0_HSNAK();
				break;
			default:
				USB_EP0_STALL();
				break;
			}
			break;

		case USB_REQ_GET_INTERFACE: // GET_INTERFACE
			in0buf[0] = usb_current_alt_interface;
			in0bc = 0x01;
			break;

		case USB_REQ_SET_DESCRIPTOR:
		case USB_REQ_SET_INTERFACE: // SET_INTERFACE
		case USB_REQ_SYNCH_FRAME:   // SYNCH_FRAME
		default:
			USB_EP0_STALL();
			break;
		}
	} 
	// bmRequestType = 0 01 xxxxx : Data transfer direction: Host-to-device, Type: Class
	else if((bmRequestType & 0x60 ) == 0x20)  // Class request
	{
		if(setupbuf[6] != 0 && ((bmRequestType & 0x80) == 0x00))
		{
			// If there is a OUT-transaction associated with the Control-Transfer-Write we call the callback
			// when the OUT-transaction is finished. Note that this function do not handle several out transactions.
			out0bc = 0xff;
		}
		else
		{
			USB_EP0_HSNAK();
		}
	} 
	/* Extended Compat ID OS Descriptor setupbuf[1] (bRequest) is equal to MS_VendorCode (0xAA is current program)*/
	else if(bmRequestType == 0xC0 && setupbuf[1] == MS_VENDORCODE) 
	{
		packetizer_pkt_size = MAX_PACKET_SIZE_EP0;
		//xprintf("Extended Compat IDr");
		packetizer_data_ptr = g_usb_extended_compat_id;
		packetizer_data_size = MIN(setupbuf[6], packetizer_data_ptr[0]);
		packetizer_isr_ep0_in();	
	}
	/* Extended Properties OS Descriptor */
	else if(bmRequestType == 0xC1 && setupbuf[1] == MS_VENDORCODE)
	{
		packetizer_pkt_size = MAX_PACKET_SIZE_EP0;
		//xprintf("Extended Properties IDr");
		packetizer_data_ptr = g_usb_extended_proper_os;
		packetizer_data_size = MIN(setupbuf[6], packetizer_data_ptr[0]);
		packetizer_isr_ep0_in();				
	}
	else  // Unknown request type
	{
		USB_EP0_STALL();
	}
}

Предпоследние два ветвления в этой функции ответственны за запросы Extended Compat ID OS Descriptor и Extended Properties OS Descriptor.
На этом этапе энумерация закончена, можно приступить к приму/передаче данных.

Bulk Tranfer

In Tranfer

Работаем с USB стеком nRF24LU1+. Часть 2 - 2
Хост хочет принять данные. Отправляет In token. Если бит inxbsy (x — номер эндпойнта) установлен, то USB контроллер отправляет данные в хост. Что бы установить это самый бит, мы должны предварительно загрузить данные в буфер inxbuf[] и сказать сколько загрузили записью в регистр inxbc, т.е. данные надо предзагрузить до прерывания.

void int_ep1in_handler(void)
{
		uint8_t i;
		for(i=0;i<64;i++){
			in1buf[i]=i;
		}
		in1bc = 64;
}

После успешной отправки данных — получили ACK — происходит прерывание INT_EP1IN (функция usb_irq) и в нем можно загрузить следующий кусок данных.

Out Tranfer

Работаем с USB стеком nRF24LU1+. Часть 2 - 3
Хост хочет отправить данные. Отправляет OUT token. Следом за токеном идут данные. После того как данные приняты срабатывает прерывание INT_EP1OUT. Данные читаются из буфера outxbuf[]. Размер данных — outxbc. После того как обработали данные нужно в регистр outxbc записать любое значение, тем самым даем понять USB контроллеру что мы готовы принимать следующий кусок данных.

void int_ep1out_handler(void)
{
	do_anything(out1buf,in1bc);
	in1bc = 0xFF; //dummy write
}
Double buffering

Если стоит задача отправить или принять данные с максимальной скоростью, то необходимо пользоваться двойной буферизацией. Пока контроллер занят приемом или отправкой данных, его буферы (inxbuf и outxbuf) не доступны. Идея в том что, появляется дополнительный буфер, куда мы пишем во время занятости контроллера, а потом просто их переключаем.
За этот режим отвечает регистр usbpair. Например, записывая туда 0x01 мы объединяем 2 и 3 in endpoint. Буфер endpoint 3 и будет являться вторым буфером.
Работаем с USB стеком nRF24LU1+. Часть 2 - 4
Однако, документация на этот режим крайне непонятная и порой взаимоисключающая. Путем долгих экспериментов у меня получился вот такой код обслуживания буферов для передачи на хост:

while(1) {
	while(in2cs != 0);
	if(i%2){
		//first buffer
		for(i=0;i<64;i++)
			in2buf[i]=0xAA;
		in2bc = 64;
	} else {
		//second buffer
		for(i=0;i<64;i++)
			in3buf[i]=0xBB;
		in2bc = 64;				
	}
	i++;
}

Ждем пока IN 2 освободиться и по очереди льем данные то в in2buf то in3buf, то при этом все время обновляем in2bc (!!!). Код особо не тестировался, применяйте с осторожностью.

Программируем Host

Весь код писался на Visual Studio 2013. Сразу установите себе Windows Driver Kit (WDK). Он добавляет шаблоны проектов USB. Советую изучить пару мануалов от Microsoft, там все подробно расписано:

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

Замеры скорости

Вот каких результатов удалось добиться:

Передача данных на хост в single buffer 360 кб/с
Передача данных на хост в double buffer 510 кб/с
Прием данных с хоста single buffer 230 кб/c
Прием данных с хоста double buffer не тестировался

Заключение

Надеюсь статья принесет пользу начинающим и не только. Исходники не особо причесаны, сильно не пинайте, выкладываю на гитхаб, пользуйтесь на здоровье.
github.com/covsh/workingtitle/tree/master/nRF24LU1P

Автор: covsh

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js