FORTH on the PicoCalc

thanks again, i think i have it all sorted out now, took a bit of messing about, and i just got my second picocalc today, and it works properly.

i’m digging into the getting started doc on the github, but any pointers you might have to how forth works underneath, how it saves and loads words, etc… would be great. i’ve lived in the world of asm, c, basic and python (etc…); but have been getting into lisp and forth more recently. might be fun to do aoc (advent of code) this year on the picocalc, in forth, while hiking, in the wilderness.

zeptoforth is an inlining native-code-compiling Forth, unlike many Forths which are ‘indirect-threaded’ or ‘direct-threaded’, i.e. it compiles words directly into native ARM Thumb instructions and, when directed to do so by defining words with [inlined] in them which do not call any non-inlined/non-constant words or, on the RP2040, do not make any use of constant pools, can inline words into them. It also attempts to inline constants as much as it can. This does have the effect that there is no good way to list words after they have been defined except as ARM Thumb assembly language.

zeptoforth, being a Forth, has distinct interpretation and compilation modes. It can compile to both the main task’s RAM dictionary and to the flash dictionary; this is controlled by specifying compile-to-ram and compile-to-flash, where compile-to-ram is the default on boot.

Code that is compiled to RAM is transient, can be forgotten in the reverse order of being compiled at runtime by defining a marker or cornerstone and then executing it or by executing forget. Code that is compiled to flash is semi-permanent and is available on boot (it should be noted that variables, values, and buffer:s compiled to flash must be followed by a reboot before they are actually accessed or else undefined behavior and likely a crash will result). Code compiled to flash cannot be arbitrarily forgotten but can be erased with the use of markers and cornerstones compiled to flash, but unlike markers and cornerstones compiled to RAM these are very often quite wasteful of flash space because they must align the current flash dictionary here address to a flash erase page boundary.

One important gotcha with zeptoforth is that trying to create a buffer with create when compiling to flash will not do what you want it to do, because the buffer will be compiled to flash and not to RAM, and thus will not be modifiable; however, create is still useful when compiling to flash for creating constant arrays in combination with ,, h,, and c,. In most cases when you want to create a statically-allocated buffer use buffer: instead.

If you specify a control structure while interpreting, it will unlike many Forths temporarily switch to compilation mode, compile to RAM, then when the outermost control structure is ended automatically execute the code compiled and then promptly forget it. However, this has the side effect that if you attempt to modify the RAM dictionary within this code, your changes will be forgotten as well.

I hope this is a short summary of some basic concepts related to zeptoforth; if you have any additional questions, just go ahead and ask!

Edit:

Some additional important points is that, as zeptoforth supports exceptions, you may want to attempt to execute code and clean up after it even if an exception has been raised, and then re-raise the exception. A good example is as follows:

: hex. ( x -- )
  base @ { old-base }
  [: hex u. ;] try
  old-base base !
  ?raise
;

In this case this is a word prints a 32-bit value as hexadecimal, first saving the base, then afterwards restoring the base even if an exception was raised, and then re-raising the exception.

It should be remembered that try always returns either 0 or the exception caught, but beneath that on the stack, if an exception was raised, it restores the data stack pointer to its original location. As a result the contents of the data stack aside from its top cannot necessarily be trusted. Therefore, we use a local variable, which is on the return stack internally, to save the base.

As for ?raise, it pops the top of the stack, if it is not 0 it treats it as an exception and raises it, or otherwise it continues executing onwards. This behavior is very useful for conditionally re-raising exceptions.

(Note that in this context [:;] defines a quotation, a sort of nested anonymous word; once it is done being defined it will push, at runtime, its execution token onto the data stack.)

Another useful note is the existence of with-allot ( bytes xt – ) and with-aligned-allot ( bytes xt – ); these take a number of bytes for a temporary buffer on the top of the current task’s RAM dictionary and an execution token to execute. It will first allot the buffer, then call the execution token, passing it the base address of the buffer. Once the called execution token returns or an exception is raised, it un-allots the buffer and, if an exception had been raised, it re-raises it. Note that with-allot differs from with-aligned-allot in that the former does not care about alignment while the latter guarantees that the buffer is cell (i.e. 4 byte)-aligned.

An example of the use of this is:

: stupid-with-allot-example ( -- )
  s" Hello, world!" dup [: { addr bytes buffer }
    addr buffer bytes move
    buffer bytes type
  ;] with-allot
;

Another useful thing which you will see a lot of the use of in the code is local variables. This is a feature missing from many more traditional Forths, but I find them very useful. E.g. to define local variables x, y, and z with the values 0, 1, and 2, compile:

0 1 2 { x y z }

To set z to 3 then compile:

3 to z

To add 1 to x you can either compile:

x 1+ to x`

or

1 +to x

Note that the default kind of local variable is a single-cell local variable that, when referenced, pushes its value, but there are also double-cell local variables, and both single-cell and double-cell local variables have variants which push their addresses rather than values (which is often very useful for defining very small temporary buffers on the return stack).

You can see this with the following:

0 1 2. 3. { W: x W^ y D: z D^ w }

where x is a normal cell local variable that pushes its value (the W: is just for consistency with the other examples; it is unnecessary), y is a cell local variable that pushes its address, z is a double-cell local variable that pushes its value (mind you 2. is a double-cell integral value), and w is a double-cell local variable that pushes its address (mind you 3. is a double-cell integral value).

I should note that local variables in zeptoforth can be defined just about anywhere in a word and can be freely intermixed with doloops, but are not arbitrarily intermixable with accesses to the return stack with >r, r>, r@, rdrop, etc. because they live internally on the return stack at runtime.

2 Likes

awesome, thanks for the detailed run through.

i’ve also noticed in the docs that i can use ctrl-w in zeptoed to save what i’ve been working on. that will be handy.

one design decision that intrigued me was setting up psram to use fat32, which on the surface seems odd, but i’ll give some context why this got my attention.

i’ve gathered the bits to make my own diy 68000 computer, including the use of (non-volatile) fram. the reason i was playing with lisp and forth was to potentially make a sort of operating system for this board. while thinking (“on paper”) about the design, i wanted to take advantage of fram being non-volatile, and have the system boot back up to where it was last. which got me on to the idea of building the memory allocator around a filesystem like fat or littlefs. i sort of got this idea from palm os, that just had stack + storage. i’d still have regular sram for my stack, and the 68k let’s you have different stacks for system and user processes.

so, was that a similar thought for using fat32 for the psram? if something i write needed to allocate 100k in ram, it could just create a file in the psram?

zeptoforth does not internally force one to use PSRAM as a FAT32 filesystem; that is just an optional user choice – I just opted in utils/build_picocalc.sh and utils/build_picocalc_zeptoip.sh to use this choice.

I opted to use PSRAM as a FAT32 filesystem on Pimoroni Pico Plus 2 and Pico Plus 2 W boards installed in the PicoCalc partly because it is relatively slow as RAM but faster than and without the wear issues of flash, whether QSPI or on SDHC cards, and because I had no other pre-designed means of managing it.

In theory one could use it as one big block of memory, but that is difficult to use effectively in practice. (Some have tried to use it elsewhere for things like framebuffers but have found that it turns into a performance bottleneck in such applications.)

About FRAM, while FRAM is faster than flash and has not been successfully determined to wear out, FRAM is also slower than SRAM, so if you use FRAM for your primary storage outside of the stack on your 68K computer, it will bottleneck its performance.

ok, i see where you are coming from now, just use psram as secondary storage because of speed impact.

the fram i have for my project is parallel (compatible with 62256 sram) and has 130ns write time, which should get me full speed at 7mhz.

Inspired by a spirograph demo written by another user in BASIC, I have written a little spirograph demo for zeptoforth on the PicoCalc. Note that as it uses hardware single-precision floating point it is only supported on the RP2350.

Here is a screenshot:

This is generated from:

160e0 80e0 70e0 0.01e0 spirograph::draw-spirograph

The source code is at:

The usage is:

spirograph::draw-spirograph ( rf rm r step – )

where:

  • rf is the outer gear’s radius as a single-precision floating-point number in pixels
  • rm is the inner gear’s radius as a single-precision floating-point number in pixels
  • r is the radius of the pen’s position in the inner gear as a single-precision floating-point number in pixels
  • step is the step between iterations of the outer angle as a single-precision floating-point number in radians

Note that this will keep on drawing until the user presses a key.

Edit: Just a note, rm need not be smaller than rf, and r need not be smaller than rm – you can get interesting results with other relationships between the input parameters.

1 Like

That sounds very similar to what I’m experiencing on (new) Plus2W with zeptoforth install now.

I fail right at the “blocks” operation, yet checking the SDRAM all looks fine. I know there’s another where the 16MBflash get formatted, it even mentions onscreen how it could take longer, but apparently isn’t quite long enough timeout even as is? Also, where do I increase that specific already-special timeout value?

I was able to immediately turn around and install both uf2 loader and picomite without problems, and they installed and worked fine, so it doesn’t appear to be any undue sensitivity, and the unit is still much too young to be hitting flash fatigue failures.

Open to any suggestions? Is there a listing anywhere of the manual steps to replicate what build_picocalc.sh does, so I can go step by step manually and see whether I can complete if I just give longer timeouts in spots? If not, seems worth making one, just for these specific debug needs.

Thanks as always, Travis!!!

-John W

When it initializes the ‘blocks’ storage it disables the timeout altogether rather than merely making it longer. It is interesting if it is failing right at this point, because I have specifically flash-nuked my Pico Plus 2W and then reinstalled right after this without a problem (where before I disabled the timeout for this source file it would fail at precisely this point).

Currently there is no direct listing of the exact steps used by utils/build_picocalc.sh, but hand-executing each step it takes from reading its source should not be too difficult (as its source is rather self-explanatory).

zeptoforth 1.14.2.6 has been released. This release should resolve the boot loop issue with PicoCalc BIOS firmware version 1.4 per comments on the zeptoforth GitHub.

(To be entirely honest, I have not tested it with this firmware because I do not want to risk opening up my PicoCalc to update the BIOS firmware on it.)

You can get it from:

Note that zeptoforth will still report a version of 1.14.2.3 on boot because the kernel binaries have not been changed since then.