I/O, ADCs, OLEDs, and RFM69s Feb 2016
Software development in Forth is about “growing” the tools you need as you go along. Over time, you will end up with a set of “words” that are tailored for a specific task. Some words will end up being more reusable than others - there’s no need to aim for generality: it’ll happen all by itself!
Let’s start with some examples for controlling GPIO pins on an STM32F103:
Define Port B pin 5 as an I/O pin:
1 5 io constant PB5
Actually, it’s easy to pre-define all of PA0..15, PB0..15, etc - see this code.
Set up PB5 as an open-drain output pin:
OMODE-OD PB5 io-mode!
Here’s how to set, clear, and toggle that output:
PB5 io-1! PB5 io-0! PB5 iox!
To read out the current value of PB5 and print the result (0 or 1), we can do:
PB5 io@ .
There are some naming conventions which are very common in Forth, such as “@” for accessing a value and “!” for setting a value. There are many words with those characters in them.
Here are all the public definitions from the io-stm32f103.fs source file on Github:
: io ( port# pin# -- pin ) \ combine port and pin into single int : io-mode! ( mode pin -- ) \ set the CNF and MODE bits for a pin : io@ ( pin -- u ) \ get pin value (0 or 1) : io! ( f pin -- ) \ set pin value : io-0! ( pin -- ) \ clear pin to low : io-1! ( pin -- ) \ set pin to high : iox! ( pin -- ) \ toggle pin : io# ( pin -- u ) \ convert pin to bit position : io-base ( pin -- addr ) \ convert pin to GPIO base address : io-mask ( pin -- u ) \ convert pin to bit mask : io-port ( pin -- u ) \ convert pin to port number (A=0, B=1, etc) : io. ( pin -- ) \ display readable GPIO registers associated with a pin
Only the header of each word is shown, as produced with “
grep '^: '
Note that this API is just one of many we could have picked. The names were chosen for their mnemonic value and conciseness, so that small tasks can be written with only a few keystrokes.
Here’s another “library”, to read out analog pins on the STM32F103 - see adc-stm32f103.fs:
: init-adc ( -- ) \ initialise ADC : adc ( pin - u ) \ read ADC value
Ah, now we’re cookin’ - only two simple words to remember in this case. Here’s an example:
init-adc PB0 adc .
Not all pins support analog, but that’s a property of the underlying µC, not the code.
I2C and SPI
The implementation of a bit-banged I2C driver has already been presented in a previous article. Unlike the examples so far, the I2C code is platform-independent because it is built on top of the “io” vocabulary defined earlier. Yippie - we’re starting to move up in abstraction level a bit!
Here’s the API for a bit-banged SPI implementation:
: +spi ( -- ) ssel @ io-0! ; \ select SPI : -spi ( -- ) ssel @ io-1! ; \ deselect SPI : spi-init ( -- ) \ set up bit-banged SPI : >spi> ( c -- c ) \ bit-banged SPI, 8 bits : >spi ( c -- ) >spi> drop ; \ write byte to SPI : spi> ( -- c ) 0 >spi> ; \ read byte from SPI
Some words are so simple that their code and comments will fit on a single line. That code can be very helpful to understand a word and should be included, as shown in these definitions.
You may be wondering which I/O pins are used for SPI and I2C. This is handled via naming: the above source code expects certain words to have been defined before it is loaded. For example:
PA4 variable ssel \ can be changed at run time PA5 constant SCLK PA6 constant MISO PA7 constant MOSI
The pattern emerging from all this, is that word definitions are grouped into
logical units as source files, and that they each depend on other words to do
their thing (and to load without errors, in fact). So the I2C code expects
definitions for “
SCL” + “
SDA” and uses the “io” words.
It’s “turtles all the way down!”, as they say…
In Forth, you can define as many words as you like, and since a word can contain any characters (even UTF-8), there are a lot of opportunities to find nice menmonics. When an existing word is re-defined, it will be used in every following reference to it. Re-definition will not affect the code already entered and saved in the Forth dictionary. Everything uses a stack, even word lookup.
If you need two bit-banged I2C interfaces, for example, you can redefine the SCL & SDA words and then include the I2C library a second time. This will generate some warnings, but it’ll work.
With the above words in our toolbelt, we’re finally able to build up something somewhat more substantial, i.e. a driver for the RFM69 wireless radio module, which is connected over SPI:
: rf-init ( group freq -- ) \ init the RFM69 radio module : rf-freq ( u -- ) \ change the frequency, supports any input precision : rf-group ( u -- ) RF:SYN2 rf@ ; \ change the net group (1..250) : rf-power ( n -- ) \ change TX power level (0..31) : rf-recv ( -- b ) \ check whether a packet has been received, return #bytes : rf-send ( addr count hdr -- ) \ send out one packet
With some utility code and examples thrown in to try it out:
: rf. ( -- ) \ print out all the RF69 registers : rfdemo ( -- ) \ display incoming packets in RF12demo format : rfdemox ( -- ) \ display incoming packets in RF12demo HEX format
This code is platform independent, i.e. once “io” and “spi” have been loaded, all the information is present to load this driver. The driver itself is ≈ 150 lines of Forth and compiles to < 3 KB.
… and more
If you want to see more, check out this driver for a 128x64 pixel OLED via I2C, plus a graphics library with lines, circles, texts which can drive that OLED. Or have a look at the usart2 code for access to the second h/w serial port. There’s even a cooperative multi-tasker written in Forth.
Everything mentioned will fit in 32 KB of flash and 2 KB RAM - including Mecrisp Forth itself.
But to make it practical we’ll need some more conventions. Where to put files, how to organise and combine them, etc. Take a look at this area for some ideas on how to set up a workflow.