Making an LED blink (and fade) Dec 2016

Just like C-based code benefits from a runtime library with utility functions, Forth can benefit from a set of pre-defined words to talk to the hardware peripherals built-into every F103 µC.

This utility code has been written from scratch for Mecrisp Forth, the STM32F1 series of µC’s and some of it also for other STM32 variants. It’s still experimental, but working out nicely. All the code lives in the embello area on GitHub, in a folder called explore/1608-forth/flib/:

$ tree -d flib/
├── any
├── fsmc
├── i2c
├── mecrisp
├── pkg
├── spi
├── stm32f1
├── stm32f4
├── stm32f7
└── stm32l0

To load some key files into flash, the easiest is to launch Folie from the 1608-forth/g6s/ directory (“Generic 64-pin Serial”), which has a number of “standard” source files to include selected titbits from the above library collection:

Here is a transcript of the entire process:

$ folie
Folie v2.7-1-g94cba5e
Select the serial port:
  1: /dev/cu.Bluetooth-Incoming-Port
  2: /dev/cu.usbmodem3430DC31
? 2
Enter '!help' for additional help, or ctrl-d to quit.
[connected to /dev/cu.usbmodem3430DC31]
!s always.fs
1> always.fs 3: Finished. Reset ecrisp-Stellaris RA 2.3.1 for STM32F103 by Matthias Koch
1> always.fs 11: ( always end: ) 00005060  ok.
1> always.fs 12: Redefine eraseflash.  ok.
!s board.fs
1> board.fs 3: eraseflash

Finished. Reset ecrisp-Stellaris RA 2.3.1 for STM32F103 by Matthias Koch
1> board.fs 6: ( board start: ) 00005800  ok.
7> hal.fs 14: Redefine bit.  ok.
1> board.fs 27: Redefine init.  ok.
1> board.fs 36: ( board end, size: ) 00008104 10500  ok.
!s core.fs
1> core.fs 3: <<<board>>>

Finished. Reset ecrisp-Stellaris RA 2.3.1 for STM32F103 by Matthias Koch
64 KB <g6s> 323F3565 ram/flash: 19340 30720 free ok.
1> core.fs 5: ( core start: ) 00008800  ok.
1> core.fs 12: ( core end, size: ) 0000A7E4 8164  ok.

If you now reset the board, you’ll get the Mecrisp welcome message, as well as some extra information generated by the init definition in board.fs:

Mecrisp-Stellaris RA 2.3.1 for STM32F103 by Matthias Koch
64 KB <g6s> 323F3565 ram/flash: 17688 20480 free ok.

Let’s examine this information in detail:

The always.fs/board.fs/core.fs files follow a convention outlined a while ago on the weblog, in an article about application structure. In short:

After these have been loaded, they will remain in flash until replaced by newer versions. The Mecrisp core contains some 350 words, the above three “sends” will add another 300 or so. A number of these are just there to factor out common code in the rest of the files. There’s no need to go through all of them, you’ll see a few useful I/O and delay words below.

All words are defined as entries in the Forth “dictionary”, which is in fact simply a linear list. There are a few words which will truncate this list and remove all definitions after them:

The first three of these words are “cornerstones”, i.e. they only exist to mark a position in the dictionary. You can also define your own cornerstones to add additional markers.

Note that after a reset, new definitions will be defined in RAM. They can be cleared by calling forgetram or reset, or by a hard reset of the µC. This makes it very convenient to try out stuff - if anything goes wrong, just reset and you’re back in a well-defined state.

Speaking of resets: certain failures are really unforgiving and lead to ARM exceptions. Due to some special code in board.fs, these will often show up as a stack trace, to try and help with identifying the cause of the problem. Here’s an example, addressing non-existent memory:

$100000 @

Unhandled Interrupt 00000003 !

Stack: [1 ] 0000002A  TOS: 00100000  *>

00000000 00005199 ( 0000516A + 0000002E ) ct-irq
00000001 FFFFFFF9
00000002 00001860 ( 00001860 + 00000000 ) @
00000003 00000220 ( 00000176 + 000000AA ) --- Mecrisp-Stellaris Core ---
00000004 00001860 ( 00001860 + 00000000 ) @
00000005 00000020
00000006 F7FFFBD7
00000007 0000460B ( 00004536 + 000000D4 ) interpret
00000008 00001860 ( 00001860 + 00000000 ) @
00000009 01000000 ( 0000797E + 00FF8682 ) <<<board>>>
0000000A 0000467F ( 0000464A + 00000034 ) quit

At this point, a hardware reset will be needed to get the prompt back. Shrug. C’est la vie!

Blinking an LED

Now let’s toggle the on-board LED of the Blue Pill, which is on bit 13 of port C, i.e. pin PC13.

First, you need to configure the pin as a “push-pull” (as opposed to “open-drain”) output:

omode-pp pc13 io-mode!  ok.

(omode-pp and io-mode! are defined in flib/stm32f1/io.fs - pc13 is in flib/pkg/pins64.fs)

Perhaps surprisingly, the LED will immediately turn on. That’s because the LED is connected as “active low”, i.e. it’s on when the I/O pin is “0”, which is the default value after power up.

Let’s turn it off (ios! stands for “I/O set!”):

pc13 ios!  ok.

And to turn it on again (ioc! stands for “I/O clear!”):

pc13 ioc!  ok.

You can also toggle it, using “pc13 iox!” (short for “I/O xor!”), and because Folie supports command history, you can repeat that command by pressing <up-arrow> and then <enter>.

To blink the LED, i.e. toggle it every 100 milliseconds, we can use this code:

: blink begin pc13 iox! 100 ms again ;

Loops need to be defined inside words, the above defines a word called “blink”, which toggles the pin, waits 100 ms, and loops. This defines blink, it does not execute it. To run it, type:


Lo and behold: the LED is blinking!

Oops… we’ve locked ourselves out. It’s blinking, but it’s no longer listening to the keyboard! But it’s easy to get out of this loop: press CTRL-C (if using SerPlus) or press the reset button.

Note that the definition of blink is gone after the reset, since it was defined in RAM:

blink blink not found.
      ^^^^^^^^^^^^^^^^   <== reported by Mecrisp

To enter it again, press <up-arrow> a few times. But let’s make it a little more convenient:

: blink begin pc13 iox! 100 ms key? until ;

Now blink will loop until a key is pressed (when using Folie, you have to press <enter>).

Dimming an LED

As it so happens, the PC13 pin on F103 is not capable of doing hardware-based pulse-width modulation (PWM). We’ll have to dim the LED in software. Here’s an example of how to do this, by defining a couple of words to perform simple tasks, building upon each other:

pc13 constant led

: led-init             omode-pp led io-mode! ;
: led-on               led ioc! ;
: led-off              led ios! ;
: on-cycle   ( n -- )  led-on ms led-off ;
: off-cycle  ( n -- )  20 swap - ms ;
: cycle      ( n -- )  dup on-cycle off-cycle ;
: dim        ( n -- )  led-init begin dup cycle key? until drop ;

Note that Forth imposes a bottom-up design: words can only use previously-defined words.

The last word is “dim” which takes a brightness as argument. Since we want to avoid flicker, this code uses a cycle time of 20 ms, during which the LED is partly on, partly off. The “on” duration is the 0..20 argument passed on the stack. E.g. to dim the on-board LED to 5%:

1 dim

It turns out that dimming is very non-linear: even at 5%, an LED is still fairly bright.

It would be tedious to enter this code interactively and to have to re-enter it after each reset. But we can also store this in a file called ex/dim.fs, and then “send” it through Folie:

!s ex/dim.fs

But why stop there? If you add “forgetram” as first line, and “1 dim” as last line, then the above send will forget the old words, define new ones, and then launch dim with argument 1. This leads to a simple – nearly-interactive – session for trying things out: edit the ex/dim.fs source code, save, switch to Folie, press <up-arrow> and <enter>, and watch the LED, then press <enter> to exit the dim loop, switch back to your editor, rinse and repeat…

If things go wrong: comment out the 1 dim line at the end, send again, and use the interactive prompt to figure out what’s going on. Switching back and, ehm… forth is very easy and quick.

This example used the “ms” millisecond delay to control the PWM ratio. You could also use the “us” delay, which takes a microsecond argument to control the dimming to a much finer level.

Once you have some new words you’d like to keep: move them to a well-named file, add a new include line to core.fs, and re-send core.fs. Voilá, you’ve expanded your µC’s vocabulary.

Note how different this workflow is from cross-compilation in C: it’s a much more gradual way of “growing” software. Most of the mental effort takes place in a very dynamic context, without having to re-flash the µC’s memory at all. No big cycles, no big uploads, but lots of little steps.

Next up: switching to a USB-connected setup.

Weblog © Jean-Claude Wippler. Generated by Hugo.