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/
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:
- launch Folie from
embello/explore/1608-forth/g6s/
- enter:
!s always.fs
- enter:
!s board.fs
- enter:
!s core.fs
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
:
!reset
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:
always.fs
defines words which are considered essential for basic useboard.fs
includes the main board- and µC-specific definitionscore.fs
loads all sorts of goodies and can be adjusted as needed
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:
- “
eraseflash
” deletes all words defined afteralways.fs
- “
<<<board>>>
” deletes all words defined afterboard.fs
- “
<<<core>>>
” deletes all words defined aftercore.fs
- “
forgetram
” is a bit different, it clears all RAM-based definitions
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:
ok.
$100000 @
Unhandled Interrupt 00000003 !
Stack: [1 ] 0000002A TOS: 00100000 *>
Calltrace:
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:
blink
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.