usb host block schema

В предыдущей статье я рассказал, как запустил свой собственный велосипед USB хост контроллер и как он работает в железе в плате Марсоход3. Там я в основном рассказывал, как взаимодействует управляющая программа с контроллером. Но вот как работает мой контроллер? Чтобы разобраться в этом нужно провести Verilog симуляцию.

Внутреннее устройство моего контроллера схематично показано рисунке выше.

Внутри контроллера есть:

  • входное FIFO, модуль generic_fifo_dc_gray fifo_in, сюда складываются все входящие команды и данные;
  • выходное FIFO, модуль generic_fifo_dc_gray fifo_out, отсюда внешние устройства будут забирать результат исполнения команды;
  • временное FIFO, generic_fifo_dc_gray fifo_out_tmp, где временно хранятся принятые из USB шины байты;
  • модуль USB передатчика ls_usb_send ls_usb_send_;
  • модуль USB приемника ls_usb_recv ls_usb_recv_;
  • машина состояний по регистру состояний state.

Рисунок довольно схематичный, но в целом отражает происходящее в хост контроллере.
Далее расскажу подробнее.

Входное и выходное FIFO имеют раздельные частоты iclk и cclk для записи и чтения. iclk - это частота интерфейса, для подключения контроллера к внешним управляющим модулям. Она должна быть достаточно высокой, чтобы гарантированно можно было заполнить входное FIFO данными на передачу быстрее, чем они будут передаваться по USB. cclk - это внутренняя частота контроллера, должна быть 12МГц, именно столько нужно для USB1.1 Вообще-то, Low Speed передает с частотой 1,5МГц, но на прием лучше иметь частоту выше, чтобы можно было синхронизироваться по сигналу выделяя фронты dp/dm. Поэтому я использую частоту в 8 раз выше, чем 1.5МГц даже для Low Speed. Из блок схемы видно, что на частоте cclk работает вся внутренняя часть контроллера. Частота же iclk используется только на внешнем интерфейсе к входному и выходному fifo.

Возможно вы спросите: "Зачем нужно временное фифо для хранения принятых данных"? Я сам долго думал, как бы получше сделать и ничего не придумал лучше. Дело в том, что пока USB пакет не будет принят полностью из подключенного устройства неизвестна длина пакета. Как в этом случае сделать протокол обмена между USB контроллером и внешним процессором? Поскольку протокол обмена через FIFO задумывался, как потоковый протокол, то в нем нужно иметь либо однобайтовые команды, либо команды с указанием длины последующих данных. В моем протоколе всего шесть команд и четыре из них однобайтовые:

  • CMD_GET_LINES - возвращает состояние линий DP/DM, служит для обнаружения момента подключения USB устройства к хосту;
  • CMD_BUS_CTRL - позволяет устанавливать состояние линий Dp/DM например в zero, для передачи сигнала Reset;
  • CMD_WAIT_EOF - дает хост контроллеру указание должаться начала 1 миллисикундного кадра, прежде чем выполнять следующие команды;
  • CMD_AUTO_ACK - по этой команде контроллер может автоматически послать пакет ACK, если только что были приняты полезные данные (не NAK).

Оставшиеся две команды могут быть многобайтовыми и для этого могут содержать в своем составе длину последующих данных:

  • CMD_SEND_PKT - в старшей тетраде команды будет длина USB пакета в байтах, данные следуют сразу после этой команды и которые будут отправляться в виде USB пакета в шину USB;
  • CMD_READ_PKT - эта команда включает режим приема пакетов из USB шины. Именно поcле этой команды активизируется модуль ls_usb_recv_ и начинает принимать байты и записывать их во временное фифо. Как только прием окончен становится известна длина принятого пакета usb_rbyte_cnt и теперь контроллер возвращает ту же самую команду CMD_READ_PKT и в старшей тетраде указывает длину пакета и тут же переписывает из временного фифо принятые данные в выходное фифо. Это и есть ответ на вопрос: "Зачем нужно временное фифо для хранения принятых данных" - хотелось сделать протокол обмена единообразным и однозначным. Для этого перед данными должна следовать код команды с указанием количества последующих данных.

На рисунке показан мультиплексор, который позволяет в выходное фифо записать либо байт-команду состояние линий USB { 2'b00, i_dp, i_dm, CMD_GET_LINES } по команде CMD_GET_LINES, либо записать байт команду-длину принятых данных { usb_rbyte_cnt, CMD_READ_PKT }, либо переписать принятые данные из временного фифо tmp_fifo_data.

На рисунке выше также вы можете видеть мультиплексор перед передающим модулем ls_usb_send_. Он переключает передаваемые данные из входного фифо или константные байты 8'h80 и 8'hD2. Константы 8'h80 и 8'hD2 используются, когда нужно автоматически передать ACK после приема актуальных данных.

Естественно, всей этой сложной логикой управляет машина состояний. В моем Verilog модуле usbhost.v целых 23 состояния. Напомню, что код проекта есть на github: https://github.com/marsohod4you/UsbHost. Там есть весь проект для среды САПР Quartus Prime и там есть и usbhost.v и текст тестбенча tb.v.

Надеюсь, что по наименованию состояний должно быть понятно их предназначение. Например, STATE_IDLE - исходное состояние. После выполнения каждой из команд машина возвращается в это исходное состояние. Состояния STATE_READ_CMD и STATE_GOT_CMD выбирают команду из входного фифо. А вот, к примеру, отправка USB пакета у меня может проходить аж несколько состояний: STATE_FETCH_SEND_BYTE, STATE_CATCH_SEND_BYTE, STATE_FETCH_SEND_BYTE1, STATE_CATCH_SEND_BYTE1, STATE_SENDING.. Если контроллер получил команду CMD_READ_PKT, то он пройдет последовательно состояния STATE_READ_DATA, STATE_READ_DATA_ALL, STATE_COPY_TMP_PKT, STATE_COPY_TMP_PKT2..

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

`timescale 1ns / 1ns
module tb;

//usb clock ~12MHz
reg clock12 = 1'b0;
always #42
	clock12 = ~clock12;

//system clock
reg clock = 1'b0;
always #5
	clock = ~clock;

reg reset=1'b0;
wire idp;
wire idm;
reg [7:0]cmd=8'h00;
reg cmd_wr=1'b0;
wire odp, odm, ooe;
wire [7:0]rdata;
reg rdata_rd=1'b0;
wire w_rd;

usbhost usbhost_(
	.rst( reset ),
	.iclk( clock ),	//interface clk
	.wdata( cmd ),	//interface commands and data
	.wr( cmd_wr ),	//write
	.wr_level(),
	.rdata( rdata ),//result data
	.rd( w_rd ),//read
	.rdata_ready( w_rd ),
	
	.cclk( clock12 ), //core clock, 12MHz
	
	.i_dp( idp ),
	.i_dm( idm ),
	.o_dp( odp ),
	.o_dm( odm ),
	.o_oe( ooe )
);

reg [31:0] cmd_time [0:255];
reg [ 7:0] cmd_val  [0:255];
reg [7:0]idx;
initial
begin
idx=0;
cmd_time[idx]=100; cmd_val[idx]=8'h01; idx=idx+1; //get lines
cmd_time[idx]=200; cmd_val[idx]=8'h32; idx=idx+1; //reset
cmd_time[idx]=199130; cmd_val[idx]=8'h22; idx=idx+1; //enable
cmd_time[idx]=203200; cmd_val[idx]=8'h03; idx=idx+1; //wait eof
cmd_time[idx]=203201; cmd_val[idx]=8'h03; idx=idx+1; //wait eof
cmd_time[idx]=203202; cmd_val[idx]=8'h44; idx=idx+1; //send pkt
cmd_time[idx]=203203; cmd_val[idx]=8'h80; idx=idx+1;
cmd_time[idx]=203204; cmd_val[idx]=8'h2d; idx=idx+1;
cmd_time[idx]=203205; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203206; cmd_val[idx]=8'h10; idx=idx+1;
cmd_time[idx]=203207; cmd_val[idx]=8'hC4; idx=idx+1; //send pkt
cmd_time[idx]=203208; cmd_val[idx]=8'h80; idx=idx+1;
cmd_time[idx]=203209; cmd_val[idx]=8'hC3; idx=idx+1;
cmd_time[idx]=203210; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203211; cmd_val[idx]=8'h05; idx=idx+1;
cmd_time[idx]=203212; cmd_val[idx]=8'h01; idx=idx+1;
cmd_time[idx]=203213; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203214; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203215; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203216; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203217; cmd_val[idx]=8'h00; idx=idx+1;
cmd_time[idx]=203218; cmd_val[idx]=8'hEB; idx=idx+1;
cmd_time[idx]=203219; cmd_val[idx]=8'h25; idx=idx+1;
cmd_time[idx]=203222; cmd_val[idx]=8'h05; idx=idx+1;
idx=0;
end

reg [32:0]counter=0;
always @(posedge clock)
begin
	counter<=counter+1;
	if( counter==cmd_time[idx] )
	begin
		cmd <= cmd_val[idx];
		cmd_wr<=1'b1;
		idx<=idx+1;
	end
	else
	begin
		cmd <= 0;
		cmd_wr<=1'b0;
	end
end

always @( posedge usbhost_.bit_impulse )
	if( usbhost_.bit_time==300)
		$dumpoff;
	else
	if( usbhost_.bit_time==1450)
		$dumpon;

wire [7:0]w_send_byte;
wire w_start_pkt;
wire w_last;
wire w_show_next;
wire w_pkt_end;

ls_usb_send ls_usb_send_(
	.reset( reset ),
	.clk( clock12 ),
	.bit_impulse( usbhost_.bit_impulse ),
	.eof( usbhost_.eof ),
	.sbyte( w_send_byte ),  		//byte for send
	.start_pkt( w_start_pkt ),		//start sending packet on that signal
	.last_pkt_byte( w_last ),		//mean send EOP at the end
	.cmd_reset( 1'b0 ),
	.cmd_enable( 1'b1 ),
	.dp( idp ),
	.dm( idm ),
	.bus_enable( ),
	.show_next( w_show_next ), 	//request for next sending byte in packet
	.pkt_end( w_pkt_end )		//mean that packet was sent
	);

reg [7:0]send_state = 0;
always @(posedge clock12)
	if( send_state==0 && usbhost_.enable_recv )
		send_state<=1;
	else
	if( send_state==1)
		send_state<=2;
	else
	if( send_state==2 && w_show_next )
		send_state<=3;

assign w_start_pkt = (send_state==1);
assign w_send_byte = (send_state==1) ? 8'h80 :
	(send_state==2) ? 8'hD2 : 0;
assign w_last = (send_state==2);
	
initial
begin
	$dumpfile("out.vcd");
	$dumpvars(0,tb);
	
	reset = 1'b1;
	#500;
	reset = 1'b0;
 
	#6000000;
	$finish();
end
endmodule

Одна из трудностей, которая может встретиться на этом пути исследования USB сигналов - необходимо проследить быстро протекающие процессы на большом промежутке времени. Что это значит? Смотрите, к примеру, я хочу смоделировать рассмотреть момент подключения USB устройства и несколько USB транзакций. USB работает кадрами длительностью 1 миллисекунда. Мне нужно увидеть хотя бы 5-10 миллисекунд, но процессы там происходят с частотой 12МГц. Если записывать все сигналы в файл временных диаграмм VCD, то объем файла получится огромным (гигабайты) и его будет трудно открывать и смотреть программой gtkwave. Можно пытаться либо записывать только некоторые сигналы, либо ограничивать запись в каких-то временных окошках, например, только в начале каждого UBS кадра. Я выбрал второй путь.

Как обычно, в начале тестбенча я пишу

initial
begin
	$dumpfile("out.vcd");
	$dumpvars(0,tb);
....

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

always @( posedge usbhost_.bit_impulse )
	if( usbhost_.bit_time==300)
		$dumpoff;
	else
	if( usbhost_.bit_time==1450)
		$dumpon;

Это простое действие сильно уменьшает размер выходного VCD файла. Правда теперь просматривая VCD в программе gtkwave ясно видно, что большая половина данных о сигналах просто отсутствует (красным цветом):

red unknowns

Ну и ладно.

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

Так же, в тестбенче есть своя простая машина состояний. Вот, например, как тестбенч начинает передавать пакет ACK, когда USB хост контроллер переключается в режим приема:

reg [7:0]send_state = 0;
always @(posedge clock12)
	if( send_state==0 && usbhost_.enable_recv )
		send_state<=1;
	else
	if( send_state==1)
		send_state<=2;
	else
	if( send_state==2 && w_show_next )
		send_state<=3;

assign w_start_pkt = (send_state==1);
assign w_send_byte = (send_state==1) ? 8'h80 :
	(send_state==2) ? 8'hD2 : 0;
assign w_last = (send_state==2);

Вообще-то для тестбенча не очень правильно опираться на внутренние сигналы тестируемого модуля типа usbhost_.enable_recv. Но иногда так проще моделировать.

Другая машина состояний иммитируют передачу команд и данных в тестируемый хост контроллер через сигналы cmd_val и cmd_wr.

После того, как тестбенч написан, можно проводить симуляцию. Я использую Icarus Verilog.
Запускаю компилятор Icarus Verilog:

>iverilog -o qqq -Igeneric_fifo tb.v usbhost.v ls_usb_recv.v ls_usb_send.v generic_fifo/generic_fifo_dc_gray.v generic_fifo/generic_dpram.v

Параметры в командной строке:

  • -o qqq задает выходной файл компилятора;
  • -Igeneric_fifo задает дополнительный путь для поиска файлов;
  • tb.v usbhost.v ls_usb_recv.v ls_usb_send.v generic_fifo/generic_fifo_dc_gray.v generic_fifo/generic_dpram.v - это список всех Verilog файлов, необходимых для симуляции.

После компиляции можно запустить симулятор Icarus Verilog:
>vvp qqq

Получится выходной файл out.vcd, который можно открыть и посмотреть с помощью программы gtkwave. На этом рисунке виден фрагмент временных диаграмм, где происходит обмен данными по шине USB:

USB transfer

Вот здесь подробнее показана передача первого пакета из хост контроллера в USB устройство:


USB pkt802d0010

А вот здесь уже покдлюченное устройство отвечает контроллеру апкетом ACK (0x80 0xD2):

USB pkt80D2

Возможно вы спросите, почему мой хост контроллер имеет отдельно сигналы dm/dm на вход и на выход, ведь в настоящей шине USB только два сигнала. Действительно, я разделил эти сигналы. Сделано это для упрощения симуляции. У меня из хост контроллера выходят o_dp, o_dm, o_oe и в контроллер входят сигналы i_dp, i_dm. В настоящем проекте сигналы объединены с использованием выходов с третьим высокоимпедансным состоянием. В реальном проекте квартуса в топ модуле можно увидеть вот такое соединение:

top module

Когда контроллер передает данные, он выставляет сигнал o_oe и выходные буфера TRI включаются, передавая сигналы o_dp/o_dm на сигналы шины USB. Когда же контроллер все передал и должен слушать, то он выключает сигнал o_oe и отключает буфера TRI. После этого на сигналы i_dp и i_dm приходит то, что передает подключенное устройство.

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


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