Learning to write 6502 was a formative experience for me as a programmer and I strongly recommend it as a pedagogical exercise if nothing else. The instruction set is tiny, and easy to grapple with, but after learning it you'll get an intuitive grasp of, for instance, "why the C programming language is the way it is."
So all that remains is to pick a platform! And there's no shortage of them. Glad to see Lynx is yet another participant in the vibrant 6502 homebrew community.
> why the C programming language is the way it is.
I’m curious specifically why you think this is. The 6502 with its poor register set and special purpose addressing mode seems to make it a relatively terrible/challenging C target.
Not OP, but I learned assembly on the 6502, and its simplicity make it easy to create a mental model of the internals of a CPU - these relate very well with a low level language like c, even if today's CPUs are so much more advanced (and difficult to study).
Perhaps a better target might be the 68000, then? Still a relatively simple architecture, but one that more closely resembles a modern system -- multiple general-purpose registers, and ones which can be used as pointers without weird workarounds. (The 8-bit data / 16-bit address nature of the 6502 makes pointers complicated.)
Could be. Later I also worked with machine code on the 68K and it stuck to me less than the 6502, but it might just be because I felt that more like a chore than when I was dabbling with the 6502 (which I learned when I was just a kid).
>The instruction set is tiny, and easy to grapple with, but after learning it you'll get an intuitive grasp of, for instance, "why the C programming language is the way it is."
If you want that, you should learn the PDP-11 instruction set, which is actually why the C language is the way it is. In particular, C is rather uniquely unsuited for segmented memory models, which many 6502 systems have, whereas other languages with lower-level features or accommodations can easily function under such a memory model, because they were designed with it in mind instead of solely for the PDP-11.
On that note, C isn't actually a low-level language and I'm aware the 6502 is an example of an instruction set it maps very poorly to, compared to how a machine code programmer would approach it, so I'm curious why you made this relationship, since it's rather tenuous.
> In particular, C is rather uniquely unsuited for segmented memory models
C was often used to program 16-bit x86 systems, and it handled that well enough.
A more relevant limitation of the 6502, however, is its stack. The 6502 stack is limited to a single page of 256 bytes, and there is no stack-relative addressing mode. This makes a C-style stack difficult to implement -- most C implementations for the 6502 implement a secondary data stack in main memory.
A good compiler would focus optimization efforts on removing variables from the stack. It would probably just reject programs that get silly with deep recursion.
For example, if the compiler can prove that a function never gets more than one active stack frame (no recursion) then the local variables don't need to be on a stack. They can be in the data section. If the compiler can prove this for two functions together, such that they will never both have active stack frames, then the variables can reside in the same memory. Going beyond this, partial sharing is possible. If function X calls function Y, the variables of function Y can reside in the locations of many of the variables of function X. It is only the variables of function X that must survive the call to Y that would need distinct memory locations.
Inlining can help too. That gets rid of the need to push a return address and so on.
I'm not sure that's a valid set of optimizations. It's certainly incompatible with a compile-then-link workflow involving multiple object files, as there's no way to safely allocate local variables across modules. Nor is this approach compatible with function pointers!
I know that moving stack variables to the data section is in fact an optimization that is in use by commercial compilers. It works fine with function pointers. There are two possible answers to the problem of a compile-then-link workflow involving multiple object files.
The first is that the optimization can be limited to within one file. Calls going between source files can sometimes limit the optimization opportunity because it may become impossible to prove that there is no mutual recursion. Functions that may be involved in recursion can not be optimized in this way. One might add a #pragma to override the compiler's determination of safety.
The second possible answer is to have the link phase do the real compiling. The supposed compile phase just preprocesses and verifies syntax.
Function pointers certainly liven things up. Suppose the function pointer of type *X is used to call a function of type X, which then exclusively calls functions of type Y. If we know that none of those type Y functions end up calling any type X functions, then that first-mentioned function of type X will not get more than one stack frame. It is thus safe to optimize with that assumption.
Of course, you can pick the system/context you want to learn in: many 8-bit computers used 6502 chips. For example, this book is well-regarded for those with Apple II nostalgia: https://archive.org/details/AssemblyLinesCompleteWagner
I'm sure there are tons of NES-targeted tutorials too.
I'd recommend the BBC Micro, since we're not talking about the 1980s and geographical limitations of the market, the BBC has a nice basic that includes a built-in assembler, has a text UI that is more pleasant on the eyes than most 8bits, has the sideways rom system to make it possible to add a debugger and other niceties, and has a simple easy-to-understand disk filing system.
6502 itself is rather simple, I wasn't so much interested in game programming as hardware programming, working with the TIA and PIA are the interesting bits to me.
The closest modern equivalent to those old 8-bit computers is the TI-84 Plus CE calculator. It has a legit (no hacks) way to load binary executables. It goes for about $100. Like many old 8-bit computers, it comes with BASIC and an almost-ASCII character set. The screen is 320x240, just a tad bigger than those old 8-bit home computers usually had, though with 16-bit color. There is a USB port. You get a bit more RAM, with about 64 KiB of about 256 KiB available for your program. The screen is at a fixed address you can write to.
The C compiler is fascinating. There is a uint24_t which maps to an unsigned int. This is because the EZ80 CPU is capable of ganging three 8-bit registers together to handle 24-bit data. Pointers are also 24-bit. The "short" and "long" types are of normal size, 16-bit and 32-bit. There is no alignment or padding. Serious stuff needs to be in assembly. The C compiler doesn't optimize very well.
So that is 3 programming languages on a $100 device that happens to be useful for all sorts of normal school classes. The calculator is fine for the SAT, ACT, AP Statistics, AP Calculus, and now even the AP sciences.
I remember having a printout of the tasm tables for x80 so I could type hex machine-code into my TI-83 (it could load hex programs as binaries). I don't recommend that approach, but I certainly learned a lot.
It had a 16-bit graphics processor, which arguably was a lot more important for its software than whether the CPU was 16-bit.
See also the TurboGrafx-16 console, which had a similar arrangement: an 8-bit 6502-descendent and a 16-bit graphics chip. It got a lot of flak for being “fake 16-bit”. I think the Lynx was better able to get away with it because it was on such a different level than other handhelds of the time (besides maybe the expensive portable version of the TG-16!), whereas the TG-16 had to go up against the 68000-based Sega Genesis.
Alternative reasoning would say modern x86 computers are 128-bit (SSE, most memory buses are this wide), 256-bit (AVX, some memory buses) or 512-bit (AVX-512).
The thing is, you can't really use any single factor to determine CPU bitness. Not memory bus width, ALU width, register width, physical address width, etc.
We usually settle with the size of an integer register, which, in modern x86's, is 64 bits. In the case of the 6502, it'd be 8. Susy, the coprocessor used for the Lynx, was not the CPU running the show and I don't think it was able to run its own standalone programs (that claim I need to check).
Learning to write 6502 was a formative experience for me as a programmer and I strongly recommend it as a pedagogical exercise if nothing else. The instruction set is tiny, and easy to grapple with, but after learning it you'll get an intuitive grasp of, for instance, "why the C programming language is the way it is."
So all that remains is to pick a platform! And there's no shortage of them. Glad to see Lynx is yet another participant in the vibrant 6502 homebrew community.
> why the C programming language is the way it is.
I’m curious specifically why you think this is. The 6502 with its poor register set and special purpose addressing mode seems to make it a relatively terrible/challenging C target.
Not OP, but I learned assembly on the 6502, and its simplicity make it easy to create a mental model of the internals of a CPU - these relate very well with a low level language like c, even if today's CPUs are so much more advanced (and difficult to study).
OP here---you nailed it. Pointers and pointers-to-pointers can seem pretty abstract unless you work one level closer to the metal for a bit.
Perhaps a better target might be the 68000, then? Still a relatively simple architecture, but one that more closely resembles a modern system -- multiple general-purpose registers, and ones which can be used as pointers without weird workarounds. (The 8-bit data / 16-bit address nature of the 6502 makes pointers complicated.)
Could be. Later I also worked with machine code on the 68K and it stuck to me less than the 6502, but it might just be because I felt that more like a chore than when I was dabbling with the 6502 (which I learned when I was just a kid).
>The instruction set is tiny, and easy to grapple with, but after learning it you'll get an intuitive grasp of, for instance, "why the C programming language is the way it is."
If you want that, you should learn the PDP-11 instruction set, which is actually why the C language is the way it is. In particular, C is rather uniquely unsuited for segmented memory models, which many 6502 systems have, whereas other languages with lower-level features or accommodations can easily function under such a memory model, because they were designed with it in mind instead of solely for the PDP-11.
On that note, C isn't actually a low-level language and I'm aware the 6502 is an example of an instruction set it maps very poorly to, compared to how a machine code programmer would approach it, so I'm curious why you made this relationship, since it's rather tenuous.
> In particular, C is rather uniquely unsuited for segmented memory models
C was often used to program 16-bit x86 systems, and it handled that well enough.
A more relevant limitation of the 6502, however, is its stack. The 6502 stack is limited to a single page of 256 bytes, and there is no stack-relative addressing mode. This makes a C-style stack difficult to implement -- most C implementations for the 6502 implement a secondary data stack in main memory.
A good compiler would focus optimization efforts on removing variables from the stack. It would probably just reject programs that get silly with deep recursion.
For example, if the compiler can prove that a function never gets more than one active stack frame (no recursion) then the local variables don't need to be on a stack. They can be in the data section. If the compiler can prove this for two functions together, such that they will never both have active stack frames, then the variables can reside in the same memory. Going beyond this, partial sharing is possible. If function X calls function Y, the variables of function Y can reside in the locations of many of the variables of function X. It is only the variables of function X that must survive the call to Y that would need distinct memory locations.
Inlining can help too. That gets rid of the need to push a return address and so on.
I'm not sure that's a valid set of optimizations. It's certainly incompatible with a compile-then-link workflow involving multiple object files, as there's no way to safely allocate local variables across modules. Nor is this approach compatible with function pointers!
I know that moving stack variables to the data section is in fact an optimization that is in use by commercial compilers. It works fine with function pointers. There are two possible answers to the problem of a compile-then-link workflow involving multiple object files.
The first is that the optimization can be limited to within one file. Calls going between source files can sometimes limit the optimization opportunity because it may become impossible to prove that there is no mutual recursion. Functions that may be involved in recursion can not be optimized in this way. One might add a #pragma to override the compiler's determination of safety.
The second possible answer is to have the link phase do the real compiling. The supposed compile phase just preprocesses and verifies syntax.
Function pointers certainly liven things up. Suppose the function pointer of type *X is used to call a function of type X, which then exclusively calls functions of type Y. If we know that none of those type Y functions end up calling any type X functions, then that first-mentioned function of type X will not get more than one stack frame. It is thus safe to optimize with that assumption.
Feel free to abstract it to "how programming languages interface with memory."
Here's an in-browser 6502 tutorial with simulator: https://skilldrick.github.io/easy6502/
Of course, you can pick the system/context you want to learn in: many 8-bit computers used 6502 chips. For example, this book is well-regarded for those with Apple II nostalgia: https://archive.org/details/AssemblyLinesCompleteWagner
I'm sure there are tons of NES-targeted tutorials too.
Anybody have good NES tutorials and also a link for a way to put it on a cart (sd or whatever)?
Also, are their free or paid-for asset libraries?
Thanks in advance!
nesdev.com is a treasure trove of information.
To put it on a cart, you'll use RetroUSB PowerPak or Everdrive N8.
Don't know about assets.
Googling [nes 6502 tutorial] pulled up a good selection of articles.
Atari 2600 programming is 6502 too, you can play with it right in the browser: http://8bitworkshop.com/v3.3.0/?=&file=examples%2Fcomplexsce...
My suggestion: anybody thinking of getting started with 6502 will probably have more fun with the Atari Lynx.
Or the C64 might be a good choice too.
Jumping into "no-framebuffer/racing-the-beam" 6502 programming on the Atari 2600 for the uninitiated is just ... cruel.
I'd recommend the BBC Micro, since we're not talking about the 1980s and geographical limitations of the market, the BBC has a nice basic that includes a built-in assembler, has a text UI that is more pleasant on the eyes than most 8bits, has the sideways rom system to make it possible to add a debugger and other niceties, and has a simple easy-to-understand disk filing system.
6502 itself is rather simple, I wasn't so much interested in game programming as hardware programming, working with the TIA and PIA are the interesting bits to me.
The closest modern equivalent to those old 8-bit computers is the TI-84 Plus CE calculator. It has a legit (no hacks) way to load binary executables. It goes for about $100. Like many old 8-bit computers, it comes with BASIC and an almost-ASCII character set. The screen is 320x240, just a tad bigger than those old 8-bit home computers usually had, though with 16-bit color. There is a USB port. You get a bit more RAM, with about 64 KiB of about 256 KiB available for your program. The screen is at a fixed address you can write to.
The C compiler is fascinating. There is a uint24_t which maps to an unsigned int. This is because the EZ80 CPU is capable of ganging three 8-bit registers together to handle 24-bit data. Pointers are also 24-bit. The "short" and "long" types are of normal size, 16-bit and 32-bit. There is no alignment or padding. Serious stuff needs to be in assembly. The C compiler doesn't optimize very well.
So that is 3 programming languages on a $100 device that happens to be useful for all sorts of normal school classes. The calculator is fine for the SAT, ACT, AP Statistics, AP Calculus, and now even the AP sciences.
I remember having a printout of the tasm tables for x80 so I could type hex machine-code into my TI-83 (it could load hex programs as binaries). I don't recommend that approach, but I certainly learned a lot.
I understand the font choice, but you might want to reconsider. It's not easy to read.
There is a typo in the chapter "Important commands that don't exist!".
NEG is described as:
Of course, it's: or:There is an INC A instruction on the 65C02 and 65816, so this is better if you have anything newer than the NMOS 6502:
It looks like the Lynx has a 65SC02 which is a 65C02 without the bit manipulation instructions, so it should have the INC A instruction.Another one: DEC (dec de)
Should be:I wonder why people say it's a 16-bit console if it's based on the 65SC02. The 65816 and 802 were 16 bit, but the SC02, AFAIK, was not.
It had a 16-bit graphics processor, which arguably was a lot more important for its software than whether the CPU was 16-bit.
See also the TurboGrafx-16 console, which had a similar arrangement: an 8-bit 6502-descendent and a 16-bit graphics chip. It got a lot of flak for being “fake 16-bit”. I think the Lynx was better able to get away with it because it was on such a different level than other handhelds of the time (besides maybe the expensive portable version of the TG-16!), whereas the TG-16 had to go up against the 68000-based Sega Genesis.
So I guess I'll say my Chromebook has 2 + 160 + 20 + 2 or about 184 cores. I'm counting the integrated GPU, of course.
The 6502 could perform 8-bit operations, but had a 16-bit address space. Hence the C64 (= 2^16)
By that reasoning, the Apple II is a 16-bit computer and the original Mac a 24-bit one.
And by that same reasoning, modern x86 computers are either 48-bit or 52-bit, depending on which limit you consider.
Alternative reasoning would say modern x86 computers are 128-bit (SSE, most memory buses are this wide), 256-bit (AVX, some memory buses) or 512-bit (AVX-512).
The thing is, you can't really use any single factor to determine CPU bitness. Not memory bus width, ALU width, register width, physical address width, etc.
We usually settle with the size of an integer register, which, in modern x86's, is 64 bits. In the case of the 6502, it'd be 8. Susy, the coprocessor used for the Lynx, was not the CPU running the show and I don't think it was able to run its own standalone programs (that claim I need to check).
Wow. I bought a Lynx with my first paycheck from my first “real” job when I was 15. I played California Games a lot.
Nice!
Another good resource is forum.6502.org as there are plenty of 6502 experts there that have built their own 6502 machines for a hobby
Including one made out of discrete components. https://monster6502.com/