Поскольку я начал изучать этот очень быстрый симулятор Verilog HDL - Verilator, то подумал, мне, чтобы лучше понять и освоить его нужен конкретный проект. Но ведь у меня их много! Почему бы мне не попробовать симулировать скажем Amber SoC - систему на кристалле с процессором ARM v2a? Когда-то я занимался этим проектом и запустил эту SoC в плате Марсоход2 и даже какой-то Linux у меня там стартовал. Я когда-то даже симулировал этот проект в Icarus Verilog, но работала та симуляция чрезвычайно медленно. Это то, что нужно. Я попробую теперь симулировать этот же Amber SoC с помощью Verilator. Посмотрим насколько он окажется быстрее.
Идея очень хорошая, но первые же мои попытки симуляции амбера с Verilator... провалились. У Verilator есть несколько ограничений. Он может быть использован только для функциональной симуляции проектов и некоторые конструкции языка Verilog HDL просто не понимает. Verilator хорошо воспринимает код для синтезируемой логики, то есть тот код, который потом будет скомпилирован в реально работающую схему проекта внутри чипа. Но Verilator игнорирует или ругается на несинтезируемые конструкции, которые обычно в Verilog пишутся в тестбенчах. При работе с проектом Amber SoC ARM v2a основные сложности получаются с моделью памяти Micron. Там есть несколько мест в коде модели памяти SDRAM Micron, на которые Verilator либо дает ошибку либо неправильно интерпретирует.
Например, вот этот участок кода в модели SDRAM Micron не нравится верилатору:
// System clock generator always begin @ (posedge Clk) begin Sys_clk = CkeZ; CkeZ = Cke; end @ (negedge Clk) begin Sys_clk = 1'b0; end end
Вродебы безобидный код, но Verilator говорит нет, так не надо писать. И главное - а как его переписать в нормальный код? А похоже в нормальный синтезируемый код это и переписать нельзя, так как тут сигнал Sys_clk устанавливается и по фронту и по спаду сигнала Clk. В реальной жизни насколько я понимаю так не бывает. Вот и Verilator так же думает.
Это только один пример. Но там такого добра много. Есть в коде Dq_reg для которого в разных местах кода применяется то блокирующее, то неблокирующее присваивание. Это так же не нравится верилатору. Самая большая проблема - в коде модели SDRAM есть очень много временных задержек, например вот таких: Dq_reg = #tAC Dq_dqm; Верилатор не может в задержки, это не синтезируемая конструкция. Он эту задержку просто игнорирует. Я попытался быстренько исправить модель и сделать ее более подходящей для функциональной симуляции с Verilator, но потратив пару дней на это безнадежное дело я понял, что тут так просто не получится. В этой модели SDRAM многое на задержках и держится. Это не починить. Как же быть?
Я поискал по интернету и нашел! Оказывается, что были люди, которые уже сталкивались точно с такой же проблемой и они уже написали модель памяти SDRAM на C++ для Verilator! Это же отлично! Попробую применить их опыт у себя в симуляции проекта Amber SoC.
Я взял модель SDRAM с гитхаба из вот этого проекта: https://github.com/fredrequin/fpga_1943/tree/master/verilator/sdr_sdram и слегка модифицировал этот исходник, чтобы можно было в память загружать нужное мне содержимое из текстовых файлов вида *.mem. У меня образ ядра Linux в виде mem файла, начинается вот так (пары адрес-значение):
// Section name .init
// Type SHT_PROGBITS, Size 0x10000, Start address 0x02080000, File offset 0x8000, boffset 0
@02080000 e35f0402
@02080004 b59ff034
@02080008 e3300000
@0208000c 1b000013
@02080010 e28f0028
@02080014 e990203c
@02080018 e3a00000
@0208001c e1520003
Как я уже писал, тестбенчи для Verilator пишутся на C++. Поэтому мне и для симуляции системы на кристалле Amber SoC нужно было писать такой тестбенч на C++. Теперь в моем тестбенче на C++ для Verilator появятся такие строки, как создание экземпляра компонента SDRAM и загрузка в память образов initrd и самого Linux:
SDRAM* sdr = new SDRAM( rows_bits, cols_bits, sdram_flags, nullptr ); sdr->load("initrd" ,204800 ,0x700000); sdr->loadMem("vmlinux.mem");
initrd - это двоичный файл, который представляет собой образ файловой системы ext2 и самое главное там содержится самая первая стартующая программа /sbin/init, которая в моем случае просто печатает Hello world.
vmlinux.mem - ну это образ ядра Linux. Казалось бы почему в SDRAM сразу не грузить бинарный файл, как initrd? Но vmlinux - это не просто бинарник. Это elf файл, которный содержит разные секции, блоки, которые нужно размещать по разным адресам. Это обычно делает загрузчик. А мой загрузчик для этой симуляции почти ничего такого не делает. Он только делает переход по адресу 0x80000 и все. Вот по этому и получается, что из бинарника vmlinux сперва нужно сделать vmlinux.mem с помощью специального инструмента elfsplitter:
sw/tools/amber-elfsplitter vmlinux >> vmlinux.mem
Мудрено все это, но этот проект вообще довольно сложный.
Идем дальше.. смотрим тестбенч. Экземпляр модуля SDRAM у нас уже создан. Теперь из него нужно брать значение из памяти для остальной системы, а из системы передавать адреса и управляющие сигналы в модуль SDRAM.
Я сделал это вот так:
// "Read" from SDRAM, put to top top->sdr_dq = (vluint16_t)sdr_dq; //calculate top module signals top->eval(); // Evaluate SDRAM C++ model vluint8_t sdr_cs_n = 0; vluint8_t sdr_cke = 1; sdr->eval ( main_time, clock, sdr_cke, sdr_cs_n, top->sdr_ras_n, top->sdr_cas_n, top->sdr_we_n, top->sdr_ba, top->sdr_addr, top->sdr_dqm, (vluint64_t)top->sdr_dq, sdr_dq );
Сделал в три шага:
1) из памяти беру данные отдаю в топ модуль.
2) вычисляю значения сигналов топ модуля
3) передаю адреса, данные и управляющие сигналы из топ модуля в модуль SDRAM
Получается синхронный обмен. Возможно тут не все чисто с точки зрения z-состояний шины данных SDRAM, но в целом вроде бы правильно.
Полный код тестбенча можно взять теперь в проекте на github https://github.com/marsohod4you/Amber-Marsohod2
в папке hw/marsohod2/my_tb_verilator
Ну и здесь, конечно, приведу весь код тестбенча для Verilator симулятора старта Amber SoC с Linux:
#include <verilated.h> #if VM_TRACE #include "verilated_vcd_c.h" #endif #include "../sdr_sdram/sdr_sdram.h" #include "Vtb.h" double main_time = 0; double sc_time_stamp () { return main_time; } int main(int argc, char **argv, char **env) { if (0 && argc && argv && env) {} Vtb* top = new Vtb; // Init SDRAM C++ model (4096 rows, 512 cols) int sdram_flags = FLAG_DATA_WIDTH_16 | FLAG_BANK_INTERLEAVING; int rows_bits = 12; //4096 int cols_bits = 8; //256 SDRAM* sdr = new SDRAM( rows_bits, cols_bits, sdram_flags, nullptr ); //sdr->loadMem("initrd.mem"); sdr->load("initrd" ,204800 ,0x700000); //sdr->load("vmlinux",1289624, 0x80000); sdr->loadMem("vmlinux.mem"); Verilated::commandArgs(argc, argv); Verilated::debug(0); top->sysrst = 0; top->clk_80mhz = 0; #ifdef VM_TRACE VerilatedVcdC* vcd = nullptr; const char* flag = Verilated::commandArgsPlusMatch("trace"); if (flag && 0==strcmp(flag, "+trace")) { printf("VCD waveforms will be saved!\n"); Verilated::traceEverOn(true); // Verilator must compute traced signals vcd = new VerilatedVcdC; top->trace(vcd, 99); // Trace 99 levels of hierarchy vcd->open("out.vcd"); // Open the dump file } #endif vluint64_t sdr_dq = 0; int clock = 0; int iT=0; while (!Verilated::gotFinish()) { clock^=1; top->clk_80mhz = clock; if(main_time>50.0 ) top->sysrst = 1; double dT=main_time/54.25347; if( (int)dT>iT ) { top->clk_uart = ~top->clk_uart; iT=(int)dT; } main_time+=6.25; if( main_time>500000000.0 ) break; // "Read" from SDRAM, put to top top->sdr_dq = (vluint16_t)sdr_dq; top->eval(); // Evaluate SDRAM C++ model vluint8_t sdr_cs_n = 0; vluint8_t sdr_cke = 1; sdr->eval ( main_time, clock, sdr_cke, sdr_cs_n, top->sdr_ras_n, top->sdr_cas_n, top->sdr_we_n, top->sdr_ba, top->sdr_addr, top->sdr_dqm, (vluint64_t)top->sdr_dq, sdr_dq ); #if VM_TRACE if( vcd ) vcd->dump(main_time); #endif } top->final(); delete sdr; delete top; #if VM_TRACE if( vcd ) vcd->close(); #endif exit(0); }
После компиляции проекта командой make можно запускать симулятор командой obj_dir/Vtb - библиотеки и исполняемый файл получаются в папке obj_dir. Запускаю симуляцию:
Instantiating 8 MB SDRAM : 4 banks x 4096 rows x 256 cols x 16 bits
Starting row : 3584, starting bank : 0
Loading 0x00032000 bytes @ 0x00700000 from binary file "initrd"...OK
Load mem file: vmlinux.mem
Mem-02080000-e35f0402-
Mem-02080004-b59ff034-
Mem-02080008-e3300000-
Mem-0208000c-1b000013-
Mem-02080010-e28f0028-
Mem-02080014-e990203c-
Mem-02080018-e3a00000-
Mem-0208001c-e1520003-
Mem-02080020-34820004-
Starting row : 0, starting bank : 0
Loading 0x0018D790 bytes @ 0x00000000 from binary file "tmp.dat"...OK
Load boot memory from boot-loader.mem
Read in 2014 lines
log file tests.log, timeout 0, test name my ARM simulation
Linux version 2.4.27-vrs1 (nick@ubuntu) (gcc version 4.5.2 (Sourcery G++ Lite 2011.03-46) ) #1 Tue Jan 22 23:48:37 PST 2013
CPU: Amber 2 revision 0
Machine: Amber-FPGA-System
On node 0 totalpages: 256
zone(0): 256 pages.
zone(1): 0 pages.
zone(2): 0 pages.
Kernel command line: console=ttyAM0 mem=8M root=/dev/ram
19.91 BogoMIPS (preset value used)
Memory: 8MB = 8MB total
Memory: 6304KB available (783K code, 222K data, 64K init)
Dentry cache hash table entries: 4096 (order: 0, 32768 bytes)
Inode cache hash table entries: 4096 (order: 0, 32768 bytes)
Mount cache hash table entries: 4096 (order: 0, 32768 bytes)
Buffer cache hash table entries: 8192 (order: 0, 32768 bytes)
Page-cache hash table entries: 8192 (order: 0, 32768 bytes)
POSIX conformance testing by UNIFIX
Linux NET4.0 for Linux 2.4
Based upon Swansea University Computer Society NET3.039
Initializing RT netlink socket
Starting kswapd
ttyAM0 at MMIO 0x16000000 (irq = 1) is a WSBN
pty: 256 Unix98 ptys configured
Serial driver version 5.05c (2001-07-08) with no serial options enabled
ttyS00 at 0x03f8 (irq = 10) is a 16450
ttyS01 at 0x02f8 (irq = 10) is a 16450
RAMDISK driver initialized: 16 RAM disks of 208K size 1024 blocksize
NET4: Linux TCP/IP 1.0 for NET4.0
IP Protocols: ICMP, UDP, TCP
IP: routing cache hash table of 4096 buckets, 32Kbytes
TCP: Hash tables configured (established 4096 bind 8192)
NET4: Unix domain sockets 1.0/SMP for Linux NET4.0.
RAMDISK: ext2 filesystem found at block 0
RAMDISK: Loading 200 blocks [1 disk] into ram disk... done.
Freeing initrd memory: 200K
VFS: Mounted root (ext2 filesystem) readonly.
Freeing init memory: 64K
BINFMT_FLAT: Loading file: /sbin/init
Mapping is 2b0000, Entry point is 8068, data_start is 8e4c
Load /sbin/init: TEXT=2b0040-2b8e4c DATA=2b8e50-2b8e83 BSS=2b8e83-2b8e88
start_thread(regs=0x21f9fa4, entry=0x2b8068, start_stack=0x2affb4)
Hello, World!
Hello, Marsohod!
Самое интересное, что если с Icarus Verilog на подобную симуляцию загрузки ядра Linux уходило минут сорок пять, то теперь, с Verilator, полный старт симулируемой системы происходит секунд за 30! Невероятно. Я честно говоря очень впечатлен получившимся быстродействием. Очень рекомендую присмотреться к Verilator. В некоторых случаях - отличный инструмент.
Подробнее...