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 (60.24 KiB) Viewed 3668 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:
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.