SamCoVT / TaliForth2

A Subroutine Threaded Code (STC) ANSI-like Forth for the 65c02
Other
29 stars 5 forks source link

Enhancement request: key? #51

Closed bjchapm closed 4 months ago

bjchapm commented 5 months ago

Hello,

I'd like to port the terminal tetris forth program to Tali, as well as Conway's game of life. Both depend on an implementation of key?. It looks like Scot has this in Liara: https://github.com/scotws/LiaraForth/commit/538203da7232b2dc955e39b2a8f58f93e757fb57 and the havekey variable is already reserved, so maybe some groundwork has already been laid. Unfortunately, I'm still learning Forth and 6502 assembly, so I don't think I can code it. Would it be possible to add key? to the enhancement list?

Thank you! Ben

SamCoVT commented 5 months ago

The issue with key? is that it is highly system dependent, so that's the kind of word I'd expect people to write specifically for their system if they need it. The biggest issue is that some systems don't have a way to check if there is a character without actually trying to get the character. For those systems, you have to arrange a buffer of at least 1 byte -- If key? gets a byte then it will save the value in the buffer where the eventual call to key will have to look first before trying to read a character.

I don't think I'm interested in adding KEY? to Tali directly, but I'd be happy to help you write it for your system in high level Forth and then show you how to re-write it in assembly. You're using a Planck system, right? Are you using serial or PS/2 keyboard for input?

SamCoVT commented 5 months ago

I think you're using the serial port for input (based on one of your previous comments). The Planck code from Jonathan Foucher (acia.s) is the kind that gets the character while checking to see if there is one, but I think you are using different serial code. If you're using the circular buffer from Garth Wilson (which is what the serial code for my own Single Board Computer is based on in platform/platform-sbc.asm), then it will be much easier because you can see if the RX buffer is empty without actually taking a character. If you can attach the code you are using (you can attach files to github issue comments), or point me to a repository with a current version, then I can show you the Forth and assembly code you need for key?.

bjchapm commented 5 months ago

Thanks for your offer! Yes, I'm using serial input only and I've grafted in your/Garth's/Kevin's/ serial routines to support the WDC 6551 with delay-based send and interrupt-driven receive. I would like to add something that could be extensible to support PS/2 in the future.

I'm attaching coding_keyq.txt which is a combination of main.s and acia.s and shows the current state of the serial code. The assembler is ca65.

My idea was to add this to main.s:

; Code support for KEY? word

have_chr:
    clc
    jsr acia_have_chr
    rts

and this to acia.s:

acia_have_chr:
                ; Support KEY? word - set carry if available and immed return
                jsr acia_buf_dif
                beq no_char_available
                rts

But I don't know if that's right and I wasn't sure about how to wire it to a new Forth word.

Thanks again

SamCoVT commented 5 months ago

Let's start entirely in Forth and with just the serial port.

With the code you are using, you can check to see if there is a character available by comparing ACIA_RD_PTR to ACIA_WR_PTR. If they are exactly equal, then there is no new character. If they are different (doesn't matter by how much), then there is at least one character. In your assembly, the acia_buf_dif routine is comparing them using subtraction, knowing that the result will be zero if there is no character.

Forth cannot see the labels from the assembler that were used to assemble it, so Tali doesn't know anything about ACIA_RD_PTR or ACIA_WR_PTR. We will need to teach it about those locations. To find out where they live on your system, we'll need to look in the labelmap file when you assembled your system. If you are using Tali's standard build system, then this will be in docs/PLATFORM-labelmap.txt where PLATFORM is the name of your platform. I'm going to use mine as an example, but I have I/O in a different spot of my memory map (uses up some high RAM locations between RAM and ROM), so the actual location is likely to be different for you. I found the following lines in my labelmap file for my Single Board Computer (they were not next to each other - I had to search for them):

ACIA_WR_PTR = 31231
ACIA_RD_PTR = 31230

Because there is no $ in front of the numbers, those are in decimal. Yours will likely have a different value. While their names imply they are pointers, they are actually indices into the array used as the serial data buffer, and they are each only a single byte (which explains the sequential addresses).

It's possible to read from these directly in Forth using C@, which reads a single byte from a given address (the C is for Character, but Forth doesn't care if it's a binary value and just reads a single byte in memory). That might look like #31231 c@ (the # guarantees the number is read in decimal, regardless of the current setting of BASE).

It's considered bad form to use "magic numbers" (eg a number just typed in the middle of the code that a reader of the code wouldn't know where it came from), so we normally give them names in forth. That might look like: #31231 constant ACIA_WR_PTR and then you can say ACIA_WR_PTR c@ and it should be clearer to others what you are doing.

To write key? in forth, then, we just need to name our memory locations and read from them to see if they are not equal (we are using not equal because that means there IS a key and key? should return true). You'll need to adjust the addresses to match your system.

\ Give names to the indices into the ACIA circular buffer.
#31230 constant ACIA_RD_PTR
#31231 constant ACIA_WR_PTR
\ KEY? will return TRUE if there is at least one key available
\ from the serial port.
: key?
  ACIA_RD_PTR c@   ACIA_WR_PTR c@   <> ;

Give that a try on your system (using your locations) and see if you can get it working. Then we can look at doing that same thing in assembly. You are on the right track with your example assembly, but you'll have to put the result on Forth's data stack.

bjchapm commented 5 months ago

How completely cool. Thank you so much for this!

$700 constant ACIA_RD_PTR
$701 constant ACIA_WR_PTR
: key? ( -- f)
    ACIA_RD_PTR c@ ACIA_WR_PTR c@ <> ; allow-native
: ktest 
    begin key? 
    if key dup 3 = 
        if abort" Ctl-C pressed" cr 
        then 
    emit ."  pressed" cr 
    then 
    again ;

This works just great.

SamCoVT commented 5 months ago

You have now unlocked the ability to poke and prod your hardware/software. C@ will let you read a byte anywhere and C! will let you store a byte anywhere (analogous to PEEK and POKE in BASIC).

If you'd like it to be faster, the next step would be to do it in assembly. Here are the things you need to know: The stack pointer is held in X (so you should avoid using X in your assembly, or push/pull it if you really need it). The stack grows downwards, so dex dex will make room on the stack (all items on the data stack are 16-bits) and inx inx will remove the top item from the stack. To actually modify the value on the stack, we use ",X" addressing so 0,X and 1,x are the top value on the stack, 2,X and 3,x are the next value on the stack, etc.

Here is key? in assembler using Tali's built-in assembler:

\ Add the assembler wordlist to the search order
assembler-wordlist >order
$700 constant ACIA_RD_PTR
$701 constant ACIA_WR_PTR
: key? ( -- f)
\ Assembler words should be run in interpreted mode
\ so we use [ to switch from compiling to interpreting
   [ 
   \ Make room on the data stack for the result
   \ Note that you can have more than one opcode on a line
   \ as this is just Forth.
   dex dex
   \ Compare the two values using subtraction.
   ACIA_RD_PTR lda
   sec \ Prepare carry for subtracting (no borrowing) 
   ACIA_WR_PTR sbc
   \ Put the result on the Forth data stack.  Result will be non-zero
   \ when there are characters available.
   0 sta.zx \ STA zero page with X indexing (low byte on data stack)
   1 stz.zx \ Store zero in high byte of result on stack.
   \ Do not put an RTS on the end - Tali will do that for you.
   ] \ Back to compiling mode
; allow-native

Just a note that you can only use allow-native as long as you don't have any flow control (think IF and CASE) or JMP instructions (branch instructions are OK).

Also, Tali's assembler has a different word for every addressing mode. Once you've added the assembler-wordlist to the search order (assembler-wordlist >order) then you can use words to see them all. There is an appendix in the manual (you can search for "Simpler Assembler Notation") that shows all of the addressing modes. Because it's Forth, the argument to an opcode goes BEFORE the name of the opcode.

You can use see to see some info about your word. You get basic info at the top, a memory dump, and a disassembly listing (with values in hex). I can't test this on my system, but I think it should work on yours.

see key?
nt: 830  xt: 83C
flags (CO AN IM NN UF HC): 0 0 0 0 0 1
size (decimal): 13

083C  CA CA AD 00 07 38 ED 01  07 95 00 74 01  .....8.. ...t.

83C        dex
83D        dex
83E    700 lda
841        sec
842    701 sbc
845      0 sta.zx
847      1 stz.zx
 ok

Also, now that you know a little about how this works, you can try see dup and I bet you can figure out what it's doing in assembly.

If you want your new word all the time, you can add it to forth_code/user_words.fs and it will be run at Tali startup.

If you want to go one step deeper, you can write this directly in assembler and add a header for it in the dictionary. If you are interested in that, let me know.