Dan's 6502 build, aka, The WOPR Jr.

Building your first 6502-based project? We'll help you get started here.
Dan Moos
Posts: 277
Joined: 11 Mar 2017
Location: Lynden, WA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by Dan Moos »

GBZM, I fully get that. I think you missed the gist of my question. Why do I need to st side RAM, when no one is accessing it but me?

Garth mostly cleared it up, if I'm understanding him right. Basically, it's go let the assembler do the hard work of arranging RAM usage in an organized way.
User avatar
BigDumbDinosaur
Posts: 9428
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by BigDumbDinosaur »

Dan Moos wrote:
ok, still a little hung up on the .DS directive and its various brethren.

Does a directive like that have any use if your program is going in ROM?
Even though your firmware would be running from ROM, you still need RAM for a hardware stack, zero page pointers, indices and counters, and input/output buffers and FIFOs. All of these items must be explicitly declared in your assembly language source code. Otherwise, the MPU won't have a clue as to where to read and write things.
Quote:
I guess what still confuses me is, with my code in ROM, why would I even have to set aside any block of memory in RAM, if nothing goes anywhere in RAM that I don't specifically designate. So if I want $0200 to $02FF to be4 a buffer for instance, I could just not ever use it for anything else.
In order for your program to know that a buffer is at $0200 and extends to $02FF, you have to say so, e.g., BUF = $0200, followed by BUFEND = $02FF. Unlike high level languages, none of this is automatic in assembly language. Also, burying "magic numbers" in code, e.g., LDA $0200 instead of LDA BUFFER, is bad practice. If you later decided to relocate that buffer you will have to find every reference to that address and change it. Let the assembler do its job by explicitly assigning names to each location in RAM that you will be using. You have to declare variables in C, so do the same in assembly language and save yourself some grief. Trust me: as your program gets bigger and bigger you will be cursing yourself if you do not follow this practice.
Quote:
Also, I had read BDD's exposition of the Kowalski directives and operators. I didn't ses mention of the use of the "<" and ">" operators being used that way.
The < and > idioms for referring to the least significant and most significant bytes of a 16 bit value are almost universal in 6502 assembly language programming. Eyes and Lichty discuss their usage in their programming manual.
asm_directives.asm
Kowalski 6502 Pseudo-Ops
(1.62 KiB) Downloaded 75 times
logical_expression_examples.asm
Kowalski Logical Expressions
(836 Bytes) Downloaded 68 times
x86?  We ain't got no x86.  We don't NEED no stinking x86!
User avatar
barrym95838
Posts: 2056
Joined: 30 Jun 2013
Location: Sacramento, CA, USA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by barrym95838 »

Here's a simple but non-trivial example:

http://rosettacode.org/wiki/FizzBuzz/As ... 2_Assembly

Code: Select all

	.lf  fzbz6502.lst	
	.cr  6502	
	.tf  fzbz6502.obj,ap1
;------------------------------------------------------
; FizzBuzz for the 6502 by barrym95838 2013.04.04
; Thanks to sbprojects.com for a very nice assembler!
; The target for this assembly is an Apple II with
;   mixed-case output capabilities and Applesoft
;   BASIC in ROM (or language card)
; Tested and verified on AppleWin 1.20.0.0
;------------------------------------------------------
; Constant Section	
;			
FizzCt	 =   3		;Fizz Counter (must be < 255)
BuzzCt	 =   5		;Buzz Counter (must be < 255)
Lower	 =   1		;Loop start value (must be 1)
Upper	 =   100	;Loop end value (must be < 255)
CharOut	 =   $fded	;Specific to the Apple II
IntOut	 =   $ed24	;Specific to ROM Applesoft
;======================================================
	.or  $0f00	
;------------------------------------------------------
; The main program	
;			
main	ldx  #Lower	;init LoopCt	
	lda  #FizzCt	
	sta  Fizz	;init FizzCt
	lda  #BuzzCt	
	sta  Buzz	;init BuzzCt
next	ldy  #0		;reset string pointer (y)
	dec  Fizz	;LoopCt mod FizzCt == 0?
	bne  noFizz	;  yes:
	lda  #FizzCt	
	sta  Fizz	;    restore FizzCt
	ldy  #sFizz-str	;    point y to "Fizz"
	jsr  puts	;    output "Fizz"
noFizz	dec  Buzz	;LoopCt mod BuzzCt == 0?
	bne  noBuzz	;  yes:
	lda  #BuzzCt	
	sta  Buzz	;    restore BuzzCt
	ldy  #sBuzz-str	;    point y to "Buzz"
	jsr  puts	;    output "Buzz"
noBuzz	dey  		;any output yet this cycle?
	bpl  noInt	;  no:
	txa  		;    save LoopCt
	pha  		
	lda  #0		;    set up regs for IntOut
	jsr  IntOut	;    output itoa(LoopCt)
	pla  		
	tax  		;    restore LoopCt
noInt	ldy  #sNL-str	
	jsr  puts	;output "\n"
	inx  		;increment LoopCt
	cpx  #Upper+1	;LoopCt >= Upper+1?
	bcc  next	;  no:  loop back
	rts  		;  yes:  end main
;------------------------------------------------------
; Output zero-terminated string @ (str+y)
;   (Entry point is puts, not outch)
;			
outch	jsr  CharOut	;output string char
	iny  		;advance string ptr
puts	lda  str,y	;get a string char
	bne  outch	;output and loop if non-zero
	rts  		;return
;------------------------------------------------------
; String literals (in '+128' ascii, Apple II style)
;			
str:	;		string base offset
sFizz	.az	-"Fizz"	
sBuzz	.az	-"Buzz"	
sNL	.az	-#13	
;------------------------------------------------------
; Variable Section	
;			
Fizz	.da	#0	
Buzz	.da	#0	
;------------------------------------------------------
	.en  		
The top three lines are housekeeping.

The Constant section contains values that don't need storage and don't change after assembly, but are named for the purpose of readability.

The String Literal section doesn't change after assembly, but does need storage allocated, so it is given a label so the assembler can place it directly after the code section. If the code section ever expands or contracts, the address of the String Literal section will change, but the assembler will automatically handle the change and modify the operands accordingly, just like it would for a branch target that moved due to a revision or addition.

The Variable section contains cells that do change during execution (the values, not the addresses), and once again the assembler is trusted to allocate space for them and keep track of their addresses as operands for the instructions using them, no matter how many times the code section may shrink or expand during subsequent revisions.

Mike B.

P.S. This is also an example of how using TABs instead of SPACEs can turn a tidy looking source into a mess, depending on where you post it. :-(
Last edited by barrym95838 on Mon Apr 10, 2017 1:03 am, edited 2 times in total.
Dan Moos
Posts: 277
Joined: 11 Mar 2017
Location: Lynden, WA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by Dan Moos »

your example of

Code: Select all

BUFFER = $0200
BUFFEND = $02FF
actually points out my confusion. Doing it THAT way makes perfect sense to me. Is that completely equivalent to a .DS statement?

I wish I were better at wording my questions. Seems to be a lot of confusion over what I am asking.
Dan Moos
Posts: 277
Joined: 11 Mar 2017
Location: Lynden, WA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by Dan Moos »

Ok, in the directive list you posted, we have

Code: Select all

.DS .RS           ;equivalent to *=*+N, where N is arg to .DS
I know what *= means. What does *=* mean?
User avatar
GARTHWILSON
Forum Moderator
Posts: 8775
Joined: 30 Aug 2002
Location: Southern California
Contact:

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by GARTHWILSON »

There's nothing wrong with doing something like the BUFEND = $02FF; but if there's ever any question about how big the buffer is, I would instead tend to have a constant telling the size of the buffer, rather than a label to the last address of the buffer. Then if you ever need to move the buffer, you only have to change the one label, not both.

My original code from the interrupts primer (which I think you started with) assumes the full 256 bytes for the buffer, regardless of where it starts (as long as it's all in RAM). I did it that way partly because I do have a use for that much, and partly because it makes the routines shorter and faster if you don't have to make the pointers wrap at something else like 128 or 64, let alone numbers like 75 or 100 which take more than just ANDing-out a bit or two in binary.

The *=* is part of *="+N.
* in this case is the program counter.
*+N means "Take the program counter's current value and add N to it."
= is the assignment operator; so
the whole things means "Take the program counter, add N to it, and put the result of that addition back in the program counter. (It's like A=A+B in BASIC.)
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?
User avatar
GaBuZoMeu
Posts: 660
Joined: 01 Mar 2017
Location: North-Germany

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by GaBuZoMeu »

Dan Moos wrote:
GBZM, I fully get that. I think you missed the gist of my question. Why do I need to st side RAM, when no one is accessing it but me?

Garth mostly cleared it up, if I'm understanding him right. Basically, it's go let the assembler do the hard work of arranging RAM usage in an organized way.
Dan,

sorry. I don't want to offend you. Just one counterquestion:

How should the assembler you have chosen know about your hardware, how should it know where is ROM,RAM,I/O or perhaps nothing?

I haven't heard about an assembler having a configuration and options settings section like C compilers for µCs have. These settings are part of the asm source.
User avatar
BigDumbDinosaur
Posts: 9428
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by BigDumbDinosaur »

Dan Moos wrote:
your example of

Code: Select all

BUFFER = $0200
BUFFEND = $02FF
actually points out my confusion. Doing it THAT way makes perfect sense to me. Is that completely equivalent to a .DS statement?

I wish I were better at wording my questions. Seems to be a lot of confusion over what I am asking.
Garth tried to unconfuse you but I'm not sure if it had the desired effect. :D Assembly language concepts can be confusing for anyone who started with a high level language, where a certain amount of behind-the-scenes automation occurs for you—BASIC is a good example of this.

Suppose you have written a keyboard input routine for your program. As the user types you have to collect keystrokes somewhere and when the user types [CR] (symbol for the return key) to "enter" his or her data, you have mark the end of the series of collected keystrokes and then go on to process the input. You collect the keystrokes in a buffer that must be large enough to hold the maximum number of keystrokes you deem to be adequate, as well as one extra space to mark the end of the string of keystrokes. Conventionally, a null ($00) is used as an end-of-string (EOS) marker. You can use whatever you want, or could even have a separate index that tells your program the offset to EOS in the buffer. You're the programmer, so it's your call.

In order to define such a buffer, you could take the following steps:
  1. Define the maximum number of keystrokes you are willing to accept. In assembly language, that definition would be encoded as maxkeys = <some_number>. For example, maxkeys = 60 means just what it says.
  2. Using the maxkeys definition, you would then define your buffer size. If you use a terminator to mark the EOS of the user's input the buffer must be one byte larger than maxkeys. So you would write bufsiz = maxkeys+1. Otherwise, bufsiz would equal maxkeys.
  3. Later on in your program, where you would be declaring dynamic storage, you would define the buffer itself as follows:

    Code: Select all

    inputbuf *=*+bufsiz
    The above would set inputbuf to the current value of the program counter and then advance the latter by bufsiz bytes. You have now reserved 61 bytes for your input buffer. Elsewhere in your program, you can read and write your buffer by using its name, not some arbitrary address.
As you can see, nothing is automatic in assembly language. The programmer defines data structure sizes, the structures themselves, where in memory they are located, the means by which they are manipulated, etc. This characteristic of assembly language has to be understood and accepted in order to successfully write programs. Assembly language enforces no discipline on the programmer. If your thinking is disorganized so will be your program. If you think spaghetti code in BASIC is bad you ain't seen nothin' compared to what assembly language spaghetti code looks like.

Here's the above sequence recapitulated:

Code: Select all

maxkeys  = 60                  ;maximum allowed keyboard input
bufsiz   = maxkeys+1           ;buffer size w/terminator

	---

         * = $0200             ;set lcation to $0200 (decimal 512)
;
inputbuf *=*+bufsiz            ;define input buffer...
;
;	———————————————————————————————————————————————————————————
;	The above statement sets the starting location of the input
;	buffer to $0200 and reserves 61 bytes.  The next storage
;	definition will be at $023D.
;	———————————————————————————————————————————————————————————
;
flag     *=*+1                 ;this flag will be at $023D
Now, if you later on decide to accept more input you'd merely redefine maxkeys and when you reassemble your program everything that refers to maxkeys will be fixed up for you, including the size of the buffer.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
Dan Moos
Posts: 277
Joined: 11 Mar 2017
Location: Lynden, WA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by Dan Moos »

I realize the assembler is ignorant of the hardware outside the CPU. I understand I have to give it very specific addresses for everything. Like I said, I think you missed what I was asking.

I didn't see why I had to specifically set aside that memory, when I could just not use it for anything else. Since I'm the only one assigning memory, at the time it seemed an unnecessary step. At the time I figured, why couldn't I just label the start of the buffer, and then never assign any memory in the range "buffer + size of buffer" for non buffer use.

What I think I've learned since, is that yeah, I could do it that way, but it's way easier to let the assembler do the hard work, and more importantly, since I'm working with a size, and not specific addresses, I can change the size of the buffer without a cascade of addresses I've foolishly hard-coded becoming invalid.

Ok, that explanation of the "*" operator was what I needed. I didn't realize that "*" actually was the program counter. I thought "*=" was a single directive. Now that I know that "*" is a stand alone symbol, "*=*+N" makes perfect sense.

If that nugget is in any of the 4 books I'm working with, (including the oft cited Lichty and Eyes), I've sure missed it. You see stuff like that used, but not explained.

Like a said before, learning 6502 assembly has been ok for me so far, but I still seems like a massive research effort to find novice level explanations of assembler directives. Just as soon as I think I have a handle, someone posts a code snippet that has yet more directives that are not in common with my assembler. I'll get it, it's just frustrating because no good one stop info source seems to exist for directives.

How did things get to where there was no standard? I guess I know how these things happen, but sheesh!

I've been trying to download the WDC programming manual from their site, but it's a dead link. Anyone know where else to find it?
Dan Moos
Posts: 277
Joined: 11 Mar 2017
Location: Lynden, WA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by Dan Moos »

I crossed posts with you BDD, so sorry if it sounds like I didn't read it. :)
User avatar
BigDumbDinosaur
Posts: 9428
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by BigDumbDinosaur »

Dan Moos wrote:
Ok, that explanation of the "*" operator was what I needed. I didn't realize that "*" actually was the program counter. I thought "*=" was a single directive. Now that I know that "*" is a stand alone symbol, "*=*+N" makes perfect sense.
The *=*+N construct is basic programming; something like it is found in many languages. If you know C you know you can write X=X+5 and the compiler will understand what you are saying. It's understanding the meaning of * that is key.
Quote:
Like a said before, learning 6502 assembly has been ok for me so far, but I still seems like a massive research effort to find novice level explanations of assembler directives. Just as soon as I think I have a handle, someone posts a code snippet that has yet more directives that are not in common with my assembler. I'll get it, it's just frustrating because no good one stop info source seems to exist for directives.
That's because there really is no standard when it comes to pseudo-ops. The only thing defined in the assembly language is the bits and pieces that actually generate code. That would be the mnemonics and address modes. MOS Technology also defined the minimum requirements for representing quantities, as well as minimum label and symbol sizes. Everything else is subject to a certain amount of interpretation by whomever writes the assembler.
Quote:
How did things get to where there was no standard? I guess I know how these things happen, but sheesh!
There is a standard for the 6502 assembly language, which was promulgated by MOS Technology way back when. WDC has continued with that standard but has added to it to accommodate new instructions and new address modes. Unfortunately, more than a few (mostly amateur-written) assemblers have appeared that don't follow the MOS/WDC standard.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
Dan Moos
Posts: 277
Joined: 11 Mar 2017
Location: Lynden, WA

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by Dan Moos »

yup. once I realized "*" specifically represented the counter, all made sense.
User avatar
GARTHWILSON
Forum Moderator
Posts: 8775
Joined: 30 Aug 2002
Location: Southern California
Contact:

Re: Dan's 6502 build, aka, The WOPR Jr.

Post by GARTHWILSON »

Dan Moos wrote:
Ok, that explanation of the "*" operator was what I needed. I didn't realize that "*" actually was the program counter. I thought "*=" was a single directive. Now that I know that "*" is a stand alone symbol, "*=*+N" makes perfect sense.

[...] still seems like a massive research effort to find novice level explanations of assembler directives. Just as soon as I think I have a handle, someone posts a code snippet that has yet more directives that are not in common with my assembler. I'll get it, it's just frustrating because no good one stop info source seems to exist for directives.
Ok, just to add another one, hopefully ahead of time so it won't mess you up further when you see it: with some assemblers, $ by itself (not followed by hex digits) is the way to get the value of the program counter. So you might see something like

Code: Select all

        ORG   $ + N
which is equivalent to the *=*+N that we had above. Yeah, there's definitely a lack of consistency.

Although the following is in the interrupts primer, including with an illustration, I should comment again that the RS-232 buffer as I'm using it there is a ring, a circle. There is effectively no beginning or end. You don't have to parse an input string all the way to the end before allowing a new string to start being accepted. It's kind of like a gas tank which does not need to be emptied before you put more gas in. (There's probably a better analogy to be had; but you probably get the idea.) Regardless of what's in it, if it's not full, you can add more. Just make sure you tell the sender to stop sending when the buffer is nearly full, so you don't overflow it, ie, you don't overwrite the oldest material in it that's still waiting to be handled.

Also, there's no requirement that the incoming data all be in strings, whether counted or null-terminated. It might for example start with a string to tell it that the next 913 bytes are binary program material to put in memory starting at address xyz. After that many bytes have come in, the next bytes might again be a string giving the computer the next instruction.
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?
User avatar
BigDumbDinosaur
Posts: 9428
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Buffers, FIFOs & LIFOs

Post by BigDumbDinosaur »

GARTHWILSON wrote:
Ok, just to add another one, hopefully ahead of time so it won't mess you up further when you see it: with some assemblers, $ by itself (not followed by hex digits) is the way to get the value of the program counter. So you might see something like

Code: Select all

        ORG   $ + N
which is equivalent to the *=*+N that we had above. Yeah, there's definitely a lack of consistency.
That is very non-standard. None of the assemblers I have ever used in the past 40 years has used the dollar sign to represent the program counter. I would find that usage to be counter-intuitive.
Quote:
Although the following is in the interrupts primer, including with an illustration, I should comment again that the RS-232 buffer as I'm using it there is a ring, a circle...It's kind of like a gas tank which does not need to be emptied before you put more gas in...
This is more directed to Dan, since Garth already knows this.

In assembly language, it's helpful to think of the structure to which Garth is referring as a FIFO (first-in, first-out), not a buffer. In academic terms, a buffer is a temporary storage structure in which the structure's logical beginning and end are the same as its physical beginning and end. You would use a buffer for collecting typed input, storing a disk block, etc.

On the other hand, a FIFO's logical beginning and end is constantly moving—hence the FIFO is "circular." An index is maintained somewhere to tell the program using the FIFO where the logical beginning is located within the FIFO's boundaries. The same index implies the location of the logical end, which is one less than the beginning. Typically that index is zero-based and is added to the starting address of the FIFO to get the actual address of the logical beginning. As you develop your TIA-232 driver routines you will get very up close and personal with FIFOs.

A stack, whether hardware or software, is a LIFO (last-in, first-out) structure and as is the case with the FIFO, the logical beginning and end constantly moves—making a FIFO "circular." Considering a hardware stack as in the 6502 family, each push to the stack decrements the stack pointer (SP) after the byte has been written to the stack (SP is postdecremented). If a push occurs while SP is $00 the pushed byte will be stored at $0100 (in the eight bit MPUs) and SP will wrap to $FF. Pulling a byte will increment SP before the byte is read from the stack and if SP is $FF when a byte is pulled SP will wrap to $00. Software stacks work in essentially the same fashion, but the "stack pointer" is (sometimes laboriously) maintained in software.
Quote:
Also, there's no requirement that the incoming data all be in strings, whether counted or null-terminated. It might for example start with a string to tell it that the next 913 bytes are binary program material to put in memory starting at address xyz. After that many bytes have come in, the next bytes might again be a string giving the computer the next instruction.
That's true. However, the term "string" most often means "character string," since such data are usually treated as a single entity, e.g., someone's name in a database. What Garth is referring to is commonly referred to in systems software engineering as a "stream," which means it is a series of bytes that individually stand on their own. An example of a stream would be a Motorola S-record, which represents binary values in a (more or less) human-readable format.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
User avatar
GARTHWILSON
Forum Moderator
Posts: 8775
Joined: 30 Aug 2002
Location: Southern California
Contact:

Re: Buffers, FIFOs & LIFOs

Post by GARTHWILSON »

BigDumbDinosaur wrote:
GARTHWILSON wrote:
Ok, just to add another one, hopefully ahead of time so it won't mess you up further when you see it: with some assemblers, $ by itself (not followed by hex digits) is the way to get the value of the program counter. So you might see something like

Code: Select all

        ORG   $ + N
which is equivalent to the *=*+N that we had above. Yeah, there's definitely a lack of consistency.
That is very non-standard. None of the assemblers I have ever used in the past 40 years has used the dollar sign to represent the program counter. I would find that usage to be counter-intuitive.
All three commercial macro assemblers I've used use the $ to return the current value of the program counter:
  • Cross-32 (C32) originally from Universal Cross-Assemblers. I've used it for 65c02 and '816, but it's also good for lots of other processors. I bought it in the mid-1990's for $99.
  • the 2500AD assembler which I've used for 65c02. My employer paid $250 for in about 1987.
  • Microchip's MPASM assembler for PIC microcontrollers.
My Forth macro assembler (which is simple enough that I wrote it in its original version in one evening), since it's in Forth, uses HERE to put the value of the dictionary pointer (ie, the next address available for code or data) on the data stack. That's not a commercial assembler, but I'm sure you would find everyone else's Forth assemblers using HERE also.
Quote:
In assembly language, it's helpful to think of the structure to which Garth is referring as a FIFO (first-in, first-out), not a buffer. [...]
The ring buffer, or circular buffer, is made (in software) to act as a FIFO, but there's no shift-register action moving data from one end to the other; and if you wanted to violate your own software rules, you could indeed dive into the middle or any part you wanted to. https://en.wikipedia.org/wiki/Circular_buffer

Quote:
Quote:
Also, there's no requirement that the incoming data all be in strings, whether counted or null-terminated. It might for example start with a string to tell it that the next 913 bytes are binary program material to put in memory starting at address xyz. After that many bytes have come in, the next bytes might again be a string giving the computer the next instruction.
That's true. However, the term "string" most often means "character string," since such data are usually treated as a single entity, e.g., someone's name in a database. What Garth is referring to is commonly referred to in systems software engineering as a "stream," which means it is a series of bytes that individually stand on their own. An example of a stream would be a Motorola S-record, which represents binary values in a (more or less) human-readable format.
Yes; the 913 bytes in the example would not be a string. Motorola S records and Intel Hex do come in text strings. The 913 bytes of binary program material I gave as an example would not be human-readable, and will undoubtedly include 00 bytes, and $0D bytes (which is <CR> in ASCII), and $0A bytes (which is <LF> in ASCII), which would be confused for terminators if it were taken as a string. I have a description of how Intel Hex files work at http://wilsonminesco.com/16bitMathTables/IntelHex.html, and an example of one is at http://wilsonminesco.com/16bitMathTables/ATAN.HEX (although it's an arctangent table, not code).
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?
Post Reply