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

Why do you need it
If you forget about any exotics, such as writing a processor model, I have long been unaccustomed to do anything in the simulator - I connected sensors to debugs of a different type, 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 failure / in motion and the like, I had to write a GPS emulation on another microcontroller.
When it was necessary to do telemetry for a machine under the KWP2000 protocol, debugging “live” was inconvenient and dangerous. And if one - oh, how uncomfortable.
The ability to debug / test on the road or somewhere, where to carry with you the entire gentleman's set is simply inconvenient (we are talking primarily about hobby projects) - a good help, so there is a place for the simulator.
Visual Studio C ++ and GCC
I am writing all the software for GCC and I would also like to assemble the model under it, using the established libraries and code that it would be difficult to assemble under MSVS. The problem was that the Proteus was built under the mingw32 DLL. Various methods including manipulations with __thiscall and associates have been tried, but the options with assembly hacks did not suit the calls.
A
moonglow friend with extensive experience in such matters suggested and showed how to rewrite the C ++ interface in C using virtual tables. Of the conveniences, apart from the possibility of assembling under Linux "on the job," the possibility, in theory, to write models even on Fortran would be a desire.
')
Mimicking 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 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 copy 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 it is not very convenient to call such a structure, wrapper functions were written, some things were automated, missing, frequently used functions were added. Even in the process of writing this article, I added a lot of new things, looking at the work from the other side.
“Make it as simple as possible, but not simpler.”
As a result, the code grew and the feeling grew that it was necessary to change something: it took less time and effort to create a model than to write the same emulator for the microcontroller. In the process of debugging models, it was necessary to constantly change something, experiment. We had to rebuild the model on every detail, and working with text data in C leaves much to be desired. Familiar, who would also be interested in this, were frightened by C (someone uses
Turbo Pascal, someone uses
Q Basic).
I remembered Lua: it perfectly integrates into C, it is fast, compact, clear, dynamic typing is all that is needed. As a result, I duplicated all the C functions in Lua with the same names, getting a completely self-contained method of creating models that does not require reassembly at all. You can simply 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 simplest models and use their example to see what and how we can do.
I will not describe how to create a graphical model itself, it is perfectly described
here and
here , so I’ll stop on writing code.
Here are 3 devices that we will consider. I wanted to first start with the flashing of the LED, but then I decided that it was too sad, I hope I had not lost.
Let's start with A_COUNTER:

This is the simplest binary counter with an internal clock generator, all its conclusions are outputs.
Each model has a DLL that describes the behavior of the model and interaction with the outside world. In our case, all models dll will be the same, but the scripts are 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},
device_pins is a mandatory global variable containing a description of the device pins. At this stage, the library only supports digital devices. Support analog and mixed types in process.
is_digital - our output works only with logical levels, while only true is possible
name - the name of the output on the graphic model. It must match exactly - the output binding inside Proteus goes by name.
The two remaining fields speak for themselves - the pin switch time in picoseconds.
Required user defined functions
In fact, there is no strict need to create something in the script. You can not write anything at all - there will be a dummy model, but for the minimum functionality you need to create a function
device_simulate . 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 state of the output to one of the levels, there is the
set_pin_state function, the first argument is the name of the output, the second is the desired state, for example, SHI, SLO, FLT, and so on
To begin with, we will do so that at the launch all the conclusions are in logical 0, with the help of one-liner /
We can refer to the output as 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. For this there is a function
timer_callback , which takes two arguments - the time and the number of the event.
Add the following call to the device_init after setting the output status:
set_callback(NOW, PC_EVENT)
PC_EVENT is a numeric variable containing the event code (we must declare it globally)
NOW means that the event handler must be called after 0 picoseconds from the current time (the function takes as argument a
pico of a second)
And here is the handler function
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
Upon the event, the
set_pin_bool function is
called , which controls the output, taking as an argument one of the two states, 1/0.
You may notice that after switching the output, the set_callback is called again, because this function is planning non-periodic events. The difference in the task time 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
Total, 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 - the 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, the benefit of the function names are identical.
Run the simulation and observe the operation of our model.

Debugging Features
The main goal was to facilitate the writing of models and their debugging, so consider some of the possibilities of displaying useful information
Text messaging
4 functions for outputting messages to the log, the latter two automatically leading to stopping 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 necessary information:
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
Let's model our ROM and podabazhim it in operating time.
Announcements of the conclusions here are no different, but we need to add the properties of our chip, first of all - the ability to load a memory dump from a file:

This is done in a text script when creating a model:
{FILE = "Image File", FILENAME, FALSE ,, Image / *. BIN}
Now we will make it so that when pausing the simulation, it was possible to view important information about the model, such as the contents of its memory, the contents of the address bus, the data bus, the running time. To output binary data in a convenient form, there is a 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 nothing needs to be released; the Lua garbage collector will do everything. And we will create a
debug type window, where we will output the variables we need and, for masking, we will transfer 32 bytes from the offset 0x1000:

Finally, we implement the algorithm of the ROM itself, ignoring the OE, VPP and other CE findings
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 display 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}, }

Performance
An interesting question that worried me. I took the model of the 4040 binary counter that comes with the Proteus 7 delivery and made my analogue.
Using a pulse generator gave the input to both models of a square wave 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 did not compare the source code, but apparently optimized the virtual machine very much in version 5.3. However, it is quite tolerant for the convenience of work.
Yes, and optimization issues, I did not even begin to engage.
This whole project was born as a spontaneous idea, and a lot more needs to be done:
Immediate plans
- Fix obvious bugs in code
- Minimize the ability to shoot yourself in the foot
- Document Doxygen code
- Perhaps go 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 with their participation in writing code, then with ideas and feedback. After all, now a lot of things are hardcoded for the goals and tasks that I needed.
Download no ads and SMS
Repository with code.
The ready-made library and debugging symbols for GDB are
here .