Introduction

If you’re here, it’s because you want to try using muforth to explore the world of RISC-V.

Currently this means that you have a HiFive1 board; it is, at the moment, the only RISC-V hardware that muforth supports.

After getting a HiFive1, the next hurdle you have to jump is getting a RISC-V-aware copy of openocd. (Read that page and then come back here.)

You do not need a GCC toolchain! Yay! Jump up and down and shout with glee! openocd is the only external tool that muforth needs in order to run code on a HiFive1.

Getting muforth

If you’ve never used muforth before, get a copy. BUILDING has the very simple instructions for building muforth.

Once it is built, cd to muforth/mu/. You should always be in this directory when running muforth code.

Connecting

Edit the file target/RISC-V/start-openocd.sh and change the openocd variable so it references your local copy of the RISC-V openocd.

If you are on a Mac and running Mavericks or later, unload the Apple FTDI driver; otherwise it will interfere with JTAG:

  sudo kextunload /System/Library/Extensions/AppleUSBFTDI.kext/

Plug your HiFive1 into a USB port. Open a second terminal window, leaving the first one sitting in muforth/mu. cd to muforth/mu in this window as well. Then type the following command to start up openocd:

  target/RISC-V/start-openocd.sh

Back in the first muforth/mu window type this:

  ./muforth -f target/RISC-V/build.mu4

This will load the RISC-V meta-compiler and the RISC-V Forth kernel (which currently loads into RAM). To connect to the board via JTAG, type

  jtag

You should see a dump of four registers, all zero except for SP. You are now connected to the target board!

Exploring

Disassembling RISC-V code

To see some of the kernel code that was loaded into RAM, try this:

  @ram dis

This will start disassembling the beginning of the Forth kernel. All memory dumping and disassembly in muforth is interactive; pressing <RET> or n will advance; pressing <BS> or p will back up; q will quit, leaving the last-viewed address on the stack.

The word @ram is a constant; executing it pushes the address of the start of RAM (“at-ram”).

You can also look at built-in features. The Debug ROM is interesting. To see it type

  0800 dis

and keep pressing <RET> to see further lines. Compare what you see here to what is documented in the debug spec version 0.11 (direct link).

Executing target Forth words

To try actually executing some code, try this:

  10305 2040 +

This pushes two hex values onto the stack, copies this stack over to the target, executes the word + on the target, which adds them and pushes their sum, and then copies that back to the target. You should see the result in the stack dump.

To list the words in the target kernel, try this:

  target words

Peering inside the FE310

Something more interesting: Let’s write a word to read the value of one of the chip’s CSRs. This one – misa – tells us which base ISA – RV32 or RV64 – and which extensions the chip supports.

  code read-misa   misa w csrr   wpush j  ;c
  read-misa

The first line defines read-misa as a “code” word. This means that its definition is entirely in RISC-V assembler. It consists of just two instructions: one to read the CSR into the w register (an alias for ABI register t0), and one to jump to a routine that pushes w onto the stack and then executes NEXT. (On an ITC Forth all code words have to end by executing NEXT, directly or indirectly.)

The second lines executes our new word, and the result should show up on the stack. By reading the privileged ISA spec (see RISC-V resources) we can peel this apart. But first, let’s print its value in binary to make it easier to see:

  binary u. hex

This switches the input and output radix to binary, prints as an unsigned number the top of the stack (the contents of misa), then switches back to hex.

Unfortunately, u. doesn’t print leading zeros, so it’s hard to tell that the top two bits – which define the base ISA – are 01 – meaning RV32. The low order 26 bits each correspond to an ISA extension; the bit is set when that extension is present. I read the result as showing that IMAC are all present – exactly what we would expect from the FE310.

Writing Forth words

Next, let’s try a simple “colon” word. A colon word is any word defined by : whose body consists of other Forth words. Most Forth code is colon words rather than code words.

  : bic   ( value mask - result)   invert and ;

bic is the logical “bit clear” operation. It takes two parameters – shown in the “stack comment” (in parentheses) on the left of the dash – and produces one result – shown to the right of the dash. The first word, invert, does a ones complement of the mask; this is then anded with the value. Any one bits in the mask clear the corresponding bit in the value. As usual, we see the result on the stack dump (which is always in hex, regardless of the setting of the input/output radix – an odd quirk of muforth).

Colon words always end with a ;. When execution reaches this point, the word returns to its caller.

Let’s try it.

  c0defeed ffff0000 bic

Hopefully the resulting value makes sense.

How numbers work

A couple of notes about numbers in muforth. Any number can be prefixed by a radix operator, which changes the radix for that number only. The operators, and their resulting radices, are:

  %  binary
  '  octal
  "  hexadecimal
  #  decimal

These can be handy if you are writing an assembler where most values are in octal but need to switch to another radix to enter a single value.

Following the (optional) radix operator can be an optional sign operator: the usual "-" character.

Another thing: numbers are generally printed with separators. This makes them much easier to parse. They can also be input with separators. Any of the following characters is a valid separator:

  . , : / _ -

You should also realize that numbers are 64-bit on the host stack (in muforth) but truncated to 32-bits when copied over to the target.

Our stack has now accumulated some junk. You can get rid of it by typing

  .

over and over. Each execution of . prints as a signed value the top of the stack, thus removing one value. If there is a ton of junk on the stack, the word sp-reset clears the stack in one go.

Writing loops

Forth has both definite and indefinite loops. Definite loops are based on a count; indefinite loops loop until some condition becomes true. Let’s look each in turn.

There are two kinds of definite loops: for/next and do/loop. We use for/next when we want to iterate a known number of times and don’t care about the value of the loop index. (It’s hidden from our code, but the loop index counts down to zero from the given starting value.) Let’s try two versions of for/next: one that simply does a calculation, and one that has a “bug” in it, so it stops every time through to let us look around.

Try this:

  : lshift  ( n #shifts)  for 2* next ;
  0abcd 4 lshift

We’ve included a stack comment to remind us what’s going on here. (We are basically re-implementing << in Forth.) Since 2* shifts left by one bit, the loop will shift left by the number of bits specified.

Now the bugged version:

  : lshift-bug  ( n #shifts)  for 2* bug next ;
  0abcd 4 lshift-bug

When you first execute lshift-bug you’ll see that the IP register has a * next to it. This means that execution of your word did not complete. We are somewhere in the body of word (in this case, lshift-bug) and we can look around. There is not much to see; however, the value on the stack has been shifted left by one bit. Now type

  cont

to continue execution. The same thing will happen. IP will be starred, and the value on the stack will have been shifted left again. Keep typing cont until IP is unstarred. Execution has completed and we should see the same result on the stack that we got with lshift.

One note here: muforth has primitive but very useful command history. You can use the up and down arrow keys to navigate the command history, and the left and right arrow keys to move around and change a previously-entered line of text. Pressing enter will execute the edited version. If you use any command-line shell, the behaviour should feel familiar. (And for the inveterately curious, this is implemented entirely in Forth! See the file lib/editline.mu4 for the gory details. It’s a whopping 175 lines of code!)

The second kind of definite loop is the do loop. It consumes two values from the stack – a limit and an index – and loops until the index reaches or crosses the limit. In the body of the loop the word i pushes the current loop index onto the stack.

A do loop always starts with do but can end with either loop or +loop. loop increments the index by 1; +loop consumes a stack value and increments the index by that value. Since it will be hard to see what’s going on if we let these run free, let’s only try the bugged versions.

Try this:

  : doit  ( limit index)  do  i bug drop  loop ;
  5 0 doit

This should count up – starting at index, continuing until index reaches limit – from 0 to 4. i will push the current index, bug will stop so we can see it on the stack, and then drop loop will throw away the current index value, and loop again, or exit.

As before, keep typing cont until IP is unstarred. Try some other limits and indices and see what happens.

Now the +loop version:

  : doit+  ( incr limit index)  do  i bug drop  dup +loop  drop ;
  -3 -20 -10 doit+

Note that we are passing an increment to doit+ (the -3 value) and using dup to make a copy of it before calling +loop so we can use it the next time around as well. Since at the end of the loop the increment will still be sitting on the stack, we drop when we are done.

As before, keep typing cont until IP is unstarred. Try some other increments, limits, and indices and see what happens. Since the stack dump shows unsigned hex values it can be hard to see what’s going on here. The first time you stop at the bug, try doing

  over .

to see the increment value (-3). And every time you stop at the bug, try

  dup .

to see the current loop index. . will sign-extend the 32-bit target value to 64-bits and then print it as a signed number. This should help. Remember too: all those loop values are in hexadecimal! So, -10 is really -16 (decimal), and -20 is really -32 (decimal).

Does the behaviour of doit+ make sense? Try different values of the increment, limit, and index.

Using negative increments with do loops seems to have odd behaviour in that the idea of “reaching or crossing” the limit seems different from the behaviour with positive increments.

The behaviour that you see here conforms to “standard” Forth practice, and is essentially a result of the implementation: the index value is translated (shifted along the integer “number line”) so that the loop terminates when the translated index crosses the threshold from 0000_0000 to ffff_ffff (when counting down) or from ffff_ffff to 0000_0000 (when counting up). You can see the translated index in the IX register during loop execution.

So what about indefinite loops? There are two varieties of these as well: one that always executes the loop body at least once (like C’s do...while) and one that tests the termination condition at or near the beginning (like C’s while). Here they are:

  begin  <loop body>  <condition> until
  begin  <condition> while  <loop body>  repeat

There is also an unending version that we won’t be trying out here:

  begin  <loop body>  again

which is useful for the main loop of an embedded application.

I’m going to write “bugged” versions of these. Feel free to try them with or without the bug. Try these definitions:

  : overflow  ( n - n')  begin  2*  bug  dup 0< until ;
  800_0000 overflow

The word 0< consumes the value on the stack (hence we dup it first) and pushes -1 (true) if the value is less than 0, and 0 (false) otherwise.

Don’t pass 0 to overflow. ;-)

  : till-even  ( n - n')  begin  dup 1 and  bug  0= while  u2/  repeat ;
  47 till-even

A bit of explanation. 0= is like 0<: it consumes a value and pushes -1 if it is equal to zero and 0 otherwise. dup 1 and makes a copy of a value and then tests the low-order bit (which is 1 if the value is odd). u2/ does an unsigned right shift by one bit. (2/ is the signed version.) I’ve placed the bug to show both the value being tested and its low-order bit.

Remember: the stack is showing hex values. If you really want to see what’s going on, set the radix to binary, and use over u. to copy and print the value on the stack each time through the loop. (over makes a copy of the second value on the stack, where dup copies the top value.)

Go forth and conquer!

I hope this whets your appetite for more! There is a lot more coming. See RISC-V support to see the current state of things.

RISC-V and SiFive documentation

I’ve compiled a page with lots of RISC-V resources.