Создание модели электронного компонента для 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 лежат тут.
    • +28
    • 23,6k
    • 4
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 4
    • 0
      А в чём писали на Lua? И как эмулировали логику нужной микросхемы? Ну, т.е. откуда знали, что микросхема должна вести себя именно так? Пользовались только datesheet'ами? Вообще, интересно, как правильно писать эмуляторы микросхем. Только по datasheet'у? Это тогда получается такой сложный reverse engineering.
      • +1
        «В чем» это в плане редактора? В vi :)
        Да, в даташитах есть временные диаграмы всех процессов. И да, тут именно симуляция.
      • 0
        Интересно, люди устраивают холивары по поводу «underscore» vs. «camelCase»
        • +1
          Для интересующихся:
          Вышла версия 0.2, в которой появились зачатки ООП и было изменено практически всё. Но ещё делать и делать.
          Например, до сих пор не дописана инструкция и комментарии к коду.

          Готовая сборка тут.
          github.com/Pugnator/openvsm/releases

          Если кто-то всё же пользуется, то я рад был бы услышать отзывы

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.