Sunday 15 January 2023

Some simple Forth programs for the Minstrel 4D

In the previous post, I looked at Testing the AY-3-8910 / YM2149 Sound Card for RC2014.

As part of that, I included several simple test programs written in Forth, the native language of the Minstrel 4D I was using (and also the Minstrel 4th, and of course the Jupiter Ace).

I asked in the post if there would be any interest in a blog post going through these programs, and there was, so here it is.

Immediate mode

The first test was entered in immediate mode, i.e. a series of commands typed in and executed immediately, rather than a program of any sort.

I will probably be doing a lot of comparisons with BASIC, since many of the readers of this blog will be more familiar with that.

In the case of BASIC, this is like doing PRINT 2+2, rather than doing 10 PRINT 2+2

The program started with two CONSTANT definitions. These are simply there to make the rest of the program easier to follow. There are in the form of value CONSTANT name, in BASIC terms, LET value = name., or in C #DEFINE name value

216 CONSTANT REGPORT
208 CONSTANT DATPORT

These are the addresses that are used to latch and address or data into the sound chip.

Once those have been set, they can be used in place of a number

0 REGPORT OUT
0 DATPORT OUT

So these would be the equivalent of

0 216 OUT
0 208 OUT

The OUT statement here is used to write to an IO port. value port OUT. (in Spectrum BASIC, OUT port, value). So this writes 0 to the register port, and the next line writes 0 to the data port.

The constants just makes it easier to follow, and also saves you having to look up which one is which, particularly when sending similar values. For example:

CTRL_REG REGPORT OUT
MODE1 DATPORT OUT

Instead of

200 216 OUT
108 208 OUT

The next line is is actually two commands, and is the first inkling of the power of Forth.

REGPORT IN .

The first part reads a value from the data port and puts it on the stack. In Spectrum BASIC LET X = IN port .

This could have been 208 IN .  but the constant hopefully makes it easier to follow.

The second port is a full stop, surrounded by space. Space is important in Forth, it can be one or more spaces and or newlines. Everything else is either a word (i.e. a command or construct) or a numeric literal. Everything including . and ; and : etc. are words, so all must be surrounded be space when you are typing them in.

When doing things like this, it's quite handy to cut and paste text over the serial link to the Minstrel 4D, so you can type in the programs in this blog.

When a program is typed in, all the whitespace is stripped off and a single space is stored. Stored programs are listed with newlines inserted when appropriate and spaces used to indent as required. Not always where you would like then (i.e. I would prefer 2000 0 to be on a separate line), but good enough most of the time.

Back to the full stop. This is the print command. It writes out to the screen the thing on the top of the stack. So to go back to Spectrum BASIC again, this is LET X = IN port : PRINT X but you don't need to worry about creating the X variable, it is all passed along the chain. I don't want to go into too much detail about this here, the stack in Forth is very powerful, see Minstrel 4D Quick Start Guide for more information.

http://blog.tynemouthsoftware.co.uk/2022/11/minstrel-4d-quick-start-guide.html

So REGPORT IN . reads a value from the register port (I have another post to follow about why that is the register port and not the data port) and prints it to the screen. You can do that in BASIC, PRINT IN port, but only with simple cases like that.

The rest of the programs continues in a similar way, writing to ports and reading back, so I won't go into that any further.

Strings

A full stop . is used to print the number on the top of the stack, If you want to use strings, you use full stop quotes and then quotes: ." and " e.g.

." Hello, World"

Spacing is important here, it should be: space ." space string" space. There is no space between . and " at the start, and no space between the end of the string and the closing "

What's the word?

It is time to start writing a program. It may be easier to think of functions in C or Python etc. BBC BASICs DEF PROC is probably closer I suppose, but maybe less familiar.

A simple example would be

: INPRINT
  REGPORT IN .
;

This defines a word called INPRINT, which does the read from a port and print to the screen, so you can just type INPRINT and it will execute IN REGPORT . The names are not case sensitive, so you can also type inprint if you prefer, but I will stick with uppercase here for clarity.

The : and ; surround the definition. So you have : name code ; The spaces around : and ; are important. They are words themselves, and all words need to be separated by white space.

So, in C, you would have something like

void inprint()
{
    byte x = in(REGPORT);
    print(x);
}

That of course is a bit limited as the port address is hard coded, so you would probably want the equivalent of

void inprint(byte port)
{
    byte x = in(port);
    print(x);
}

In Forth, you don't have a parameter list as such, you again use the stack, so the more generic version is just

: INPRINT
  IN .
;

When this is run, there is no value before IN for the port number, so how do you put one there? Easy, you just put it before the word, so

REGPORT INPRINT

In function terms, you could think of that as passing a parameter to a function, but in Forth terms, it is just using the stack as before.

DO..LOOP

Next, I will go to a different blog post, one about the Digital I/O Card for RC2014 (and Minstrel 4D).

Here, a simpler version of the program I included, one that writes very fast to the IO port, counting up in binary on the LEDs, too fast to see.

0 CONSTANT LEDPORT
: FASTLEDONCE
    256 0
    DO
      I LEDPORT OUT
    LOOP
;

This is a "do loop".

number_of_steps starting_value DO stuff LOOP

Sort of FOR I = 0 TO 255 : stuff : NEXT I but it is often easier to think in terms of the number of steps, rather than thinking in terms of N-1, like the stop at 255.

Within the loop, I can be used to get the current count, so here the value of I is being written to the LED port (set using a constant as previously discussed). So this writes 0, 1, 2, 3 etc. up to 255 to the LED port. But that only does it once. You could do another DO .. LOOP to make it do that 10 times, but I will use another construct instead.

BEGIN...UNTIL

This is sort of the while loop in C terms, and is BEGIN stuff condition UNTIL. This does stuff until condition is true (i.e. not 0).

: FASTLEDLOOP
  BEGIN
    FASTLEDONCE
    0
  UNTIL 
;

This is a simple example, the condition will always be 0, so it will keep looping until you press BREAK (shift + space).

You could set a condition, say you have a switch wired to the Digital IO card that returns 0 unless a switch is pressed, so test that each loop after the LEDs have been flashed.

0 CONSTANT SWITCHPORT
: FASTLEDLOOPSTOP
  BEGIN
    FASTLEDONCE
    SWITCHPORT IN
  UNTIL
;

Note there is no . after the IN, this is not printing the result, it is reading it, and then leaving it on the top of the stack. When UNTIL comes around, it will check the top of the stack for the condition to test and loop back to BEGIN, UNTIL it is true.

Previously this was the 0 that was hard coded, now it is the result of the IO read. This again is passing things around using the stack, so no need for extra variables, just make sure all the bits are in the right order.

Combined all in one, that would be

0 CONSTANT LEDPORT
0 CONSTANT SWITCHPORT

: FASTLED
  BEGIN
    256 0
    DO
      I 0 OUT
    LOOP
    SWITCHPORT IN
  UNTIL
;

The equivalent in C, would be something like

do
{
  for(int i=0; i<256; i++)
  {
    out(i);
  }
}
while(in(0) == 0);

Or in BASIC, something like

10 FOR I = 0 TO 255
20 OUT 0, I
30 NEXT I
40 IF IN(0) = 0 THEN GOTO 10

IF..ELSE..THEN

OK, if you aren't already confused, then let me throw this at you. Forget what you know about BASICs IF THEN, and C's if { } else {} and consider this alone and it makes perfect sense, honest.

condition IF stuff THEN rest of program

So you have a condition first, that comes off the stack. That is evaluated, and IF condition is true, stuff is executed. THEN it gets on with the rest of the program.

16 0
DO
  I 10 < IF
    SPACE
  THEN
  I .
  CR
LOOP

In this extract, there is a loop, 16 steps from 0 to 15, and the value I is printed to the screen. There are various very powerful ways to format strings in Forth, but I've not fully worked those out yet, so this is a simple version to pad out single digit numbers

Here the condition is I 10 <  This is a less that test, in the form of a b < or "is a less than b". This is where it starts to look like reverse Polish notation for anyone familiar with early digital calculators.

So the condition is true when I is less than 10. IF this is the case, it prints an extra space using the SPACE word, THEN it goes on to the rest of the code.

(This could have been IF ."  " THEN, but SPACE is easier as you have to remember there is one space after ." for separation, then the actual space to be printed, then the closing quote).

When I put the preview version of this post out to my Patreon supporters, it was suggested that would make a nice word that could be reused.

: .2DIGITS
  DUP 10 < IF
    SPACE
  THEN
  .
;

So the test function would become

: TEST
  16 0
  DO
    I .2DIGITS
    CR
  LOOP
;

That also introduces the concept of keeping the stack in balance. It is important to keep track of when you are putting things on the stack, and when you are taking them back off again. The < test would take the last value off the stack to test it, and then it would be thrown away. When it came to the . to print it, that would not be there and it would go to the previous value from the stack, which is not what you wanted. DUP takes one thing from the stack, then duplicates it and puts both copies back. That way, you can use one for the test, and one for the print. (There is also ?DUP that only does the duplication if the value is non-zero, which is quite handy, but lets not get too complicated just yet). Thanks to George Beckett for that suggestion.

Taking another extract from one of the sound card test programs, the test condition, in this case, it was the result of reading the register port.

  REGPORT IN 31 <
  IF
    ." AY-3-8910/2"
  ELSE
    ." YM2149"
  THEN
  ."  Detected"

Here, REGPORT IN reads a value and leaves it on the stack, then we get our test. In this case, the first value comes off the stack, just after it is placed there by the IN command. The second value is the literal value 31. The result is put on the stack, and used by the following IF command.

condition IF stuff ELSE otherstuff THEN rest of program

IF the condition is true, it will print AY-3-8910/2, ELSE, it will print YM2149, THEN it will go on with the rest of the program and print Detected.

You see, that does make sense. Just not IF you have been writing IF THEN statements for 40 years. THEN it takes a bit of getting used to

Full example

Here is a full program from the sound card blog post

216 CONSTANT REGPORT
208 CONSTANT DATPORT

: DETECT
  CLS
  1 REGPORT OUT
  31 DATPORT OUT
  
  REGPORT IN 31 <
  IF
    ." AY-3-8910/2"
  ELSE
    ." YM2149"
  THEN
  ."  Detected"
  CR
  CR
;

The full program adds two new words, CLS which is just like the BASIC CLS, and clears the screen. (PRINT "♥️" for Commodore people who seem to think that is in any way acceptable).

The second is CR. This just prints a carriage return, because the standard stuff . command is actually equivalent to PRINT "stuff"; (with a semicolon, which does not move to the next line) rather than PRINT "stuff" (without a semicolon, which does).

Homework

I think that is enough for now. Just a few simple constructs that are enough to write little test programs I used in the last few posts. Hopefully that will give you a taster of how powerful Forth can be. And I haven't even mentioned how much faster these examples would run compared to BASIC equivalents as so many of these map down fairly directly into machine code.

I will leave you with another of the programs from the sound card post. see if you can now understand what it is doing.

216 CONSTANT REGPORT
208 CONSTANT DATPORT

: REGS
  CLS
  ." YM2149" CR
  ." Real" CR
  CR
  
  16 0
  DO
    I .
    I 10 < IF ."  " THEN
    I REGPORT OUT
    255 DATPORT OUT
    I REGPORT OUT
    REGPORT IN .
    CR
  LOOP
  CR
;


Further Reading

Here are some links that contain further information to help you go Forth in your programming ability.

I would also highly recommend the Jupiter Ace Forth Programming manual by Steven Vickers. This is the manual that originally came with the Jupiter Ace. It was reprinted for the 35th Anniversary, so should be available from all good bookshops. And Amazon.



Forth programmers: how did I do, did I get anything wrong?

Non-Forth programmers: did any of that make sense?

Non programmers: don't worry, there will be more pictures of bits of computers next week.


Advertisements

Minstrel 4D

The Minstrel 4D kits are shipping now, you can order one from The Future Was 8 bit

https://www.thefuturewas8bit.com/minstrel4d.html

More info in a previous post:

http://blog.tynemouthsoftware.co.uk/2022/08/minstrel-4d-overview.html


Z80 Kits

The YM2149 Sound Card and RC2014 Digital IO Module are available from Z80Kits.com

https://z80kits.com/shop/ym2149-sound-card/

https://z80kits.com/shop/digital-i-o-module/


Patreon

You can support me via Patreon, and get access to advance previews and behind the scenes updates. These are often in more detail than I can fit in here, this post contains bits from three Patreon posts, and there is a forth and fifth to follow. This also includes access to my Patreon only Discord server for even more regular updates.

https://www.patreon.com/tynemouthsoftware