В предыдущей статье я рассказал, как запустил свой собственный велосипед 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 ясно видно, что большая половина данных о сигналах просто отсутствует (красным цветом):
Ну и ладно.
На что еще нужно обратить внимание в тестбенче? Посмотрите, что в самом тестбенче вставлен экземпляр модуля 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 устройство:
А вот здесь уже покдлюченное устройство отвечает контроллеру апкетом ACK (0x80 0xD2):
Возможно вы спросите, почему мой хост контроллер имеет отдельно сигналы dm/dm на вход и на выход, ведь в настоящей шине USB только два сигнала. Действительно, я разделил эти сигналы. Сделано это для упрощения симуляции. У меня из хост контроллера выходят o_dp, o_dm, o_oe и в контроллер входят сигналы i_dp, i_dm. В настоящем проекте сигналы объединены с использованием выходов с третьим высокоимпедансным состоянием. В реальном проекте квартуса в топ модуле можно увидеть вот такое соединение:
Когда контроллер передает данные, он выставляет сигнал o_oe и выходные буфера TRI включаются, передавая сигналы o_dp/o_dm на сигналы шины USB. Когда же контроллер все передал и должен слушать, то он выключает сигнал o_oe и отключает буфера TRI. После этого на сигналы i_dp и i_dm приходит то, что передает подключенное устройство.
Надеюсь это описание поможет понять, как работает мой простой USB хост контроллер, демонстрация которого была проведена в предыдущей статье..
Подробнее...