- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код
- Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС
- Разработка программ для центрального процессора Redd на примере доступа к ПЛИС
- Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd
- Веселая Квартусель, или как процессор докатился до такой жизни
- Методы оптимизации кода для Redd. Часть 1: влияние кэша
- Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин
- Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы
- Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
- Работа с нестандартными шинами комплекса Redd
- Практика в работе с нестандартными шинами комплекса Redd
- Проброс USB-портов из Windows 10 для удалённой работы
- Использование процессорной системы Nios II без процессорного ядра Nios II
- Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
- Разработка простейшего логического анализатора на базе комплекса Redd
- Разработка логического анализатора на базе Redd – проверяем его работу на практике
- Делаем голову шинного USB-анализатора на базе комплекса Redd
- Моделируем поведение Quartus-проекта на Verilog в среде ModelSim
- Моделирование прошивки в среде ModelSim с использованием моделей на языке SystemC
Проверка передачи потоков данных в шину AVALON_ST
Начать проверку стоит с чего-то не очень сложного. Скажем, с простой передачи данных в шину AVALON_ST. Но как это организовать? Разглядывая файлы ulpi_driver.h и ulpi_driver.cpp, я пришёл к грустному выводу: надо вызывать функцию
void ulpi_driver::write(sc_uint <8> data, bool last)
Всё замечательно, кроме одного: мы решили, что моделирование выполняется Verilog-кодом. Кто же вызовет функцию? Стоит отметить, что существует механизм взаимного вызова сишных функций из кода Verilog и обратно. Но в данном конкретном случае, во-первых, я потратил день, но так и не понял, как это сделать. Все красивые примеры из статей разбивались о сообщения ModelSim об ошибках… А во-вторых, не факт, что это вообще возможно именно в нашем случае. Мы же создаём объект типа ulpi driver. И как вызвать не просто функцию, а функцию объекта?
Возможно, крупные специалисты в комментариях разъяснят, как можно это сделать. Пока же, предлагаю просто добавить в модельку ulpi_driver ещё один порт add_test_data и, соответственно, поток, который этот порт будет отслеживать. Verilog код будет дёргать за эту верёвку, а SystemC часть, увидев перепад, инициирует кучку записей (вспоминается шутка: «Полуавтомат: нажал кнопку — полная лопата песка»).
Теперь заголовок класса выглядит так (новая строка — последняя):
SC_MODULE (ulpi_driver)
{
public:
//-------------------------------------------------------------
// Interface I/O
//-------------------------------------------------------------
// Clock and Reset
sc_out clk;
sc_in rst_i;
// I/O
sc_inout > ulpi_data;
sc_out ulpi_dir_o;
sc_out ulpi_nxt_o;
sc_in ulpi_stp_i;
sc_in add_test_data;
Новая потоковая функция проста до безобразия:
void genTestDataThread(void)
{
while (true)
{
write (0x11,false);
write (0x22,false);
write (0x33,false);
write (0x44,false);
write (0x55,false);
write (0x66,true);
wait ();
}
}
Начало конструктора же выглядит так (добавлен псевдоним для сигнала и запуск нового потока с указанием, что функция wait() у него будет ждать положительного перепада на новом входном порту):
SC_HAS_PROCESS(ulpi_driver);
ulpi_driver(sc_module_name name): sc_module(name),
m_tx_fifo(1024),
m_rx_fifo(1024),
oscillator ("clk66",sc_time(15,SC_NS)),
rst_i ("rst"),
ulpi_data ("data"),
ulpi_dir_o ("dir"),
ulpi_nxt_o ("nxt"),
ulpi_stp_i ("stp"),
add_test_data ("add_test_data")
{
SC_THREAD(drive);
SC_THREAD(clkThread);
SC_CTHREAD(genTestDataThread,add_test_data.pos());
Поехали моделировать. Для начала проверим, как ловятся данные, пробегающие по шине.
Вот такой код для дёрганья верёвки я добавил в блок initial файла sim1.sv:
// Проверяем приём данных,
// для чего просим модель выработать их
#50;
add_test_data = 1;
#20;
add_test_data = 0;
#150;
add_test_data = 1;
#20;
add_test_data = 0;
И вот соответствующий участок на временной диаграмме (жёлтым я подкрасил линии, соответствующие шине AVALON_ST). Общий план:
Вроде, всё верно. Вижу данные 0x11, 0x22, 0x33… А дальше идёт 0x112. Маска 0x100 — признак команды. Но откуда она здесь появилась? Всё в порядке, в модели ULPI мы видим код, который в случайные места вставляет именно команду 0x12, а если точнее, то 0x02 с флагом rx_active (врать не буду, на данный момент я понятия не имею, что это за флаг, но вижу его в тексте модели, в этом и прелесть готовых точно работающих моделей: всё можно постигать постепенно):
do
{
// RX_CMD
if (!(rand() % 4))
{
last = false;
// RX_CMD (RX_ACTIVE = 1)
drive_rxcmd(0x2, true, false);
}
// RX_DATA
else
В общем, с виду всё работает. Возможно, при боевой проверке что-то и вылезет, но пока переходим к проверке записи в регистры ULPI.
Проверка записи в регистры ULPI
Чтобы произвести проверку записи в регистры ULPI, надо добавить модель шины AVALON_MM. В позапрошлой статье мы уже делали подобное, записывая в некую шину. Давайте добавим задачку, которая пишет именно в AVALON_MM (в том режиме, в каком это нужно мне):
task AvalonMmWrite (input reg[1:0] A, input reg[31:0] D);
begin
address = A;
writedata = D;
write = 1;
@(posedge ulpi_clk);
#1
write = 0;
end
endtask
Теперь сделаем чтение. Тоже не в произвольном, а только в требуемом для данного проекта режиме:
task AvalonMmRead (input logic[1:0] A, output logic [31:0] D);
address = A;
read = 1;
@(posedge ulpi_clk);
D = readdata;
#1
read = 0;
endtask
Для прогона этого теста, добавляем в блок initial такой текст:
#210;
// Ну что, попробуем записать что-нибудь в регистр...
// Пусть мы хотим записать в Reg5
AvalonMmWrite (0,5);
// Данные 0x12. А почему нет?
AvalonMmWrite (1,'h12);
// Ждём снятия BSY
AvalonMmRead(3,temp);
while (temp & 'h1)
begin
AvalonMmRead(3,temp);
end;
#30;
При прогоне этого теста у меня появился хороший повод показать процесс поиска проблемы на практике. Чтобы контролировать факт записи в регистр, его надо добавить на времянку. Для этого выбираем элемент ULPI, для него раскрываем массив m_reg и вытаскиваем m_reg[5] на график:
Вот времянка, соответствующая процессу. Я подкрасил линии шины AVALON_MM, ответственные за чтение, фиолетовым. Линии AVALON_MM, ответственные за запись — голубым. Ну, а линии ULPI — жёлтым.
Мы видим цикл записи номера регистра (addr=0, data=5). Мы видим цикл записи данных для регистра (addr=1 data=0x12). Мы знаем, что факт записи в порт со смещением 1 инициирует процесс записи в регистр ULPI. Мы видим, как этот процесс идёт, даже мы видим, что в регистр в итоге попала константа 0x12. Эта часть работает безупречно. Что мы не видим? А не видим мы бит BSY при чтении адреса 0x03 на шине AVALON_MM. Почему? Давайте добавим на диаграмму пару сигналов из модуля DUT. Сигнал «S» для RS триггера и собственно сам сигнал BSY.
Я их сразу подкрашу красным. Повторяем моделирование, получаем:
Собственно, видно, что сигнал write_bsy взводится с задержкой на один такт. Поэтому надо в коде чтения статуса заменить исходно написанную мною строку:
// Регистр статуса
3 : readdata <= {30'b0, read_busy, write_busy};
на
// Регистр статуса
3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};
Пересобираем, проверяем…
С установкой BSY стало лучше. С чтением — не очень. Если точнее, то чтение мы сможем отрегулировать, задав латентность шины, равной единице. Но я люблю латентность, равную нулю. И само собой, вечно вспоминаю об указанной проблеме только во время моделирования. Но не беда: на момент получения той времянки, процесс чтения AVALON_MM у меня был организован так:
// Обслуживание AVALON_MM на чтение
always_ff @(posedge ulpi_clk)
begin
if (read == 1)
begin
case (address)
// Регистр адреса (чисто для самоконтроля)
0 : readdata <= {26'b0, addr_to_ulpi};
// Регистр данных
1 : readdata <= {23'b0, data_from_ulpi};
// 2 - регистр управления, а он - только на запись
// Регистр статуса
3 : readdata <= {30'b0, (reg_request | read_busy), (have_reg | write_busy)};
default: readdata <= 0;
endcase
end
end
Переписываем ему заголовок на:
// Обслуживание AVALON_MM на чтение
always_comb
begin
...
То есть, заменили синхронный процесс на комбинационный (always_ff на always_comb). Пересобираем, прогоняем моделирование:
Ну вот. Теперь из порта считывается единица, пока автомат занят. Ну, а мы теперь примерно выяснили, как моделирование позволяет быстро выявлять и устранять проблемы. Дальше просто пробежимся по остальной функциональности.
Проверка чтения из регистров ULPI
Для проверки чтения из регистров ULPI считаем содержимое пары из них, чтобы убедиться, что будут читаться различные значения. Для этого, сделаем такой код:
// Считаем регистр 4
AvalonMmWrite (0,4); // Адрес
AvalonMmWrite (2,1); // Провоцируем чтение
AvalonMmRead(3,temp);
while (temp & 'h2)
begin
AvalonMmRead(3,temp);
end;
// Читаем результирующие данные
AvalonMmRead(1,temp);
#30;
// Считаем регистр 5
AvalonMmWrite (0,5); // Адрес
AvalonMmWrite (2,1); // Провоцируем чтение
AvalonMmRead(3,temp);
while (temp & 'h2)
begin
AvalonMmRead(3,temp);
end;
// Читаем результирующие данные
AvalonMmRead(1,temp);
#30;
Вот времянка чтения регистра 4:
Что в регистре действительно лежит содержимое 0x41, мы можем убедиться, просмотрев текущее значение массива регистров:
Ну, а значение 0x12 мы в регистр 5 записали собственноручно при тесте записи. Убеждаемся, что считается именно оно:
Заключение
Мы получили навыки отладки кода Verilog путём его моделирования. Главный посыл, на который давил автор: если что-то в поведении системы непонятно, всегда можно вывести на временную диаграмму внутренние сигналы модулей. Благодаря им всегда можно понять, что не так не только снаружи, но и «под капотом». Этого невозможно достичь реальными средствами измерения. Моделирование же даёт нам такую уникальную возможность. В рамках же цикла статей, поведенческая модель по завершении процесса отладки работает. А значит, можно переходить ко внедрению разработанной «головы» шинного USB-анализатора в систему и проведению боевых испытаний. Но этим мы займёмся уже в следующей статье.