Page 1 of 1

X16 and the BASIC USR function (multi-argument and 65C02 special powers revealed)

Posted: Mon Oct 09, 2023 8:40 am
by voidstar
I'm no expert about USR, but here are some notes on what I've learned about it. Will evolve this article as I learn more.

I came across the following article for the C64 and wanted to see how well it would all port over to the X16:
https://www.defiancestudios.com/2020/03 ... rough-usr/


So - is USR even useful? Why not just POKE your code with a DATA sequence and just SYS over to wherever you POKE'd it?

Well - the intent is USR is help share data between your BASIC program and machine code. Variables in BASIC are actually 16-bit signed integers. Sort of - it's worse than that, as all the variables in BASIC are actually stored in some 5 byte structure (so they can "morph" between an integer type and floating point; this later evolved into the VARIANT type in VisualBasic). On top of that, BASIC has its own funky floating point representation (this being pre-IEEE float-spec days). So in BASIC if you say Z=42, how do you get that value 42 over into an accumulator register, so you can do some logic on the machine code side? This is where the USR function is suppose to help.

But in your ML code you don't really want anything to do with floating point values. The parameter you pass to USR gets translated (or copied) over into a zeropage region called the FAC (it should be 5 bytes but it seems to me as much as 8 bytes is actually used). You shouldn't really need to know or care where that is, but we may come back to that in a moment.

The system has two helper-functions, called fac2ya and givayf. I'm not sure what category these are - they aren't traditional KERNAL calls. But fac2ya converts the FAC content over into registers Y and A. TBD, I can't find this function yet on the X16 - so there are some missing details here - but the general point is, if you pass a small integer value to USR (like USR(250) ) your integer value is going to show up in the A register. For larger values >255 or passing actual floating point, some other stuff happens - but we'll just focus on integer values <= 255 for now.

Then in your ML code, you manipulate Register A and/or Register Y. Then call givayf and your register values get written back to the FAC and popped back as the return value of your USR call. Note from my experiments on the X16, the contents of the FAC are corrupted several times over by the time the systems returns back to your BASIC call - which is why the FAC address doesn't matter, you can't PEEK into it anyway after your code resumes from the USR call. I did find the giveayf function in the X16, but you'll still have to do some figuring-out on how to interpret the return value of USR. If you don't do anything (i.e. if you don't call giveayf in your machine code), then the default behavior is that USR returns the same value that was passed in.

Ok - so, none of that sounds really useful: in order to use USR, you have to spend a billion instructions converting the FAC to/from your registers anyway, so what's the point? Well - exactly. Your USR ML better to be extremely useful to make all this worthwhile. So it's a judgement call. For now, it's just a curiosity - so let's press on.


Here is a chart comparing the pertinent addresses related to USR:

Code: Select all

         C64      X16 
fac    = $0061    00C3          // 5 bytes  (6th byte for sign?)
frmnum = $ad8a    D209 frmnum?  // get and evaluate data for type mismatch
usradd = $0311    0311 same     // pointer for USR funtion for ml
fac2ya = $b1aa    ????          // fac1 to int in y(lo) a(hi)    
givayf = $b391    D998 givayf0? // int y(lo) a(hi) to fac1
err    = $a437    D827 fcerr?   // BASIC error handling routine
In the next post I'll give a more complete example - including how to pass multiple arguments and something nifty the 65c02 can do.

Re: X16 and the BASIC USR function (multi-argument and 65C02 special powers revealed)

Posted: Mon Oct 09, 2023 9:26 am
by voidstar
Attached is a BASLOAD-compatible example of how to setup and use the USR function. The PRG is the same thing but in tokenized BASIC form - so it's available if you're not yet familiar with BASLOAD.

First, you need one of those typical loops to POKE your machine code into memory. Or you could load it from a BIN file or something (BLOAD). The example attached uses a GOSUB to INSTALLoML1 (machine language entry #1) using a sequence of DATA values. That should be fairly straightforward. What might not be straightforward is where does this code come from?

You can fire up a big formal assembler. But if you know a little bit of 6502, you can also draft some stuff up just using the online 6502 assembler here: https://www.masswerk.at/6502/assembler.html

Just knowing a few basics here is useful:
.org to set your start address (originate)
you can assign labels to values, like my_number = $FF
.byte and .word for some data segments

Here is an example of how I used it for the code used in the example:
 
6502asm.png
6502asm.png (60.24 KiB) Viewed 3712 times
.
The LISTING at the bottom is what is helpful. I wish they had a button to prefix "$" to the hex values - because you're going to need to do that when pasting that into X16 BASIC. For a dozen or so instructions, just do it manually. But for something more substantial, obviously a script will help.


And now an import "reveal" about the 65c02: the site above is a 6502 assembler. Notice in the example Listing at the bottom, it doesn't have any code listed for INA (increment A). Because that's not a valid 6502 instruction. And it's neat that this assembler doesn't care - it will put a place holder for you, so you can fill in the blank as needed. The only issue to keep in mind is then that since it doesn't know how many bytes your "hypothetical instruction" takes, the addresses after that point might not be correct. So that's where this requires some mental skills to keep all that in mind - INA is just one byte, so you'll need to add a byte to any addresses you need past that point (note: if you know how many bytes the instruction takes, you could proxy it and its operands with NOP).

INA is an instruction added to the 65c02 precisely for that convenience of being able to increment the A-accumulator. It's opcode is $1A. It's a long history on why this wasn't present in the original 6502 (boils down to minimizing cost and getting that chip to market ASAP). Purist will want to solve their problems without using these luxury opcodes (and because that makes the code more easily portable back to traditional 6502 systems) - I'm highlighting here just to show how the 6502 online assembler can still be useful, and how to handle cases where maybe you do want to these additions.


So decide where you want to your machine code to go, or stick with my default of $2000. Draft out some ML. Then massage it into DATA statements. In the attached example is just my typical approach - I like having the asm comments along with the DATA (because I'll then adjust the code right there in the DATA here and there). But a long sequence of DATA values is fine too.


Now, the USR call: By default USR is setup to call some nominal ERROR HANDLING function - just for lack of anything else to do. You can have multiple USR machine code functions, but you need to update the USR pointer before using them. As on the C64, this remains at address $311 and $312. Remember when doing 6502 instructions, byte order is reversed. So if you want to setup your machine code at address $2000 (the typical default that I use), you initialize USR like this:

Code: Select all

  POKE $0311,$00
  POKE $0312,$20

Next, check out this funky syntax (the PRINT A part is optional, just for debugging aid):

Code: Select all

	A=USR(0)7:PRINT A
	A=USR(1)(6):PRINT A
	A=USR(2)5:PRINT A
Normally USR just has one argument, like USR(42), but in the above two values are being passed. And note that the value can be in a parenthesis or not - no functional difference. HOWEVER, this multiple arguments ONLY WORKS if your USR ML code is setup/prepared to actually use them. Don't use multiple arguments if your ML code doesn't actually need them, and don't leave the extra arguments around - you'll get SYNTAX ERROR if your ML code doesn't actually use them.

The way these subsequent arguments to USR are used is by calling another "internal function" called frmnum. For X16, I found that this seems to be at $D209. My general understanding is that it basically just runs the same "get next argument" that the USR BASIC function is using anyway. What that means is, there is nothing really magical about it, it is just "going through the motions" again to read another argument just like it did for the first one. WHICH MEANS, it converts the value to the FAC and you have to use fac2ya again for this now-next-new value [ TBD, and true I haven't found that function yet, but standby ].

So that's neat. If you're not doing a JSR to a frmnum in your ML code, then don't add additional arguments to your USR call (or you'll get SYNTAX ERROR). Then I don't think there is any limit to the number of arguments - just whatever the line length limit of BASIC might be.


USR AND DEBUG: The x16emu supports a debug opcode, $DB. So if nothing else, you can define an ML sequence that just has that opcode (and a JSR $60 to get back from the USR function). This can let you use USR to stop as interesting "observation points" within your BASIC execution (again, only if running the emulator with -debug). That can be neat for exploring what BASIC is doing behind the scenes, or just having some stop points in your code to observe some things. This is commented out in the example attached (and note that adding it can influence the target address offsets, similar to what was mentioned earlier - so keep that in mind too when using it; sometimes you might add benign NOPs as placeholders to swap in a debug instruction later). NOTE: The emulator also has some "registers" (addresses) it writes in, so you can programmatically detect if it's in use - so you can dynamically decide whether to write the $DB or not. (it's sort of like: how can you tell you're in a virtual machine? Often the host environment leaks some way to tell)



Thoughts on the elusive fac2ya function: I'll keep looking for awhile, but as a fallback we can probably write our own version of this. Or as another fallback, if we just look at the trends of what is being passed in and back out, there is probably some "scale value" to adjust expectations. For example on the output, if we don't use Y (and just have one single return value), it looks like we just need to divide by 512. That's tentative (and also slow and not very robust), but it's a starting point.


So not 100% useful yet - and maybe some other folks are more experienced or have tips here (and I'm sure there is a lot more to all this, the intent here was a simple initial orientation). If nothing else, the USR for debug points is kind of useful. Another area this might be useful is it might make using the UserPort from within BASIC actually more viable.