Тестбенч приемника USB

Очень часто бывает довольно трудно разобраться как работает тот или иной модуль. Можно часами рассматривать принципиальную схему устройства или его описание на языке Verilog и ничего не понимать в ней. Пожалуй самый правильный способ разобраться - симуляция проекта или отдельных его модулей. Я уже как-то писал на эту тему здесь и здесь.

Сейчас попробуем еще что нибудь посимулировать.
Я предлагаю взять для наших экспериментов модуль приемника USB из последнего моего USB проекта и просимулировать его. USB - это довольно важная тема для меня.

Для того, что бы просимулировать наш модуль USB приемника нужно написать еще одну программу - тестбенч (testbench). Написание тестбенча ни чем не легче описания самого устройства - это в общем такое же программирование. При написании тестбенча программист так же может совершать ошибки. К сожалению цена этих ошибок может быть высока. Если симуляция произведена не очень тщательно, не правильно, то устройство в реальной жизни может не работать или работать не правильно. Хорошо если проект предназначен для программируемых микросхем ПЛИС (CPLD). Тогда не работающее устройство можно просто перешить. А вот если проект - это заказная микросхема типа ASIC (Application Specific Integrated Circuit), то после того, как микросхема выпущена в кремнии в ней изменить уже ничего нельзя. Цена ошибки - миллион долларов Wink Ну мы таких микросхем не делаем, поэтому нам бояться нечего. Мы программируем простую плату Марсоход.

Еще раз о том, что такое тестбенч.

Что такое тестбенч (testbench)?

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

Еще одно важное замечание. Модуль приемника USB у меня написан на языке Verilog. Тестбенч я тоже пишу на языке Verilog, но далеко не все языковые конструкции в моем тестбенче являются "синтезируемыми". Да это и не нужно сейчас. Описание на языке, которое потом может быть преобразовано в "схему" устройства, логические триггера, логику, связи между ними - это синтезируемое описание. Такой у меня модуль USB приемника. А вот сам тестбенч - это просто некое описание системы, каких-то сигналов. Из этого описания нельзя получить "схему" или прошивку для ПЛИС. Важно понимать эту разницу.

Дальше посто я приведу текст моего тестбенча для USB приемника с подробными комментариями.

 


//зададим масштаб времени для симуляции и точность вычисления временных интервалов
`timescale 1ns / 1ps

//объявляем имя нашего тестбенча
module test;

//управляющие сигналы сброса и тактовой частоты
reg reset, clk;

//usb сигналы, которые мы будем генерировать
reg dp,dm;

//сигнал разрешения работы нашего исследуемого модуля
reg enable;

//выходные сигналы исследуемого модуля
wire [7:0]rdata; //принятый байт
wire rdata_ready; //сигнал, показывающий, то байт был принят
wire [3:0]rbyte_cnt; //порядковый номер принятого байта в USB пакете
wire EOP; //обнаруженный сигнал конца пакета
wire usb_reset; //обнаруженный сигнал сброса на USB шине

//объявляем экземпляр исследуемого модуля и подключаем к нему наши входные/выходные сигналы
ls_usb_recv my_usb_receiver(
reset,
clk,

dp,
dm,
enable,

EOP,

rdata,
rdata_ready,
rbyte_cnt,
usb_reset);

//генерируем тактовую частоту 12 мегагерц (полупериод равен 41,666.. наносекунд)
always
#41.667 clk = ~clk;

//переменная содержит количество единиц переданных подряд по шине USB
integer ones_cnt;

//начинаем генерировать сигналы
//наша задача в какой-то мере иммитировать поведение USB хоста
initial
begin
//значения сигналов в начальный момент времени
reset = 1; //сигнал сброса активен
clk = 0;
enable = 0;
ones_cnt = 0;
dp = 0;
dm = 1;

//пауза 200 наносекунд
#200

//ждем фронта тактовой частоты и затем снимаем сигнал сброса и разрешаем модулю работать
@(posedge clk)
reset = 0;
enable = 1;

//пауза 100 наносекунд
#100;

//посылаем USB сообщение "keep_alive"
//это состояние на шине USB появляется в начале каждого нового USB frame каждую миллисекунду
//используем вызовы "задач" (это как подпрограммы) описанных ниже
send_ls_se0;

//пауза длинной 2 бита при низкоскоростной передаче (1,5Мгц)
ls_pause(2);

//иммитируем запрос на чтение из устройства "Configuration Descriptor"

//хост контроллер передает первый USB пакет SETUP
send_ls_byte(8'h80); //SYN
send_ls_byte(8'h2D); //PID SETUP
send_ls_byte(8'h01); //Addr and CRC5
send_ls_byte(8'hE8);
send_ls_se0; //конец пакета

//пауза длинной 2 бита при низкоскоростной передаче (1,5Мгц)
ls_pause(2);

//хост контроллер передает второй USB пакет DATA0
send_ls_byte(8'h80); //SYN
send_ls_byte(8'hC3); //PID DATA0
send_ls_byte(8'h80); //Data 8 bytes
send_ls_byte(8'h06);
send_ls_byte(8'h00);
send_ls_byte(8'h02);
send_ls_byte(8'h00);
send_ls_byte(8'h00);
send_ls_byte(8'hFF);
send_ls_byte(8'h00);
send_ls_byte(8'hE9); //CRC16
send_ls_byte(8'hA4);
send_ls_se0; //конец пакета

//здесь устройство должно отвечать, но нам это сейчас не интересно
//завершение симуляции
end

//описываем "подпрограммы" в виде задач (task)

//эта задача генерирует на шине USB состояние SE0
//в этом состоянии обе линии USB и dp и dm находятся в нуле
//этим состоянием заканчивается передача любых пакетов
//и этим состоянием начинается каждый USB кадр.
task send_ls_se0;
begin
@(posedge clk);
#0;
ones_cnt = 0;
dp = 1'b0;
dm = 1'b0;
repeat(16) @(posedge clk);
#0;
dp = 1'b0;
dm = 1'b1;
end
endtask

//эта задача просто выдерживает паузу длинной несколько бит передачи
//у низкоскоростных устройств скорость передачи в 8 раз меньше (1,5Мгц),
//чем у высокоскоростных (12Мгц)
task ls_pause(input integer num_bits);
begin
repeat(8 * num_bits) @(posedge clk);
#0;
end
endtask

//эта задача посылает байт по шине USB
task send_ls_byte(input [7:0]sbyte);
integer i;
begin
//в цикле передаем бит за битом (младшие вперед)
for(i=0; i<8; i=i+1)
begin
//в шине USB бит "0" кодируется сменой сигналов dp и dm на противоположные
if(sbyte[i]==0)
begin
dp = ~dp;
dm = ~dm;
ones_cnt = 0;
end
else
begin
//запоминаем количество переданных подряд единиц
ones_cnt = ones_cnt+1;
end
ls_pause(1);

//если передано подряд 6 единиц, то искуственно вставляем бит "0"
if(ones_cnt==6)
begin
dp = ~dp;
dm = ~dm;
ones_cnt = 0;
ls_pause(1);
end
end
end
endtask

//останавливаем симуляцию через 100 микросекунд
initial
begin
#100000 $finish;
end

//изменения сигналов запишем в файл out.vcd
initial
begin
$dumpfile("out.vcd");
$dumpvars(0,test);
end

//выведем на консоль изменение некоторых сигналов
initial
$monitor($stime,, reset,, clk,,, dp,, dm,, EOP);

endmodule

Теперь, когда тестбенч написан, попробуем провести функциональную симуляцию. Сейчас я использую пакет iverilog. Возьму из моего последнего USB проекта (
USB ScratchBoard ( 180752 bytes )
) файл исследуемого модуля ls_usb-recv.v и положу его рядом с файлом моего тестбенча text.v

Запускаю из коммандной строки компилятор Verilog из пакета iverilog:

>iverilog -o qqq test.v ls_usb_recv.v

Теперь запускаю симулятор:

>vvp qqq
Смотрю получившиеся временные диаграммы с помощью программы GtkWave (поставляется вместе с iverilog).

>gtkwave out.vcd

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

симулируем модуль USB приемника verilog

Посмотрите, ведь принятые байты совсем не те, что я посылал! Как же так? Почему первый принятый байт 0xB0, а второй 0x25? Почему так, ведь я посылал байты 0x80 и 0x2D?

Хорошо, что с помощью GtkWave можно посмотреть состояния регистров внутри модуля приемника. Если вы посмотрите, например, на регистры clk_counter[2:0] и num_ones[2:0], то увидите, что до начала приема пакета их состояние было неопределено. Они показаны красным цветом. В эти моменты времени симулятор просто не знал, что в этих регистрах. Вот это и отразилось на результате - он стал не правильным. На самом деле, конечно, когда микросхема ПЛИС только начинает работать, значения всех регистров строго определенное - например там всегда нули. Именно поэтому реально модуль работал правильно. Однако, с другой стороны, правила хорошего стиля описания модулей требуют наличия сигнала сброса для всех регистров. Тогда и симуляция будет проходить гладко и не будет неожиданностей с включением микросхемы.

Итак, что на делать? Исправим модуль приемника USB так, что бы при сигнале ассинхрнного сброса регистры приемника обнулялись и возвращались в исходное состояние. Вот здесь исправленный текст модуля приемника USB.

Опять компилируем, симулируем и смотрим GtkWave:

симулируем модуль USB приемника verilog

Ну вот! Теперь все правильно! Все переданные наи данные были правильно приняты нашим исследуемым модулем! Можно расслабиться?

На самом деле, к сожалению, не совсем так. Наши условия, которые мы создали при симуляции, они слишком "тепличные", не похожие на реальную "суровую" жизнь. При написании нашего тестбенча мы сделали допущение, что передающая сторона (хост контроллер) и принимающая сторона (наш исследуемый модуль ls_usb_recv.v) работают от одного источника синхросигналов - это сигнал clk. На самом же деле в жизни все конечно не так. И передатчик и приемник имеют свои отдельные не синхронные генераторы и даже их частоты могут немного отличаться. Приемник USB должен синхронизироваться по принимаемым данным. Каждый перепад сигналов dp и dm на линиях USB должны подстраивать фазу приема в USB устройстве. Каждый бит "0" передается как перепад сигналов dp и dm - изменение их на противоположные.
При передаче "единиц" состояние линий не меняется, но если их много (если их шесть), то передатчик должен принудительно послать один бит "ноль", чтобы приемник смог засинхронизироваться. Приемник потом удалит этот лишний "ноль" ('это алгоритм bit stuffing).

Ну вот это то же можно проверить с помощью симуляции!
Изменим наш тестбенч так, что бы для приемника был свой отдельный генератор. Примерно вот так (я умышленно для приемника установил другую частоту, НЕ 12Мгц). Вот фрагмент нового тестбенча, красным цветом выделены изменения:


//объявляем имя нашего тестбенча
module test;

//управляющие сигналы сброса и тактовой частоты
reg reset, clk, clk2;

//usb сигналы, которые мы будем генерировать
reg dp,dm;

//сигнал разрешения работы нашего исследуемого модуля
reg enable;

//выходные сигналы исследуемого модуля
wire [7:0]rdata; //принятый байт
wire rdata_ready; //сигнал, показывающий, то байт был принят
wire [3:0]rbyte_cnt; //порядковый номер принятого байта в USB пакете
wire EOP; //обнаруженный сигнал конца пакета
wire usb_reset; //обнаруженный сигнал сброса на USB шине

//объявляем экземпляр исследуемого модуля и подключаем к нему наши входные/выходные сигналы
ls_usb_recv my_usb_receiver(
reset,
clk2,

dp,
dm,
enable,

EOP,

rdata,
rdata_ready,
rbyte_cnt,
usb_reset);

//генерируем тактовую частоту 12 мегагерц (полупериод равен 41,666.. наносекунд)
always
#41.667 clk = ~clk;

//генерируем отдельную тактовую частоту для приемника
always
  #42 clk2 = ~clk2;


//переменная содержит количество единиц переданных подряд по шине USB
integer ones_cnt;

//начинаем генерировать сигналы
//наша задача в какой-то мере иммитировать поведение USB хоста
initial
begin
//значения сигналов в начальный момент времени
reset = 1; //сигнал сброса активен
clk = 0;
clk2 = 0;
enable = 0;
ones_cnt = 0;
dp = 0;
dm = 1;

//пауза 200 наносекунд
#200

 


Теперь при симуляции мы увидим, что частоты приемника и передатчика "расходятся":

симулируем приемник USB на Verilog

Но тем не менее, приемник работает правильно:

симулируем приемник USB на Verilog

Смотрите все байты приняты правильно!
Кстати, можно увидеть, что на 74 микросекунде приемник удалит лишний бит "ноль" согласно алгоритма bit stuffing:

симулируем приемник USB на Verilog

Вот примерно так и производится симуляция. Нужно проверять все мыслимые и не мыслимые ситуации и смотреть как ведет себя разрабатываемое устройство.

Smile

 

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