История SystemC
Кто давно занимается моделированием и симуляторами, должен помнить такое слово: SystemC. Двадцать лет назад этот проект возник как новое слово в моделировании цифровых систем. По сути это была библиотека классов Си++ с набором абстракций типа сигнал, порт, событие и т.п. Всё это было в исходных текстах, и работало в принципе достаточно эффективно, но код выглядел слишком громоздко и запутанно. Моделирование в SystemC на уровне сигналов не прижилось. Постепенно стали широко доступны более адекватные средства на языках Verilog и SystemVerilog, и через десять лет SystemC выпал из обоймы.Сейчас, если вам нужно промоделировать цифровую (и не только цифровую, а произвольную дискретную) систему, берите Modelsim от Альтеры. Он входит в бесплатную версию The Intel Quartus Prime Lite Edition.
Но SystemC умер не совсем. Он эволюционировал на системный уровень. В переводе с инженерного жаргона, так называется подход, когда вы моделируете ваш девайс не как набор отдельных проводов или логических элементов И/ИЛИ/НЕ, а как набор крупных блоков: процессор, память, сетевой контроллер. Блоки соединяются параллельными шинами адрес/данные, по которым пересылаются не логические уровни 0 или 1, а пакеты данных, обычно много (десятков или тысяч) байт.
Для моделирования на системном уровне поверх старого SystemC был разработан новый набор классов и интерфейсов, получивший название TLM, или иногда TLM2. Расшифровывается как Transaction Level Modeling, version 2.0. Была ещё (несовместимая) версия TLM 1.0, но она оказалась совсем не в дугу, и про неё поспешили забыть. Везде ниже под TLM понимается TLM версии 2.0.
Существует несколько книжек по SystemC, довольно устаревших, и ни одна из них не описывает TLM. Книжек собственно по TLM нету, увы. Только PDF документация, входящая в дистрибутив systemc-2.3.3 (это последняя на настоящий момент версия). Но документация эта вполне неплохая.
TLM поддерживается некоторыми востребованными коммерческими пакетами, например Synopsys VDK. Фактически, если какой-то пакет заявляет о поддержке SystemC, обычно имеется в виду именно TLM. Примитивный уровень SystemC уже никто не использует.
Установка
Библиотека SystemC, включая TLM, присутствует в виде готового компонента в большинстве современных юниксов. К примеру, на маке установить её можно командой:
brew search systemc
На Ubuntu установка делается так:
sudo apt-get install libsystemc-dev
Модули SystemC
Элементарный моделируемый блок представляет собой класс Си++, в терминологии SystemC называемый модулем. К примеру:
class Memory : public sc_core::sc_module {
public:
Memory(sc_core::sc_module_name name)
: sc_module(name)
{
/// инициализация, выделение памяти ///
}
~Memory()
{
/// освобождение памяти ///
}
/// прочие дела ///
};
Модули наследуются от класса sc_module, который содержит всякие полезные методы для управления временем, событиями, синхронизации с другими модулями и т.п. Всякий модуль получает имя, чтобы легче отслеживать его при трассировке и отладке. Модули бывают пассивные (как выше) и активные. Пассивный модуль отзывается на обращения к нему со стороны других модулей, но по своей инициативе действий не предпринимает. Пассивный модуль обычно не управляет временем (не вызывает wait()).
Активный модуль
Активный модуль представляет собой отдельный поток управления (thread) и служит инициатором действий в системе. От пассивного модуля он отличается присутствием кострукций SC_HAS_PROCESS() и SC_THREAD(). Метод main_thread() в примере ниже выполняется в отдельном потоке параллельно с другими модулями. Он должен содержать вызовы wait(), чтобы модельное время продвигалось вперёд.
class Processor : public sc_core::sc_module {
public:
SC_HAS_PROCESS(Processor);
Processor(sc_core::sc_module_name name)
: sc_core::sc_module(name)
{
SC_THREAD(main_thread);
}
virtual ~Processor() {}
private:
void main_thread()
{
/// алгоритм работы модуля ///
}
};
Что такое транзакция
Процесс функционирования моделируемой системы представляет собой взаимодействие модулей, разворачивающееся во времени. Скажем, процессор читает из памяти некоторое количество байтов, и это занимает сколько-то наносекунд. Потом процессор перемалывает данные (ещё несколько наносекунд), и отправляет их в трансивер для отправки по сети (сколько-то нано или миллисекунд).Согласно методике TLM, каждое элементарное взаимодействие это пересылка пакета данных от одного модуля другому. Такое взаимодействие называется транзакцией и реализуется в виде передачи ссылки на структуру типа tlm_generic_payload. К примеру, для отправки данных (операция записи), транзакция заполняется так:
tlm::tlm_generic_payload trans;
trans.set_command(tlm::TLM_WRITE_COMMAND);
trans.set_address(address);
trans.set_data_length(nbytes);
trans.set_data_ptr(data);
Транзакция содержит четыре основных компонента: - Тип операции: запись (отправка) или чтение (приём) данных
- Адрес данных в адресном пространстве целевого модуля
- Размер данных в байтах
- Указатель на буфер с данными
Есть еще другие второстепенные компоненты, но это уже детали, пока для нас несущественные.
Сокеты как механизм общения модулей
Чтобы иметь возможность отправлять или отвечать на транзакции, каждый модуль объявляет у себя поле класса сокет. Можно иметь несколько сокетов в модуле, при необходимости. При инициализации каждому сокету-отправителю назначается сокет-получатель из другого модуля. Таким образом все модули объединяются в связанную взаимодействующую систему.Пример сокета-получателя в (пассивном) модуле памяти:
class Memory : public sc_core::sc_module {
public:
...
tlm_utils::simple_target_socket<Memory> socket;
...
};
Пример сокета-отправителя в (активном) модуле процессора:
class Processor : public sc_core::sc_module {
public:
...
tlm_utils::simple_initiator_socket<Processor> socket;
...
};
Связывание сокетов при инициализации:
// Создаём объекты модулей памяти и процессора.
Memory mem("mem1");
Processor cpu("cpu1");
// Привязываем сокет-получатель к сокету-отправителю.
cpu.socket(mem.socket);
Отправка транзакций
При взаимодействии с другим модулем обычно стоит одна из двух задач: или записать данные по некоторому адресу в другом модуле, или прочитать их оттуда. Нам нужен буфер с данными (unsigned char buffer[]), размер данных в байтах (unsigned nbytes) и адрес данных в модуле назначения (unsigned address). Скажем, операция записи выглядит следующим образом:
// Создаём транзакцию и наполняем её нужными параметрами.
tlm::tlm_generic_payload trans;
trans.set_command(tlm::TLM_WRITE_COMMAND);
trans.set_address(address);
trans.set_data_ptr(buffer);
trans.set_data_length(nbytes);
// Здесь мы получим время выполнения операции модулем-получателем.
sc_core::sc_time latency = sc_core::SC_ZERO_TIME;
// Отправляем транзакцию через сокет.
socket->b_transport(trans, latency);
// Продвигаем время на нужную величину.
wait(latency);
// Анализируем код ошибки.
if (trans.get_response_status() != tlm::TLM_OK_RESPONSE)
SC_REPORT_FATAL("Write", "Bad response\n");
Заметьте: в сокете-отправителе имеется метод b_transport(), которым мы пользуемся для отправки транзакции модулю-получателю. Название расшифровывается как Blocking Transport function. При этом в переменной latency мы получаем длительность времени, потребовавшегося получателю на выполнение запрошенного действия. Чтобы учесть длительность операции, мы вызываем метод wait(), который продвигает модельное время на нужную величину. Во время выполнения wait() в других модулях могут происходить другие события. Нужно помнить, что все модули выполняются логически параллельно и одновременно. Пока одни модули "ждут", другие заняты своими делами.
Также при возврате транзакция содержит код ошибки, который можно получить вызовом get_response_status(). Модуль-получатель может ответить кодом ошибки, скажем, если мы обращаемся по некорректному адресу, или с размером данных, который не поддерживается.
Приём транзакций
Когда сокет-получатель принимает запрос на транзакцию, он вызывает соответствующий локальный метод модуля для её обработки. Набор нужных методов для сокета регистрируется в конструкторе модуля при инициализации. Например:
class Memory : public sc_core::sc_module {
public:
tlm_utils::simple_target_socket<Memory> socket;
Memory(sc_core::sc_module_name name)
: sc_module(name)
{
...
socket.register_b_transport(this, &Memory::b_transport);
...
}
...
private:
virtual void b_transport(tlm::tlm_generic_payload &trans, sc_core::sc_time &delay)
{
...
}
};
В конструкторе Memory() для сокета вызывается метод register_b_transport(), который регистрирует приватный в этом модуле метод b_transport() для обработки входящих транзакций. Когда модуль-отправитель сформирует для нас транзакцию и вызовет в своём сокете метод b_transport(), по цепочке будет вызван наш метод b_transport() с параметрами от отправителя.
Обработка транзакций
Выполнение транзакции в методе b_transport() пассивного модуля может выглядеть следующим образом:
virtual void b_transport(tlm::tlm_generic_payload &trans, sc_core::sc_time &delay)
{
// Извлекаем параметры из транзакции.
tlm::tlm_command cmd = trans.get_command();
uint64_t addr = trans.get_address();
unsigned char * ptr = trans.get_data_ptr();
unsigned int len = trans.get_data_length();
// Проверяем, не выходит ли адрес за границы памяти.
if (addr + len > 0x1000000) {
trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
SC_REPORT_FATAL("Memory", "Unsupported access\n");
return;
}
if (cmd == tlm::TLM_READ_COMMAND) {
// Чтение: копируем данные из внутреннего буфера.
memcpy(ptr, &mem_buf[addr], len);
}
if (cmd == tlm::TLM_WRITE_COMMAND) {
// Запись: копируем данные во внутренний буфер.
memcpy(&mem_buf[addr], ptr, len);
}
// Операция занимает три наносекунды модельного времени.
delay += 3 * sc_core::SC_NS;
// Все хорошо.
trans.set_response_status(tlm::TLM_OK_RESPONSE);
}
Заметьте: пассивный модуль, обрабатывающий транзакцию, не пользуется вызовом wait(). Вместо этого он наращивает значение параметра delay и даёт возможность вызывающему модулю продвинуть модельное время. Другие возможности TLM
У сокетов есть другие методы для прочих применений: неблокирующие операции, получение прямого указателя в память, бэкдор для отладочного доступа (за нулевое время) и прочие. Всё это можно в подробностях и с картинками посмотреть в документе TLM_2_0_presentation.pdf в дистрибутиве SystemC.Но и описанного выше вполне достаточно, чтобы строить полезные симуляции масштабных систем. Вот первый пример, который я нагуглил: процессор RISC-V, способный выполнять операционную систему FreeRTOS. Код на Гитхабе: https://github.com/mariusmm/RISC-V-TLM

no subject
Date: 2021-01-28 10:20 (UTC)написать тул для моделирования, из которого торчат блестящие безопасные рычажки - ну ок
а вот когда вообще всё на уровне крестового кода - это только для труъ джедаев, то есть очень узкого круга тех страшно далёких от народа, хто может в железо и софт на высоком уровне плюс обладает железными нервами и дисциплиной
no subject
Date: 2021-01-28 14:14 (UTC)Это в бизнес-софте могут наворотить сотню уровней, и там С++ неприменим.
no subject
Date: 2021-01-28 19:13 (UTC)Поэтому так трудно найти хорошего симуляторщика. Все боятся стрельнуть в ногу, никто не умеет делать симуляторы.
no subject
Date: 2021-01-28 19:17 (UTC)no subject
Date: 2021-01-28 20:21 (UTC)коты делали функциональный симулятор интеловского железа в интеле, поэтому немного в курсе
no subject
Date: 2021-01-28 21:42 (UTC)Сам я стараюсь воздерживаться от Си++ везде, где это возможно. Я видел большие проекты, которые крэшились только от того, что были написаны на Си++. Мало того, провал проекта был большим облегчением для фирмы. :)
no subject
Date: 2021-01-29 00:31 (UTC)зато позволяет писать что угодно на любом уровне абстракции для мэйнстримной железной архитектуры. Для системного или очень требовательного к производительности софта это единственный вариант, но лучше только слоёв нижнего уровня, чтобы наружу торчали специально обученные рычажки, за которые можно дёргать каким-нть питоном или обёрткой вроде qt
дубина для стучания по головам потяжелее, гайдлайны и архитектурные митинги позволяют как-то выжывать
no subject
Date: 2021-01-29 01:55 (UTC)