Why does the 6502 JSR instruction only increment the return address by 2 bytes?
Simply because the PC is already pushed before the second address byte is read. That way, the CPU need only buffer the lower target address byte and later read the higher one directly into the PC.
The workings are described in great detail in the original 1976 MCS 6500 Family Programming Manual (*1) in section 8.1 JSR - Jump to Subroutine on p.106..109. Same for how RTS resolves this in 8.2 RTS - Return from Subroutine (p.109..112)
The six clock cycles of a JSR are essentially:
- Read Opcode ($20); Increment PC
- Read ADL; Increment PC
- Buffer ADL
- Push PCH; Decrement S
- Push PCL; Decrement S;
- Read ADH;
And interleaved with the next instruction:
- Load PC with ADH/ADL; Fetch next OP with new PC
Step 3 is another result of making the 6502 as small as possible
It seems the RTS pops the value from the stack and increments it again before setting the PC to the corrected value.
Yes, it does so during the last cycle of an RTS. In fact, in doing so, it rereads the last byte of the JSR instruction again (and discards it).
My question is: Why?
The main reason is to save circuitry. The way it operates, it avoids the need to buffer the upper address byte. Otherwise it would have needed a whole additional 8 bit register to hold that value. The 6502 has only 16 registers total. Adding one more would be a considerable cost.
It's worth keeping in mind that the main success criteria for the 6502 wasn't its inherent beauty or the friendly smile of its developers. It was being dirt cheap. Not just a few percent cheaper, but up to ten times lower than its competition. Woz selected it for exactly that reason for the Apple II and so did Atari and others. It's also why Commodore bought MOS, it's well known how cost-aware Tramiel was ;)
Having to add a whole register just for a single function is, in that context, a no-go, especially if there's a way to do it in microcode.
Why not just let JSR increment the PC by 3 instead of 2, and let RTS just pop and jump?
It has to be incremented anyway, so no real gain here.
This looks like a far more logical approach.
In what logic? Maybe in CS class ivory tower logic, but real hardware is about implementing a concept in best possible fashion, not 'as in the books'. The important thing is the function provided, which is the same in either case.
The resulting effect is that any function that uses the pushed address, such as for accessing parameters, will need to increment it (or use an offset of one). This is in line with the general 6500 philosophy to spend as little hardware as necessary to provide a function, and let everything else be done by software.
Quite RISC-like, isn't it?
*1 - Always a great first read, together with the MCS6500 Family Hardware Manual.