Sequential Program Simulation

With the message and timer callbacks explained before, you can model already everything.

However, when you want to model a sequential execution, it can be quite cumbersome to do this with the event based mechanism. For example if you want to model the execution of a software program like this

  • Get inputs (from Moddy messages), takes 100 µs

  • Calculate, takes 1ms

  • Set outputs, takes 200 µs

With the event based mechanism, you would need a state machine in the timer’s callback routine, which is not easy to understand.

So Moddy allows an alternative way to model those sequential programs, simply called “Programs” in the following.

To create a “program”, your Part must be a subclass of either VThread or VSimpleProg. (More about the difference between them in chapter VSimpleProg and VThread).

Program Model

class Client(moddy.VSimpleProg):
    def __init__(self, sim):
        super().__init__(sim=sim, obj_name="Client", parent_obj=None)
        self.create_ports('QueuingIO', ['net_port'])

    def run_vthread(self):
        while True:
            self.wait(1.2*MS)
            self.net_port.send('test', 100*US)
            self.busy(100*US, 'TX1', whiteOnBlue)
            self.net_port.send('test1', 100*US)
            self.busy(100*US, 'TX2', whiteOnRed)
../_images/0200_vthread.png

In the above example, you see a very simple sequential program, that is

  • Idle (waiting) for 1.2ms

  • Performing a send “TX1” operation, which takes 100 µs

  • Performing a send “TX2” operation, which takes 100 µs

Moddy parts derived from VThread or VSimpleProg must implement a method run_vthread(). This method contains your sequential program model.

It is called at the start of the simulation time (simulation time 0).

The run_vthread() method is not supposed to exit/return, that’s why it contains an endless loop. (However, for remote controlled vThreads it makes sense to exit/return, see Remote Controlled vThreads for more information)

Since Moddy 1.8, you don’t need a subclass of VThread or VSimpleProg anymore. You can pass instead a function to the constructor of VThread or VSimpleProg via the target parameter. This function (in the example below bob_prog is then called from run_vthread():

def bob_prog(self: moddy.VSimpleProg):
    # bob starts talking
    self.head.send("Hi Joe", 1)

    while True:
        msg = self.wait_for_msg(None, self.head)

if __name__ == '__main__':
    simu = moddy.Sim()

    VSimpleProg( sim=simu, obj_name="Bob", target=bob_prog, elems={ 'QueuingIO': 'head' } )

The program model in run_vthread() can control the timing of the program via “system calls”:

  • wait() delays the program execution for a specified time or until an event occurred, indicating that the program is idle. No status box is shown on the sequence diagram life line while a program is waiting

  • busy() delays the program execution for the specified time. A user defined status box (e.g. ‘TX1’ in the example above occurs) is shown on the life line while the program is busy.

Note

Moddy executes each run_vthread() method in a separate python thread. But don’t worry: Race conditions resulting from concurrent execution cannot occur in Moddy, because Moddy executes exactly only one thread at each time, either the simulator thread or one of the vThreads. There is no need to protect your data via mutexes.

Communication between Moddy Programs and other Moddy Parts

Like normal Moddy parts, Moddy Programs shall communicate with other parts only via Moddy messages. To send messages, a Moddy program can use standard output ports. There is no difference to other parts.

But a standard input port executes a callback method. It cannot tell the program that a message has been received (other than setting a global variable). For this reason, Moddy provides “buffering input ports”.

Buffering Input Ports

A buffering input port buffers received messages in the part’s local memory and eventually wakes up the program. There are two types of buffering ports

  • Sampling Port: A sampling port is used if the receiver is only interested in the most recent message.

    • A sampling port buffers only the last received message.

    • A read from the sampling buffer does not consume the buffered message

  • Queuing Port: A queuing port is used if the receiver wants to get all messages.

    • A queuing port buffers all messages in a fifo queue. The queue depth is infinite.

    • A read from the buffer consumes the first message

Buffering input ports are derived from VtInPort.

A program reads a message from a buffering ports via the read_msg() method.

It can check the number of messages in the buffer through the n_msg() method.

A program can wait for new message using the wait() method, or alternatively wait for a message and read the first available message through wait_for_msg().

The exact behavior depends on the type of buffer port (Sampling or Queuing) and will be explained in the following.

Sampling Input Ports

Recall from previous chapter:

  • A sampling port buffers only the last received message.

  • A read from the sampling buffer does not consume the buffered message

A sampling input port (VtSamplingInPort) is created with the create_ports() method, usually from the program’s constructor:

self.create_ports('SamplingIn', ['in_p1'])

The read_msg() method on a sampling input port returns the most recent message received. If no message at all was received, it returns either the “default” (if provided) or raises a BufferError exception.

If you call read_msg() and now new message has arrived since the last read_msg() call, you get the same message again. Example:

msg = self.in_p1.read_msg(default='123')

The n_msg() method returns 0 if no message was received at 0, or 1 otherwise. A program can call wait() so that is woken up if a message arrives on the port:

self.wait(20, [self.in_p1])

A sampling input port wakes up a waiting program with every message that arrives.

The following snippet demonstrates the use of a sampling port. MyThread1 is a program that has a sampling input port in_p1, while StimThread is firing messages to that port.

class MyThread1(moddy.VSimpleProg):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='Thread', parent_obj=None)
         self.create_ports('SamplingIn', ['in_p1'])

     def show_msg(self):
         msg = self.in_p1.read_msg(default='No message')
         self.annotation(msg)

     def run_vthread(self):
         cycle=0
         while True:
             cycle += 1
             self.show_msg()
             self.busy(18,cycle, busy_appearance)
             self.show_msg()
             self.busy(14,cycle, busy_appearance)
             self.wait(20,[self.in_p1])


 class StimThread(moddy.VSimpleProg):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='Stim', parent_obj=None)
         self.create_ports('out', ['to_t1_port'])

     def run_vthread(self):
         count=0
         while True:
             count+=1
             self.wait(15)
             self.to_t1_port.send('hello%d' % count,5)

This would result in the following sequence diagram:

../_images/0210_samplinginport.png

Queuing Input Ports

Recall from previous chapter:

  • A queuing port buffers all messages in a fifo queue. The queue depth is infinite.

  • A read from the buffer consumes the first message

A queuing input port (VtQueuingInPort) is created with the create_ports() method, usually from the program’s constructor:

self.create_ports('QueuingIn', ['in_p1'])

The read_msg() method on a queuing input port returns the first message of the queue. If no message is in the queue, it raises a BufferError exception.

The n_msg() method returns the number of messages in the queue (0 if none).

A program can call wait() so that is woken up when a the first message an empty queue arrives on the port:

self.wait(20, [self.in_p1])

Warning

Call wait() only on empty queuing ports, otherwise the program will not be woken up (because the wakeup happens only at empty->non-empty transitions)! Alternatively, use wait_for_msg()

The following snippet demonstrates the use of a queuing port. MyThread1 is a program that has a queuing input port in_p1, while StimThread is firing messages to that port.

class MyThread1(moddy.VSimpleProg):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='Thread', parent_obj=None)
         self.create_ports('QueuingIn', ['in_p1'])

     def get_all_msg(self):
         lst_msg = []
         while True:
             try:
                 msg = self.in_p1.read_msg()
                 lst_msg.append(msg)
             except BufferError:
                 break

         self.annotation(lst_msg)


     def run_vthread(self):
         cycle=0
         while True:
             cycle += 1
             self.busy(33, cycle, busy_appearance)
             self.get_all_msg()
             self.wait(20, [self.in_p1])
             self.get_all_msg()


 class StimThread(moddy.VSimpleProg):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='Stim', parent_obj=None)
         self.create_ports('out', ['to_t1_port'])

     def run_vthread(self):
         count=0
         while True:
             count+=1
             self.wait(15,[])
             self.to_t1_port.send('hello%d' % count,5)
../_images/0220_queuingport.png

Since Moddy 1.8, wait_for_msg() is available. This method waits for a message on any of the specified ports and returns the first available message:

def run_vthread(self):
    while True:
        # Wait for a message on either in_p1 or in_p2.
        # Because 2 ports have been specified, wait_for_msg returns a tuple with (msg, port) or None
        rv = self.wait_for_msg(30, [self.in_p1, self.in_p2])

        # Wait for a message on in_p2.
        # Because 1 port has been specified, wait_for_msg returns a just the msg or None
        rv = self.wait_for_msg(30, self.in_p2)

System Calls for Sequential Programs

In the previous chapters, the system calls wait() and busy() were already briefly introduced. Now in more detail:

wait() delays the program execution for a specified time or until an event occurred. The model is indicating with wait() that the program is idle. Therefore no status box is shown on the sequence diagram life line while a program is waiting.

moddy.vthread.VThread.wait(self, timeout, ev_list=[])

Suspend vThread until one of the events in ev_list occurs or timeout

Parameters
  • timeout – time to wait for events. If None, wait forever. A timeout value of 0 is invalid.

  • ev_list (list) – list of events to wait for. Events can be VtSamplingInPort, VtQueuingInPort, VtIOPort, or VtTimer object. If ev_list is empty (or omitted), wait for timeout unconditionally.

Returns

‘ok’ if one of the events has been triggered (it does not tell you which one), ‘timeout’ if timeout

Raises

TerminateException – if simulator stopped

busy() delays the program execution for the specified time. The model is indicating with busy() that the program is performing some operation. Therefore, a user defined status box appears on the life line while the program is busy.

moddy.vthread.VThread.busy(self, time, status, status_appearance={})

tell the scheduler how much time the current operation takes

Parameters
  • time – the busy time. May be 0

  • status – the status text shown on the sequence diagram life line

  • status_appearance – defines the decoration of the status box in the sequence diagram. See svgSeqD.statusBox

Returns

always ‘ok’

Raises

TerminateException – if simulator stopped

Concurrent Program Execution/RTOS Simulation

Moddy comes with a simulation of a simple RTOS (real time operating system) scheduler (vt_sched_rtos:vt_sched_rtos).

With this feature, you can model SW threads that run concurrently on a single CPU core. The scheduler has the following features:

  • 16 thread priorities - 0 is highest priority

  • Priority based scheduling. Low priority threads run only if no higher thread ready.

  • Threads with same priority will be scheduled round robin (when one of the same priority threads releases the processor, the next same priority thread which is ready is selected). Note that the round robin occurs not periodically. It happens only when one task executes wait().

First, you create the scheduler object:

sched= vt_sched_rtos(sim=simu, obj_name="sched", parent_obj=None)

Then you add the threads (subclasses of VThread) that shall be scheduled by the scheduler:

t1 = MyThread1(simu)
t2 = MyThread2(simu)
t3 = MyThread3(simu)
sched.add_vthread(t1, prio=0)
sched.add_vthread(t2, prio=1)
sched.add_vthread(t3, prio=1)

Example snippet:

class MyThread1(moddy.VThread):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='hiThread', parent_obj=None)
     def run_vthread(self):
         print("   VtHi1")
         self.busy(50,'1',busy_appearance)
         print("   VtHi2")
         self.wait(20,[])
         print("   VtHi3")
         self.busy(10,'2',busy_appearance)
         print("   VtHi4")
         self.wait(100,[])
         print("   VtHi5")
         self.wait(100,[])
         while True:
             print("   VtHi5")
             self.busy(10,'3',busy_appearance)
             self.wait(5,[])

 class MyThread2(moddy.VThread):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='lowThreadA', parent_obj=None)
     def run_vthread(self):
         print("   VtLoA1")
         self.busy(50,'1',busy_appearance)
         print("   VtLoA2")
         self.wait(20,[])
         print("   VtLoA3")
         self.busy(20,'2',busy_appearance)
         print("   VtLoA4")
         self.busy(250,'3',busy_appearance)

 class MyThread3(moddy.VThread):
     def __init__(self, sim ):
         super().__init__(sim=sim, obj_name='lowThreadB', parent_obj=None)
     def run_vthread(self):
         print("   VtLoB1")
         self.busy(50,'1',busy_appearance)
         print("   VtLoB2")
         self.wait(20,[])
         print("   VtLoB3")
         self.busy(100,'2',busy_appearance)
         print("   VtLoB4")
         self.busy(250,'3',busy_appearance)

 simu = sim()
 sched= vt_sched_rtos(sim=simu, obj_name="sched", parent_obj=None)

 t1 = MyThread1(simu)
 t2 = MyThread2(simu)
 t3 = MyThread3(simu)
 sched.add_vthread(t1, 0)
 sched.add_vthread(t2, 1)
 sched.add_vthread(t3, 1)

Resulting in the following sequence diagram:

../_images/0230_rtos.png

Note: The “PE” status indicator tells you that the thread is “preempted”, i.e. it would be ready to run, but it has to wait for the CPU resource, because a higher priority thread is busy.

VSimpleProg and VThread

VSimpleProg is a specialization of VThread. VSimpleProg uses an exclusive scheduler for the program, so there are no concurrent threads. A VThread must be explicitly assigned to a scheduler.

Example for a VSimpleProg. This creates a moddy part “Producer” with a single thread attached:

class Producer(moddy.VSimpleProg):
    def __init__(self, sim):
        super().__init__(sim=sim, obj_name="Producer", parent_obj=None)
        self.create_ports('out', ['net_port'])

    def run_vthread(self):
        while True:
            self.wait(100*US)
            self.net_port.send('test', 100*US)

Remote Controlled vThreads

Remote Controlled vThreads are for instance useful to model computer systems that execute software, but only if the computer is turned on, and another moddy part shall be able to turn on/off the computer. Another example could be Software processes which shall be terminate-able and restart-able.

Normal vThreads are started automatically at the beginning of the simulation and they are supposed to run until the end of the simulation. Since Moddy 1.5.0, remote controlled vThreads are supported. Subclasses of vThreads like VSimpleProg also have the remote control feature.

Remote Controlled vThreads can be controlled (started and killed) from other moddy parts via a moddy input port called _thread_control_port.

Remote Controlled vThreads are not started automatically, but wait for a “start” message on the _thread_control_port. To create a remote controlled VThread, pass remoteControlled=True to the VThread’s __init__ method. In another moddy part, create an output port and bind it to _thread_control_port.

Extract from tutorial 6_vthread_remote_controlled.py:

class MyRcThread(moddy.VThread):
    def __init__(self, sim):
        super().__init__(
            sim=sim,
            obj_name="rcThread",
            parent_obj=None,
            remote_controlled=True,
        )

# This thread controls the remote controllable VThread
def stim_prog(self):

    # @2s: initial start of rcTread
    self.wait_until(2)
    self.rc_port.send("start", 0)

    ...

    # @180s: kill rcThread
    self.wait_until(180)
    self.rc_port.send("kill", 0)


if __name__ == "__main__":
    SIMU = moddy.Sim()
    SIMU.tracing.set_display_time_unit("s")

    SCHED = moddy.VtSchedRtos(sim=SIMU, obj_name="sched", parent_obj=None)
    rc_thread = MyRcThread(SIMU)
    util_thread = moddy.VThread(
        sim=SIMU,
        obj_name="utilThread",
        target=util_prog,
        elems={"out": "to_rc_port"},
    )
    SCHED.add_vthread(rc_thread, 0)
    SCHED.add_vthread(util_thread, 1)

    STIM = moddy.VSimpleProg(
        sim=SIMU, obj_name="Stim", target=stim_prog, elems={"out": "rc_port"}
    )

    SIMU.smart_bind(
        [
            ["rcThread._thread_control_port", "Stim.rc_port"],
            ["utilThread.to_rc_port", "rcThread.from_util_port"],
        ]
    )

On the _thread_control_port, only two string parameters are supported:

  • start - Start or restart the VThread’s run_vthread() method. If the run_vthread() is already active, the start message is ignored.

  • kill - Force the run_vthread() to abort the current busy() or wait() call by raising a VThread.KillException. When the run_vthread() has terminated, all pending timers are stopped, all receive queues in the input ports are cleared and no messages can be received while terminated. The kill message is ignored if the VThread is already terminated.

Note

Variable persistence: When a thread is re-started, all local variables are lost and must be reinitialized. However, be aware that variables stored in the SimPart object (e.g. self.myvar) will survive a restart.