As often mentioned, Forth enables an interactive development approach: you enter commands on the µC itself, and things get done right away. No make, no compile step, no uploads.
But that’s an over-simplification, because it ignores the fact that most code needs to be written and saved to file. Unless truly one-off of course, and you don’t care about losing the source code once it has run. But usually, we really can’t develop a meaningful application on just the µC.
A more practical development foundation on the µC side is in fact set up like this:
- load the 20 KB Mecrisp core onto the µC (just once, unless flash memory gets messed up)
- add convenience code and drivers, also in flash, i.e.
always / board / core
- use cornerstones to manage flash memory as a stack of word sets
- tweak
core.fs
when module dependencies change, and reload over the old one
The idea being that all of the above code is fairly stable and well-debugged, so that we don’t have to go back and change this part very often. These are the 500..700 “words” we build on.
Now comes the main part, which can be split into a number of phases:
- thinking about what our project needs to do, and how to gradually map it to code
- designing and building pieces which stand on their own, such as h/w drivers
- trying out snippets of logic, to see what works and figuring out those pesky little details
- putting it all together, i.e. working out potential the interactions and global state
- finalising the code, removing debug statements, and making it start on power-up
Each of these phases is different. Since Forth is a bottom-up system, you can’t try out (or even write) code before the primitive words it uses have been defined. What you could do, is insert dummy word definitions, as a way to ignore some details while working on the bigger picture:
: think ;
: edit ;
: debug true ;
: rinse ;
: app
think
begin
edit
debug while
rinse
repeat ;
app
The above is a valid Forth application, even if somewhat nonsensical. So one
approach would be to put this code in a source file, say myapp.fs
, and then
use Folie to send it to an attached µC using “!s myapp.fs
”. But that’s a
top-down approach, even if the words are all empty stubs for now: it leads us
to think from the top down and drives the development from that direction.
A more effective approach is to not write this code at all, but just keep it in mind as part of our thought process. Instead, we focus on the pieces that we know we’re going to need, and just start implementing these pieces one by one.
If you’ve set up your project as a sub-directory of the 1608-forth/
area in
Embello (highly recommended), then one way to get going is to set up each new
set of related words as an “exploration”: a source file in an ex/
sub-directory, i.e. 1608-forth/myapp/ex/think.fs
.
Uploading to RAM is fastest, avoiding frequent flash erases, so a
starting point for developing the code for say think
is to create an
ex/think.fs
file with the following content:
\ this is the think code ...
compiletoram? [if] forgetram [then]
\ include ../../flib/other/module/you/might/need.fs
: think ." Thinking..." ;
think
The forgetram
at the start is very convenient, because it lets us re-send the
source file to replace the previous version. You can ignore the compileram?
logic for now. As we start implementing the logic of think
, we end up going
through this process over and over again:
- edit
ex/think.fs
on the host - save and switch to the terminal window running Folie
- enter
!s ex/think.fs
(tab-completion means we could also type “!s ex/t<tab>
”) - see what happened, and repeat this cycle by going back to editing
Sometimes (haha: if you’re optimistic …) things won’t work, and the µC may crash or hang. No big deal: hit ctrl-c, and you’re back at the prompt, with all RAM contents conveniently gone.
Occasionally, one of the lower-level words you added won’t be working quite
right. In that case, you can comment out the think
call at the end, and call
the troublesome words interactively, while manually setting up the necessary
arguments. This is where Forth’s interactivity shines: the ability to quickly
try something out, completely ad-hoc.
You can still manually load a changed file, e.g.”!s ex/think.fs
” - even it has
already be added to core.fs
and stored in flash. It will simply generate a few
harmless Redefine
warnings. Just keep in mind is that everything compiled
in flash will continue to point to the original version!
If you’re into test-driven development (TDD): have a look at varint.fs and varint-test.fs as examples of how to write test code. By placing that code in a separate file, you’ll be able to run the tests at any time, with the benefit of TDD when it comes to future changes and re-factoring.
If you’re using the multi-tasker, something to watch out for is that
forgetram
may cause trouble, since your task might still be referenced in
the task chain. A quick solution is to replace forgetram
with reset
, which
causes a full re-init instead of just clearing the RAM definitions.
At some point, ex/think.fs
will be deemed stable, and you’ll want to move on to the
next set of words to implement for our application. You can now permanently add
the think
definition and everything written for it to flash, as follows:
- make a copy of the
core.fs
file in your project area, i.e.myapp/core.fs
add the following line to it, somewhere before the
<<<core>>>
definition at the end:include ex/think.fs
enter
<<<board>>>
to remove the currentcore.fs
definitions from flashenter
!s core.fs
to add your updated version, including the newthink
definitions
From now on, the µC will have the new think
implementation stored in flash, in
the same way as all the other “standard” definitions. You’re ready to move to
the next part of your project.
Note that the integration step comes last. Assuming that same example as before,
at some point, you’ll want to make everything work together. That’s easy: create
a file dev.fs
with that definition of app
given above. Since all the pieces
it relies on have presumably already be implemented and stored in flash (via
your custom core.fs
), it should compile just fine.
To start up the completed app, enter the magic spell: “app
+ <enter>
” …
and if all is well, your application will start running. If not: go back to the
previous mode, and test things in isolation.
Ok, let’s assume everything works as intended. Let’s make the app run on
power-up. No serial port, no development, just running it. Create a file
“freeze.fs
” with the following contents:
\ install frozen myapp with auto-start on reset
include core.fs
compiletoflash
include dev.fs
: init init unattended app ;
Then type “!s freeze.fs
” on the Folie command line. The following will happen:
include core.fs
- wipes the old definitions and installs the latest versioncompiletoflash
- sets up to compileapp
itself to flash memoryinclude dev.fs
- compiles that app main loop we just created: init ... ;
- gets redefined to call our application code on power-up
For details on unattended
, see this
article. Note that it won’t start up
“app
” on reset if you have a serial port connected. In that case, you can
simply type “app
” yourself to have the same effect.
PS. The above approach uses Folie as host
front-end, but this will also work with picocom
and other terminal emulators,
if they can send throttled source code (ctrl-a + “s” in picocom).