Closed bjchapm closed 4 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?
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?
.
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
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.
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.
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.
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 thehavekey
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 addkey?
to the enhancement list?Thank you! Ben