Традиционно считается, что моделирование цифровой аппаратуры это технически очень сложная задача. Программы-симуляторы для 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-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)