Традиционно считается, что моделирование цифровой аппаратуры это технически очень сложная задача. Программы-симуляторы для Verilog и VHDL стоят жутких денег. SystemC хоть и бесплатный, но сущий ад для разработчиков. Хуже того: уровень абстракции имеет тенденцию повышаться, и сейчас мало кто вообще понимает, что происходит в симуляторе на самом нижнем уровне. Все нужные алгоритмы были описаны в научных статьях в начале 90-х и похоронены в библиотеках под толстым слоем пыли. В современных книжках в лучшем случае рассказывается про очередь событий и упоминаются дельта-циклы. Живем как питекантропы, поддерживая огонь в костре и не умея его зажечь.
Изначально я задумал это как задачку для продвинутых студентов: сваять в качестве курсовой работы простейший RTL-симулятор для цифровых схем. Но когда стал придумывать 'правильный ответ', оказалось все не так просто. Копать пришлось несколько глубже, чем может осилить студент. Поэтому пришлось превратить это просто в красивую иллюстрацию технологии симуляции. Исходники можно взять здесь: https://github.com/sergev/vak-opensource/tree/master/hardware/rtlsim
Основные установки:
1) Язык Си. Максимальная простота и ясность.
2) Простые структуры данных. Никаких объектов или шаблонов.
3) Событийно-управляемое моделирование: максимальная эффективность.
4) Динамическая память не используется.
5) Сопроцессы реализованы посредством стандартных POSIX-функций семейства getcontext(), входящих в библиотеку glibc.
Вся реализация уложилась примерно в 200 строчек Си-шного кода. Это, наверное, самая красивая программа, которую мне доводилось делать.
Пример симуляции есть в предыдущем посте: https://vak.dreamwidth.org/213855.html
Можно сравнить с тем же примером на Верилоге: https://vak.dreamwidth.org/213634.html
Значения
Время
Сигналы
Тип signal_t -- это структура данных, реализующая логический сигнал или шину, с шириной от 1 до 64 бит. Примерный эквивалент верилоговского типа reg. Определяется как:
Сигналы объявляются в программе как статические переменные, используя макрос signal_init(). В качестве параметров задаются имя и начальное значение сигнала, например:
Тип process_t -- структура данных, реализующая сопроцесс (lightweight thread), который моделирует поведение некоторого аппаратного блока. Для переключения сопроцессов используются стандартные POSIX-совместимые функции семейства swapcontext(), присутствующие в библиотеке glibc. Каждый процесс соответствует одному блоку 'always' в Верилоге. Определяется как:
Каждый процесс имеет собственный стек, выделяемый из стека родительского процесса. Обычно процессы создаются функцией main() при старте, так что расходуется основной стек программы. При создании процесса задается имя, главная процедура процесса и размер стека. Например:
Функция process_wait() приостанавливает процесс до изменения значения одного из сигналов в списке чувствительности, в соответствии с нужной полярностью фронта.
Функция process_wait(ticks) приостанавливает текущий процесс на указанный интервал модельного времени.
Глобальная переменная:
Изначально я задумал это как задачку для продвинутых студентов: сваять в качестве курсовой работы простейший RTL-симулятор для цифровых схем. Но когда стал придумывать 'правильный ответ', оказалось все не так просто. Копать пришлось несколько глубже, чем может осилить студент. Поэтому пришлось превратить это просто в красивую иллюстрацию технологии симуляции. Исходники можно взять здесь: https://github.com/sergev/vak-opensource/tree/master/hardware/rtlsim
Основные установки:
1) Язык Си. Максимальная простота и ясность.
2) Простые структуры данных. Никаких объектов или шаблонов.
3) Событийно-управляемое моделирование: максимальная эффективность.
4) Динамическая память не используется.
5) Сопроцессы реализованы посредством стандартных POSIX-функций семейства getcontext(), входящих в библиотеку glibc.
Вся реализация уложилась примерно в 200 строчек Си-шного кода. Это, наверное, самая красивая программа, которую мне доводилось делать.
Пример симуляции есть в предыдущем посте: https://vak.dreamwidth.org/213855.html
Можно сравнить с тем же примером на Верилоге: https://vak.dreamwidth.org/213634.html
Значения
typedef unsigned long long value_t;
Тип value_t -- 64-битное беззнаковое целое, используется для счетчика времени и для значений сигналов. Для увеличения эффективности, его можно переопределить в unsigned long, если не нужны сигналы шириной больше 32-х бит.Время
value_t time_ticks;
Внешняя переменная time_ticks отслеживает виртуальное модельное время.Сигналы
Тип signal_t -- это структура данных, реализующая логический сигнал или шину, с шириной от 1 до 64 бит. Примерный эквивалент верилоговского типа reg. Определяется как:
typedef struct {
...
const char *name;
value_t value;
...
} signal_t;
Программа пользователя может читать значения полей name и value, но не должна их изменять.Сигналы объявляются в программе как статические переменные, используя макрос signal_init(). В качестве параметров задаются имя и начальное значение сигнала, например:
signal_t clock = signal_init ("clock", 0);
Изменить значение сигнала можно вызовом функции signal_set(). Это соответствует неблокирующему присваиванию в Верилоге:
signal_set (&clock, 1);
ПроцессыТип process_t -- структура данных, реализующая сопроцесс (lightweight thread), который моделирует поведение некоторого аппаратного блока. Для переключения сопроцессов используются стандартные POSIX-совместимые функции семейства swapcontext(), присутствующие в библиотеке glibc. Каждый процесс соответствует одному блоку 'always' в Верилоге. Определяется как:
typedef struct {
...
const char *name;
...
} process_t;
Программа пользователя может читать значения поля name, но не должна его изменять.Каждый процесс имеет собственный стек, выделяемый из стека родительского процесса. Обычно процессы создаются функцией main() при старте, так что расходуется основной стек программы. При создании процесса задается имя, главная процедура процесса и размер стека. Например:
process_init ("counter", do_counter, 4096);
Здесь do_counter() - это процедура процесса. Для простых процессов обычно хватает стека в 4 килобайта. Главная процедура процесса работает по типовому алгоритму: устанавливает 'список чувствительности' и входит в бесконечный цикл, ожидая события и затем вычисляя новые значения для выходных сигналов. Например:
void do_counter()
{
process_sensitive (&clock, POSEDGE);
for (;;) {
process_wait();
...
signal_set (&count, count.value + 1);
...
}
}
Функция process_sensitive(signal, edge) добавляет сигнал и полярность фронта к 'списку чувствительности' текущего процесса. Она выделяет небольшую область памяти из стека (посредством alloca). Список чувствительности может быть произвольного размера. Полярность фронта задается флагом OSEDGE, NEGEDGE или 0 (оба фронта).Функция process_wait() приостанавливает процесс до изменения значения одного из сигналов в списке чувствительности, в соответствии с нужной полярностью фронта.
Функция process_wait(ticks) приостанавливает текущий процесс на указанный интервал модельного времени.
Глобальная переменная:
process_t *process_current;
указывает на текущий процесс. Поле process_current->name можно использовать для выдачи сообщений трассировки.
no subject
Date: 2013-02-26 01:10 (UTC)initial begin a = 0; b = 0; end
initial repeat (10) #10 begin a <= a + 2; b <= b + 3; end
assign abuf = a;
always @* begin sum = abuf + b; $display(sum); end
Я пишу
void do_buf () { process_sensitive (&a, 0); for (;;) { process_wait(); signal_set (&abuf, a.value); } } void do_sum () { process_sensitive (&abuf, 0); process_sensitive (&b, 0); for (;;) { process_wait(); printf ("%lld\n", abuf.value + b.value); signal_set(&sum, abuf.value + b.value); } } int main (int argc, char **argv) { int i; process_init ("buf", do_buf, 4096); process_init ("sum", do_sum, 4096); for (i = 0; i < 10; ++i) { process_delay (10); signal_set(&a, a.value + 2); signal_set(&b, b.value + 3); } process_delay(100000); return 0; }Что я делаю не так? :)
no subject
Date: 2013-02-26 01:16 (UTC)no subject
Date: 2013-02-26 01:40 (UTC)no subject
Date: 2013-02-26 01:51 (UTC)no subject
Date: 2013-02-27 06:16 (UTC)no subject
Date: 2013-02-27 06:38 (UTC)no subject
Date: 2013-02-27 19:36 (UTC)Вот мой вариант:
Вот Synopsys:
Chronologic VCS simulator copyright 1991-2012 Contains Synopsys proprietary information. Compiler version G-2012.09; Runtime version G-2012.09; Feb 27 11:25 2013 0000040000010002 b76ea027fb17fa95 554ac26b1fb14e61 $finish called from file "random.v", line 2545. $finish at simulation time 160800 V C S S i m u l a t i o n R e p o r t Time: 160800 CPU Time: 0.530 seconds; Data structure size: 0.0Mb Wed Feb 27 11:25:51 2013Тексты здесь, можешь попробовать на своих верилогах:
https://vak-opensource.googlecode.com/svn/trunk/hardware/rtlsim/random.v
https://vak-opensource.googlecode.com/svn/trunk/hardware/rtlsim/random.c
no subject
Date: 2013-02-27 21:17 (UTC)Кстати, как в твоей модели написать
always begin $display("waiting for var1"); forever begin @(var1) $display("var1 changed, now waiting for var2"); @(var2) $display("var2 changed, now waiting for var1"); end endno subject
Date: 2013-02-27 23:31 (UTC)Чтобы такое сделать, нужна пара новых функций для изменения списка чувствительности на ходу. Структура данных hook_t цепляет процесс к сигналу. Выглядело бы примерно так:
hook_t hook; for (;;) { process_hook (&hook, &var1, 0); process_wait(); process_unhook (&hook); printf ("var1 changed, now waiting for var2\n"); process_hook (&hook, &var2, 0); process_wait(); process_unhook (&hook); printf ("var2 changed, now waiting for var1\n"); }no subject
Date: 2013-02-27 23:39 (UTC)no subject
Date: 2013-02-28 00:03 (UTC)no subject
Date: 2013-02-28 03:16 (UTC)no subject
Date: 2013-02-28 05:33 (UTC)no subject
Date: 2013-03-02 23:57 (UTC)process_wait1 (&var, POSEDGE);
А также для двух и трех переменных:
process_wait2 (&var1, POSEDGE, &var2, NEGEDGE);
process_wait3 (&var1, 0, &var2, 0, &var3, 0);
Достаточно универсальный симулятор получается. Попробую что-нибудь механическое смоделировать.
no subject
Date: 2013-03-03 01:46 (UTC)no subject
Date: 2013-02-27 23:22 (UTC)void одна_функция_для_процесса () {
process_sensitive (&a, 0);
process_sensitive (&b, 0);
for (;;) {
process_wait();
signal_set (&с, f (a, b));
}
}
сделать
void функция_инициализации_для_процесса () {
process_sensitive (&a, функция_эвалюации_для_процесса);
process_sensitive (&b, функция_эвалюации_для_процесса);
}
void функция_эвалюации_для_процесса () {
signal_set (&c, f (a, b));
}
Вообще переключение контекстов - это ртосная ментальная модель.
no subject
Date: 2013-02-27 23:32 (UTC)no subject
Date: 2013-02-27 22:29 (UTC)1. В VCS (насколько я помню по работе в VCS team в 2002 году) есть куча оптимизаций для уплощения и удаления лишних ивентов. Например
assign a = b + c;
assign d = e + f;
assign g = a + d;
при отсутствии других ссылок на a и d уплощается в одно выражение которое не требует ивентов.
2. В VCS все данные для многобитных сигналов хранятся в упакованном виде - например 32 битный вектор типа bit хранится просто как сишный, а 32-битный вектор типа reg - как два числа.
3. VCS оптимизирован для больших дизайнов и имеет большой стартап. Benchmark длительностью полсекунды является бессмысленной.
Вообще в VCS насколько я смутно помню никаких longjump-ов и swapcontext-ов не было, а сгенерированный код разбивался на простые функции, представляющие из себя линейные участки кода.
no subject
Date: 2013-02-27 23:31 (UTC)1. Поэтому и написано не assign, а always @* c неблокирующим присваиванием, чтобы приходилось очереди событий строить.
2. В смысле, что будь это однобитные переменные, VCS был бы еще быстрее? Пожалуй.
3. Да, конечно. Будь бенчмарк подлиннее, VCS был бы еще быстрее.
Соревноваться с VCS в производительности с надеждой выиграть смысла нет. Есть смысл получить очень короткий код, позволяющий писать на Си как на верилоге с производительностью того же порядка.
no subject
Date: 2013-02-27 23:58 (UTC)Мне нужно показать тебе последнюю инкарнацию CycleC в 2001 году. Ты бы заценил. Как нибудь устроим поход в лес и ты увидишь.
no subject
Date: 2013-02-28 00:06 (UTC)no subject
Date: 2013-02-28 03:18 (UTC)no subject
Date: 2013-02-28 20:13 (UTC)if (! _setjmp (old_context)) longjmp(new_context, 1);Пришлось задействовать свою упрощенную реализацию setjmp/longjmp и добавить флаг -U_FORTIFY_SOURCE, чтобы избавиться от всяких внутренних проверок стека в libc.no subject
Date: 2013-02-28 20:20 (UTC)no subject
Date: 2013-02-28 20:49 (UTC)no subject
Date: 2013-03-01 22:51 (UTC)asm ( "mov %%esp, 0(%1) \n" /* Save ESP to context[0] */ "call 1f \n" "1: pop %%ebx \n" /* Compute address of label 1 */ "lea 2f-1b(%%ebx), %%ebx \n" "mov %%ebx, 4(%1) \n" /* Save address of label 2 to context[1] */ "mov 0(%0), %%esp \n" /* Restore ESP from context[0] */ "mov 4(%0), %%ecx \n" /* Get address from context[1] */ "jmp *%%ecx \n " /* Jump to address */ "2: " : : "r" (process_current->context), "r" (old->context) : "bx", "si", "di", "bp");no subject
Date: 2013-03-01 23:35 (UTC)no subject
Date: 2013-03-02 00:21 (UTC)mov r5,(%0)+ mov sp,(%0)+ mov $1f,(%0) mov (%1)+,r5 mov (%1)+,sp jmp (%1) 1:no subject
Date: 2013-03-03 01:47 (UTC)