Разработка контроллера протокола MIL-STD-1553B на ПЛИС. Часть 4
Моделирование HDL-кода проекта в ModelSim
Используя ModelSim, мы сможем моделировать работу устройства, создав функциональный тестбенч для нашего проекта. Как должен быть реализован наш тестбенч-файл?
Первое. Тестбенч должен формировать пакеты протокола MIL-STD-1553B с адресом 1 и субадресом 3. После этого необходимо считать принятые данные из памяти ОЗУ.
Второе. Первоначально следует записать данные в ОЗУ с субадресом 5. После этого в соответствии с протоколом MIL-STD-1553B считать данные.
Главное, все действия, осуществляемые в ходе моделирования, должны выводиться на консоль, чтобы мы могли сделать выводы о работоспособности нашего HDL-проекта.
Таким образом, мы будем рассматривать HDL-проект в качестве черного ящика, устройство которого нас не интересует. Нам важен отклик системы на входные воздействия. Такой подход к созданию тестбенча оправдан в том случае, когда HDL-проект периодически модернизируется, создаются различные его версии, и малейшее изменение в коде может привести к его частичной неработоспособности. В задачу такого тестбенча входит именно автоматическая проверка откликов системы.
Модуль tb.v
Для начала подключим головной модуль HDL-проекта:
`timescale 1ns/1ps; module tb (); reg clk; reg reset; //1553B - channel A reg DI1A, DI0A; wire DO1A, DO0A; wire RX_STROB_A; wire TX_INHIBIT_A; //1553B - channel B reg DI1B, DI0B; wire DO1B, DO0B; wire RX_STROB_B; wire TX_INHIBIT_B; //MEM DEV 3 interface reg [4:0] addr_rd_dev3; reg clk_rd_dev3; wire [15:0] out_data_dev3; wire busy_dev3; //MEM DEV 5 interface reg [15:0] in_data_dev5; reg [4:0] addr_wr_dev5; reg clk_wr_dev5; reg we_dev5; wire busy_dev5; Top_MIL_1553B Top_MIL_1553B( clk, reset, //MKIO interface - channel A DI1A, DI0A, DO1A, DO0A, RX_STROB_A,TX_INHIBIT_A, //MKIO interface - channel B DI1B, DI0B, DO1B, DO0B, RX_STROB_B, TX_INHIBIT_B, //Memories interface addr_rd_dev3, clk_rd_dev3, out_data_dev3, busy_dev3, in_data_dev5, addr_wr_dev5, clk_wr_dev5, we_dev5, busy_dev5 );
Директивой timescale мы указали единицу временного контроля (1 нс) и разрешение по времени при симуляции (1 пс).
Далее приводится описание сигналов, которое является интерфейсом ввода/вывода top-level модуля. Сигналы, являющиеся входными для Top_MIL_1553B.v, приведены как регистры. Таким образом, мы получаем возможность присваивать им значения.
Подключение самого модуля Top_MIL_1553B.v объяснять не требуется.
В тестбенч-модуле можно использоваться несинтезируемые конструкции языка описания аппаратуры, что увеличивает возможности моделирования проектов. Использование несинтезируемых конструкций максимально приближает Verilog к обычному языку программирования.
В нашем тестбенче мы будем использовать процедуры, которые позволят объединить повторяющиеся моменты моделирования, а также более грамотно оформить модуль с точки зрения программирования.
Процедура инициализации массивов данных, используемых при моделировании:
reg [15:0] tb_array_dev3 [0:31]; reg [15:0] tb_array_dev5 [0:31]; task array_init; integer i; begin $display(" | data test dev3 | data test dev5 "); $display("******************|*************"); for (i = 0; i <= 31; i = i + 1) begin tb_array_dev3[i] = {$random} % (2**16-1); tb_array_dev5[i] = {$random} % (2**16-1); $display("%d | %h\t\t | %h\t\t", i,tb_array_dev3[i], tb_array_dev5[i]); end end endtask
Как видим, процедуры на языке Verilog описываются конструкцией task…endtask. Следует обратить внимание на то, что локальные переменные и сигналы объявляются до ключевого слова begin. В этой процедуре есть только одна переменная i-типа — integer. С помощью системной задачи $display мы вводим в консоль шапку таблицы с 16-разрядными данными, которые и будем использовать при моделировании.
В цикле for(;;) идет заполнение массивов случайными 16-разрядными данными в диапазоне от 0 до 216. Случайное число мы получаем, применяя системную функцию $random. Следует отметить, что если б мы записали функцию $random без фигурных скобок, то получили бы знаковый диапазон случайных значений — от –216 до +216.
Последней системной функцией $display мы выводим на экран 16-разрядные значения для каждого из субадресов. Читателя могу смутить различные «иероглифы», использованные в этой функции. Но это всего лишь атрибуты форматирования. Мы выводим на экран три аргумента. Так вот значение i будет отображаться в десятичном виде (атрибут %d), а значения обоих массивов — в шестнадцатеричном виде (атрибут %h). Также здесь присутствует атрибут табуляции — \t.
Немного опережая события, покажем, как будет выглядеть консоль после процедуры array_init (рис. 18).
Процедура отправки слова в соответствии с протоколом MIL-STD-1553B:
task word_transmit (input sync, input [15:0] data); integer i; reg [39:0] shift_reg; reg parity; begin //set sync if (sync) shift_reg[39:34] = 6'b111000; else shift_reg[39:34] = 6'b000111; //set field for(i=0; i<=15; i=i+1) shift_reg[(i*2+2) +: 2] = {data[i],~data[i]}; //set parity parity = 1'b1; for(i = 0; i<= 15; i = i + 1) begin if (data[i] == 1'b1) parity = ~parity; else parity = parity; end shift_reg[1:0] = {parity,~parity}; //send for(i = 0; i <= 39; i = i + 1) begin DI1A = shift_reg[39-i]; DI0A = ~shift_reg[39-i]; #500; end DI1A = 0; DI0A = 0; end endtask
Эта процедура должна выдавать на дифференциальные входы основного канала (DI1A и DI0A) последовательность манчестерского кода с правилами, рассмотренными в разделе описания протокола. Одним из отличий word_transmit от array_init является наличие двух аргументов процедуры. Аргумент sync сообщает процедуре, какой синхросигнал использовать (1 — командное слово, 0 — информационное слово). Аргумент data[15:0] сообщает процедуре, что именно нужно передать (на рис. 5 — биты 4–19).
В теле процедуры под комментарием //set sync в сдвиговый регистр загружается синхросигнал.
Под текстом комментария //set field мы преобразовываем поле данных для передачи в манчестерский код. (Вспомним, что каждый бит информации в манчестере кодируется двумя противоположными уровнями.) Здесь читатель может удивиться, увидев странную запись адресации разрядов регистра shift_reg[(i*2+2) +: 2]. Обычная запись адресации части регистра shift_reg[a:b] применяется лишь в том случае, когда a и b являются константами или не требуется работать с отрезками вектора. В нашем же случае используется выражение, и придется применить синтаксис shift_reg[a+:b], где a — младший разряд нужной нам части регистра, b — ширина нужной нам части регистра. Например, shift_reg[3+:2] <= 2’b11 эквивалентно shift_reg[4:3] <= 2’b11.
Нам осталось определить бит паритета посылки, чтобы заполнить младшие два разряда регистра shift_reg. Под текстом комментария //set parity приведен расчет бита паритета (чтобы общее количество битов в поле данных плюс сам бит паритета было нечетным).
Наконец, нам осталось передать получившийся манчестерский код на дифференциальные входы основного канала DI1A и DI0A. Это реализует цикл for(;;) под текстом комментария //send. Через каждые 500 нс цикл считывает регистр shift_reg[39:0] от старшего разряда к младшему.
Процедура приема слов:
task word_receiver; integer i; reg [39:0] tb_man_reg; reg [19:0] tb_data_reg; begin //input for(i = 39; i >= 0; i = i — 1) begin tb_man_reg[i] = DO1A; #500; end //decode for(i = 0; i <= 19; i = i + 1) tb_data_reg[i] = tb_man_reg[i*2+1]; //indicate if(tb_man_reg[39:34] == 6'b111000) $display("Received Status Word. ADDRESS = %d, status bits = %b", tb_data_reg[16:12], tb_data_reg[11:1]); else if (tb_man_reg[39:34] == 6'b000111) $display("Received Data Word. DATA = %h", tb_data_reg[16:1]); end endtask
Под текстом комментария //input происходит накопление данных с частотой, соответствующей скорости обмена данных протокола. Так как выход данных нашего устройства дифференциальный, то в тестбенч-модуле целесообразнее работать с позитивной составляющей дифференциального сигнала.
Далее под текстом комментария //decode проводится декодирование манчестерского кода, накопленного в регистре tb_man_reg[39:0].
Итак (код процедуры под комментарием //indicate), принятое слово необходимо распознать либо как ответное, либо как информационное, которые отличаются (как мы знаем из раздела описания протокола) синхросигналом. Если принято ответное слово (синхросигнал SYNC C на рис. 4), то системной задачей $displayвыводим на консоль соответствующее сообщение со значением ADDRESS в десятичном виде (%d, tb_data_reg[16:12]) и набором битов статуса в бинарном виде (%b, tb_data_reg[11:1]). Если же принято информационное слово, то выводим соответствующее сообщение на консоль с полем данных в шестнадцатеричном виде (%h, tb_data_reg[16:1]).
На рис. 19 показано, как будет выглядеть консоль после отработки процедуры word_receive.
Нам осталось создать процедуры записи и чтения внутренней памяти RAM:
task read_ram3 (input [4:0] addr); begin addr_rd_dev3 = addr; #31.25 clk_rd_dev3 = 1'b1; #31.25 clk_rd_dev3 = 1'b0; end endtask task write_ram5 (input [15:0] data, input [4:0] addr); begin in_data_dev5 <= data; addr_wr_dev5 <= addr; we_dev5 <= 1'b1; #31.25 clk_wr_dev5 = 1'b1; #31.25 clk_wr_dev5 = 1'b0; we_dev5 <= 1'b0; end endtask
Закончив с объявлением процедур, мы можем перейти непосредственно к ходу моделирования.
Нашему устройству необходимо тактирование и аппаратный сброс:
initial begin clk = 0; #15.625 forever #15.625 clk = !clk; end initial begin reset = 1; repeat (10) @(posedge clk); reset = 0; end
Все блоки initial выполняются параллельно. В первом блоке мы генерируем тактовый сигнал с частотой 32 МГц (половина периода равна 15,625 нс). Во втором блоке генерируется положительный импульс сброса reset. Здесь использован несинтезируемый (большинством синтезаторов) оператор repeat, который в данном примере выполняется в течение первых 10 тактов clk.
В следующем блоке initial будет показан основной ход моделирования с использованием процедур, которые мы объявили ранее.
Итак, нам нужно смоделировать работу нашего устройства, осуществив обмен данными по протоколу MIL-STD-1553B с субадресами 3 и 5. Ход моделирования можно разбить на этапы:
- инициализация входных сигналов проекта;
- инициализация массивов данных для моделирования;
- отправка командного слова с субадресом 3 и семи информационных слов;
- чтение внутренней памяти RAM субадреса 3 с выводом данных на консоль;
- запись во внутреннюю память RAM субадреса 5 с выводом данных на консоль;
- отправка командного слова с субадресом 5.
Приведем блок initial полностью:
integer i; initial begin //init input signals {DI1A, DI0A} = {2'b00}; {DI1B, DI0B} = {2'b00}; addr_rd_dev3 = 5'd0; clk_rd_dev3 = 1'b0; in_data_dev5 = 16'd0; addr_wr_dev5 = 5'd0; clk_wr_dev5 = 1'b0; we_dev5 = 1'b0; //test data init $display("\n"); $display("*************************"); $display("*** TEST DATA INIT ***"); $display("*************************"); array_init; #10000; //wait 10 us //packet for subaddr 3 $display("\n"); $display("*************************"); $display("*** TESTING SUBADDR 3 ***"); $display("*************************"); word_transmit (1,{5'd1,1'b0,5'd3,5'd7}); $display($time," Transmitted Command Word — ADDRESS 1, SUBADDRESS 3, WORD 7"); for (i=0; i<7; i=i+1) begin word_transmit(0,tb_array_dev3[i]); $display($time," Transmitted Data Word - DATA %h", tb_array_dev3[i]); end #30000; //wait 30 us //read mem_dev3 $display("\n"); for (i=0; i<7; i=i+1) begin read_ram3(i); $display("Read MEM_DEV3, addr = %d, data = %h", i, out_data_dev3); end #10000; //wait 10 us $display("\n"); $display("************************"); $display("***TESTING SUBADDR 5 ***"); $display("************************"); //write mem_dev5 for (i=0; i<5; i=i+1) begin write_ram5(tb_array_dev5[i],i); $display("Write MEM_DEV5, addr = %d, data = %h", i, tb_array_dev5[i]); end //packet for subaddr 5 word_transmit (1,{5'd1,1'b1,5'd5,5'd5}); end
Под текстом комментария //init input signals приводится назначение начальных состояний входных портов модуля Top_MIL_1553B. Это сделано для того, чтобы в окне симуляции Wave мы не видели неопределенных значений.
Под комментарием //test data init на консоль выводится соответствующая надпись для наглядности моделирования. Далее идет вызов процедуры array_init, рассмотренной ранее. Кстати, в системной задаче $displayу нас впервые применен атрибут форматирования \n, означающий переход на следующую строку.
Под //packet for subaddr 3 представлена последовательность вызова процедур word_transmit. Сначала выводится на консоль заголовок, говорящий о том, что мы начинаем моделировать субадрес 3. Следующий вызов процедуры word_transmit имеет аргументы (1, {5’b1, 1’b0, 5’d3, 5’d7}). Первый аргумент с «1» означает, что мы собираемся передать командное слово. Вторым аргументом мы указываем адрес ADDRESS (5’d1), признак передачи WR (1’b0), субадрес SUBADDR (5’d3) и количество передаваемых слов (5’d7). В цикле for(;;) происходит передача семи информационных слов одно за другим, которые содержат данные, созданные с помощью процедуры array_init ранее.
Далее нам нужно считать внутреннюю память и вывести данные на консоль (текст кода под комментарием //read mem dev3). Тем самым мы можем проверить правильность работы описанного в HDL-коде алгоритма приема данных от контроллера канала оконечному устройству по протоколу MIL-STD-1553B.
Теперь нам осталось промоделировать прием данных от оконечного устройства к контроллеру канала. Как мы помним, для таких целей мы «зарезервировали» субадрес 5. Для начала заполним внутреннюю память RAM(текст кода под комментарием //write mem dev5) данными в количестве пяти 16-разрядных слов, созданных ранее с помощью функции array_init.
Затем, чтобы инициировать поток принимаемой информации (ответное слово + пять информационных слов) от оконечного устройства, которым является наш HDL-проект, необходимо отправить командное слово с аргументами (1,{5’d1,1’b1,5’d5,5’d5}). Первым аргументом является признак командного слова, второй аргумент состоит из адреса оконечного устройства ADDRESS (5’d1), признака приема WR (1’b1), субадреса SUBADDR(5’d5) и количества принимаемых слов (5’d5). Отправка командного слова приводится под комментарием //packet for subaddr 5.
В блоке initial мы организовывали передачу данных от контроллера канала (как понял читатель, наш тестбенч выполняет именно функцию контроллера канала) на оконечное устройство и фиксировали происходящее на консоли. Теперь нам осталось «научиться» ловить ответные данные от оконечного устройства и выводить их на консоль, чтобы проверить работоспособность проекта.
Приведем еще один блок initial, который будет вызывать процедуру word_receiver в нужный момент времени:
initial forever if (DO1A ^ DO0A) begin word_receiver; end else begin @clk; end
В то время, когда магистраль не активна, драйвер протокола не должен быть нагружен, поэтому дифференциальные сигналы DO1A и DO0A имеют противоположное значение только в момент передачи данных от оконечного устройства к контроллеру канала. Именно в этот момент запускается процедура word_receiver, которая будет фиксировать информацию от оконечного устройства и выводить ее на консоль.
На рис. 20 приведена информация, выведенная на консоль в результате моделирования нашего проекта.
Таким образом, просто запустив моделирование, можно проверить работоспособность проекта полностью, сравнивая ту информацию, которая отправляется в устройство, с той, которая принимается от него.
Запустить процесс моделирования можно с помощью GUI-интерфейса ModelSim. Этот способ подробно был рассмотрен в [2]. В этом же источнике показан более рациональный способ запуска моделирования и оформление временных диаграмм — с использованием do-файла. В таком файле прописываются скрипт-команды на языке tcl, которые позволяют компилировать проект, запускать симулятор и оформлять окно Wave симулятора автоматически и так, как нам удобнее.
Рассмотрим текст нашего do-файла (назовем его make.do). Содержимое файла make.do можно разделить на четыре части:
- компиляция файлов проекта;
- запуск симулятора;
- оформление окна Wave;
- запуск процесса моделирования.
Для компиляции проекта необходимо использовать команду vlog со списком имен всех файлов проекта:
vlog tb.v \ Top_MIL_1553B.v \ Receiver.v \ Transmitter.v \ RT_control.v \ device3.v \ device5.v \ mem_dev3.v \ mem_dev5.v
Список можно представить одной строкой, а также переносить на следующую строку с помощью знака «\».
Следующей строкой мы запускаем симулятор:
vsim work.tb
Здесь work — название библиотеки, которая по умолчанию указывается при создании проекта, а tb — имя нашего тестбенч-файла.
Следующим шагом будет оформление окна симуляции Wave. Читатель должен помнить, что отладка HDL-проекта пройдет настолько быстро, насколько удобно и функционально будет оформлено окно Wave.
Для начала пропишем пути к модулям проекта:
set dir_receiver /tb/Top_MIL_1553B/Receiver set dir_transmitter /tb/Top_MIL_1553B/Transmitter set dir_rt_control /tb/Top_MIL_1553B/RT_control set dir_device3 /tb/Top_MIL_1553B/RT_control/device3 set dir_device5 /tb/Top_MIL_1553B/RT_control/device5
Здесь мы просто назначаем длинным путям отдельное имя, которое неоднократно будем использовать далее при добавлении отдельных сигналов.
Для добавления сигналов в окно Wave служит команда add wave с различными атрибутами. Например, чтобы выделять некоторые сигналы в группы, используется атрибут —group <имя группы>, причем если в имени группы присутствуют пробелы, то это имя необходимо указывать в фигурных скобках {}.
Добавим два глобальных тактовых сигнала и сигнал аппаратного сброса:
add wave -noupdate -format Logic -label clk /tb/clk add wave -noupdate -format Logic -label reset /tb/reset
Следует указать, что если мы не станем использовать атрибут -label <имя сигнала>, то в окне Wave будет указан полностью путь до сигнала, что не совсем удобно.
Далее добавим сигналы, ответственные за основной канал драйвера протокола:
add wave -noupdate -group {1553B channel A} \ -format Logic -label DI1A /tb/DI1A add wave -noupdate -group {1553B channel A} \ -format Logic -label DI0A /tb/DI0A add wave -noupdate -group {1553B channel A} \ -format Logic -label DO1A /tb/DO1A add wave -noupdate -group {1553B channel A} \ -format Logic -label DO0A /tb/DO0A add wave -noupdate -group {1553B channel A} \ -format Logic -label RX_STROB_A /tb/RX_STROB_A add wave -noupdate -group {1553B channel A} \ -format Logic -label TX_INHIBIT_A /tb/TX_INHIBIT_A
Можно заметить, что здесь сигналы указаны в составе общей группы с именем 1553B channel A. Аналогичным способом нужно сформировать сигналы для резервного канала, сигналы интерфейсов внутренней памяти и каждого из модулей проекта.
В качестве еще одного примера приведем добавление сигналов модуля RT_control.v:
add wave -noupdate -group RT_control \ -format Logic -label clk $dir_rt_control/clk add wave -noupdate -group RT_control \ -format Logic -label rx_done $dir_rt_control/rx_done add wave -noupdate -group RT_control \ -format Logic -radix hexadecimal \ -label rx_data $dir_rt_control/rx_data … add wave -noupdate -group RT_control \ -format Logic -radix unsigned \ -label addr_wr_dev5 $dir_rt_control/addr_wr_dev5 add wave -noupdate -group RT_control \ -format Logic -label clk_wr_dev5 $dir_rt_control/clk_wr_dev5 add wave -noupdate -group RT_control \ -format Logic -label we_dev5 $dir_rt_control/we_dev5 add wave -noupdate -group RT_control \ -format Logic -label busy_dev5 $dir_rt_control/busy_dev5
Здесь в некоторых сигналах указан атрибут -radix, который позволяет указывать отображение сигнала в шестнадцатеричном (hexadecimal), десятичном беззнаковом (unsigned) и других форматах.
Наконец нам осталось запустить симуляцию, например на 500 мкс. Для этого добавим в наш do-файл команду:
run 500 us
Таким образом, только набрав в консоли текст do make.do, вы автоматически скомпилируете проект и запустите его на симуляцию с оформленным окном Wave.
Проанализировав результаты моделирования, то есть изучив полученные временные диаграммы и сообщения в консоли, разработчик проекта на ПЛИС может с максимальной уверенностью убедиться в работоспособности проекта. Если же в ходе разработки была допущена ошибка, то исправление ее не займет много времени.
На рис. 21 представлено окно Wave после запуска make.do. Здесь раскрыты две группы сигналов — основной канал протокола и модуль RT_control.v. На рис. 22 показан увеличенный фрагмент передачи пакета для субадреса 5.
Заключение
Описанный проект контроллера протокола MIL-STD-1553B автор реализовал на ПЛИС EP3C55F484. Фиттер САПР Quartus 9.0 Web Edition разместил проект на 333 логических элементах (LE). Из этого числа модуль передатчика занял 57 LE, а модуль приемника — 74 LE. Под RAM-память было выделено два блока памяти M9K. Как видим, все это не потребовало большого количества ресурсов. Автор для своего проекта использовал именно эту ПЛИС, с количеством более 55 тысяч LE, потому что в составе реального устройства, помимо драйвера протокола, было еще много другой периферии разного уровня сложности и требующей различных вычислений. Очевидно, что под реализацию только контроллера протокола можно использовать менее сложную ПЛИС.
Читателю, помимо временных диаграмм симулятора, будет полезно увидеть реальные осциллограммы пакетов протокола MIL-STD-1553B. На рис. 23 представлена осциллограмма начала пакета передачи информации от контроллера канала на оконечное устройство. Верхняя осциллограмма — сигнал между трансформатором и драйвером. Нижняя осциллограмма — сигнал между драйвером и ПЛИС. По синхросигналам можно отличить командное слово от информационного.
На рис. 24 представлено то же командное слово, но в большем масштабе. На рис. 25 показана разность амплитуд сигналов в магистрали (до трансформатора) и после преобразования в трансформаторе.
На рис. 26 видно, что по мере прохождения по магистрали уровень сигнала падает. Уровень сигнала ответного слова тут чуть выше, чем у командного и информационного слова.
На рис. 27 приведен пакет передачи данных на оконечное устройство. Осциллограммы сняты между драйвером и ПЛИС.
Использование грамотно написанных тестбенч-модулей открывает перед разработчиком широкие возможности при проверке и отладке проекта. Такие модули просто необходимы при создании сложных проектов.
- ГОСТ Р 52070-2003. Интерфейс магистральный последовательный системы электронных модулей.
- Дайнеко Д. Реализация CORDIC-алгоритма на ПЛИС // Компоненты и технологии. 2011. № 12.
- IEEE Standart Verilog Hardware Description Language. 2001.
- Mentor Graphics. ModelSim Tutorial. May, 2008.
- Дайнеко Д. Разработка контроллера протокола MIL-STD-1553B на ПЛИС. Ч. 1 // Компоненты и технологии. 2013. № 12.
- Дайнеко Д. Разработка контроллера протокола MIL-STD-1553B на ПЛИС. Ч. 2 // Компоненты и технологии. 2014. № 1.
- Дайнеко Д. Разработка контроллера протокола MIL-STD-1553B на ПЛИС. Ч. 3 // Компоненты и технологии. 2014. № 2.