Проводим моделирование системы для проверки работоспособности головы USB-анализатора

В прошлых статьях мы прошли достаточно большой путь к созданию шинного анализатора USB. Мы разработали логический анализатор, набив руку на самой технологии, дальше – разработали голову USB-анализатора. Затем – выяснили, что код получился сложным, так что его следует отмоделировать. Но вот беда, доступная модель микросхемы ULPI разработана на языке SystemC. Мы научились работать с этим языком. Наконец-то, мы владеем всеми необходимыми навыками и готовы провести проверку нашего ядра. Приступаем!

Предыдущие статьи цикла

  1. Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти
  2. Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd
  6. Веселая Квартусель, или как процессор докатился до такой жизни
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
  16. Разработка простейшего логического анализатора на базе комплекса Redd
  17. Разработка логического анализатора на базе Redd – проверяем его работу на практике
  18. Делаем голову шинного USB-анализатора на базе комплекса Redd
  19. Моделируем поведение Quartus-проекта на Verilog в среде ModelSim
  20. Моделирование прошивки в среде 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-анализатора в систему и проведению боевых испытаний. Но этим мы займёмся уже в следующей статье.

 

Источник

FPGA, Redd, verilog, ПЛИС, системное программирование, шина AVALON_ST, шинный USB-анализатор

Читайте также