...

пятница, 3 октября 2014 г.

Создание модели электронного компонента для Proteus на Lua

Есть у меня несколько проектов-долгостроев, один из которых — создание компьютера на базе 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: %.4X\nData: %.4X\n", ADDRESS, string.byte(rom, ADDRESS)))
dump_to_debug_popup(debugpop, rom, 32, 0x1000)
elseif debugpop then
print_to_debug_popup(debugpop, string.format("Address: %.4X\nData: %.4X\n", 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



Сделаем что-нибудь для нашего «отладчика»:


создадим программный UART, в который будем выводить содержимое шины данных


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 %.2X\r\n", 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 лежат тут.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий