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!

Digital I/O

Let’s start with some examples for controlling GPIO pins on an STM32F103:

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 '^: ' io-stm32f103.fs”.

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.

Analog I/O

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.

RFM69 driver

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.

Weblog © Jean-Claude Wippler. Generated by Hugo.