Есть у меня несколько проектов-долгостроев, один из которых — создание компьютера на базе CDP1802. Основную плату моделировал на бумаге и в Proteus.
Довольно скоро встал ребром вопрос: как быть с элементами, которые отсутствуют в Proteus?
На многих ресурсах подробно описано, как создать свою модель на С++ в Visual Studio.
К сожалению, при сборке под линуксом этот вариант не очень удобен. Да и как быть, если не знаешь С++ или нужно редактировать модель на лету для отладки?
Да и просто хочется сосредоточиться на моделировании, максимально упростив все остальное.
Так появилась идея делать симуляторные модели с помощью скриптов — на Lua.
Заинтересовавшихся прошу под кат (гифки на 2Мб).
Зачем это надо
Если забыть про всякую экзотику, вроде написания модели процессора, я давно отвык что-либо делать в симуляторе — подключил датчики к отладкам разного вида, осциллограф в руки, мультиметр, JTAG/UART и отлаживай себе.
Но когда понадобилось проверить логику работы программы при отказе GPS/в движении и тому подобном, пришлось писать эмуляцию GPS на другом микроконтроллере.
Когда было необходимо сделать телеметрию для машину под протокол KWP2000, отлаживать «на живую» было неудобно и опасно. Да и если одному — ой как неудобно.
Возможность отлаживать/тестировать в дороге или где-то, куда таскать с собой весь джентльменский набор просто неудобно (речь в первую очередь про хобби проекты) — хорошее подспорье, так что место симулятору есть.
Visual Studio C++ и GCC
Весь софт я пишу под GCC и модель я хотел так же собирать под ним, используя наработанные библиотеки и код, которые собрать под MSVS было бы затруднительно. Проблема заключалась в том, что собранная под mingw32 DLL вешала Proteus. Были перепробованы разные способы включая манипуляции с __thiscall и сотоварищи, а варианты с ассемблерными хаками вызовов не устраивал.
Друг moonglow с огромным опытом в таких делах предложил и показал как переписать С++ интерфейс на С, используя виртуальные таблицы. Из удобств, кроме возможности сборки под линуксом «без отрыва от производства», возможность, в теории, писать модели хоть на фортране — было бы желание.
Мимикрируем под С++
Идея с «эмуляцией» виртуальных классов на практике выглядит так:
Оригинальный С++ заголовок виртуального класса выглядит так
class IDSIMMODEL
{
public:
virtual INT isdigital ( CHAR* pinname ) = 0;
virtual VOID setup ( IINSTANCE* instance, IDSIMCKT* dsim ) = 0;
virtual VOID runctrl ( RUNMODES mode ) = 0;
virtual VOID actuate ( REALTIME time, ACTIVESTATE newstate ) = 0;
virtual BOOL indicate ( REALTIME time, ACTIVEDATA* newstate ) = 0;
virtual VOID simulate ( ABSTIME time, DSIMMODES mode ) = 0;
virtual VOID callback ( ABSTIME time, EVENTID eventid ) = 0;
};
А вот версия на С; это наш псевдо-класс и его виртуальная таблица
struct IDSIMMODEL
{
IDSIMMODEL_vtable* vtable;
};
Теперь создаем структуру с указателями на функции, которые внутри класса (их мы создадим и объявим отдельно)
struct IDSIMMODEL_vtable
{
int32_t __attribute__ ( ( fastcall ) ) ( *isdigital ) ( IDSIMMODEL* this, EDX, CHAR* pinname );
void __attribute__ ( ( fastcall ) ) ( *setup ) ( IDSIMMODEL* this, EDX, IINSTANCE* inst, IDSIMCKT* dsim );
void __attribute__ ( ( fastcall ) ) ( *runctrl ) ( IDSIMMODEL* this, EDX, RUNMODES mode );
void __attribute__ ( ( fastcall ) ) ( *actuate ) ( IDSIMMODEL* this, EDX, REALTIME atime, ACTIVESTATE newstate );
bool __attribute__ ( ( fastcall ) ) ( *indicate ) ( IDSIMMODEL* this, EDX, REALTIME atime, ACTIVEDATA* data );
void __attribute__ ( ( fastcall ) ) ( *simulate ) ( IDSIMMODEL* this, EDX, ABSTIME atime, DSIMMODES mode );
void __attribute__ ( ( fastcall ) ) ( *callback ) ( IDSIMMODEL* this, EDX, ABSTIME atime, EVENTID eventid );
};
Пишем нужные функции и создаем один экземпляр нашего «класса», который и будем использовать
IDSIMMODEL_vtable VSM_DEVICE_vtable =
{
.isdigital = vsm_isdigital,
.setup = vsm_setup,
.runctrl = vsm_runctrl,
.actuate = vsm_actuate,
.indicate = vsm_indicate,
.simulate = vsm_simulate,
.callback = vsm_callback,
};
IDSIMMODEL VSM_DEVICE =
{
.vtable = &VSM_DEVICE_vtable,
};
И так далее, со всеми нужными нам классами. Так как вызывать такое из структур не очень удобно, были написаны функции-обертки, какие-то вещи были автоматизированы, были добавлены отсутствующие, часто используемые функции. Даже в процессе написания этой статьи я добавил много нового, посмотрев на работу с другой стороны.
«Сделай настолько просто, насколько это возможно, но не проще»
В итоге код рос и все более нарастало ощущение, что нужно что-то менять: на создание модели уходило сил и времени не меньше, чем на написания такого же эмулятора для микроконтроллера. В процессе отладки моделей требовалось постоянно что-то менять, экспериментировать. Приходилось пересобирать модель на каждой мелочи, да и работа с текстовыми данными в С оставляет желать лучшего. Знакомые, которым такое тоже было бы интересно, пугались С (кто-то использует ТурбоПаскаль, кто-то QBasic).
Вспомнил о Lua: прекрасно интегрируется в С, быстр, компактен, нагляден, динамическая типизация — все что надо. В итоге продублировал все С функции в Lua с теми же названиями, получив полностью самодостаточный способ создания моделей, не требующий пересборки вообще. Можно просто взять dll и описать любую модель только на Lua. Достаточно остановить симуляцию, подправить текстовый скрипт, и снова в бой.
Моделирование в Lua
Основное тестирование велось в Proteus 7, но созданные с нуля и импортированные в 8-ю версию модели вели себя превосходно.
Создадим несколько простейших моделей и на их примере посмотрим, что и как мы можем сделать.
Я не буду описывать, как создать собственно графическую модель, это отлично описано тут и тут, поэтому остановлюсь именно на написании кода.
Вот 3 устройства, которые мы будем рассматривать. Я хотел сначала начать с мигания светодиодом, но потом решил, что это слишком уныло, надеюсь, не прогадал.
Начнем с A_COUNTER:
Это простейший двоичный счетчик с внутренним генератором тактов, все его выводы — выходы.
У каждой модели есть DLL, которая описывает поведение модели и взаимодействие с внешним миром. В нашем случае, у всех моделей dll будет одна и та же, а вот скрипты — разные. Итак, создаем модель:
Описание модели
device_pins =
{
{is_digital=true, name = "A0", on_time=100000, off_time=100000},
{is_digital=true, name = "A1", on_time=100000, off_time=100000},
{is_digital=true, name = "A2", on_time=100000, off_time=100000},
{is_digital=true, name = "A3", on_time=100000, off_time=100000},
--тут пропущены однотипные определения для остальных выводов
--чтобы не прятать под кат
{is_digital=true, name = "A15", on_time=100000, off_time=100000},
}
device_pins это обязательная глобальная переменная, содержащая описание выводов устройства. На данном этапе библиотека поддерживает только цифровые устройства. Поддержка аналоговых и смешанных типов в процессе.
is_digital — наш вывод работает только с логическими уровнями, пока возможен только true
name — имя вывода на графической модели. Он должен точно соответствоват — привязка вывода внутри Proteus идет по имени.
Два оставшихся поля говорят сами за себя — время переключения пина в пикосекундах.
Необходимые функции, объявляемые пользователем
На самом деле, нет строгой необходимости создавать что-то в скрипте. Можно вообще ничего не писать — будет модель пустышка, но для минимального функционала нужно создать функцию device_simulate. Эта функция будет вызываться, когда изменится состояние нод (проводников), например, изменится логический уровень. Есть функция device_init. она вызывается (если существует) однократно сразу после загрузки модели.
Для установки состояния вывода в один из уровней есть функция set_pin_state, первым аргументом она принимает имя вывода, вторым — желаемое состояние, например, SHI, SLO, FLT и так далее
Для начала сделаем так, чтобы на запуске все выводы находились в логическом 0, с помощью однострочника/
Мы можем обращаться к выводу как через глобальную переменную, к примеру, A0, Так и через её имя как строковую константу «А0» через глобальную таблицу окружения _G
function device_init()
for k, v in pairs(device_pins) do set_pin_state(_G[v.name], SLO) end
end
Теперь нам нужно реализовать сам счетчик; Начнем с задающего генератора. Для этого есть функция timer_callback, принимающую два аргумента — время и номер события.
Добавим в device_init после выставления состояние вывода следующий вызов:
set_callback(NOW, PC_EVENT)
PC_EVENT это числовая переменная, содержащая код события (её мы должны объявить глобально)
NOW означает что вызвать обработчик события нужно через 0 пикосекунд от текущего времени (функция принимает как аргумент пикосекунды)
А вот и функция обработчик
function timer_callback(time, eventid)
if eventid == PC_EVENT then
for k, v in pairs(device_pins) do
set_pin_bool(_G[v.name], get_bit(COUNTER, k) )
end
COUNTER = COUNTER + 1
set_callback(time + 100 * MSEC, PC_EVENT)
end
end
По событию вызывается функция set_pin_bool, которая управляет выводом принимая как аргумент одно из двух состояний — 1/0.
Можно заметить, что после переключения вывода снова вызывается set_callback, ибо эта функция планирует непериодические события. Разница в задании времени из-за того, что set_callback будет вызвана в будущем, поэтому нам нужно добавить разницу во времени, а time как раз содержит текущее системное время
device_pins =
{
{is_digital=true, name = "A0", on_time=100000, off_time=100000},
{is_digital=true, name = "A1", on_time=100000, off_time=100000},
{is_digital=true, name = "A2", on_time=100000, off_time=100000},
{is_digital=true, name = "A3", on_time=100000, off_time=100000},
{is_digital=true, name = "A4", on_time=100000, off_time=100000},
{is_digital=true, name = "A5", on_time=100000, off_time=100000},
{is_digital=true, name = "A6", on_time=100000, off_time=100000},
{is_digital=true, name = "A7", on_time=100000, off_time=100000},
{is_digital=true, name = "A8", on_time=100000, off_time=100000},
{is_digital=true, name = "A9", on_time=100000, off_time=100000},
{is_digital=true, name = "A10", on_time=100000, off_time=100000},
{is_digital=true, name = "A11", on_time=100000, off_time=100000},
{is_digital=true, name = "A12", on_time=100000, off_time=100000},
{is_digital=true, name = "A13", on_time=100000, off_time=100000},
{is_digital=true, name = "A14", on_time=100000, off_time=100000},
{is_digital=true, name = "A15", on_time=100000, off_time=100000},
}
PC_EVENT = 0
COUNTER = 0
function device_init()
for k, v in pairs(device_pins) do set_pin_state(_G[v.name], SLO) end
set_callback(0, PC_EVENT)
end
function timer_callback(time, eventid)
if eventid == PC_EVENT then
for k, v in pairs(device_pins) do
set_pin_bool(_G[v.name], get_bit(COUNTER, k) )
end
COUNTER = COUNTER + 1
set_callback(time + 100 * MSEC, PC_EVENT)
end
end
Все остальное — объявление, инициализация модели и так далее делается на стороне библиотеки. Хотя разумеется, все то же самое можно сделать на С, а Lua использовать для прототипирования, благо названия функций идентичны.
Запускаем симуляцию и наблюдаем работу нашей модели
Возможности отладки
Основной целью было облегчение написания моделей и их отладки, поэтому рассмотрим некоторые возможности вывода полезной информации
Текстовые сообщения
4 функции для вывода в лог сообщений, причем две последнии автоматически приведут к остановку симуляции
out_log("This is just a message")
out_warning("This is warning")
out_error("This is error")
out_fatal("This is fatal error")
Благодаря возможностям Lua легко, удобно, быстро и наглядно можно выводить любую нужную информацию:
out_log("We have "..#device_pins.." pins in our device")
Теперь перейдем ко второй нашей модели — микросхемы ПЗУ, и посмотрим на
Всплывающие окна
Смоделируем нашу ПЗУ и подебажим её во время работы.
Объявления выводов тут ничем не отличается, но нам нужно добавить свойств нашей микросхеме, в первую очередь — возможность загрузить дамп памяти из файла:
Делается это в текстовом скрипте при создании модели:
{FILE=«Image File»,FILENAME,FALSE,,Image/*.BIN}
Теперь сделаем так, что при постановке на паузу симуляции можно было посмотреть важную информацию о модели, такую как содержимое её памяти, содержимое адресной шины, шины данных, время работы. Для вывода бинарных данных в удобной форме есть memory_popup.
function device_init()
local romfile = get_string_param("file")
rom = read_file(romfile)
mempop, memid = create_memory_popup("My ROM dump")
set_memory_popup(mempop, rom, string.len(rom))
end
function on_suspend()
if nil == debugpop then
debugpop, debugid = create_debug_popup("My ROM vars")
print_to_debug_popup(debugpop, string.format("Address: %.4XnData: %.4Xn", ADDRESS, string.byte(rom, ADDRESS)))
dump_to_debug_popup(debugpop, rom, 32, 0x1000)
elseif debugpop then
print_to_debug_popup(debugpop, string.format("Address: %.4XnData: %.4Xn", ADDRESS, string.byte(rom, ADDRESS)))
dump_to_debug_popup(debugpop, rom, 32, 0x1000)
end
end
Функция on_suspend вызывается (если объявлена пользователем) во время постановки на паузу. Если окно не создано — создадим его.
Память передается в библиотеку как указатель, ничего высвобождать потом не нужно — все сделает сборщик мусора Lua. И создадим окно debug типа, куда выведем нужны нам переменные и для масовки сдампим 32 байта со смещения 0x1000:
Наконец, реализуем сам алгоритм работу ПЗУ, оставив без внимания OE, VPP и прочие CE выводы
function device_simulate()
for i = 0, 14 do
if 1 == get_pin_bool(_G["A"..i]) then
ADDRESS = set_bit(ADDRESS, i)
else
ADDRESS = clear_bit(ADDRESS, i)
end
end
for i = 0, 7 do
set_pin_bool(_G["D"..i], get_bit(string.byte(rom, ADDRESS), i))
end
end
Сделаем что-нибудь для нашего «отладчика»:
device_pins =
{
{is_digital=true, name = "D0", on_time=1000, off_time=1000},
{is_digital=true, name = "D1", on_time=1000, off_time=1000},
{is_digital=true, name = "D2", on_time=1000, off_time=1000},
{is_digital=true, name = "D3", on_time=1000, off_time=1000},
{is_digital=true, name = "D4", on_time=1000, off_time=1000},
{is_digital=true, name = "D5", on_time=1000, off_time=1000},
{is_digital=true, name = "D6", on_time=1000, off_time=1000},
{is_digital=true, name = "D7", on_time=1000, off_time=1000},
{is_digital=true, name = "TX", on_time=1000, off_time=1000},
}
-- UART events
UART_STOP = 0
UART_START = 1
UART_DATA=2
-- Constants
BAUD=9600
BAUDCLK = SEC/BAUD
BIT_COUNTER = 0
-----------------------------------------------------------------
DATA_BUS = 0
function device_init()
end
function device_simulate()
for i = 0, 7 do
if 1 == get_pin_bool(_G["D"..i]) then
DATA_BUS = set_bit(DATA_BUS, i)
else
DATA_BUS = clear_bit(DATA_BUS, i)
end
end
uart_send(string.format("[%d] Fetched opcode %.2Xrn", systime(), DATA_BUS))
end
function timer_callback(time, eventid)
uart_callback(time, eventid)
end
function uart_send (string)
uart_text = string
char_count = 1
set_pin_state(TX, SHI) -- set TX to 1 in order to have edge transition
set_callback(BAUDCLK, UART_START) --schedule start
end
function uart_callback (time, event)
if event == UART_START then
next_char = string.byte(uart_text, char_count)
if next_char == nil then
return
end
char_count = char_count +1
set_pin_state(TX, SLO)
set_callback(time + BAUDCLK, UART_DATA)
end
if event == UART_STOP then
set_pin_state(TX, SHI)
set_callback(time + BAUDCLK, UART_START)
end
if event == UART_DATA then
if get_bit(next_char, BIT_COUNTER) == 1 then
set_pin_state(TX, SHI)
else
set_pin_state(TX, SLO)
end
if BIT_COUNTER == 7 then
BIT_COUNTER = 0
set_callback(time + BAUDCLK, UART_STOP)
return
end
BIT_COUNTER = BIT_COUNTER + 1
set_callback(time + BAUDCLK, UART_DATA)
end
end
Производительность
Интересный вопрос, который меня волновал. Я взял модель двоичного счетчика 4040, идущего в поставке Proteus 7 и сделал свой аналог.
Используя генератор импульсов подал на вход обоим моделям меандр с частотой 100кГц
Proteus's 4040 = 15-16% CPU Load
Библиотека на С = 25-28% CPU Load
Библиотека и Lua 5.2 = 98-100% CPU Load
Библиотека и Lua 5.3a = 76-78% CPU Load
Не сравнивал исходники, но видимо очень сильно оптимизировали виртуальную машину в версии 5.3. Тем ни менее, вполне терпимо за удобство работы.
Да и вопросами оптимизации я даже не начинал заниматься.
Весь этот проект родился как спонтанная идея, и ещё много чего нужно сделать:
Ближайшие планы
- Пофиксить явные баги в коде
- Максимально уменьшить возможность выстрелить себе в ногу
- Документировать код под Doxygen
- Возможно, перейти на luaJIT
- Реализовать аналоговые и смешанные типы устройств
- С плагин для IDA
Разумеется, хотелось бы найти единомышленников, желающих помочь если и не участием в написании кода, то идеями и отзывами. Ведь сейчас многое захардкодено под цели и задачи, которые нужны были мне.
Скачать без рекламы и смс
Репозиторий с кодом.
Готовая библиотека и отладочные символы для GDB лежат тут.
Автор: Pugnator