Shop OBEX P1 Docs P2 Docs Learn Events
UPDATED X2 with examples -- Modular Programming in Spin — Parallax Forums

UPDATED X2 with examples -- Modular Programming in Spin

localrogerlocalroger Posts: 3,451
edited 2013-07-24 08:15 in Propeller 1
UPDATE: I have created a little example application which demonstrates how the technique is used with full example code:

clockdemo - Archive [Date 2013.07.19 Time 12.06].zip

UPDATE X2:

Another much more extensive example, showing a very wide variety of user interface functions and background processing shared among multiple objects:

moddemo - Archive [Date 2013.07.24 Time 09.05].zip

I recently embarked on a fairly elaborate project in Spin, and quickly ran into the usual issues with not being able to access important low-level objects like the serial, keyboard, and video I/O drivers from multiple high level application objects. My solution depends on an old trick but to an extent I'm not sure anyone has taken it before. Have a look at the archive _README_.txt, bearing in mind that this thing is only about half finished...
───────────────────────────────────────
Parallax Propeller Chip Project Archive
───────────────────────────────────────

 Project :  "Aani"

Archived :  Wednesday, July 17, 2013 at 8:02:07 AM

    Tool :  Propeller Tool version 1.2.5


            Aani.spin
              │
              ├──Keyboard.spin
              │
              ├──VGA_40x18_rom_text.spin
              │    │
              │    └──vga_40x18_rom_rc.spin
              │         │
              │         └──vga_40x18_rom_hc.spin
              │
              ├──Cog2Serial.spin
              │    │
              │    └──SVT3.spin
              │
              ├──RTCEngine.spin
              │
              ├──SVT3.spin
              │
              ├──StrFmt.spin
              │
              ├──bufstrfmt.spin
              │    │
              │    └──StrFmt.spin
              │
              ├──tdfmt.spin
              │    │
              │    └──StrFmt.spin
              │
              ├──configuration.spin
              │    │
              │    ├──Spin_I2C_Driver.spin
              │    │
              │    └──ui_methods.spin
              │         │
              │         ├──VGA_40x18_rom_text.spin
              │         │    │
              │         │    └──vga_40x18_rom_rc.spin
              │         │         │
              │         │         └──vga_40x18_rom_hc.spin
              │         │
              │         ├──Keyboard.spin
              │         │
              │         ├──StrFmt.spin
              │         │
              │         ├──bufstrfmt.spin
              │         │    │
              │         │    └──StrFmt.spin
              │         │
              │         └──dummybkg.spin
              │
              ├──ui_methods.spin
              │    │
              │    ├──VGA_40x18_rom_text.spin
              │    │    │
              │    │    └──vga_40x18_rom_rc.spin
              │    │         │
              │    │         └──vga_40x18_rom_hc.spin
              │    │
              │    ├──Keyboard.spin
              │    │
              │    ├──StrFmt.spin
              │    │
              │    ├──bufstrfmt.spin
              │    │    │
              │    │    └──StrFmt.spin
              │    │
              │    └──dummybkg.spin
              │
              └──setup.spin
                   │
                   ├──Keyboard.spin
                   │
                   ├──VGA_40x18_rom_text.spin
                   │    │
                   │    └──vga_40x18_rom_rc.spin
                   │         │
                   │         └──vga_40x18_rom_hc.spin
                   │
                   ├──configuration.spin
                   │    │
                   │    ├──Spin_I2C_Driver.spin
                   │    │
                   │    └──ui_methods.spin
                   │         │
                   │         ├──VGA_40x18_rom_text.spin
                   │         │    │
                   │         │    └──vga_40x18_rom_rc.spin
                   │         │         │
                   │         │         └──vga_40x18_rom_hc.spin
                   │         │
                   │         ├──Keyboard.spin
                   │         │
                   │         ├──StrFmt.spin
                   │         │
                   │         ├──bufstrfmt.spin
                   │         │    │
                   │         │    └──StrFmt.spin
                   │         │
                   │         └──dummybkg.spin
                   │
                   ├──ui_methods.spin
                   │    │
                   │    ├──VGA_40x18_rom_text.spin
                   │    │    │
                   │    │    └──vga_40x18_rom_rc.spin
                   │    │         │
                   │    │         └──vga_40x18_rom_hc.spin
                   │    │
                   │    ├──Keyboard.spin
                   │    │
                   │    ├──StrFmt.spin
                   │    │
                   │    ├──bufstrfmt.spin
                   │    │    │
                   │    │    └──StrFmt.spin
                   │    │
                   │    └──dummybkg.spin
                   │
                   ├──StrFmt.spin
                   │
                   └──bufstrfmt.spin
                        │
                        └──StrFmt.spin


────────────────────
Parallax, Inc.
www.parallax.com
support@parallax.com
USA 916.624.8333

Before you start wondering what kind of mushrooms I've been chomping on, bear in mind that all of these objects have been carefully purged of VAR data types so that multiple instances use the same data. This allows me to put as many instances as I want, and as you can see I have gone to town.

Particular features of this project to note:

* The video and serial objects do not have methods like DEC and HEX. Those are implemented by the single object STRFMT, which can have a rich collection of methods because there is only one of it.

* STRFMT uses buffers supplied by the caller, generally local arrays, so it has no overhead at all and is even multi-cog thread safe.

* BUFSTRFMT is a wrapper for the STRFMT functions which provides a persistent return data buffer.

These low-level methods are extremely universal in scope and can be, and are, dropped anywhere they are needed, so you can do stuff like this anywhere:
vid.str(bsf.hex(myval, 8))

* UI_METHODS implements generic engines for popping up alerts, picking from a list, and doing keyboard input.

This code is not application specific, but is more focused on the particular hardware setup in use, using the video and keyboard drivers extensively. It will show up beneath several application modules. Right now it's under SETUP, which implements actual menus for setting up the system, but it will also be used in several procedural modules which implement operator user interfaces in the final system.

* TDFMT implements two-way conversions between stripped numeric and the system packed time/date format and string formatting of times and dates.

This isn't application specific either, but is specific in many ways to the DS1307 time/date module I'm using.

* SVT3 defines the Propeller pin assignments for the hardware module I'm using.

This defines the hardware and is universal for anything I write using this PCB.

* CONFIGURATION is application code which consists mostly of a massive CON block laying out where system setups are, but also implements a few methods for saving changes and fetching descriptive strings for editing. It will be needed by every application block because it determines how they will work.

* SETUP is application code which draws and navigates the system setup menus. Its top method will be called from the main thread to enter system setup and it will return to resume normal day to day operation.

There will be numerous other application modules all of which will have convenient access to whatever lower level methods they need to accept input and draw the user interface.

This is proving to be a very powerful way to fit a more complex program than one would normally expect to accommodate in 32K. If anyone would like more details I can provide some more of the lower level code and examples. I'm also open to suggestions...

Comments

  • Mike GreenMike Green Posts: 23,101
    edited 2013-07-17 08:28
    The size of your modules depends a lot on how you access the variables. Accessing VAR variables uses shorter bytecodes than using BYTE[<addr>]. If you do a lot of the latter, it makes your code bigger. In similar circumstances, I copied some values to local (stack) variables for more efficient access.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-17 08:49
    Mike Green wrote: »
    The size of your modules depends a lot on how you access the variables.

    The main difference between what I'm doing here and 'normal' spin is using DAT instead of VAR, which has about the same overhead, and making very heavy use of the resulting reusability of the objects.

    The trick is to keep layers straight so you don't end up dropping an object under itself. You don't want to drop serial under video or vice-versa, as they are at the same layer. Fortunately there's no need to, since all the methods that might need them for debugging are at the next layer up in UI_METHODS.
  • Dave HeinDave Hein Posts: 6,347
    edited 2013-07-17 09:20
    Mike is correct about the efficiency of using VAR variables directly versus accessing DAT variables using BYTE[]. It produces smaller code and is faster. So what is the "trick" you're using? Is it changing VAR variables to DAT variables, and using a pointer to access multiple instances? I've used that extensively in spinix. It uses modified versions of FullDuplexSerial and FSRW to access instances with a pointer. Is that the type of thing you're talking about?

    The spinix version of FSRW copies variables from DAT space to local variables on the stack, and then copies them back to DAT space before exiting the method. This is done using LONGMOVE, which is fairly efficient compared to the time it takes to execute other Spin instructions.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-17 10:32
    Dave Hein wrote: »
    It uses modified versions of FullDuplexSerial and FSRW to access instances with a pointer. Is that the type of thing you're talking about?

    That is a very primitive version of what this is a much more advanced example of. It is about making low-level objects of arbitrary complexity available to multiple higher abstraction objects, not as an occasional trick but as a fundamental aspect of the program design.

    It started out as a way to share code which is ordinarily duplicated, for example the output methods like DEC, HEX, and BIN which end up in every output object. It also lets you drop output objects anywhere you need them, for example under FSRW if you need to output some debug info even though the application uses the same object from a much different place.

    It's about being able to organize projects of a certain complexity as separate objects at all. For example, I have some fairly complex background processing going on with serial data while the main VGA/keyboard user interface may be being drawn by any of several high level objects depending on what mode the system is in. By structuring the program this way, that background module can be placed under ALL the other objects which need to keep it updated while doing their own thing.

    This isn't about fine tuning performance or memory (although it saves lots of duplicate code). Those are trees. This is about the forest of how to lay out a relatively complex application so that resources can be shared between separate components.
  • Dave HeinDave Hein Posts: 6,347
    edited 2013-07-17 10:51
    I still don't see the "trick". It sounds like you're just putting common methods in an object that can be shared by other objects. Your also passing a pointer to access one of several instances stored in DAT memory. Are you using method pointers? That's been done also. Where's the trick. It might be clearer if you posted your code.

    EDIT: Maybe you're saying that you removed the redundant DEC, HEX and BIN methods, and replaced them with a string object that all other objects reference. But that can't be the trick you're talking about. It must be something else.

    EDIT2: I'm sure there's something there that I'm not getting. Can you describe a specific example that's more advanced than the primitive scheme I described?

    EDIT3: OK, after re-reading the OP, it seems like the trick is to remove redundant code from objects and move them to objects that can be accessed by other objects, correct?
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-17 12:35
    Edit3 much warmer. It's best not to think of this as a "trick." It is an application design strategy. I suppose this means I haven't described it very well.

    Making a special version of fullduplexserial that uses DAT instead of VAR so you can stick a copy of it in FSRW when you're debugging is a trick. You do that too much though and you end up sticking something inside itself, which the compiler doesn't like.

    This application is designed in layers. All object layers other than the top are designed from the ground up to be shared by multiple parent objects. This isn't an afterthought; it's a fundamental design principle. You can treat them as being "virtually global" within their layer. This allows redundant code to be extracted and put in single-instance objects which are shared, not just at the level of the serial or video driver, but throughout the application.

    For example, in ui_methods I have an elaborate routine which does the equivalent of a BASIC Input statement with lots of options. It's over 100 longs of byte code. The usual Spin strategy would require me to either put all the application code that needs that function in a single top object, or duplicate it in each object that needs it. However, since all my lower level I/O objects used by ui_methods can be shared, with very little care I can make the more abstract ui_methods itself shareable too. This means I can break up my top level application into multiple objects without having to duplicate those nontrivial functions or falling into them from some elaborate common state machine.

    In this application I will have a topmost object which displays the weight and time and date and a main menu. From that screen you can select options -- setup, weigh in, weigh out, report, etc. Each of those can be a different object now even though all of them need the ui_methods, the background processing call, and so on. Code which is generic doesn't have to be crammed in an object with code that isn't so that the whole application can get to it.
  • Dave HeinDave Hein Posts: 6,347
    edited 2013-07-17 12:51
    OK, that sounds like a good approach to building a clean program. Thanks for the explanation.
  • Cluso99Cluso99 Posts: 18,069
    edited 2013-07-17 17:14
    localroger: Seems impressive to me.

    It has always been a problem to access other modules from different levels and this solves this nicely.

    The other thing that has amazed me is that all those output modules use different fdx.tx, vid.out, etc. By reediting those objects and removing the dec, bin, hex, etc the out/tx can be standardised to out. The same with in/check/etc. I solved a lot of this by using a simple stub redirector object called stdin and stdout. That way I can swap the input and out methods on the fly (not that I have actually achieved that yet, but I can simply compile with a different out and in objects withought changing any code, just by using mapped mailboxes.

    Whenever we get to this level of code, we always seem to have an SD card, so it would also make sense to have an error routine where the strings could be on a fat file, with just the code number being passed. Unfortunately for us, at the moment we have a few SD drivers. I have advanced to using Kye's but it is necessary to separate some of the code so that it can be called via mailboxes. I am part way into this. Again, unfortunately the low level pasm calls are different from the previous rounds of fsrw. I am interested to hear what you have done in this arena.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-17 19:15
    Hey Cluso, this project actually doesn't have a SD card. In this project I'm using high EE and the T/D chip for nonvolatile storage. But I'm so old I remember actually looking up ERROR 9 in the printed documentation to find out it meant "array access out of bounds."

    The main thing here is, yes, standardizing calls for "right-justified decimal number," and eliminating redundant code to put that functino in every output device. This method does it without mailboxes and the support code for them. (Ironically, my dual cog cog-buffered serial object uses mailboxes, but I've abstracted them back into FDserial style calls.) Nearly everything goes through the actual hardware drivers via a str() method. Other formatting is done by strfmt so it's the same for everybody, and bufstrfmt gives a way to use the common object without manually allocating a buffer.

    It is kind of a shame you can't just use the local vars to return a string result but the local var buffer on the stack gets clobbered when you call anything else after returning. Bufstrfmt borrows a launched PASM image (from in this case the keyboard object) to circularly allocate semi-persistent storage, so you can just drop vid.str(bsf.hex(cnt,8)) in the code pretty much anywhere in the application and it will Just Work. Because keyboard's PASM image is 1,200 bytes that gives room to do lots of complex string manipulation without worrying about the lifespan of the oldest chunk.

    This is a lot faster and involves a lot less code than the mailbox with substitution approach, and involves less modification to existing objects.
  • Cluso99Cluso99 Posts: 18,069
    edited 2013-07-17 20:16
    localroger wrote: »
    This is a lot faster and involves a lot less code than the mailbox with substitution approach, and involves less modification to existing objects.

    I kind of doubt your statement...

    to send a char to the output device...
    stdout.out(char)

    Here is the StdOut.spin object file (note here we have the out, dec, hex, str, etc routines in one place, but they would be better in their own separate object as you have done)
    VAR
      long  pRENDEZVOUS
    PUB start(rendezvous)
      pRENDEZVOUS := rendezvous                             'get rendezvous location
    PUB out(c)
    '' Print a character
      c |= $100   '0.FF -> 100..1FF                         'add bit9=1 (allows $00 to be passed)
      repeat while long[pRENDEZVOUS]                        'wait for mailbox to be empty (=$0)
      long[pRENDEZVOUS] := c                                'place in mailbox for driver to act on
    
    
    '----------it would be better to place the routines below into a separate object, together with other string manipulation routines----------
    PUB str(stringptr)
    '' Print a zero-terminated string
      repeat strsize(stringptr)
        out(byte[stringptr++])
    PUB dec(value) | _i
    '' Print a decimal number
      if value < 0
        -value
        out("-")
      _i := 1_000_000_000
      repeat 10
        if value => _i
          out(value / _i + "0")
          value //= _i
          result~~
        elseif result or _i == 1
          out("0")
        _i /= 10
    PUB hex(value, digits)
    '' Print a hexadecimal number
      value <<= (8 - digits) << 2
      repeat digits
        out(lookupz((value <-= 4) & $F : "0".."9", "A".."F"))
    PUB bin(value, digits)
    '' Print a binary number
      value <<= 32 - digits
      repeat digits
        out((value <-= 1) & 1 + "0")
    
    
    The output driver (vga, serial, tv, or whatever) just monitors the mailbox for a character. Couldn't be simpler.

    This way, the underlying driver (vga/serial/etc) can just be changed on the fly without the user program ever knowing.

    So, it really doesn't matter whether you predefine mailboxes or use buffers. The result and access are the same. You would just be referring to your serial drivers buffers as buffers where I refer to them as mailboxes, only because I predefine their location in hub.

    However, I will grant you that in video/tv, you may put the character directly into the screen buffer. But then you are in control of the screen buffer, and have to take account of all its complexity including cr and lf, screen size, etc. I just implement a minimal VT100 style terminal within the vga/tv driver, so the user is just writing to a terminal device.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-18 09:33
    Cluso, even if mailboxes are better for character input and output, that's not the point of my technique here. This also works for high level functions like popping up a message box, picking an item from a list, or performing user key-character input. A fairly complex background I/O maintenance function which might normally be given a cog can be called from all user input loops wherever they are in the program. And this can all be done with a fairly normal calling structure instead of arranging for elaborate memory drops.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-18 16:32
    Wow it is amazing how much code this whole thing is saving now that I'm using it to write application code. BufStrFmt is amazing. Real code that works:
    bsf : "bufstrfmt"
    ...
    daysleft  byte  0
    ...
    vid.str(bsf.concat(bsf.concat(string("You Have "),bsf.rdec(daysleft, 2)),string(" Days Left."))
    

    The complexity of such expressions that is safe depends on how deep a circular buffer you give BufStrFmt. Borrowing Keyboard's PASM image it has 1200 bytes to work with.

    Or say you want to print a box?
    vid.out("*")
    repeat 40
      vid.out("-")
    vid.str(string("*",13))
    repeat 8
      vid.out("|")
      repeat 40
        vid.out(" ")
      vid.str(string("|",13))
    vid.out("*")
    repeat 40
      vid.out("-")
    vid.out("*")
    

    But that is sooooo pre-BufStrFmt.
    vid.out("*")
    vid.str(bsf.chstr("-",40))
    vid.str(string("*",13))
    repeat 8
      vid.out("|")
      vid.str(bsf.chstr(" ",40))
      vid.str(string("|",13))
    vid.out("*")
    vid.str(bsf.chstr("-",40))
    vid.out("*")
    

    It's both smaller and way faster. I'd estimate the code to draw and navigate a menuing system is coming out about half the size it would have been without the new pseudo-global string helper objects.
  • jazzedjazzed Posts: 11,803
    edited 2013-07-18 18:09
    I like it.

    Did something slightly similar years ago: http://forums.parallax.com/showthread.php/104762-Towards-a-one-line-spin-printf
  • Cluso99Cluso99 Posts: 18,069
    edited 2013-07-18 18:30
    localroger wrote: »
    Cluso, even if mailboxes are better for character input and output, that's not the point of my technique here. This also works for high level functions like popping up a message box, picking an item from a list, or performing user key-character input. A fairly complex background I/O maintenance function which might normally be given a cog can be called from all user input loops wherever they are in the program. And this can all be done with a fairly normal calling structure instead of arranging for elaborate memory drops.
    Sorry, I did not mean that it was (the point of your techniques). It is more of an adjunct to your techniques.

    I have regularly found the same problem as yours, and then had to find ways to circumvent it.

    But you have more than one technique here. The BufStrFmt is a great addition! It goes way further than Kye's string formatting. Often I find that I would like to put a group of "-" on the screen/terminal. Currently I just inline define the string (but I am not short on memory) thus printString(String("
    ")). And mostly, this is not time sensitive, so repeating a single character if fine.

    Keep up the great work :)
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-18 19:32
    Thanks Cluso. I will drop the current actual code on this thread tomorrow so you can see it instead of just talking about it. The ability to return a string result from a function without manually allocating a buffer for it just makes the ballgame completely different. Besides, youse guys lay eyes on my code you might see something I missed to make it even better.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-18 19:50
    Jazzed -- your stuff is similar except that it seems to use a single buffer. The breakthrough with BufStrFmt is that it uses a circular buffer so that multiple results can remain alive while a more complicated operation is processed. It's still very very simple so the code is very small. You can't trust the results more than transiently but if you keep control of the buffer and you know its size you can safely do some pretty complex operations that look like they're being done in a garbage collected environment.
  • FernandFernand Posts: 83
    edited 2013-07-19 02:06
    Looking forward to see some code. Can you put up a working project so we can actually play with it and run it?
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-19 11:13
    I have added an example project to the top comment. It is similar to the clock demo I posted a couple of weeks ago but much cleaner. Runs on a demo or protoboard with VGA and PS/2 keyboard. I'll put the link here too:

    clockdemo - Archive [Date 2013.07.19 Time 12.06].zip
  • msrobotsmsrobots Posts: 3,709
    edited 2013-07-19 17:47
    well done @localroger.

    Your circular buffer is almost a GC... that is nice by itself.

    As for using the same objects multiple times and let the compiler prune duplicates - I used that for my prop-ISAM experiment and found out it is working very good. But I never did this as consequent as you are doing it now, so I am amazed about the result.

    You are saving tons of lines of spin-code. The object-tree is overwhelming but the source files are small and clean. Now we need a editor with code completion like Visual Studio to ease the object tree/method navigation...

    I really like it. A very interesting way to structure a spin program.

    Enjoy!

    Mike
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-19 20:06
    msrobots -- I thought about actually trying to complete it as GC, and realized the overhead would kill the efficiency I was getting. It's possible to abuse this system unintentionally, just as it would grind a primitive 8-bt BASIC interpreter to dust successively concantenating bytes to a long string. As long as one keeps the depth of an immediate string calculation within what the buffer can handle and one uses the result immediately, it's almost as good as GC and far simpler and faster.

    Even as I was assembling this demo I was seeing the potential for new strategies. It's a whole new way of thinking about Spin programming.

    Also, am I the only one who wonders why the PS/2 keyboard object needs a 1,200 byte dead after launching PASM image, when fullduplexserial is under 256 bytes? Methinks maybe a lot of stuff is done in PASM there that could have been done in Spin. OTOH it does make a nice roomy bufstrfmt buffer.
  • Cluso99Cluso99 Posts: 18,069
    edited 2013-07-19 20:19
    The advantage of keeping the processing within the pasm object (as in PS2 driver) is that the driver becomes an intelligent driver and delivers only what is required. Thus processing time of the main program is reduced. Of course, the disadvantage is more code space in the cog (not a problem) and the space used in hub before loading (undesirable). However, by reusing the hub space, the overall hub space is reduced because you don't have to do the intelligent work in spin.
    So, what we really need is a standard way of loading those objects without wasting hub space. Many of us have done this at various times, but there is still no accepted standard :(
  • AribaAriba Posts: 2,690
    edited 2013-07-20 15:47
    A PS/2 keyboard does not send ASCII codes, so a big part of the cog ram is filled with a conversion table.
    I don't think this table will get much shorter if you do the conversion in Spin.
    There is a big advantage of having the table in cog ram together with the PASM code: If you want support keyboards with different layouts for other countries, only the PASM image must change. I have this image normally on the SD card as "PS2_KBRD.COG" and load it at runtime before I start the keyboard.

    Andy
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-20 18:13
    OK, OK, so with keyboard I get a well deserved 1200 byte buffer for my string service. Trees seem to win out over the forest around here big time.
  • localrogerlocalroger Posts: 3,451
    edited 2013-07-24 08:15
    Another example for the Demoboard. This is the setup module for the actual application I am working on. It demonstrates a wide variety of I/O techniques, background processing to maintain a clock without using a cog despite multiple modules handling the user interface at various times, and uses less than half of Hub RAM while exposing extremely powerful string and I/O functions for use by the rest of the business logic code.

    moddemo - Archive [Date 2013.07.24 Time 09.05].zip
  • Mmmmmmm

    Please don't mind me,
    I am adding myself to this conversation so I can learn from it.



Sign In or Register to comment.