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

в 6:30, , рубрики: diy или сделай сам, Lua, моделирование цепей, симуляция

Есть у меня несколько проектов-долгостроев, один из которых — создание компьютера на базе CDP1802. Основную плату моделировал на бумаге и в Proteus.
Довольно скоро встал ребром вопрос: как быть с элементами, которые отсутствуют в Proteus?
На многих ресурсах подробно описано, как создать свою модель на С++ в Visual Studio.
К сожалению, при сборке под линуксом этот вариант не очень удобен. Да и как быть, если не знаешь С++ или нужно редактировать модель на лету для отладки?
Да и просто хочется сосредоточиться на моделировании, максимально упростив все остальное.
Так появилась идея делать симуляторные модели с помощью скриптов — на Lua.
Заинтересовавшихся прошу под кат (гифки на 2Мб).

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

Зачем это надо

Если забыть про всякую экзотику, вроде написания модели процессора, я давно отвык что-либо делать в симуляторе — подключил датчики к отладкам разного вида, осциллограф в руки, мультиметр, 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:

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

Это простейший двоичный счетчик с внутренним генератором тактов, все его выводы — выходы.

У каждой модели есть 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 использовать для прототипирования, благо названия функций идентичны.
Запускаем симуляцию и наблюдаем работу нашей модели

Создание модели электронного компонента для Proteus на 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")

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

Благодаря возможностям Lua легко, удобно, быстро и наглядно можно выводить любую нужную информацию:

out_log("We have "..#device_pins.." pins in our device")

Теперь перейдем ко второй нашей модели — микросхемы ПЗУ, и посмотрим на

Всплывающие окна

Смоделируем нашу ПЗУ и подебажим её во время работы.
Объявления выводов тут ничем не отличается, но нам нужно добавить свойств нашей микросхеме, в первую очередь — возможность загрузить дамп памяти из файла:

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

Делается это в текстовом скрипте при создании модели:

{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:

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

Наконец, реализуем сам алгоритм работу ПЗУ, оставив без внимания 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

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

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

создадим программный 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 %.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

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

Производительность

Интересный вопрос, который меня волновал. Я взял модель двоичного счетчика 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js