fignition‎ > ‎Documentation‎ > ‎

Understand It

FIGnition is designed to be understandable. In time, this section will expand to cover:

  • Understanding aspects of the Forth Language.
  • Understanding the FIGnition system.
  • Understanding the FIGnition firmware.
  • Understanding the FIGnition hardware.

Understanding Forth

The Stack

No Forth user guide would be complete without an explanation of stacks....

Text Processing

FIG-Forth uses a number of ad-hoc methods for processing text: the Terminal Input Buffer; The PAD and the (for want of a better phrase) the word buffer. To understand them you'll need to know first how FIGnition organises its memory.

The Terminal Input Buffer (TIB). The Terminal Input Buffer is the primary text buffer in FIG-Forth and is returned by the command tib. Normally it starts at the address 0x8020 and has space for 128 characters. For example, the command query requests a line of text and stores it in the terminal input buffer, terminating it with a 0 character. The command delimiter word reads text from the TIB until the specified delimiter is found and copies it into the word buffer (storing it as a length byte followed by the text itself). The command  -find reads the next space-delimited word from the tib into the word buffer and then searches the dictionary and if it finds a matching name returns its parameter field address (which can then be converted to its code field address using pfa>cfa and executed).

The TIB can be redirected to another area. For example, load reads a Flash block into high RAM and then redirects the TIB there, the interpreter then continues interpreting text from that buffer.

The PAD. The PAD is used to convert numbers into text. It starts 68 bytes after the word buffer and works backwards. This is because the easiest way to convert numbers into text is via the least-significant digit first. Consider the number 987. If we divide by 10 we end up with 98 remainder 7 (a single digit). That's easy to convert into the character '7' and then we can do the same with 98 giving us 9 remainder 8 which we can convert to '8' and place infront of the '7' => '87'.

The 'word' buffer. This is used for temporary text processing. In FIG-Forth it's set to the address of here.

Understanding FIGnition

FIGnition's Input / Output Pins


FIGnition's Memory

As standard FIGnition has 8Kb of external RAM (in chip U2) and 384Kb of Flash storage (in chip U3). Kb means 1024 bytes (see the Simple English Wikipedia Entry).

Unlike people, who store vast quantities of memories without being aware of exactly which brain cell stores which part of a memory, a computer needs to know exactly where every byte of memory is and this is done by numbering them all like we number houses in a street or runners in a race or mobile phone numbers of our friends. FIGnition can specify up to 65536 different locations for bytes numbered from 0 to 65535 or 0 to 0xffff in hexadecimal notation. So you can think of FIGnition's memory as 65536 boxes each of which can contain a number from 0 to 255 and each of which is tagged with its location (called an address as in the address of a house), which is also a number from 0 to 65535.

When FIGnition runs programs or when you use @, !, c@, c! or cmove the memory is organised as follows:


So, the Forth ROM always starts at 0x6000 (24Kb) (there's room for 8Kb of ROM though <4Kb is currently used) and RAM always starts at 0x8000 (32Kb).

Avr Memory.

FIGnition is implemented using an AVR microcontroller which has its own memory: 1Kb of internal RAM; 16Kb of fast internal Flash memory; 1Kb of EEPROM memory and 32b of very fast memory called registers. AVR Microcontrollers don't organise their memory in one big chunk, but divide it into 'program' memory and 'data' memory. The AVR Firmware itself can be read by trying to read before or after the Forth ROM, but its starting location depends on the firmware revision itself. The AVR's data memory is organised as follows:


At the moment only a few system variables are properly accessible, this will change in future firmware revisions. The Data stack and Return stack are held in internal memory for performance reasons, there are up to 256b shared between them. The AVR controller's hardware can be directly accessed via ic@, ic!, but much of it is used for implementing FIGnition. The EEPROM however is currently free for use, it provides 1Kb of non-volatile memory.

The Keypad

The Keypad uses 6 I/O pins to read 8 keys. They’re arranged as follows:


You can see that if SW1 is pressed, there will be a connection between PortC<0> and PortD<7>; similarly if SW7, is pressed there will be a connection between PortC<2> and PortB<0>. For every switch there’s a unique connection between a PortC pin and either PortD<7> or PortB<0>.

The firmware configures all of the PortC<0>..PortC<3> pins to activate their internal pull-up resistors, so normally all of these pins will read 1 (because they’re at a high voltage). To detect which keys have been pressed the firmware then configures PortB<0> and PortD<7> so that one of those pins are a normal input and the other outputs a 0.

If PortD<7> outputs a 0, then, for example pressing SW1 will connect PortC<0> with 0V and will read low. However, if SW5 is also pressed PortC<0> will connect with PortB<0> (an input) and it won’t make any difference to PortC<0>. Therefore by setting PortD<7> to output 0, but leaving PortB<0> as an input we can read SW1 to SW4 and ignore SW5 to SW8.


Reversing the configuration of PortD<7> and PortB<0> allows us to read SW5 to SW8 and ignore SW1 to SW4. Thus we can read the state of all 8 keys using only 6 input pins and importantly, no additional hardware.

This system has one limitation when reading keys: if certain combinations of 3 keys are pressed, it will appear as though 4 keys are pressed. For example, if we press SW1, SW5 and SW3 it will look as though SW7 is also pressed. That’s because PortC<2> will connect to PortD<7>; PortD<7> will connect to PortC<0> and PortC<0> connects to PortB<0>. So, PortC<2> connects to PortB<0> and it looks like SW7 is being pressed.


The second layer of keypad scanning involves key debouncing. At the speeds that the microcontroller runs, a keypress looks like a sequence of intermittent contacts before it settles down. The keypad code therefore doesn’t accept keys presses until they have settled, which is given to be about 2/25 of a second and it does this by simply comparing the previous keypad reading with the current one and if they’re the same, then it’s said to be settled.

Real keyboards for modern computers work in a very similar way; they usually use a matrix of simple connections and the keyboard’s firmware eliminates debounce.

The FIGnition keypad scanner has a third layer for reading keys, because of the complexity of having every key acting as a shfit key for any of the others. It does this by monitoring which key was pressed first and when two keys are pressed.

SPI Driver

Firmware Rev0.8.0 and later includes a rudimentary SPI driver. The key difficulty with supporting SPI peripherals on FIGnition is that SRAM is also an SPI device, so an entire SPI transaction must be carried out between successive SRAM instructions. The SPI driver does this by specifying the SPI transaction within a struct:

typedef struct {
    byte port;
    byte portBit;
    tBigEndianWord src;
    tBigEndianWord srcLen;
    tBigEndianWord dst;
    tBigEndianWord dstLen;
} tSpiStruct;


And expects the struct, its source and destination data to all be in internal RAM.

The command spi expects a pointer to the SpiStruct to be on the stack. The first byte specifies port (its RAM address) and portBit (actually 1<<the actual bit number) used to connect to the SPI device, src points to the source data to be sent to the SPI device (which must also be in internal RAM), srcLen specifies its length. dst specifies where to store the result data from the SPI device, it must also point to internal RAM and must be dstLen bytes long.

spi pulls Port<1<<portBit> low, then sends srcLen bytes starting at src to the SPI device, then sends dstLen bytes of 0 to the SPI device reading each byte returned and storing it in dstLen. Thus spi can cope with any SPI device whose protocol is that simple. For example, the Amic Flash device is simple enough to be covered by spi.

Here's a short example. Let's say you want to read the ID of the Amic Flash. The flash is on PORTB ( 0x26) portBit 2. So port=0x26 (=38) and portBit = 1 2 << .

The Read ID command (RDID) is 0x9f. The protocol is a 1 byte command so srcLen=1. For some of the Flash devices it'll be 0x7f, then 0x37, then a 2 byte ID and for others it'll be 0x37, then a 2 byte ID. So, first we just send the command and read 1 byte back, test it for 0x7f and if it is we retry with 4 bytes else we retry with 3 bytes. We'll put the entire structure and return data in udg0 onwards. First the definition:

: cdata <builds does> ;

vram 600 + const udg0
cdata AmicRdidData 38 c, 1 2 << c, ( port and port bit mask)
    udg0 10 + , ( src addr )
    1 ,
    udg0 11 + , (dst addr )
     1 ,
     hex 9f decimal ( rdid )

: AmicRdid
   AmicRdidData udg0 11 cmove ( store data in internal RAM)
   udg0 spi ( start the SPI operation)
   udg0 11 + ic@ 127 = if
        14 4 ( rdid is 4 bytes with the id at offset 14 )
   else
      13 3 ( rdid is 3 bytes with the id at offset 13 )
    then
   udg 8 + i! ( store the new length )
   udg0 spi ( retry but with the correct length)
   udg0 + i@ .hex
;


So, here we have a fairly complex routine, but it can be handled by our SPI driver.

Atomic Port Access

FIGnition provides a comprehensive routine for both modifying ports atomically and reading them, called >port>. A number of FIGnition's I/O ports are used by FIGnition's interrupt routines, they read, modify and write them. If your normal code does the same thing it could crash FIGnition if your code is interrupted. Consider this:

hex 2b decimal const portD
hex 2a decimal const dddD
hex 29 decimal const pinD

: bit> 1 swap << ;

: mess
   dddD ic@ 6 bit> or dddD ic! ( make bit 6 output )
   begin portD ic@ 6 bit> xor portD ic! ( toggle bit 6) inkey emit
   again
;


This will cause the keyboard to fail some of the time, because the following will happen:

dddD ic@
Somewhere here the keyboard interrupt will run and modify dddD
6 bit> dddD ic!
Which writes the old version of dddD before the keyboard interrupt modified it.

Doing similar things with different ports will quicly cause crashes. The standard way to handle this at the lowest level in embedded (and mobile device) systems is to provide a read-modify-write routine which operates while interrupts are disabled. The rmw routine is short enough ( <1µs) so that it doesn't mess up critical interrupts. In the case of >port> interrupts are disabled for about 0.25µs.

What you do is provide the bitset of bits you want to clear and the bits you want to set, then orMask andMask >port> reads the port, clears the bits that are set to 0 in the andMask and then sets the bits that are set to 0 in orMask. You can use this to safely modify a single bit:

: notmess
   6 bit> -1 dddD >port> drop ( clear nothing, set bit 6 to make it an output )
   0 ( toggle value )
   begin
   dup dup -1 xor portD >port> drop ( toggle the output )
   6 bit> xor ( invert the toggle value )
   inkey emit
   again
;


Which safely modifies the port (note: I haven't yet tested the notmess and mess routines, but the theory is correct).

>port> has the interesting feature that it returns the old value of the port. Thus it can be used to implement basic, shared memory routines, preparing the way for when FIGnition's firmware supports a form of multitasking. Here we'd do something like:

hex 4b decimal gpior2

: waitLockInit ( called globally, once)
  0 gpior2 ic!
;

: release ( accessBit -- )
  bit> -1 xor 0 swap gpior2 >port> ( clear the accessBit )
  drop
;

: waitLock ( accessBit -- )
  bit>
  begin
  dup 0 gpior2 >port>
  0= until ( waits until gpior2 was released )
  drop
;


Here multiple tasks can compete for shared memory locations using bits in gpior2 as spin-locks. waitLock competes for access and only one of them will find that the bit had been 0 before their waitLock sets it (note: it's not harmful to set the bit if it's already set). Thus >port> is all you need!