Making A Solar-Cell Tester With Mecrisp-Stellaris Forth

In the last two articles on Forth, I’ve ranted about how it’s beautiful but strange, and then gotten you set up on a basic system and blinked some LEDs. And while I’ve pointed you at the multitasker, we haven’t made much real use of it yet. Getting started on a Forth system like this is about half the battle. Working inside the microcontroller is different from compiling for the microcontroller, and figuring out the workflow, how to approach problems, and where the useful resources are isn’t necessarily obvious. Plus, there’s some wonderful features of Mecrisp-Stellaris Forth that you might not notice until you’ve hacked on the system for a while.

Ideally, you’d peek over the shoulder of someone doing their thing, and you’d see some of how they work. That’s the aim of this piece. If you’ve already flashed in our version of Mecrisp-Stellaris-plus-Embello, you’re ready to follow along. If not, go back and do your homework real quick. We’ll still be here when you’re done. A lot of this article will be very specific to the Mecrisp-Stellaris flavor of Forth, but given that it runs on tons of ARM chips out there, this isn’t a bad place to be.

Getting Acclimatized

The first thing you’re going to need to get used to in Forth is the stack. You know that old chestnut about people only being able to keep five (seven?) things in their mind at one time? Forth puts that to the test.

Last time, I briefly mentioned the .s (“print stack”) command. In the Hackaday Edition, I’ve re-defined the standard Mecrisp .s to be a little bit less verbose, and to my eyes more readable. If you find yourself hitting .s a lot, and you will, I’ve also written a function that (temporarily) overwrites the “ok.” prompt by appending a stack printout to it, every time you hit enter. Type print.stack and hit enter one more time to see how it works. Hitting the reset button, or typing reset will wipe everything in RAM, and that includes the stack-printing prompt, so you’ll be back to a clean slate.

Now is probably a good time to play around with the stack operators. Have you read the Mecrisp glossary? Check out the list of stack juggling operations there. Turn on print.stack and play around until they all make sense.

Have you run words yet? It spits out a linked list of every word that Forth knows, along with the memory locations where they live, and some extra details — too much detail, unless you’re debugging the system itself. There’s an extra, nonstandard, word in Mecrisp that just prints out the function names: list. Give that a shot now. If you haven’t already defined a few words, do so.

: hw ." hello, world!" cr ;

is a good one to have on hand.

Layers in Memory

Mecrisp-Stellaris’ memory is divided into two gross locations: RAM and flash. All words that are listed before the “— Mecrisp-Stellaris Core —” mark are in RAM, and will be lost on reset or power-down. New RAM functions will be appended to the front of the list.

After the “— Mecrisp-Stellaris Core —” mark come functions in flash. In the early parts of flash, before “— Flash Dictionary —” is the standard Mecrisp Forth core. From there until “<>” are words taken from the Mecrisp distribution that are generally useful, including some debugging functions and multitasking. Preceding “<>” are the contributions from the Embello libraries, including a lot of GPIO definitions, and those before “<>” were added just for this article series.

What’s not obvious is that all of these markers with brackets surrounding them are cornerstones. These allow you clear out flash up until that memory location. So if you’ve added some extra functions into flash, and want to clear back out to the Hackaday Edition default state, you can type <<Hackaday-extras>>. The extra functions will be erased, and the chip reset. (Note that this loses whatever was in RAM!) The command eraseflash will get you back to the “— Flash Dictionary —” marker.

Overwriting History

If you define a word twice with the same name, you’ll have two versions of the word in the dictionary. When a word is called or compiled, the interpreter looks through memory, from the top of RAM down, and then from the end of flash back to the beginning. Words with the same name in RAM thus get called before those in flash. What can be particularly odd about Forth is that, because it compiles in real-time, the word that is referred to in any calling word is the one that was on the top at the time the calling word was defined. Making tangled histories is a sure way to go insane.

: foo ." foo!" ;
: bar foo ."  bar!" ;
bar foo! bar! ok.
: foo ." bizzle!" ; Redefine foo.  ok.
foo bizzle! ok.
bar foo! bar! ok.

On the other hand, here’s a nice way to work that takes advantage of these various memory features. RAM gets erased on every reset, giving you a clean slate, but by using cornerstones, flash isn’t immutable either. Of course, the deeper in flash you have to erase, the more words you have to redefine later, assuming that some of them were useful. This suggests a layered approach to development, with the most “core” words innermost in flash. That’s also natural because words need to be defined to be called, so defining the basics first makes sense anyway.

You can compile a new word to RAM (the default) by calling compiletoram or to flash by invoking compiletoflash. Prototype your words in RAM. Feel free to overwrite them as many times as you want, but remember that you have to redefine any dependent words after you change something upstream to keep the calling history intact. Once you’re done with a chunk of development, type reset and clear RAM. Now redefine these words into flash. If you develop your application from the bottom up, you’ll find that this all hangs together nicely. When you’ve discovered a bug in something that’s already written to flash, the cornerstones come to the rescue.

Finally, this layered nature of Forth definitions can be really handy. For example, a function init that’s defined in flash gets run on each reset. It includes things like setting the processor speed and the system tick, that you probably don’t want to mess with. Because you can overwrite init, and any compilation uses the words available at the time of compilation, you can simply layer your functionality onto init: : init init ." howdy!" cr ;. The first “init” is the name of the new word, and the second is calling the pre-existing init, and doing all of the setup. The remainder of the definition is yours to play with. On restart, everything will get executed in the order it was defined.

Creature Comforts

I write most code in an editor (Vim) and then send it over to the chip to play around with. Very specifically, I wrote a script called forth_upload.sh that contains the following:

[[ $1 ]] && TERM=$1 || TERM=/dev/ttyUSB0
DELAY=0.2
while read -r f; do echo "$f" ; echo "$f" > $TERM ; sleep ${DELAY}s ; done

To send a file to the serial port, and thus to Forth, forth_upload.sh < myfunctions.fs will work. It loops through each line in the file, allowing a 0.2 second delay for the system to compile anything. This delay is too long, in my experience, but the bugs from having it short are lousy to find. Folie takes the approach of looking for the “ok.” prompt to speed things up, but this has issues of its own. If you’re using Vim, you can send individual words or a full file with the following:

nnoremap <Leader>u vip:<Home>silent <End>w !forth_upload.sh<cr>
inoremap <Leader>u <Esc>:silent .w !forth_upload.sh<Cr>
nnoremap <Leader>U :silent w !forth_upload.sh<Cr>

This setup lets me develop and tweak functions in an editor that I like, and then send them nearly instantly into the Forth system to test out. I keep a terminal window open that’s always logged into the Forth system, so I can watch the new words get defined in, and then start playing around with them interactively. It’s pretty sweet.

The Solar Cell Tester

Now to a quick real example that will make use of all of the above. I recently decided to characterize a bunch of small solar panels that I had in my junk drawer. This means adjusting the load on the panel and noting down the voltage generated by the panel and the current through the load. I hooked up two multimeters and wrote down some numbers, but this is obviously a job for a microcontroller.

As is clear from the image, this is a quick lashup. The larger chunk of copper-clad has a current-measurement resistor and a pair of resistors configured as a voltage divider to step down the panel voltage to something the 3.3 V ADC can handle.

Dangling off that is the (silvered) variable load circuit, which is entirely sub-optimal: a MOSFET dissipates the heat by being turned half on by a PWM’ed voltage fiiltered by that 100 uF capacitor. The 9 V battery and optoisolator were cobbled on to ensure a high enough voltage to fully open the MOSFET, which wanted more than 3.3 V. This horrible, but functional, load circuit was a later addition — the first version just had a potentiometer here, until that got smoked by running too much current through it.

The pushbuttons are used to start and stop recordings from the device out on the balcony without having to run back inside. The procedure was to alligator-clip in a new panel, and hold the white button while it made recordings. The small black button is pressed once to demarcate a new cell’s data. The data goes back to my laptop over UART serial through an ESP8266 running esp-link as a transparent WiFi serial bridge, seen in the upper left, right next to the recycled laptop batteries. Hot glue and cardboard round out the high-tech build.

To the Datasheet!

This kind of quick and hands-on tool-building is where Forth shines. The Jeelabs Forth libraries already have some functions that simplify the ADC setup, but they actually didn’t work for me, so I turned to the datasheet. The bare minimum that one needs to get the ADC working is to turn on the ADC peripheral clocks, enable the ADC unit, and then select the ADC sampling time. We might also want to run the ADC calibration procedure to make sure our readings are correct. This is all the sort of low-level detail that you’d have to do with any microcontroller — or lean on a library that does it for you.

Reading the datasheet, to turn on the ADC system clock, we need to set the ADC1EN bit in the RCC-APB2ENR memory register. Whatever. Mecrisp-Stellaris has some support code that reads these values from files that are provided by the manufacturer. I’ve included them in the core/registers/ directory. The memory map file had nice mnemonics for all of the registers, but I’m not thrilled with the way that the individual bit names are handled. That’s a yak to shave on another day, I’m trying to get stuff done.

Here’s the set of bit-level ADC words that I came up with:

\ Hookup
\ PB0 - AIN8 is connected to panel voltage
\ PB1 - AIN9 is connected to current sense resistor

: adc.rcc-enable  9 bit RCC-APB2ENR bis! ; \ set ADC1EN
: adc.set-adon    0 bit ADC1-CR2    bis! ; \ set ADON to enable ADC
: adc.set-cal-bit 2 bit ADC1-CR2    bis! ;

: adc.cal-done?    2 bit ADC1-CR2    bit@ 0= ;
: adc.isdone?      1 bit ADC1-SR     bit@ ;

 9 bit creates a number with the ninth bit set and all others zero, and RCC-APB2ENR bis! sets the desired bit in the relevant ADC control register. All in all, pretty opaque, but also one-for-one with the datasheet’s instructions. Good naming of these bottom-layer words makes things a little better one layer up. For instance, the word that is responsible for running a calibration (set one bit, wait until another is clear) is relatively readable: : adc.cal adc.set-cal-bit begin adc.cal-done? until ;.

And here we see our first flow control, the begin...until construct. The order is strange, right? Begin starts the loop, The test function adc-cal-done? executes, leaving a true or false value on the stack, and then until reads this value and loops back to begin until the value is true. This is the same as a C while (!adc-cal-done()){;} busy-wait loop. And it’s an introduction to another Forth idiom: using ? for anything that returns a Boolean.

Test, Fix, and Test Again

So let’s test these words out. Initializing the ADC and reading it are easy: Now let’s take them for a run and make sure that the values are what we’re expecting. Whipping up a quick test fixture in Forth is one of its main strengths. : test-adc begin adc@ . cr 10 ms key? until ; defines a word that reads the ADC, prints the value, and repeats every ten milliseconds until you hit enter. If you do this with a solar panel under a fluorescent light, for instance, you’ll pick up the mains frequency ripples as they hit your panel, which is something you might not have been thinking about. The ability to play around with your new words as soon as you define them makes this sort of investigative coding flow naturally.

True story time. When I was writing this code, I had set a too-short sample time on the ADC inputs. This causes the value from one reading to affect the next because the ADC’s internal capacitor doesn’t have time to charge or discharge fully. I was getting too-high readings for the current when the voltage was high, and too-low readings for current when voltage was low. I debugged this fairly quickly by writing a routine that read each channel a few times in a row and printed the values out, and I left those words in for posterity.

( Take 8 samples of each, for debugging )
: read-both 8 0 do read-V . loop 8 0 do read-I . loop ;
: readloop begin read-both cr 100 ms key? until ;

Feedback from this testing exercise lead me to switch up the sample times on the ADC to their maximum value, because nothing here was time critical and I was using a fairly high-impedance source. (The default is to sample as quickly as possible.) After that change, I tested again, and everything was spot on. These kinds of quick interactive tests and fixes are pretty common when debugging a new hardware device, and Forth makes going back and forth to fix them fairly easy.

Wrap it Up

After using the system for a few times off my desk, it became obvious that I needed to add those two buttons to enable remote operation. Some additional blinking routines were added as well. Even though these were an afterthought, it was easy enough to wrap them up in tasks and let them all run concurrently.

One task looks for the data acquisition button, while another looks for the “next sample” button and times out long enough that I won’t activate it twice by accident. I really liked the way the button timeout task worked with the multitasking-enabled ms command. You can see all this in the project’s GitHub.

On the computer side, since all of the data is coming in over a WiFi-hosted serial port, ncat was used to dump it to a file, where it was separated into chunks based on the separators, turned into valid CSV datasets, and plotted. These are all one-liners, essentially, but I tossed them in the repository as well.

I’m not a Forth guru — I’ve only been using Mecrisp on microcontrollers for a year now — so I’ll gladly take constructive criticism on the code. This was a quick hack, but it did just what I needed and was a joy to program (except for debugging the ADC configuration). A ten-pack of these cheap ARM boards with a decent Forth setup flashed in would be a great thing to have on hand for any hacking project. Let us know if you’re doing the same!

14 thoughts on “Making A Solar-Cell Tester With Mecrisp-Stellaris Forth

  1. It’s very easy to find yourself without source when doing a typical FORTH workflow. But that’s not such a big deal as the de-compilation of FORTH is often built-in and very easy.

    1. … except in Mecrisp-Stellaris, unfortunately, which trades off some very optimized compilation and the corresponding dramatic speed increase for the visibility of code. Mattias implemented a “see” command in the Mecrisp libs, but it shows the assembly / machine code instead of the Forth.

      That said, the resulting code runs about as fast as compiled C, so I’m not complaining. Editing in Forth, even one with screens and so on, is still kinda clumsy compared to a real editor anyway.

  2. Nice series Elliott. You asked for tips :-) In good Forthography you define new words to make it readable and versatile. Even if they are trivial. In you final example

    ( Take 8 samples of each, for debugging )
    : read-both 8 0 do read-V . loop 8 0 do read-I . loop ;
    : readloop begin read-both cr 100 ms key? until ;

    Consider making it to an arbitrary number of reads.

    : read-both dup 0 do read-V . loop 0 do read-I . loop ;

    Use it ‘8 read-both’ I duplicate the top of the stack so that it can be used by both loops.

    Since there is a dup in there, what not define something like this.

    : times dup ;
    : read-both 0 do read-V . loop 0 do read-I . loop cr ;

    Then use,

    8 times read-both

    The number of reads can be outside further.

    10 times readloop

    1. Totally. But! :)

      Test rig code. I just tend to type it in and not think about it. The 8 was chosen to be surely big enough to allow time for the ADC to settle if that was the issue, which it ended up being. And in the end it settled in two or three samples.

      Those words existed for about 30 minutes, until I could verify that the ADC was playing right, and then were never used again.

      But your example is much sweeter. The “times” type words, where the functionality of the word is totally separate from the naming, but it’s chosen so that it fits syntactically into a sentence, still give me the fits in Forth. I’m very much used to the C paradigm of naming functions for what they do, rather than naming them so that they make the resulting code read right.

      More extreme examples are words that are defined to do nothing because they make the code read like language, or demarcate regions, or whatever. Forth is weird.

  3. The company I work for used to sell a line of SBCs with Forth on them. Starting in the 1980s with fig-Forth on 8088 processors, then Forth-83 on 8088, 80196, and 68000. Our final board with Forth provided an ANSI-compatible Forth on the 68332. All compilation was done on the SBCs themselves.

    Our workflow used Windows 3.1’s terminal program, which had the nice feature that you could set it up to wait for each character to be echoed back, or to wait for a specific character to be sent back after each line. We’d set it up to wait for a carriage return, and added a pair of dictionary entries named DOWNLOAD and END-DOWNLOAD to disable and re-enable character echo on input for better speed, because our serial ports were set for 9600 baud. In 1984, that was pretty fast (the company’s first board talked at 4800 baud), and then we kept that speed for compatibility. Not echoing each character sped transfer up significantly.

    To further speed up transfer of large files (once we moved away from screens), we wrote a preprocessor that did file inclusion and comment/blank line stripping.

    I kept a copy of Terminal from Windows 3.1 around until I lost it (along with a number of other files of similar vintage) last year. Should have had more backups, I guess.

    @TheRegnirps –

    I’d decompose things differently, probably more like this:

    : times ( n — n 0 ) 0 ;
    : read-both ( last+1 first — ) 2dup do read-V . loop do read-I . loop ;

    Same effect, but it makes more sense to me.

    1. I’m allergic to putting 2 in front of a word that does not do math. :-) Old habits from FIG (Ragsdale 6502) and Forth79 where DDUP does the same thing. Or I would over over it. But it is cleaner to have all the loop arguments together in one place.

      What company? National did a hybrid module with native Forth in 80’s.

      1. I have a similar problem with DDUP, because, to me, the leading “D” means “double,” and we’re dealing with two numbers here, not a double-sized number. Ain’t semantics fun?

        Ah, the 6502 FIG implementation. I remember using that on a Rockwell-65 system at Life Imaging in the early 1980s, and typing in the listings on my Apple II at home. I can’t remember the name of the Forth system I bought for the Apple before that, but I replaced it with FIG because it wasn’t a proper implementation. Among other deficiencies, */ was defined as a multiply followed by a divide, but without maintaining extra precision.

        I work for Vesta Technology. The company was founded in Denver in 1984. Currently, we’re in Golden. We still make SBCs, but nothing with Forth anymore. Sad story: back in the late 1990s, management found out that we were losing sales merely for offering Forth as an option for people who didn’t want to use C or BASIC (because “Forth is for hobbyists, so your boards must also be just for hobby use.”), so Forth went away. Too bad; I found it to be a very productive environment.

  4. Thanks for your articles Elliot – and also Matthias and JCW for all the work done! Maybe Forth will get a new breath of life among new generation hackers.
    I was introduced to Forth back in the early 80’s by a programming genius who co-founded the Pentyre Group. We did a lot of embedded work using Triangle Digital Systems’ Forth boards and later even designed our own embedded Forth eurocard format board, the IDS2500. This little puppy went on to be a core communications controller for London Underground and was also licenced to National Grid engineering team.
    Following on from that, a number of manufacturing test rigs for Lucas Industries, including the final test platform for the Ford ‘world’ car control system.
    The beauty of Forth from an engineering perspective is the ability to interactively test at a low level during the development – there is no abstraction as in C etc. This is _the_ language for hot metal programming.
    Also, as a historical footnote, there was a dedicated chip in the 80’s – the Rockwell 65F12, along with a couple of dual-stack processors custom designed to implement low-level native Forth words in silicon. Fast beasts!
    FWIW, a Forth word can be considered as operating in the same fashion as a C function – it takes call parameters from the stack and returns results on the stack.

  5. Just finished this series of articles. I’ve never seen Forth before – and now I love it. I think the “if Python and Assembly had a baby” quote is perfect – and the end result (REPL-like-Lisp compilation in real-time within the microcontroller) is… simply amazing. I tried to look under the hood, but sadly, the git submodules in the repository are gone – which is a nice opportunity: I’ll build my own Forth :-)

    I may be wrong, but I get the feeling that people loving Lisps also love Forth – and vice versa :-)

  6. I’m just getting started using Mecrisp-Stellaris Forth on the micro:bit v1, to test an external board. I’m using the tio terminal with Debian. To get a non-stair-cased output I use this command:

    tio -b 115200 /dev/ttyACM0 -m INLCRNL

    To enable the forth_upload.sh script to work with vim, I found I have to put the shell script or a link to the shell script in the working directory. I had to modify the example .vimrc examples slightly e.g.

    nnoremap u vip:silent w !./forth_upload.sh

    I welcome improvement suggestions.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.