These are some ideas about how to structure the flash and RAM memory for applications built on top of Mecrisp Forth.
First off: 64 KB of flash memory turns out to be plenty for very substantial applications. All of the current JeeLabs Energy Monitor code easily fits within 64K (even though the Olimexino has 128K). And that includes the RF69, OLED with graphics, ADC with DMA, pulse counters, DCF decoding, as well as the core hardware abstraction layer, clock and time management, SPI and I2C drivers, timers, PWM, RTC, interrupt-based UART with ring buffers, and the multi-tasker.
Code is added on-the-fly to the Forth “dictionary”, either in flash memory, or in RAM. All RAM-based definitions are lost on reset or a power-cycle, so this is really more for development than for permanent use.
The dictionary acts like a “stack of code” (and constant data). Mecrisp includes primitive words to erase and re-use flash memory, but this needs to be done with care since words can cross flash page boundaries. The solution to this is the “cornerstone” word, which can be called to set a marker on a flash page boundary. The use of cornerstones is really simple - this defines one:
cornerstone blah
And this erases all definitions added to the dictionary after that definition:
blah
Note that blah
itself remains in the dictionary, it’s not self-destructive
like the traditional “forget
” mechanism used in other Forth systems.
With cornerstones, we can define an app structure which simplifies development and makes it easy to replace just the top layer with new code. This works really nicely in combination with the “include” mechanism in Folie, by setting up source files as follows:
Layer “A” (always.fs)
\ this file must be loaded on top of a pristine Mecrisp image
compiletoflash
... lots of definitions ...
: init ... ; \ essential startup configuration (clock, console, ...)
: cornerstone ... ;
cornerstone eraseflash
This uses the trick from a previous article of having some
code stay around essentially forever, by redefining the built-in eraseflash
word. With the above code loaded into a fresh Mecrisp setup, we effectively
install some code and init
word which sets the stage for permanent use.
Entering “eraseflash
” at the Mecrisp command prompt will always revert the
system and flash state to this configuration.
Layer “B” (board.fs)
The next layer is for “board” definitions, i.e. words which are for a specific µC and board, and words which allow abstracting away some of the basic differences. This is where a µC-specific implementation of the SPI driver resides - so that everything on top can see the same API:
eraseflash
compiletoflash
... lots of definitions: pins/buttons/LEDs, main h/w interfaces ...
cornerstone <<<board>>>
Board definitions rarely need reloading, their purpose is to simplify the code in the next layers. This would probably be called the “runtime library” in other languages.
Note how the source file starts with an eraseflash
to restore the dictionary
to the “always” state of layer A, before it adds the board definitions.
A side effect of calling eraseflash
or a cornerstone, is that Mecrisp will
reset itself afterwards, and run the init
word again. So all board definitions
take place in the context set up by init
.
Layer “C” (core.fs)
The “core” layer is intended for well-tested code, such as hardware drivers and
libraries, which are needed for this specific application. It is likely to
consist mostly of include
lines for folie
, to bring in certain features.
Here is the outline of a sample “core.fs” source file:
<<<board>>>
compiletoflash
... lots of definitions: includes and tested application code ...
cornerstone <<<core>>>
As the code for a new app solidifies, it can be added to this source file (inline or via includes).
In this case, the source code starts with <<<board>>>
to wipe out any other
definitions added afterwards (i.e. the previous version of itself). And it ends
by adding a new conerstone of its own.
Layer “D” (dev.fs)
This layer is for active development. It is not stored in flash but in RAM. That means that the code and definitions will all disappear on reset. The source code reflects this difference:
reset
... more definitions: work in progress, debug words ...
No more cornerstones now (they can only be used for flash memory). The goal here is to make reloading as fast as possible. A lot of development will take place in this layer, with code moved to the “C” layer once it’s working and deemed stable.
Layer “E” (exp.fs)
Sometimes, development needs more work and more exploration before we can settle on a design and work out all the details. Layer “E” is for experimentation and exploration, i.e. code which is not necessarily going to end up in the real application - tests, little helpers, custom debug words, and actual calls to these words - the goal here is even faster turnaround:
include dev.fs
... yet more definitions, but also actual calls to start up things...
The intention is to allow several different versions of this source code, so
that there could be “e<sometag>.fs
” files for all sorts of trials. And we can
easily keep all of them around forever, just in case we ever need to chase that
tricky issue again one day.
In this case, we don’t start off with a call to reset
but we include the
dev.fs
source code (which includes the reset) to make sure all those words are
loaded. With a bit of luck, we can simply hit up-arrow + return to include this
file over and over again, as part of a fast-paced edit-run cycle.
Layer “F (final.fs)
The last layer is the one which turns the board into a complete application, starting up the minute is is powered up:
<<<core>>>
compiletoflash
... lots of definitions: includes and application code ...
: init ... init ; \ this will auto-start the application
Note that this layer does not load the D and E layers. It’s flash-only.
This one is risky, in that it “seals” the application. If the app is not working properly and if we haven’t built in an escape hatch of some sort, we will not regain control of the command prompt. In that case, all that’s left is to reflash the board by other means: a ROM-based boot or SWD.
One way out is to build a delay into init
, allowing some key press or other
external event to bypass a full auto-run. That way, a reset follow by some
specific action will get us back to that oh so powerful Forth command prompt.
An even better alternative though, is to build in support for an interactive command prompt, while running all the application code from a second background task, using the multi-tasker. This way, we can peek and poke while the application is actually running, and even develop enhanceents and new features on the fly (in RAM, for easy recovery).
So there you have it: a layered application structure, with built-in support for permanent console redirection, board-specific extensions, driver/library use, fast-turnaround development cycles, and an application freeze for turnkey use. It’s just a thought experiment for now, but worth a try!
There’s no IDE in sight: just your preferred source code editor, Folie, and Mecrisp.
As you may have noticed, the above layers are called A, B, C, D, E, and F - easy to remember!