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.

 

VM – Updated Instructions

Assign Assign an expression to a variable. Execute a pre-parsed expression tree and store the resulting value in a variable. Expression can either be written as an algebraic math expression or a list of simple operations separated with ;
Call Call a function. Call creates a return entry on the stack and will continue with next instruction as End of the function is reached. Call can in contrary to raise also capture event’s raised by Raise.
Decode Unpack a bit field
Encode Pack a bit field
End End a conditional block of code by jumping to a pre-calculated ix on the stack. For (as an example) will insert it’s own ix on the stack, End will jump to that ix and remove it from the stack.
Exit Exit the VM. Will terminate the current module.
For For Loop. Will create a stack return entry causing the next End instruction to jump back to the loop start.
If If statement to allow high Level if..elsif..else with Expressions.
Nop No Operation. Basically not needed except usefully for testing basic performance.
Raise Call an event with parameters without creating a return entry. As the vent reach “End” it will return to the previous “Call” return point.
Switch TBD
While Loop while an expression is true.

The VM consist of only 12 very high level instructions that each equals a high Level Language control statement. The actual Assembly language will use more keywords, but this is the actual binary instructions.. This design will secure that applications are small and execute with high performance. I will add more intructions if I need more for optimization reasons, but 12 instructions are also good for keeping a low footprint.

Plain Servo Example

I will use the next days to wrap up a working version of the VM so I can test code footprint and performance. What I expect is actually quite high overall performance. Each instruction will be significantly slower than an ARM instruction -probably in the region of 1:1000, so I am hoping for an instruction speed of 50,000++. This will however be a learning experience.

Raw performance alone is however not that important, having a responsive system that we can use to control advanced operations with a few lines of code is. With a correct written Plain application a module will spend it’s life in idle waiting for an event from C code and respond to that.

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

            External C Channel chan[32]

            External C Func UpdateChannel(uint32 c)
            Event FrequencyError
            Event DutyError
            Event PositionError
            Event AnalogueError
            End

            External C Func EnableEvent(uint32 c)

            External C Event ChannelChanged(uint32 c)
                        if(chan[c].digitalValue=1)
                                   chan[c-4].servoPosition = 180
                        else
                                   chan[c-4].servoPosition = 0
                        end
                        UpdateChannel(c-4)
            End

            int16 x

            for x=1 to 4
                        chan[x].mode = ChannelMode.Servo
                        chan[x].frequency=50
                        chan[x].servoPosition=0
                        UpdateChannel (x)
            end

            for x=5 to 8
                        chan[x].mode = ChannelMode.DigitalIn
                        UpdateChannel (x)
            end

            for x=5 to 8
                        EnableEvent(x)
            end
End

The code snip above is an attempt to create a test program for the 32 x Servo/IO Hat. In this case I connect 4 servoes to channel 1-4 and a digital switch to channel 5-8. If I set channel 5 high I want servo 1 to move to pos 180 etc.

The actual Plain program is in this case 20 instructions and probably < 200 bytes of stored instruction Space.

Pay attention to event “ChannelChanged“. The rest of the code is just configuration, this event is the actual control code. What should happen is that as the digital input on channel 5-8 change we receive a call from C code and change the relevant servo channel as a response. After that we go back to idle and just wait for an event.

Binding to C code is planned to be done automatically by “External C”. In the example of ChannelEvent the RTL will expect to find a C event with that exact name and parameter. It’s too early to say if this will stand ground as I implement the VM – lets see.

VM – New Object Descriptor

I drafted this new Object Descriptor. What I do here is heavy bit-fiddling that I in general will not recommend. We will be reading this from Flash that is 2-3 times slower than SRAM, but I think this will be ok since these extensions will not be used often.

E=1 means we have extensions. The first bit on the extension head is a C-Continue bit that indicate if it is more extensions – the last tag will have C=0. Each Tag have a fixed size or indicator of fixed size. I have also drafted Tag’s for extended offset in case we need larger data areas sometime and indexing of arrays.

VM – Dropping the stack

Do I actually need a separate stack in the VM?

The current draft use 2 memory areas – one for variables and one for the stack. This is influenced by legacy CPU design. But. the reality is that Plain do not allow dynamic memory so the variable area is static size and can be estimated exactly. The only dynamic memory we have is actually the stack that can fit on top of that rather than using a separate memory buffer.

This would simplify the VM quite a lot, but I need to walk through instructions and re-visit binding to C code to see if this Works out.

VM – Extended Object Descriptor

I mentioned the possibility to embed expressions into If, Call, Raise, While etc to avoid having a Plain Assembly that split into several instructions. This is a 1st draft where I truncate the Data-Type flag in the Object Descriptor and use the MSB to flag an Extended Object Descriptor.

The Extension here contain a Tag indicating that this is a math table and number of entries to follow. The math entry is 10 bytes “as is”, so I will look into ways of truncating the Object Descriptor. One idea is to drop Size and truncate Type to 4 bits. Size is actually known since all build-in types have a fixed size and I can add another Tag entry to specify non-standard extensions. I am even considering truncating Data Offset to 10 bits (1K) and have a Tag extension to address larger offsets. 1K is actually quite a lot of data for an embedded design, so a majority of instructions would work perfectly with the truncated descriptor. This would reduce the Object Descriptor from 4 to 2 bytes and math entry from 10 to 6.

The Tag also allow me to add things like table indexing and actually address data larger than 64Kb if we need to.

Truncated 16 bit Object Descriptor:

  • 1 Extension bit flag.
  • 4 Type bits. This leaves us 16 build- in data types (Bit, Byte, int16,uint16,int32,uint32,real32,real64,string, number) + 6 spare codes.
  • 1 Stack/Data bit so we can differ between stack and data offset,
  • 10 Data Offset bits to address 1Kb

The price I pay for this is extra coding in the VM (larger footprint) + we are digging into binary decoding of structures that will reduce performance a little bit. I like the idea, so lets see if it stick or if we can come up with a better one.

VM – Raise

The introduction of the 32 bit Object Descriptor bloat the instruction set, but we currently only have 12 high-level instructions left in the VM – This is Assign, Call, Decode, Encode, End, Exit, For, If, NOP, Raise, While and Switch. Just for the record – this is actual instructions, not keywords supported by the Assembler syntax.

I have to walk through the details of all of them, but the one that currently have my focus is Raise and how it will behave on the stack together with a Call..On..End sequence. Lets do a few examples and see if we can get this to work:

Example 1:

Foo MyFunc
            Raise Error
Event Error
End

Call MyFunc

In this case we call MyFunc that return with the event Error – which will call the default and do nothing. (1) Call create a stack entry, (2) raise re-use that stack entry and (3) End consume the stack entry.

Example 2:

Foo MyFunc
            Raise Error
Event Error
End

Call MyFunc
On Error
            println "Error"
End

This is a bit more tricky as we now override the Error event, but how will Raise know which Error to call? The answer is that the Assembler need to create an event table for the call – this is by default the one for the function, but in this case with an overridden Error event. It also means we need to modify Raise to use an enumerated event code (generated by the Assembler) to indirect index the Event body. As raise in this case call Error and println it reach End that consume the stack entry.

 Example 3:

 Foo MyFunc
            Raise Timeout
Event Timeout
            Raise Error
Event Error
End

Call MyFunc
On Error
            println "Error"
End

In this case I cascade the events – (1) we Call MyFunc creating the stack entry, (2) Raise Timeout that re-use the stack entry, (3) Raise Error that is overridden and run into End that consume the stack entry.

Example 4:

Foo MyFunc
            Raise Timeout
Event Timeout
            println "Timeout"
Event Error
End

Call MyFunc
On Error
            println "Error"
End

In this case we Raise Timeout that run into a “Event” – basically terminating with an hidden End that will consume the stack entry and jump to after Call..On..End.

I need to extend the Call instruction to include the event table + modify the Raise instruction, but that seems to be it.

VM – New Stack

The previous stack design was to store only data descriptors on the stack – not data itself. The side-effect of this is that everything becomes global data. The alternative to pass a copy also have a side effect if I pass an object + passing an auto-decided combination of data and references is not nice either. We also need to look at details on restoring stack after a function return as well as how do we return Event parameters? And all of this should happen automatically as I don’t want the developer to worry about the stack at all.

I want to keep the design with pushing and popping references – Object References this time. As the reference point to actual data we can either point to the global copy or a new, local copy depending on what we want. “Reference” is a good keyword prefix for a new variable type that only point to data.

In the same way as we used the index top for stack we use the 64kb area for stack variables. We create a separate stack index for 32 bit Object Descriptors and a separate SRAM buffer for stack temporary storage. In the real buffer table we might use 1Kb for data and 1Kb for stack carefully calculated by the Assembler.

This figure illustrate our modified Stack. As with data we create a buffer for the VM and use of this for each module. The two buffers are arranged so data is the lower part and stack the higher forming a max of 64Kb. Size of data can be calculated, size of stack need to be estimated.

The Object Index is not a real table as this is embedded into the instructions as they access parameters, but we need an actual Stack Entry Table to hold data for each call. As we make a call we add an entry to this table – as we execute End we remove the entry and free the stack back to the previous Free Stack Offset.

The only issue that is not covered is Raise. I will get back to that later – we will have a few try and fails before we get this going…

VM – Object Index

The new Object Descriptor. Notice that we now offset data rather than indexing into an array.

This illustration show how a Assign would look like with a 32 bit Object Descriptor. We don’t need a descriptor on TIX since the type will follow math rules. The fields marked yellow are those who expand from 16 bit to 32 bit. Testing a few instructions we can expect a 50% increase in instruction size if we go down this path.

This last illustration shows how Object Descriptors link into a Module SRAM Buffer that is part of a VM SRAM Buffer.

Looking at a 100 line Plain Assembly program we can estimate that we grow from 800 byte to 1200 byte if we extend the instructions with Object Descriptors. Using a 16 bit index and an additional lookup table will save ca 280 bytes, but I will only use this if Module size becomes a major issue.