Creating an electronic component model for Proteus on Lua

    I have several long-term construction projects, one of which is the creation of a computer based on CDP1802. The main board was modeled on paper and in Proteus.
    Pretty soon the question arose: what to do with elements that are absent in Proteus?
    Many resources describe in detail how to create your own C ++ model in Visual Studio.
    Unfortunately, when building under Linux, this option is not very convenient. And what if you don’t know C ++ or need to edit the model on the fly for debugging?
    And I just want to focus on modeling, simplifying everything else as much as possible.
    So the idea came up to do simulation models using scripts - on Lua.
    For those interested, I ask for a cut (2Mb gifs).



    Why is it necessary


    If you forget about all kinds of exotic things, like writing a processor model, I have long lost the habit of doing anything in the simulator - I connected the sensors to various kinds of debugs, an oscilloscope in my hands, a multimeter, JTAG / UART and debug myself.
    But when it was necessary to check the logic of the program in case of GPS / motion failure and the like, I had to write GPS emulation on another microcontroller.
    When it was necessary to do telemetry for a machine using the KWP2000 protocol, debugging live was inconvenient and dangerous. Yes, and if one - oh how uncomfortable.
    The ability to debug / test on the road or somewhere where to carry the entire gentleman's kit with you is simply inconvenient (it is primarily about hobby projects) is a good help, so there is room for the simulator.

    Visual Studio C ++ and GCC


    I write all the software under GCC and I also wanted to build the model under it, using the developed libraries and code that it would be difficult to build under MSVS. The problem was that the assembled under mingw32 DLL hung Proteus. Various methods were tried including manipulations with __thiscall and comrades, and the options with assembler call hacks did not suit.
    A moonglow friend with vast experience in such matters suggested and showed how to rewrite the C ++ interface in C using virtual tables. Of the amenities, in addition to the possibility of assembling under Linux "without interruption from production", the ability, in theory, to write models even on Fortran - would be a desire.

    Mimic under C ++


    The idea of ​​"emulating" virtual classes in practice looks like this:
    The original C ++ virtual class header looks like this
    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;
    };
    


    And here is the C version; this is our pseudo-class and its virtual table

    struct IDSIMMODEL
    {
    	IDSIMMODEL_vtable* vtable;
    };
    


    Now we create a structure with pointers to functions that are inside the class (we will create them and declare them separately)
    
    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 );
    };
    


    We write the necessary functions and create one instance of our "class", which we will use
    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,
    };
    


    And so on, with all the classes we need. Since calling such a structure is not very convenient, wrapper functions were written, some things were automated, missing, often used functions were added. Even while writing this article, I added a lot of new things, looking at work from the other side.

    “Make it as simple as possible, but not easier.”


    As a result, the code grew and the feeling that something needs to be changed grew more and more: it took no less time and effort to create a model than to write the same emulator for a microcontroller. In the process of debugging models, it was constantly necessary to change something, to experiment. I had to reassemble the model on every little thing, and work with text data in C leaves much to be desired. Familiar people who would be interested in this too were scared of C (someone uses Turbo Pascal, someone uses Q Basic).

    I remembered about Lua: it integrates perfectly with C, fast, compact, visual, dynamic typing - all that is needed. As a result, I duplicated all C functions in Lua with the same names, getting a completely self-sufficient way to create models that do not require reassembly at all. You can just take the dll and describe any model only on Lua. It is enough to stop the simulation, correct the text script, and again into battle.

    Modeling in Lua


    The main testing was conducted in Proteus 7, but the models created from scratch and imported into the 8th version behaved excellently.

    Let's create some simple models and see how and what we can do using their example.
    I will not describe how to create a graphical model itself, it is perfectly described here and here , so I will focus on writing code.
    Here are 3 devices that we will consider. At first I wanted to start with a blinking LED, but then I decided that it was too dull, I hope it was right.
    Let's start with A_COUNTER:



    This is the simplest binary counter with an internal clock generator, all its outputs are outputs.

    Each model has a DLL that describes the behavior of the model and its interaction with the outside world. In our case, all dll models will have the same, but the scripts will be different. So, create a model:

    Model description


    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 a required global variable containing a description of the device pins. At this stage, the library only supports digital devices. Support for analog and mixed types in the process.
    is_digital - our output works only with logical levels, while only true
    name is possible - the name of the output on a graphical model. It should exactly match - the binding of the output inside Proteus goes by name.
    The two remaining fields speak for themselves - the pin switching time in picoseconds.

    Required User-Defined Functions


    In fact, there is no strict need to create something in a script. You can write nothing at all - there will be a dummy model, but for minimal functionality you need to create the device_simulate function . This function will be called when the state of the nodes (conductors) changes, for example, the logical level changes. There is a device_init function . it is called (if exists) once immediately after loading the model.
    To set the output state to one of the levels, there is the set_pin_state function , the first argument it takes the name of the output, the second - the desired state, for example, SHI, SLO, FLT, and so on.

    First, let us make all the outputs at logical 0, with using single line /
    We can refer to the output both through a global variable, for example, A0 , and through its name as a string constant “A0” through the global environment table _G
    function device_init()      
        for k, v in pairs(device_pins) do set_pin_state(_G[v.name], SLO) end     
    end
    


    Now we need to implement the counter itself; Let's start with the master oscillator. To do this, there is a timer_callback function that takes two arguments - time and event number.
    Add the following call to device_init after setting the output state:
    set_callback(NOW, PC_EVENT)
    


    PC_EVENT is a numerical variable containing the event code (we must declare it globally)
    NOW means that you need to call the event handler after 0 picoseconds from the current time (the function takes a pic of a second as an argument )
    And here is the function handler
    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
    


    On an event, the set_pin_bool function is called , which controls the output by accepting as an argument one of two states - 1/0.

    You may notice that after switching the output, set_callback is called again, because this function schedules non-periodic events. The difference in the time setting is due to the fact that set_callback will be called in the future, so we need to add the time difference, and time just contains the current system time

    So that's what happened
    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
    



    Everything else - declaration, model initialization, and so on is done on the library side. Although, of course, all the same can be done in C, and Lua can be used for prototyping, since the names of the functions are identical.
    We start the simulation and observe the work of our model



    Debugging features



    The main goal was to facilitate the writing of models and their debugging, so we will consider some options for displaying useful information

    Text messages


    4 functions for outputting to the message log, the last two automatically leading to the stop of the simulation

    out_log("This is just a message")
    out_warning("This is warning")
    out_error("This is error")
    out_fatal("This is fatal error")
    




    Thanks to the capabilities of Lua, you can easily, conveniently, quickly and clearly display any information you need:

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


    Now let's move on to our second model - ROM chips, and look at
    Popup windows

    We will model our ROM and make it run during operation.
    Announcements of conclusions here are no different, but we need to add the properties of our microcircuit, first of all - the ability to load a memory dump from a file:



    This is done in a text script when creating the model:
    {FILE = "Image File", FILENAME, FALSE ,, Image / *. BIN}


    Now let’s make it so that when the simulation was paused, you could see important information about the model, such as the contents of its memory, the contents of the address bus, data bus, and operating time. To output binary data in a convenient form, there is 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
    

    The on_suspend function is called (if declared by the user) during pause. If the window is not created, create it.
    The memory is transferred to the library as a pointer, then you do not need to release anything later - everything will be done by the Lua garbage collector. And create a debug window of the type where we need the variables and for masquerading we will dump 32 bytes from the offset 0x1000:



    Finally, we will implement the algorithm of the ROM itself, ignoring OE, VPP and other CE conclusions

    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
    




    Let's do something for our "debugger":
    create a software UART, in which we will output the contents of the data bus
    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
    




    Performance


    An interesting question that worried me. I took the model of the 4040 binary counter that comes with the Proteus 7 and made my analogue.
    Using a pulse generator, it fed an input signal to both models with a frequency of 100 kHz

    Proteus's 4040 = 15-16% CPU Load
    Library C = 25-28% CPU Load
    Library and Lua 5.2 = 98-100% CPU Load
    Library and Lua 5.3a = 76- 78% CPU Load

    I didn’t compare the sources, but apparently very optimized the virtual machine in version 5.3. Nevertheless, it is quite tolerant for the convenience of work.
    And I didn’t even start to deal with optimization issues.

    This whole project was born as a spontaneous idea, and much more needs to be done:

    Immediate plans


    • Fix explicit bugs in the code
    • Minimize the ability to shoot yourself in the foot
    • Document code under Doxygen
    • Maybe switch to luaJIT
    • Implement analog and mixed device types
    • With plugin for IDA


    Of course, I would like to find like-minded people who want to help if not by participating in writing the code, then by ideas and feedback. Indeed, now much is hardcoded for the goals and objectives that I needed.

    Download without ads and SMS


    Repository with code.
    The finished library and debugging symbols for GDB are here .

    Also popular now: