Симуляция системы на кристале Amber ARM v2a SoC с помощью Verilator

verilator amber arm soc

Поскольку я начал изучать этот очень быстрый симулятор 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. В некоторых случаях - отличный инструмент.

 


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