UPDATED X2 with examples -- Modular Programming in Spin
localroger
Posts: 3,451
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...
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:
* 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...
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
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.
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.
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.
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?
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.
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.
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.
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) 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.
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?
But that is sooooo pre-BufStrFmt.
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.
Did something slightly similar years ago: http://forums.parallax.com/showthread.php/104762-Towards-a-one-line-spin-printf
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
clockdemo - Archive [Date 2013.07.19 Time 12.06].zip
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
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.
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
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
moddemo - Archive [Date 2013.07.24 Time 09.05].zip
Please don't mind me,
I am adding myself to this conversation so I can learn from it.