Plain – Distributed Processing Part 2

Our concept of distributed processing is going to need some attention to details in the Assembler/VM design, but it will work. We  now have two mechanisms involved:

  • We have an easy way to synchronize data between several modules
  • We can make on module execute logic on a different module.

The principles of how we do this through easyIPC is easy, but we need to dig into the detailson some some loose ends in our design. One such issue is module addressing in a larger network.

The block diagram above consist 7 devices in a system. We have two RPI’s one for controlling the actuator/sensor sub systems, and one for HMI. The HMI contains a Led Hat where I want to blink Leds from the Servo and stepper controllers.

 LedHat example

use System
Module LedHat
            Object LedGroup
                        Bit Led1
                        Bit Led2
                        Bit Led3
            End
            interface C LedGroup leds[1..6];
            Interface Func SetLedStatus(uint32 group, Bit l1, Bit l2, Bit l3)
                        Transaction leds[group]
                                    leds[group].Led1 = l1
                                    leds[group].Led2 = l2
                                   leds[group].Led3 = l3
                        Update
            End
End

32xIO example

use System
use LedHat
Module 32xIO
            LedHat.SetLedStatus(1,0,0,1)
End

These two code examples are the Plain code I expect to write.

to be continued in part 3…

Plain – Distributed Processing Part 1

Distributed processing is at the core of what we do in Plain. A module depends on other modules and can use that as part of it’s own logic. A simple function call or an event can connect with a different process that may be local on the same device or located on a different device.

The Plain developer do not need to be concerned about the details of how this happens. This is an important abstraction that also is a core Plain concept – we focus on what we do and leave the details of how to C/C++ developers. This makes Plain an extension to – not an replacement of – host languages.

Distributed Processing needs a communication protocol to be able to communicate between processes on the same or different devices. This is what easyIPC does, it enable network wide communication between devices regardless wherever this is Ethernet, RS485, CAN or some wireless protocol. easyIPC allows us to send messages with content that only need to be understood by the sender and receiver, In this case the Plain VM.

use MyHostModule
module MydeviceModule
     MyHostModule.Println ("Hello World")
End

This example assume we have a plain module located top-side that is called from a module located on a device.

What will happen in the example above is that MyHostModule.println is assembled into a Call Instruction. As the “Call” in this case is to a different module we will in effect be calling a C function that communicate with the other module. In this case the C code need to detect that this is a remote process and assemble an easyIPC message that is sent. As we send this we also start a timeout counter.

The remote module receive the message and call “println” in MyHostModule. This may be a plain module, but it can also be a C#, Python, Java, C/C++ function on the host computer.

I need to dig into the details later as I have a few loose ends here at present. How do we call a module on multiple devices etc – we need a broadcast and selection mechanism etc.

As the remote function finish it will return a Plain event. Our VM will receive this event and forward it to the calling module. But, it will also respond with a Timeout if we do not get a response within a given time frame.

Plain is designed to allow simple code – we can chose to code all this with a “don’t care” attitude, or we can add On statements to control the behaviour depending on the success of our call.

use MyHostModule
module MydeviceModule
     MyHostModule.Println ("Hello World")
     On Timeout(uint32 msTimeout)
         // no responce, we might have 
         // lost communication
     On Continue
         // it's been printed
     End
End

In this second example we catch the events as they return.

use MyHostModule
module MydeviceModule
     spawn MyHostModule.Println ("Hello World")
End

This example add the keyword spawn rather than the default “call”. The difference is that Spawn makes the call, but will continue directly without waiting.

Many loose ends we need to discuss here…

to be continued in part 2…

Plain – Modules

All code in Plain Assembly is within a module. A module is in effect a process, a stand-alone Plain application that is downloaded to the VM and executed.

A module is used by a different module with a use statement

use system
module mymodule
    system.println("Hello World")
end

This example re-use the module “system” in “mymodule”.

Only one module can be declared in a single file and the file prefix must be the same as the module name. A module can be declared in multiple files (TBD).

Modules can contain global code that is the “main” on a module executed top Down.

The current draft allows content to be mixed in no particular order – you can write statements, declare a function and write new statements using that function etc. It is a discussion that we maybe should be stricter and implement concepts used in Pascal where everything must be declared in groups. This part of the Pascal syntax is known to enhance structure and readability. The drawback is that we start bloating our modules by doing this. Large modules will benefit, while small modules will not (TBD).

Components inside a module is private to that module and cannot be accessed from other modules unless they are prefixed with the keyword “Interface”.

...
Module 32xIO
    ...
    Interface Object Channel
        ...
    End
    ...
End

A module can be controlled by another module

StartProcess (MyModule)

This will cause the VM to start executing the module MyModule in parallel with the current module. It will only signal the module to execute – it takes no parameters and will return events like a normal function.

Functions like StartProcess, StopProcess, IsProcessRunning etc are part of theVM’s own C interface and accessed through the System module.

Notice that StartProcess will start executing the global code in a module as a process, it does not affect interface parts of the same module. Also keep in mind that a module can be remote, so a call to system.println() might execute a function located top-side on your host computer. What happens in this case is that we send the “call” through easyIPC to the different device and return the event the same way. This is part of the distributed processing logic build into Plain VM/easyIPC.

Plain – Getting Started

I need to start on the assembler soon so this is a good time to summarize the assembly syntax and rules.

Plain-Assembly is an assembly language on the same level as a 3rd generation language. It is called an assembler because we maintain one statement one instruction. I deliberately avoid the word compiler because there will be a higher level Plain language later. I will write the basic proposal for assembly language and it will also be updated on the documentation pages. I also intend to write a few pdf docs, but lets mature the concept first.

 Getting started

use system
module mymodule
    println ("Hello World")
end

The example above is the classic hello world. What happen here is that we access a standard module “system” that enable the core C firmware. What “system” we use is defined in an XML file we call “repository” – basically a small database which in this case define that we have a module “System” that is in the C firmware. Other modules we use will also need to be defined in the repository.

Plain is module oriented, so all code must be within a module statement. In this case we access system.println to output “Hello World” to the standard console. The console is whatever we have defined in the C firmware, usually the serial port on our SWD connector.

 pa -r repository -a mymodule

 This is the proposed command line for the assembler. -r add a repository, -a add a plain assembly file. Input is “mymodule.pln” and output is “mymodule.rtl”.

The RTL file uses a xml format describing the binary content we will download to the device. I need a xml format so I can read and inspect the content of the RTL, but the plain downloader will convert this into binary easyIPC commands that are sent to the device.

We have in reality decided what device we assemble for my selecting the repository file. The assembler will include this as verification statements in the RTL, so any attempt to download this to a different device will fail.

 pd mymodule <device>

 This is the proposed command line for the download utility.

VM – FOR Instruction

One of my challenges is that our new Object Descriptor have a dynamic size if you take into account the extensions. To deal with this I decided that if E=1 (Extension) we use the Offset as an offset into the dynamic part of the instruction, meaning that every instruction will have a static and one optional dynamic part.

This enables me to read the static part as a fixed size and pass the static Object Descriptor into a ResolveObjectDescriptor() function that will give me a number (assuming this is a number) back. The example from the For instruction below illustrates the changed design.

In this case I know that For is 6 fixed entries and the length-6 = size of extensions. If Length=3F I look for actual, extended length as the 7th 16 bit entry. This also means that if we need the Extension we are forced to replace the Data Offset as well.

.

VM – Registers

All CPU’s, even VM’s have a set of global variables referred to as “Registers”. These are the internal data used to execute instructions. Our VM is no exception, but the registers we need differ from instruction to instruction due to their high level nature.

“Registers” in this context is variables in our VM struct. The variables needed for an instruction is stored on the C stack. I will in general not use recursive calls, but it is an option that we use recursive calls on instructions like for, while etc since they otherwise needs to be decoded for every iteration. This is however optimization that I will look into later.

The capability to execute expressions require a math table. As this table will be of some size I need to minimize the footprint impact. One way of doing that is to take into account that we will be only executing one instruction at the time regardless of how many VM’s we have, so I can get away with a single, static table. This is not optional for performance, but it is required due to SRAM usage. The actual C code struct is listed below:

typedef struct _MathEntry
{
   unsigned mathOp:4;
   unsigned t1:4;
   unsigned t2:4;
   unsigned tix:4;
   unsigned do1:16;
   unsigned do2:16;
}vm_MathEntry;

I am a bit unsure about this one as I have 48 bits per entry. It might very well be that performance will be better if I use 64 bits since we use a 32 bit computer – I will test this one and see if it matters.

Plain – Alternative Syntax

I have a friend that constantly complain that Plain syntax don’t look like C, so here it is – the C’ifed alternative version…

Use System;
Module Servo32
{
            Enum Byte ChannelMode
            {
                        DigitalIn,
                        DigitalOut,
                        AnalogueIn,
                        AnalogueOut,
                        PWMOut,
                        Servo,
            };

            Object Channel
            {
                        ChannelMode mode;       
                        uint32 frequency;
                        uint32 duty;
                        uint32 servoPosition;
                        real32 analogueValue;
                        bit digitalValue;
           };

            Interface C Channel chan[32];
            on Update(uint32 x)
            {
                        Transaction (chan[c])
                        {
                                   if(chan[c].digitalValue=1)
                                                           chan[c-4].servoPosition = 180;
                                   else
                                                           chan[c-4].servoPosition = 0;
                                   end
                        }Update;
            }

            int16 x;

            Transaction (chan[1..8])
            {
                        for x=1 to 4
                        {
                                    chan[x].mode = ChannelMode.Servo;
                                   chan[x].frequency=50;
                                    chan[x].servoPosition=0;
                        }
 
                        for x=5 to 8
                        {
                                    chan[x].mode = ChannelMode.DigitalIn;
                        }
            }Update;
}

This is just an alternative syntax for the fun of it – I lost a few details in the transfer + we could need a few more changes. But, the reason I am not using this syntax is because the other syntax form is closer to an Assembly Language – one line one instruction concept. I also believe that since we will use C/C++ to fuel Plain the languages should be very different. I have working in C++, Managed C++ and C# at the same time and it was confusing to keep the languages apart.

Updated Servo Example

Use System
Module Servo32
            Enum Byte ChannelMode
                        DigitalIn
                        DigitalOut
                        AnalogueIn
                        AnalogueOut
                        PWMOut
                        Servo
            End

            Object Channel
                        ChannelMode mode       
                        uint32 frequency
                        uint32 duty
                        uint32 servoPosition
                        real32 analogueValue
                        bit digitalValue
           End

            Interface C Channel chan[32]
            on Update(uint32 x)
                        Transaction (chan[c-4])
                                   if(chan[c].digitalValue=1)
                                                           chan[c-4].servoPosition = 180
                                   else
                                                           chan[c-4].servoPosition = 0
                                   end
                        Update
            End
 
            int16 x

            Transaction chan[1..8]
                        for x=1 to 4
                                    chan[x].mode = ChannelMode.Servo
                                    chan[x].frequency=50
                                    chan[x].servoPosition=0
                        end

                        for x=5 to 8
                                    chan[x].mode = ChannelMode.DigitalIn
                        end
            Update
End

I updated my servo example based on the Transaction draft, and this might actually work. I replaced External with Interface as suggested, but I also add event’s to variables that can be caught with On. Once we receive and Update event we need to do a transacion ourselves since we will update the servo entry.

 What I am most happy with here is less code + that we can’t go wrong since the interface keyword will force the use of transactions. I am not sure how I will get the Assembler to keep track of what entries we actually do transactions on with arrays involved so I need to work on that. I like the idea of having events on variables and not only on objects. It makes sense in this case since we need an event on one channel to operate another.

I am reaching a point where I can only do so much without a working VM and Assembler! It basically gets harder to see the missing details or flaws in the drafts.

Plain – Transactions

With a working draft of the VM I need to move on Plain Assembly syntax. By addressing syntax I challenge the VM as well and it will drive a few changes. I expect the number of instructions to increase as I cover topics. One such topic is how to maintain data integrity during multi-threaded transactions?

My Servo example assume that I only have one module accessing the Channel array. It also uses manual functions to synchronize array content. I want the assembler to handle aspects of multi-threading automatically, but I also need to cover integrity of data transactions and I wonder if I can do this with a single mechanism.

Using the 32xServo/IO Hat as an example we have an array of 32 objects. We have at least one C module and maybe several Plain modules reading/writing to that array simultaneously and we need a mechanism that ensure that they only access consistent data – not data that is in the middle of a change.

extern C Channel chan[32]

I already declare this data with the keyword “Extern” and a module name which in this case is “C”. This tells the assembler that this is an interface variable that need some special care. It might be that “Interface” is better than “Extern” in the syntax.

Transaction( chan[0])
            chan[0].mode = ChannelMode.Servo
            ...
            Commit( chan[0])
            ...
            if (error)
                        Rollback( chan[0])
            end
            ...
Update

In this draft I introduce 4 new keywords. We set a new rule that data declared extern only can be written to inside a transaction-Update block. We add Rollback and Commit with a rule that if we hit Update this is an automated Commit if no commit/rollback parameter is added.

Don’t worry about the keywords or syntax details for now. We can change this later if we find better alternatives.

This will add 4 instructions to our instruction set.

Transaction Lock a list of variables from being changed by other modules. This can only be used on variables declared as extern.
Update Complete a transaction. Will free the locked variables and terminate the transaction block. The only optional parameter is Commit or Rollback. No parameters means we Commit. Update Commit or Update will also raise events associated with the changes.
Commit Commit allow us to commit selected variables and raise events associated with these changes. A commit statement will commit everything or only the parameters attached.
Rollback Will rollback any changes to variables listed or all variables if we have no parameters. This is to ensure data integrity in case of errors.