Создание модели электронного компонента для 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 — наш вывод работает только с логическими уровнями, пока возможен только truename — имя вывода на графической модели. Он должен точно соответствоват — привязка вывода внутри 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, 0×1000) 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, 0×1000) end end Функция on_suspend вызывается (если объявлена пользователем) во время постановки на паузу. Если окно не создано — создадим его.Память передается в библиотеку как указатель, ничего высвобождать потом не нужно — все сделает сборщик мусора Lua. И создадим окно debug типа, куда выведем нужны нам переменные и для масовки сдампим 32 байта со смещения 0×1000:
Наконец, реализуем сам алгоритм работу ПЗУ, оставив без внимания 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 лежат тут.