From SPIN to PASM - Code Documentation Style - Just curious
Beau Schwabe
Posts: 6,574
I realize that this can't be done in every case because of the complexities of the program, but on occasion when the initial program you write starts in Spin and you later convert portions of it to PAsm, would the type of documentation I have included below be helpful from an educational perspective for someone trying to learn PAsm?
...code on the left is PASM, code on the right is a descriptive SPIN equivalent. The single remark stroke is the literal translation from Spin for that 'block'. A 'block' defined by the group of PASM instructions NOT separated by a space. A double remark referring to an implied translation from Spin for that 'block'. Squiggle brackets indicate where in the PASM block a Spin conditional (i.e. IF/THEN/ELSE) takes place.
RepeatLoop1 'repeat
mov temp, _ProgramMemoryAddress ' characterbyte := ProgramMemory[offset]
add temp, _offset
rdbyte _characterbyte, temp
add _offset, #1 ' offset += 1
add _ColumnIndex, #1 ' ColumnIndex +=1
mov temp, #text#cols ' if ColumnIndex > (text#cols -2) or characterbyte == 13
sub temp, #2
cmp temp, _ColumnIndex wc { if ColumnIndex > (text#cols -2) }
'C flag is clear if V1 => V2
'C flag is set if V1 < V2
cmp _characterbyte, #13 wz { if characterbyte == 13 }
'Z flag is set if V1 = V2
if_c_or_z add _LineIndex, #1 ' LineIndex += 1
if_c_or_z mov _ColumnIndex, #0 ' ColumnIndex := 0
cmp _LineIndex, _Index wc ' if LineIndex => Index
'C flag is clear if V1 => V2
'C flag is set if V1 < V2
if_c jmp #RepeatLoop1 ' quit
'' Jump to RepeatLoop1 if above condition is false
'' cmp _characterbyte, #13 wz ' if characterbyte <> 13
''(redundant code, Z flag previously set with same compare in above code)
if_nz sub _offset, #1 ' offset -= 1
...code on the left is PASM, code on the right is a descriptive SPIN equivalent. The single remark stroke is the literal translation from Spin for that 'block'. A 'block' defined by the group of PASM instructions NOT separated by a space. A double remark referring to an implied translation from Spin for that 'block'. Squiggle brackets indicate where in the PASM block a Spin conditional (i.e. IF/THEN/ELSE) takes place.

Comments
It all depends on the quality of the original code and its comments. In the case you've posted, the Spin code and its comments are pretty good in terms of understandability. I can well imagine another program with cryptic variable names and useless comments that is converted to assembly with the comments being the original Spin code and the converted code being even less understandable than the original Spin code.
I can also imagine assembly code that's not a direct translation of Spin code, that's optimized in some fashion where using the Spin code as comments may make it less understandable than well crafted comments that apply to the actual assembly code.
It all comes down to trying to comment as if you're telling a story about the code and what it does. Sometimes it's useful to tell part of the story literally with lots of details and sometimes it's useful to tell other parts of the story metaphorically using the meaning of the actions rather than the actions themselves.
Understood, ... this is more of an exercise for someone wanting to learn PASM that has a good grasp with SPIN. ...and agreed, there are some things that are difficult at best to describe with comments because the translation from one to the other is implied more so that it is followed in a literal step-by-step or line-by-line architecture. So heavy commenting can be more cumbersome trying to convey what's implied sometimes.
Below is a whole section where original SPIN code was converted to PASM with comments in the style mentioned above. I just wonder if this helps or confuses anybody more ...
''########################################################################################################################### ' Spin to PASM conversion ''########################################################################################################################### mov _MemoryFlag, #0 'MemoryFlag := 0 mov _LineIndex, #0 'LineIndex := 0 mov _ColumnIndex, #0 'ColumnIndex := 0 mov _offset, #0 'offset := 0 RepeatLoop1 'repeat mov temp, _ProgramMemoryAddress ' characterbyte := ProgramMemory[offset] add temp, _offset rdbyte _characterbyte, temp add _offset, #1 ' offset += 1 add _ColumnIndex, #1 ' ColumnIndex +=1 mov temp, #text#cols ' if ColumnIndex > (text#cols -2) or characterbyte == 13 sub temp, #2 cmp temp, _ColumnIndex wc { if ColumnIndex > (text#cols -2) } 'C flag is clear if V1 => V2 'C flag is set if V1 < V2 cmp _characterbyte, #13 wz { if characterbyte == 13 } 'Z flag is set if V1 = V2 if_c_or_z add _LineIndex, #1 ' LineIndex += 1 if_c_or_z mov _ColumnIndex, #0 ' ColumnIndex := 0 cmp _LineIndex, _Index wc ' if LineIndex => Index 'C flag is clear if V1 => V2 'C flag is set if V1 < V2 if_c jmp #RepeatLoop1 ' quit '' Jump to RepeatLoop1 if above condition is false '' cmp _characterbyte, #13 wz 'if characterbyte <> 13 'Z flag is set if V1 = V2 ''(redundant code, Z flag previously set with same compare in above code) if_nz sub _offset, #1 ' offset -= 1 mov _LineIndex, #0 'LineIndex := 0 mov _ColumnIndex, #0 'ColumnIndex := 0 mov _i, #0 'i := 0 RepeatLoop2 'repeat mov temp, _ProgramMemoryAddress ' characterbyte := ProgramMemory[i+offset] add temp, _i add temp, _offset rdbyte _characterbyte, temp cmp _characterbyte, #255 wz { if characterbyte == 255 } 'Z flag is set if V1 = V2 if_z mov _MemoryFlag, _LineIndex ' MemoryFlag := LineIndex if_z jmp #RepeatLoop2Done ' quit add _i, #1 ' i += 1 add _ColumnIndex, #1 ' ColumnIndex +=1 mov _ColumnOffset, #1 ' ColumnOffset := 1 mov temp, #text#cols ' if ColumnIndex > (text#cols -2) sub temp, #2 cmp temp, _ColumnIndex wc 'C flag is clear if V1 => V2 'C flag is set if V1 < V2 if_c mov _ColumnOffset, #1 ' ColumnOffset := 2 cmp _characterbyte, #13 wz { if ColumnIndex > (text#cols -2) or characterbyte == 13 } 'Z flag is set if V1 = V2 if_c_or_z add _LineIndex, #1 ' LineIndex += 1 if_c_or_z mov _ColumnIndex, #0 ' ColumnIndex := 0 ' cmp _characterbyte, #13 wz { if characterbyte <> 13 } 'Z flag is set if V1 = V2 ''(redundant code, Z flag previously set with same compare in above code) if_z jmp #ConditionFalse1 '' Jump to ConditionFalse1 if characterbyte == 13 '' otherwise perform the proceedure below. '' This jump just eliminates the need to place if_nz '' on every command below that pertain to the condition mov temp, _DisplayMemoryAddress ' Display[ColumnIndex+(LineIndex*text#cols)-ColumnOffset] := characterbyte add temp, _ColumnIndex ''<-- Add ColumnIndex to the Display offset mov count, #text#cols ''<1- The next three lines Multiply (LineIndex*text#cols) and add to the Display offset Multiply1 add temp, _LineIndex ''<2- djnz count, #Multiply1 ''<3- sub temp, _ColumnOffset ''<-- Subtract ColumnOffset from the Display offset wrbyte _characterbyte, temp ''<-- Finally write characterbyte to the Display offset result ConditionFalse1 cmp _LineIndex, #text#rows wc ' if LineIndex => text#rows 'C flag is clear if V1 => V2 'C flag is set if V1 < V2 if_c jmp #RepeatLoop2 ' quit '' Jump to RepeatLoop2 if above condition is false RepeatLoop2DoneHere's how I like to do it: I find vertical scrolling easier to consume than horizontal, but that's just me.
DAT 'This is the PASM program data. PASM programs originate in the hub, and are copied into COGs before 'they are executed. If PASM programs need to operate with data in the HUB, the address of that data 'needs to either be hard coded into the COG program, or be passed to it via the PAR register, or 'single, common hard coded HUB memory location. 'For this example, the PAR register method will be used. 'You should note the order of tasks and differences in variable use. cogstart org 0 'This is the beginning of the PASM program to be loaded into 'one of the other COGs '*********************************************************** ' PRI init(pin) | i '*********************************************************** mov _demo_sound, PAR 'the address of demo_sound was passed to the cog, meaning 'this is a pointer, not a value. rdlong _I, _demo_sound 'get the number of sound performance data sets, into I mov _J, _demo_sound 'move pointer to J, and ... add _J, #4 'add 4 bytes to it, so that J points to first sound data 'element. 'At this point, this particular COG has not had the pin and direction setup done, so let's 'do that. mov _K, _I 'Number of demo sound data sets shl _K, #8 'Multiply by 8, because that's the data set size add _K, _demo_sound 'add to base pointer to obtain address of out_pin! rdlong _out_pin, _K 'Get the value of out_pin add _out_pin, #1 'add one for left channel output (comment for mono setups) '*********************************************************** ' pin_direction := 1 << pin '*********************************************************** mov _pin_direction, _one 'shift a 1 _out_pin digits left to form bit mask shl _pin_direction, _out_pin '*********************************************************** ' audio_out_pin_off := pin_direction ^ one '*********************************************************** mov _audio_out_pin_off, _pin_direction 'prepare to form other bit_mask xor _audio_out_pin_off, _all_set '*********************************************************** ' DIRA |= pin_direction '*********************************************************** or DIRA, _pin_direction '*********************************************************** ' OUTA &= audio_out_pin_off '*********************************************************** and OUTA, _audio_out_pin_off '************************************************************* ' PUB play_sound | i, j, k '*************************************************************Each SPIN statement is followed by the block of code that aligns with it. That does not always happen, but when it does, it's kind of nice. Other more elemental comments then run along side the code, in conversational style, detailing the sub-operations necessary to reproduce the SPIN program element in the block.Overall, doing this is a help, no matter the style. After looking at the code Kye has done, I think lots of comments is always better, unless the comments are just Smile. Even then, if there is a chance that something good is in those comments, I'll take it. For my own code, I find the block style above readable, and the most valuable part of it is seeing the little "chunk tasks" to combine with the whole. It's not always clear to me what the progression of thinking was, and often that's important. I sometimes go long periods of time between being able to build on a project. Lots of little things get forgotten. The blocks tend to help me reconstruct where I was at on things, or if looking at another persons code, where they were headed as well.
There is also a difference between a learning code example, and just comments on code that is just code, authored for whatever reason. Once a person begins to grok how PASM is going to work, I'm sure they will move to the next level, just doing the kinds of things Eric mentions below.
One thing I'm still not clear about is which characters are 'special'. @ seems to be special in Spin. Ditto ~. So is #. Using the same logic, is _ special? ie is _offset different to offset, or is it just a variant on my_variable?
A style of commenting that I find very helpful is to comment excessively the first instance of something that a reader might find confusing. Later instances of that code could have briefer comments.
Several times I have looked at pasm and thought it was spin, due to some similarities. I believe they mesh together and it would be beneficial to know how to put small sections of pasm in spin and call it into play. This fits well with what you are doing. We could have a place to consult with lots of these routines, especially the ones that are small, easily called and used.
Right now, there's information all over the place. Only those guys working with the prop chip for years seem to have put it all together in their minds. Just starting out, its a challenge. So a place with converted routines would be welcomed for spin programmers wanting to incorporate some pasm into their code, for gains of speed and less code overhead.
Humanoido
'
Sounds like the start of an awesome project!
'
I think you would both get carpal tunnel before you had to many REM's for me, But I do agree that some times the end result is more important then the code that got it there.
Its kind of a paradox I guess.
'
I find what both of you have posted to be very informative and easy to understand.
'
I wish I had more time to do this sort of thing, oddly I sorta like mundane, tedious translations like this ... :-) ... I'll see what I can do during longer LVS (Layout Versus Schematic) runs with Propeller II if I can fit something like this in as I'm writing objects.
Dr_Acula,
The @, @@, ~, ~~, # aren't necessarily any more special in Spin than they are in Pasm. One big difference is that Pasm has specific fields such as Instruction, Destination, Source, Conditional, Flags, etc. that aren't always conducive to using those 'shortcuts' that are actually easier to use in Spin.
The _ is something that I use, especially when converting something from Spin to Pssm. The reason is that during the 'migration' and testing the same variable name can't be used in both Spin and Pasm simply because it's already been declared. Adding a _ to the beginning tells me that it possibly has a duplicate. This method also has a way of keeping the meaning of the variable intact when I see it visually.
ALL
Here is a kick-start of the type of vision I have... This is basically a step-by-step approach to converting a Spin PUB routine into a callable Pasm routine.
Suppose we have a program like the one below and we want to convert the SpinSimple
routine to Assembly...
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C C := A + B C := C + C + C Result := C...The first thing that we are going to want to do is create an Assembly header
that can be called from SpinSimple.
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C C := A + B C := C + C + C Result := C DAT PasmSimple...next we need to asses the variables we are using in Spin and create duplicates
in Pasm.
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C C := A + B C := C + C + C Result := C DAT PasmSimple _A long 0 _B long 0 _C long 0... now we need to call the Pasm from Spin and copy the necessary variables from
Spin into Pasm.
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C cognew(@PasmSimple,@A) C := A + B C := C + C + C Result := C DAT PasmSimple mov temp, par '' get address of variable 'A' ; PAR + 0 rdlong _A, temp '' read contents of address into _A add temp, #4 '' get address of variable 'B' ; PAR + 4 rdlong _B, temp '' read contents of address into _B cogid temp '' terminate cog cogstop temp _A long 0 _B long 0 _C long 0 temp long 0...next we want to take the first equation 'C := A + B' and incorporate that within Pasm.
at this point we can remove or remark the Spin version of this equation.
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C cognew(@PasmSimple,@A) ' C := A + B C := C + C + C Result := C DAT PasmSimple mov temp, par '' get address of variable 'A' ; PAR + 0 rdlong _A, temp '' read contents of address into _A add temp, #4 '' get address of variable 'B' ; PAR + 4 rdlong _B, temp '' read contents of address into _B mov _C, _A '' C := A + B add _C, _B add temp, #4 '' get address of variable 'C' ; PAR + 8 wrlong _C, temp '' write contents _C to address cogid temp '' terminate cog cogstop temp _A long 0 _B long 0 _C long 0 temp long 0... but why doesn't this program work? ... Welcome to Parallel processing and one of the first gotchas!!
The program does exactly what you told it to do, but there are no LED's lit? with the example numbers 1 and 2,
C should equal 9, and LED's 0 and 3 should be lit. Why not?
The answer is because when you launch the cognew, the Pasm program loads and starts the process of Adding
A and B, but at the 'same time' (or at least after the cog has been loaded) the original cog that
called the Pasm has already started evaluating the answer of _C from C + C + C and arives at an answer of
zero BEFORE the cog that was called has time to finish.
What to do? There are a couple of things that can be done. One is to insert a delay after the new cog
has been started. This is wasteful as far as idle cpu time.
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C cognew(@PasmSimple,@A) waitcnt(clkfreq+cnt) ' C := A + B C := C + C + C Result := C DAT PasmSimple mov temp, par '' get address of variable 'A' ; PAR + 0 rdlong _A, temp '' read contents of address into _A add temp, #4 '' get address of variable 'B' ; PAR + 4 rdlong _B, temp '' read contents of address into _B mov _C, _A '' C := A + B add _C, _B add temp, #4 '' get address of variable 'C' ; PAR + 8 wrlong _C, temp '' write contents _C to address cogid temp '' terminate cog cogstop temp _A long 0 _B long 0 _C long 0 temp long 0...another method is to use another variable as a 'flag' within Pasm that can be cleared when it's job is
complete.
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C,flag flag := 1 cognew(@PasmSimple,@A) repeat while flag == 1 ' C := A + B C := C + C + C Result := C DAT PasmSimple mov temp, par '' get address of variable 'A' ; PAR + 0 rdlong _A, temp '' read contents of address into _A add temp, #4 '' get address of variable 'B' ; PAR + 4 rdlong _B, temp '' read contents of address into _B mov _C, _A '' C := A + B add _C, _B add temp, #4 '' get address of variable 'C' ; PAR + 8 wrlong _C, temp '' write contents _C to address add temp, #4 '' get address of variable 'flag' ; PAR + 12 wrlong _Flag, temp '' write contents _Flag to address cogid temp '' terminate cog cogstop temp _A long 0 _B long 0 _C long 0 temp long 0 _Flag long 0Now, the last two programs should work, one uses a delay (<- ok) , and the other uses a flag (<- better). What we
want to do now is move the remaining portion of Spin into PAsm also. Right now all we have is a hybrid PUB routine.
So moving C := C + C + C into Asm would look like this ...
PUB StartMain|LED LED := SpinSimple(1,2) dira[23..16]~~ outa[23..16] := LED repeat PUB SpinSimple(A,B)|C,flag flag := 1 cognew(@PasmSimple,@A) repeat while flag == 1 ' C := A + B ' C := C + C + C Result := C DAT PasmSimple mov temp, par '' get address of variable 'A' ; PAR + 0 rdlong _A, temp '' read contents of address into _A add temp, #4 '' get address of variable 'B' ; PAR + 4 rdlong _B, temp '' read contents of address into _B mov _C, _A '' C := A + B add _C, _B 'Note: since we are done with _B, I'm using it here as a temp variable ' so the actual contents of the real temp variable can be preserved mov _B, _C '' C := C + C + C add _B, _C add _C, _B '<- This saves a line of code ; could have done another ' add _B, _C ... but then you'd still ' need to get _B into _C or just use _B later (two instructions down) ' when writing _C back. add temp, #4 '' get address of variable 'C' ; PAR + 8 wrlong _C, temp '' write contents _C to address add temp, #4 '' get address of variable 'flag' ; PAR + 12 wrlong _Flag, temp '' write contents _Flag to address cogid temp '' terminate cog cogstop temp _A long 0 _B long 0 _C long 0 temp long 0 _Flag long 0...So that's basically a step-by-step for creating a custom Pasm PUB routine.
Beau Schwabe
This is outstanding!
It's like you were reading my mind and know what we need for our projects.
Humanoido
Ok so the general consensus, I gather ... not only from this thread, but several people that I talked to at the EXPO ... Is that I will try to adopt and/or adapt this method of commentary to my future objects where this type of commentary is applicable. This way if you at least understand Spin you get a good idea of what the Pasm code equivalent is doing.
At the same time I will try to compile various snippets, tricks, shortcuts that I may discover and learn along the way. And who knows, at some point maybe a book will form out of the debris ... "101 ways to PASM your SPIN" :-)
Thanks & keep up the good work!
OT - is there a "rumored" date on the release of the Prop II? I do hope its 32bit with 64 i/o's
MPLAB has a View/Disassembly Listing which is quite handy. Like the following...
128: if (character >= '0' && character <= '9')
0070 0E30 MOVLW 0x30
0072 5CDF SUBWF 0xfdf, W, ACCESS
0074 E336 BNC 0xe2
0076 50DF MOVF 0xfdf, W, ACCESS
0078 0839 SUBLW 0x39
007A E333 BNC 0xe2
The 128: line is a line from your C program, then below that is the assembly code for that one line. You can see that the assembly code is using ASCII 0 (30) and ASCII 9 (39) [as it should in this case].
And you can see how the same line in your higher level language program is translated by the compiler into assembly.
Then you can look up one assembly line at a time (MOVLW, SUBWF, etc.) and learn what each is doing.
And along with that read the processor data sheet and learn what each command is doing "inside" the processor.
I think one of the first tools of this nature I used was DDT or Dynamic Debugging Tool. With that I could "peek inside" programs and see how they worked. Here is a bit on that...
http://www.iso.port.ac.uk/~mike/interests/chistory/documents/cpm-22-manual/ch4.html
FYI - ASCII codes...
http://en.wikipedia.org/wiki/ASCII
This is also how I learned Assembly and sort of where I would like to see this go as a teaching tool. Not necessarily a symbolic compiler from Spin to Pasm, but a text translator instead.
Using your example the output might look like this...
'if (character >= '0' && character <= '9') cmp character, #$30 wc if_c jmp #fail cmp character, #$39 wc,wz if_nc_and_nz jmp #fail pass {do stuff here if condition is true} fail {continue here if the condition is true or false}The latter is hopeless when displayed in the forums code boxes as you have to scroll left and right all the time to follow it. Even when opened in a wide text editor window its hard work to read. The example in Beau's last post is much easier on the eyes.
Like Beau I got into assembler this way however in my day it was:
1) Write what you want to do in ALGOL. This was never compiled but just used as a pseudo code, like Spin or C in the examples here.
2) Write your 6809 assembler code from the above pseudo code.
3) Manually translate the assembly language to hex bytes for the EPROM programmer.
It was some years later that I first had the luxury of a real compiler and could see what it produced.
Beau:
Well of course that is what a lot of compilers do. High level language text in, low level assembly language text out. You then use an assembler and linker to get the final binary.
So that teaching tool would be a real compiler. It's PASM output could be compiled into a binary with the Prop Tool.
... I guess that means ... Hmmm, time to go to the think tank. :smilewinkgrin:
1. In my Zero footprint debugger, it is possible to see (by tracing) exactly what spin code translates to in pasm. Of course, all the overheads of spin are present including all the pushes and pops.
2. A much easier way is to look at the optional listing that bst and homespun create.
Now of course, both these are bytecode expansions and not the original spin code.
I wonder if a tool to list the pasm equivalent of a line of Spin code could be useful??? Or will it generate just too much 'fluff' to be useful ???
I think that would be wonderful!
It is very difficult to "think" in assembly. Everything goes so slow and one small step at a time. (Slow as in all the lines of code you need to write to do something simple.)
If a person learning this can see how a line from spin translates into many lines of assembly, then look at it and think huhhhh?????
Then figure out what the first assembly line is doing.
Then the next line.
And the next...
Then suddenly... :idea:
FYI - I learned how things worked when I was young by taking things apart and seeing what was inside.
And that was what I was doing too when I learned assembly. I had a simple working program to examine. Then a tool to look inside and see what was there (the disassembled code). Then a book to look up what each assembly command did.
Yes, but the topic here is about imparting PASM, not byte code, skills to beginners by documenting PASM code with spin syntax comments.
This has led Beau to the natural conclusion of, why not a translator Spin to PASM? Which is actually a compiler anyway.
I'm also concerned about the "fluff" problem here. The interesting techniques in PASM cannot even be represented in Spin, at least not in any concise sensible way. For example:
1) The heavy use of JMP in a non structured ways.
2) The difference between variables in COG or in HUB.
3) The distinction between signed and unsigned arithmetic.
4) How would you represent the coroutines of FullDuplexSerial?