Jump to content
IGNORED

Session 17: Asymmetrical Playfields - Part 1


Recommended Posts

By now you should be familiar with how the '2600 playfield works. In summary, there are three playfield registers (PF0, PF1, PF2) and these hold 20 bits of playfield data. The '2600 displays this data twice on every scanline, and you can have the second half mirrored, if you wish. Playfield is a single-colour, but each half of the screen may be set to use the colours of the players (more about those, later!). In short, though, we have a fairly versatile system just great for PONG-style games.

 

Pretty soon, though, programmers started doing much more sophisticated things with the TIA - and especially with the playfield registers - than just displaying symmetrical (or mirrored) playfields.

 

Since writes to TIA immediately change the internal 'state' of the TIA, and since the TIA and 6502 work in tandem during the display of a TIA frame, there's no reason why the 6502 can't modify things on-the-fly in the middle of scanlines. For example, any write to playfield registers will IMMEDIATELY reflect in changes to the data that the TIA is sending for a particular scanline. I qualify this slightly by my non-knowledge if these immediate changes are on a per-pixel basis, or on a per-byte basis. Something for us all to play with!

 

In any case, as will probably have become obvious to you by now, it is possible to display different 'shape' on the left and right of any scanline. As stated, if we left the TIA alone then it would display the same (or a mirrored version) data on the left and right halves of the screen - coming from its 20 pixel playfield data. But if we modify any of the playfield registers on-the-fly (that is, mid-scanline) then we will see the results of that modification straight away when the TIA draws the rest of the scanline.

 

Let's revisit briefly our understanding of the TIA and frame timing. Please refer to the earlier sessions where the timing of the TIA and 6502 were covered. In summary, there are exactly 228 colour-clocks of TIA 'time' on any scanline - 160 of those clocks are actual visible pixels on the screen and 68 of them are the time it takes for the horizontal retrace to occur.

 

Our 'zero point' of any scanline is the beginning of horizontal retrace. This is the point at which the TIA re-enables the 6502 if it has been halted by a WSYNC write. At the beginning of any scanline, then, we know that we have exactly 68 colour clocks (=68/3 = 22.667 cycles) before the TIA starts 'drawing' the line itself.

 

You should already be familiar with the horizontal resolution of '2600 playfield - exactly 40 pixels per scanline. I use the term 'pixels' interchangeably here - to mean a minimum unit of graphic resolution. For the playfield, there are 40 pixels a line. But the TIA has 160 colour-clocks per line, and in fact sprite resolution is also 160 pixels per line. Another way of looking at this is that each playfield pixel is 4 colour-clocks wide, and each sprite pixel is 1 colour clock wide (as a minimum, anyway - this can be adjusted to give double-wide and quadruple-wide sprites. We'll get to sprites soon, I promise!)

 

It's quite important to understand the timing of things. Let's delve a bit more deeply into the synchronisation between the 6502 and the TIA, and have a close look at when/where each pixel of the playfield is actually being drawn.

 

As stated above, the first 68 cycles of each scanline are the horizontal retrace period. So the very first pixel of playfield (which is 4 colour-clocks wide, remember!) starts drawing on TIA cycle 68 (of 228 in the line). So if we want that pixel to be the right 'shape' (ie: on or off, as the case may be) then we really have to make sure we have the right data in the right bit of PF0 before cycle 68.

 

Likewise, we should really make sure that the second pixel has its correct data set before cycle 72 (68 + 4 colour clocks). In fact, you should now understand that the 4 playfield pixels at the left of the scanline occupy TIA colour clocks (68-71) (72-75) (76-79) and (80-83). The very first pixel of PF1, then, starts displaying at clock 84. So we need to make sure that data for PF1 is written before TIA clock 84. And so it goes, we should make sure that PF2 data is written to PF2 before the TIA starts displaying PF2 pixels. And that happens on clock (84 + 8 * 4 = 116)

 

Finally, we can now see that PF2 will take 32 colour clocks (because it's 8 pixels, at 4 clocks each). As it starts on TIA clock 116, it will end on clock 147. The obvious calculation is 147 (end) - 68 (start) = 80 colour clocks. Which nicely corresponds to 20 pixels at 4 colour clocks each. OK, that's straightforward, but you should now follow exactly the correspondence between TIA colour clocks and the start of display of particular pixels on any scanline.

 

Now, what happens at colour clock 148? The TIA starts displaying the second half of the playfield for the scanline in question, of course! Depending on if the playfield is mirrored or not, we will start seeing data from PF2 (mirrored) or from PF0 (non-mirroed).

 

Now, and here's the really neat bit - and the whole trick behind 'asymmetric' playfields - we know that if we rewrite the TIA playfield data AFTER it has been displayed on the left half of the scanline, but BEFORE it is displayed on the right half of the scanline, then the TIA will display different data on the left and right side of the screen.

 

In particular, this method tends to use a non-mirrored playfield. We noted that PF0 finished displaying its 4 pixels on colour clock 83 (inclusive). So from colour clock 84 onwards (up to 148, in fact), we may freely write new data to PF0 and we won't bugger anything currently being displayed. That's 60 colour clocks of time available to us.

 

Time to revisit the timing relationship between the 6502 and the TIA. The TIA has 228 colour clocks per scanline, but the 6502 speed is derived from the TIA clock through a divide-by-three. So the 6502 has only 76 cycles (228/3) per scanline. So if there are 60 colour clocks of time available to change PF0, that corresponds to 60/3 = 20 cycles of 6502 time. Further conversions between TIA time and 6502 cycles show us that it must start after TIA cycle 84 (= 84/3) = 6502 cycle 28, and it must end before TIA cycle 148 (6502 cycle 148/3 = 49.3333). Aha! How can we have a non-integer cycle? We can't, of course. All this tells us is that it is IMPOSSIBLE to exactly change data on TIA colour clock 148. We can change TIA data on any divisible-by-three cycle number, since the 6502 is working in tandem with the TIA but only gets a look-in every 3 cycles.

 

This inability to exactly time things isn't a problem for us now, as we have already noted that there are 60 TIA colour clocks in which we can effect our change for PF0.

 

PF1 and PF2 operate in exactly the same fashion. PF1 is displayed from clocks 84-115 and on the right-side from clock 164 onwards (remember the right-side starts at clock 148, PF0 takes 16 colour-clocks (4 pixels at 4 colour-clocks each). So to modify PF1 so it displays different right-side and left-side visuals, we need to modify it between colour clock 116 and 164. That gives us a narrower window of time in which we can make our modification - just 48 colour clocks. But still, we can do that, right?

 

Finally, PF2 is displayed from clock 116-147 (let's check, that's 32 colour clocks inclusive - 32 = 8 pixels x 4 clocks per pixel. Yep!). And on the right-side of the scanline, PF2 will display from clock 164 + 32 = 196 to clock 227. 227 - 196 = exactly 32 colour clocks. Voila! So the window of opportunity for PF2, so to speak, is from colour clock 148 to 195 inclusive. That's another 48 clocks.

 

So to summarise the timing for writing the right-hand-side PF register updates, we can safely modify PF0 from clocks 84 - 147, PF1 from clocks 116 - 163 inclusive, and PF2 from 148 - 195 inclusive. NOte the overlap on these times. We could safely modify PF1 on (say) cycle 116, and then modify PF0 on cycle 130, and finally modify PF2 on cycle 190. The point being, it's not the ORDER of the modifications to the playfield registers than count - it's the TIMING that counts. As long as we modify the registers in the period when the TIA isn't drawing them, we won't see glitches on the screen.

 

Well, now you have all the information you need to generate an asymmetrical playfield. But there's one thing you need to remember - once you write data to the TIA, the TIA retains that 'state', or the data that you last wrote. So if you want an asymmetrical playfield, you not only have to write the new data for the right-half of the scanline, you have to write the right data for the left side of the NEXT scanline!

 

In fact, we already covered that. As long as PF0 is written before cycle 68 then it will display OK on the left.... etc. So a typical asymmetrical playfield kernel will be writing 6 playfield writes (two to PF0, two to PF1, two to PF2) on each and every scanline. As you can imagine, you don't get a lot of change out of just 76 cycles of 6502 time per scanline, when as a minimum a load/store is going to cost you 5 cycles of time - and in most cases more like 6 or 7. That can equate to 40 or more cycles of your 76, JUST drawing the playfield data. Ouch!

 

Rather than give you a code sample this session, I'd like you to grab the last playfield code and convert it to display an asymmetrical playfield. Doesn't have to be fancy - just demonstrate a consistent change between left and right halves of the screen, writing PF0, PF1 and PF2 twice each on each scanline. Once you've mastered this concept you can truly say you're on the way to programming a '2600 game!

 

Please post your asymmetrical playfield code (just the scanline section, thanks) here, and we can all have a look at how you are doing it.

 

See you next time!

 

 

Text in bold contains additions and corrections made since the original post.

  • Thanks 1
Link to comment
Share on other sites

I haven't done the math, but this seems to work. :D

 

Does the Sleep macro correspond to Machine cycles ?

 


               ldx #192

Picture         stx COLUPF

               sleep 5

               lda #%00000000

               sta PF0

               sleep 10

               lda #%11110111

               sta PF1

               sleep 10

               lda #%00011111

               sta PF2

               sleep 5

               lda #%00010000

               sta PF0

               lda #%11001100

               sta PF1

               sta PF2

               dex

               sta WSYNC

               bne Picture

Link to comment
Share on other sites

To develop asymetrical playfields, I set up a simple spreadsheet, with each row representing one CPU cycle. I can then put cycle numbers in one column, PF changes in the second, and the instructions in another.

 

My recommendation is to make sure the last CPU cycle of a PF update is either before or after the PF is active. Tighter timing may be possible, but would need to be tested on actual hardware.

Link to comment
Share on other sites

Does the Sleep macro correspond to Machine cycles ?

 

As Thomas said, "Yup. :idea: "

 

 

Now, here's the long version...

 

The sole purpose of the SLEEP macro is to waste a specified number of machine cycles in a "safe" manner. Wasting machine cycles via the SLEEP macro essentially boils down to executing NOP instruction(s). A NOP, by design, is "safe" because executing it does not change the value of any CPU registers, status flags or memory. The only thing that has changed is that the CPU has burned away two machine cycles for every NOP it executed while tending to the busywork of doing "absolutely nothing."

 

The NOP instruction is just perfect for burning away machine cycles in multiples of two. Insert a NOP into your code and you've just wasted two machine cycles. Insert two NOP's and you've wasted (slept for) four. This is exactly what the SLEEP macro does when you specify an even number of wasted machine cycles. "SLEEP 2" instructs the assembler to insert a single NOP into your code to burn away 2 machine cycles. "SLEEP 10" instructs assembler to insert five NOP's, and so on.

 

But what if you wanted to waste an odd number of machine cycles? There's no such thing as a half a NOP. What's needed is a different time-wasting instruction, similar to NOP, that burns off a single machine cycle. Sadly, no such instruction exists. Everything the 6507 CPU does (or can be tricked into doing) takes a minimum of two machine cycles. Therefore, a "SLEEP 1" can never be achieved. All is not lost with regard to odd numbers of SLEEP times, however. All that is needed is to find and use a three-cycle instruction that performs the same as the standard two-cycle NOP in order to do a "SLEEP 3." If such an instruction can be found, then all odd sleep times greater than 1 can be satisfied by using one "three-cycle NOP" and then by wasting the rest of the time with an appropriate number of standard two-cycle NOP instructions.

 

 

The Quest for the Three-cycle NOP

 

It doesn't exist. At least, no three-cycle NOP instruction was ever documented or legitimized by the people who gave us the 6507. The documented set of machine instructions for the 6507 is an itemized list of engineer-tested opcodes (i.e. "legal instructions"), along with their corresponding assembler mneumonics and details about the parameters they take, flags they effect, etc. The opcodes associated with each documented machine instruction range in value from $00 to $FF (all possible values you can stick into a single byte), but they do not include every value in this range. Why not? Because the engineers simply did not have the time or desire to test every possible opcode. Some of the undocumented opcodes (the so-called "illegal opcodes"), however, serendipitously do useful and predictable things. In particular, opcode $04 performs a three-cycle NOP!

 

There's no reason to get too hung up on using the three-cycle NOP simply because it is a "illegal opcode." If you use it, your CPU won't crash, and you won't be subject to fines, imprisonment or the scorn of your fellow programmers. The three-cycle NOP works flawlessly. It is PERFECT in every respect, other than its behavior was not officially documented when the microprocessor was introduced. Pitty.

 

The three-cycle NOP differs from the standard two-cycle NOP in that the three-cycle flavor consumes two bytes of code because it requires a one-byte parameter, whereas the standard two-cycle NOP requires no parameter and takes only a single byte of code. It doesn't really matter what the follow-on parameter for the three-cycle NOP is, but it's gotta be there. Otherwise, the CPU will gobble up the next byte of code after the $04 (probably your next carefully laid instruction) and use THAT as it's expected parameter. For the sake of standardization, everyone is encouraged to use a $00 as the parameter to the three-cycle NOP. Hence, the byte pair of ($04, $00) is assembled into your code whenever you use the three-cycle NOP. The byte pair of ($04, $01) should do the exact same thing, but why make things overly complex?

 

 

Some Assembly Is Required

 

Getting a three-cycle NOP into your code via an assembler requires a couple of things.

 

1. You need to use a mneumonic which an assembler can read and recognize as the three-cycle NOP and which can be differentiated from the standard two-cycle NOP.

 

2. You need an assembler version which accepts the three-cycle NOP mneumonic and inserts the byte pair ($04,$00) into your code when it sees it.

 

Beginning with DASM version 2.20.01, you can specify the three-cycle NOP as follows:

 

nop 0

 

The DASM assembler will recognize that as the three-cycle NOP (with parameter $00) and insert the byte pair ($04,$00) into your code. Prior versions of DASM will interpret the command as an illegal addressing mode (because a two-cycle NOP isn't supposed to get a parameter).

 

 

Wasting Machine Cycles with the SLEEP macro

 

Okay now. You've got DASM 2.20.xx. And you have an assembly code source file which includes MACRO.H (the include file which defines the SLEEP macro). Now, if you wanted to burn 6 machine cycles, you could do this:

 

nop; 2

nop; 2 

nop; 2

 

You could accomplish the exact same thing with this:

 

SLEEP 6

 

The SLEEP macro inserts the exact same code into your assembled output as in the previous method.

 

 

If you wanted to burn 7 machine cycles, you could do this:

 

nop 0; 3

nop; 2 

nop; 2

 

Or you could do this:

 

SLEEP 7

 

which, once again, inserts the exact same code (one three-cycle NOP and two 2-cycle NOP's) as in the previous method.

 

That, in a nutshell, is what Thomas' SLEEP macro does and how it works. Have I put you to SLEEP yet?

 

 

Ben

Link to comment
Share on other sites

Using 2 tables for left and right data, each table contains 8 bytes of data

 


DrawField       

               nop

               ldx #0                  

               ldy #0



INNER           lda PFLeftData,Y

               sta PF0

               lda PFLeftData,Y

               sta PF1

               lda PFLeftData,Y

               sta PF2

               lda PFRightData,Y

               sta PF0

               SLEEP 10

               lda PFRightData,Y

               sta PF1

               lda PFRightData,Y

               sta PF2

               inx

               iny

               cpy #8  ;Eight bytes of data in each table

               bne Cont

               ldy #0  ;Reset y data counter

               

Cont            sta WSYNC             

               cpx #192

               bne INNER

Link to comment
Share on other sites


StartOfFrame

   

  ; Start of new frame

  ; Start of vertical blank processing

   

               lda #%00000000

               sta CTRLPF    ; copy playfield



               

               lda #$AE

               sta COLUBK   ; set the background color (sky)





               lda #0

               sta VBLANK

   

               lda #2

               sta VSYNC

   

               sta WSYNC

               sta WSYNC

               sta WSYNC   ; 3 scanlines of VSYNC signal

   

               lda #0

               sta VSYNC



      ;------------------------------------------------------------------

      ; 37 scanlines of vertical blank...

           

               ldx #0

VerticalBlank

               sta WSYNC

               inx

               cpx #37

               bne VerticalBlank

           

      ;------------------------------------------------------------------

      ; Do 192 scanlines of color-changing (our picture)

       

               ldx #0   ; this counts our scanline number
;--------------------------------------------------------------------------



TopSkyLines

               sta WSYNC

               lda #$0A

               sta COLUPF    ; set playfield color (cloud)

               

               lda #%11100000

               sta PF0       ; write graphics for PF0(a) - cloud

               lda #%11000000

               sta PF1       ; write graphics for PF1(a) - cloud

               lda #0

               sta PF2       ; zero out graphics for PF2(a)



               SLEEP 12



               lda #0

               sta PF0

               

               SLEEP 8



               lda #0

               sta PF1

               lda #$1E

               sta COLUPF   ; set playfield color (sun)

                               

               nop

               lda #%01100000

               sta PF2

               

               inx

               cpx #4          ; are we at line 4?

               bne TopSkyLines   ; No, so do another




;--------------------------------------------------------------------------                    



MidSkyLines

               sta WSYNC

               lda #$0A

               sta COLUPF    ; set playfield color (cloud)

               

               lda #%11110000

               sta PF0

               lda #%11100000

               sta PF1

               lda #0

               sta PF2



               SLEEP 12



               lda #0

               sta PF0

               

               SLEEP 8



               lda #0

               sta PF1

               lda #$1E

               sta COLUPF   ; set playfield color (sun)

                               

               nop

               lda #%11110000

               sta PF2

               

               inx

               cpx #14          ; are we at line 14?

               bne MidSkyLines   ; No, so do another




;--------------------------------------------------------------------------

LastSkyLines

               sta WSYNC

               lda #$0A

               sta COLUPF    ; set playfield color (cloud)

               

               lda #%01100000

               sta PF0

               lda #%11000000

               sta PF1

               lda #0

               sta PF2



               SLEEP 12



               lda #0

               sta PF0

               

               SLEEP 8



               lda #0

               sta PF1

               lda #$1E

               sta COLUPF   ; set playfield color (sun)

                               

               nop

               lda #%01100000

               sta PF2

               

               inx

               cpx #18          ; are we at line 18?

               bne LastSkyLines   ; No, so do another

                               
;--------------------------------------------------------------------------

                   

               lda #0   ; PF0 is mirrored <--- direction, low

                                ; 4 bits ignored

               sta PF0

               sta PF1

               sta PF2

                                                   

MiddleLines

               sta WSYNC

               inx

               cpx #140

               bne MiddleLines

                                   

               sta WSYNC

               lda #$C6

               sta COLUPF   ; set playfield color (tree leaves)

               

               SLEEP 50



               lda #%00010000

               sta PF2   ; set graphics for right side leaves

               

               inx


;--------------------------------------------------------------------------



Leaf1stLines

               sta WSYNC

               lda #0

               sta PF2   ; clear left side

               

               SLEEP 50



               lda #%00010000

               sta PF2   ; write graphics for PF2(b) - right side leaves

               

               inx

               cpx #149          ; are we at line 149?

               bne Leaf1stLines   ; No, so do another
;--------------------------------------------------------------------------

Leaf2ndLines

               sta WSYNC

               

               lda #%10000000

               sta PF0       ; write graphics for PF0(a) - left side tree leaves

               lda #0

               sta PF1       ; zero out graphics for PF1(a and b)

               sta PF2       ; zero out graphics for PF2(a)



               SLEEP 18



               lda #0

               sta PF0   ; zero out graphics for PF0(b)

               

               SLEEP 18



               lda #%00111000

               sta PF2   ; write graphics for PF2(b) - right side tree leaves

               

               inx

               cpx #152          ; are we at line 152?

               bne Leaf2ndLines   ; No, so do another


;--------------------------------------------------------------------------



Leaf3rdLines

               sta WSYNC

               

               lda #%11000000

               sta PF0       ; write graphics for PF0(a) - left side tree leaves

               lda #%10000000

               sta PF1       ; write graphics for PF1(a) - left side tree leaves

               lda #0

               sta PF2       ; zero out graphics for PF2(a)



               SLEEP 16



               lda #0

               sta PF0   ; zero out graphics for PF0(b)

               

               SLEEP 8



               lda #0

               sta PF1   ; zero out graphics for PF1(b)

                               

               SLEEP 6



               lda #%00111000

               sta PF2   ; write graphics for PF2(b) - right side tree leaves

               

               inx

               cpx #156          ; are we at line 156?

               bne Leaf3rdLines   ; No, so do another




;--------------------------------------------------------------------------

Leaf4thLines

               sta WSYNC

               

               lda #%11100000

               sta PF0       ; write graphics for PF0(a) - left side tree leaves

               lda #%11000000

               sta PF1       ; write graphics for PF1(a) - left side tree leaves

               lda #0

               sta PF2       ; zero out graphics for PF2(a)



               SLEEP 16



               lda #0

               sta PF0   ; zero out graphics for PF0(b)

               

               SLEEP 8



               lda #0

               sta PF1   ; zero out graphics for PF1(b)

                               

               SLEEP 6



               lda #%00111000

               sta PF2   ; write graphics for PF2(b) - right side tree leaves

               

               inx

               cpx #158          ; are we at line 158?

               bne Leaf4thLines   ; No, so do another




;--------------------------------------------------------------------------

LastLeafLines

               sta WSYNC

               

               lda #%11100000

               sta PF0       ; write graphics for PF0(a) - left side tree leaves

               lda #%11000000

               sta PF1       ; write graphics for PF1(a) - left side tree leaves

               lda #0

               sta PF2       ; zero out graphics for PF2(a)



               SLEEP 16



               lda #0

               sta PF0   ; zero out graphics for PF0(b)

               

               SLEEP 8



               lda #0

               sta PF1   ; zero out graphics for PF1(b)

                               

               SLEEP 6



               lda #%01111100

               sta PF2   ; write graphics for PF2(b) - right side tree leaves

               

               inx

               cpx #164          ; are we at line 164?

               bne LastLeafLines   ; No, so do another




;--------------------------------------------------------------------------



               lda #%00000001

               sta CTRLPF    ; reflect playfield

               lda #0

               sta PF2   ; zero out graphics for PF2(a and b)

               lda #$20

               sta COLUPF    ; set playfield color (tree trunk)

               lda #%10000000

               sta PF0





               

                   
;--------------------------------------------------------------------------



TreeTrunkLines

               sta WSYNC

               

               inx

               cpx #182          ; are we at line 182?

               bne TreeTrunkLines   ; No, so do another


;--------------------------------------------------------------------------

                   

               lda #$DA

               sta COLUPF   ; set playfield color (grass)

               

               lda #$98

               sta COLUBK   ; set background color (water)



               lda #%11111111

               sta PF0   ; fill in graphics for PF0 (a and b)

               sta PF1   ; fill in graphics for PF1 (a and b)

               lda #%00000111

               sta PF2   ; set graphics for first line of pond





               
;--------------------------------------------------------------------------
; Pond Lines Follow: 

               

               sta WSYNC

               inx

               lda #%00001111

               sta PF2   ; set graphics for second line of pond

               

               sta WSYNC

               inx

               lda #%00011111

               sta PF2   ; set graphics for third line of pond

               

               sta WSYNC

               inx

               lda #%11111111

               sta PF2   ; fill in graphics for PF2 (a and b) - grass
;--------------------------------------------------------------------------

               

               

               

Bottom6Lines

               sta WSYNC

               inx

               cpx #192

               bne Bottom6Lines

           

      ;--------------------------------------------------------------------------

           

               lda #%01000010

               sta VBLANK   ; end of screen - enter blanking

           

      ; 30 scanlines of overscan...

       

               ldx #0

Overscan

               sta WSYNC

               inx

               cpx #30

               bne Overscan

           

               jmp StartOfFrame

Link to comment
Share on other sites

Hey, that's gorgeous, Sheldon! A very nice, serene pic.

 

One thing you might want to do is clear out the three playfield registers just before you enter the vertical blanking at the bottom of your code sample (or set the playfield color to black). Otherwise, you're going to get a border that's the same color as your grass at the top of your pic that's three scanlines tall (from the 3 scanlines worth of VSYNC).

 

Something like this will do the trick...

 


Bottom6Lines 

sta WSYNC 

inx 

cpx #192 

bne Bottom6Lines 



; CLEAR THE PLAYFIELD REGISTERS

 LDA #0

 STA PF0

 STA PF1

 STA PF2


;-------------------------------------------------------------------------- 



lda #%01000010 

sta VBLANK; end of screen - enter blanking 


; 30 scanlines of overscan... 





ldx #0 



Overscan 

sta WSYNC 

inx 

cpx #30 

bne Overscan 



jmp StartOfFrame

 

Ben

Link to comment
Share on other sites

Although Pitfall Harry has done an exceptional job explaining how SLEEP 2n+1 and NOP 0 work, I have to disagree with his statement that NOP 0 is merely undocumented through some kind of oversight.

 

Let me explain. Back when the 6502 was designed (although I wasn't there, I do have a BASc in Computer Engineering), the team first came up with a list of instructions / capabilities they wanted their microprocessor to have. This wish list would have been whittled down (or beefed up) depending on the transistor "budget" they had to work with.

 

But one of the important things that was done was to arrange the instructions into patterns so bits (or combinations of bits) in the opcode could be used to select registers, addressing mode and instruction type. This simplification reduced the number of transistors; a very good thing.

 

But this also meant that the effect some of the undefined opcodes could be defined by inference. So opcode $04 is effectively NOP ZP, a two byte instruction requiring three cycles and not changing any registers, including the status register. There are more bizzare opcodes like $A7 (aka LAX ZP), which loads both the A and X registers simultaneously from zero page memory. And some simply crash the processor.

 

However, because these instructions are not documented, the chip maker was under no obligation to ensure they worked, no matter how useful they were. This was particularly true for later revisions or enhancements. It was common for a chip maker, or cloners trying to add value, to add new instructions to later versions by using those "unused" opcodes.

 

Personally, although NOP ZP does waste 3 cycles, there are other documented 2 byte instructions which accomplish the same thing (especially on the 2600), so I will use them instead.

Link to comment
Share on other sites

Although Pitfall Harry has done an exceptional job explaining how SLEEP 2n+1 and NOP 0 work, I have to disagree with his statement that NOP 0 is merely undocumented through some kind of oversight.

 

The reasons I offered for why NOP 0 went undocumented were pure speculation on my part, which is something I should have been more clear about. Thanks for setting the record straight. The facts, as usual, are much more interesting.

 

Ben

Link to comment
Share on other sites

Personally, although NOP ZP does waste 3 cycles, there are other documented 2 byte instructions which accomplish the same thing (especially on the 2600), so I will use them instead.

But all other instructions rely (branches) or change the flags (BIT). This makes using the SLEEP macro a bit complicated and "unsafe". And especially during the design process of a kernel you have enough other problems to solve.

 

And safe options (e.g. JMP) used one more byte.

Link to comment
Share on other sites

Personally, although NOP ZP does waste 3 cycles, there are other documented 2 byte instructions which accomplish the same thing (especially on the 2600), so I will use them instead.

But all other instructions rely (branches) or change the flags (BIT). This makes using the SLEEP macro a bit complicated and "unsafe". And especially during the design process of a kernel you have enough other problems to solve.

 

And safe options (e.g. JMP) used one more byte.

 

 

Note: The SLEEP macro is written to be switchable between use and non-use of 'illegal' opcodes. If you don't want 'em, then you can switch 'em off - just see the macro's documentation/comments.

 

I think it's really a matter of personal preference. If you're not worried about using illegal opcodes, use the SLEEP macro as-is. If you are worried, then just be aware that the sleep macro will change the flags when used.

 

As far as using illegal opcodes for other things... there are some things you just CAN'T DO on a '2600 without them. Interleaved ChronoColour sprites are one of those things.

Link to comment
Share on other sites

Ok, I understand why it wasn't recommended to use the undocumented/illegal opcodes when the machine was still in production. But, correct me if I'm wrong, AFAIK there are no 2600's that have problems with these opcodes, right? And any new hardware/software that can run 2600 programs is going to have to implement these opcodes in the exact same way, right?

 

Personally, I don't see the problem -- unless there are 2600s out there that just won't work with them...

Link to comment
Share on other sites

Ok, I understand why it wasn't recommended to use the undocumented/illegal opcodes when the machine was still in production.  But, correct me if I'm wrong, AFAIK there are no 2600's that have problems with these opcodes, right?

AFAIK that's right.

 

And any new hardware/software that can run 2600 programs is going to have to implement these opcodes in the exact same way, right?

That's what I would expect.

Link to comment
Share on other sites

Sorry if I went off on a bit of a rant back there.

 

Ok, I understand why it wasn't recommended to use the undocumented/illegal opcodes when the machine was still in production.  But, correct me if I'm wrong, AFAIK there are no 2600's that have problems with these opcodes, right?  And any new hardware/software that can run 2600 programs is going to have to implement these opcodes in the exact same way, right?

 

Personally, I don't see the problem -- unless there are 2600s out there that just won't work with them...

 

I agree with Thomas for the most part. Undocumented opcodes like NOP ZP and LAX should be okay to use for the most part, but there are others with far less predictable results (and typically less usefullness) which should probably be avoided.

 

But on the other hand, apparently MacStella doesn't handle undocumented opcodes. And who knows what the VCS on a chip will and will not do.

 

And as for all 2600s being the same, there are still occasional reports of problems with various games on certain 2600s.

Link to comment
Share on other sites

Undocumented opcodes like NOP ZP and LAX should be okay to use for the most part, but there are others with far less predictable results (and typically less usefullness) which should probably be avoided.

 

I agree with you that most of the illegal opcodes have very limited usefulness, if any. It's rather pointless to structure your program to "take advantage" of a never-before-used, exotic opcode if you can accomplish the same thing in fewer machine cycles or in less code space (whichever is the focus of your optimizations). But what's this about "predictable results?" I would think that a specific microprocessor would execute any instruction, undocumented or not, the exact same way every time. Perhaps an instruction will be affected by conditions such as crossing a page boundary to give "different" cycle-time results, but details such as that are definitely predictable. It's all a matter or taking the time to characterize each instruction and then documenting it. After 20+ years, I would have thought the 6502 opcode geeks would have wrapped that project up long ago.

 

Maybe "unpredictable results" is a convenient way of saying that certain illegal opcodes exhibit different behaviours when run on different variations of the 6502 chip. But if you nail down a specific microprocessor in a specific hardware implementation (e.g. a 6507 wired into a 2600 motherboard), then the results are chartable, consistant, predictable, stable, etc. Just like ANY OTHER pedigreed opcode.

 

But on the other hand, apparently MacStella doesn't handle undocumented opcodes.

 

If Stella doesn't handle undocumented opcodes, I really see that as a failure on the part of that particular emulator. If Stella's vision is to emulate an Atari 2600, then it should emulate the illegal opcodes. Because a programmer back in the day COULD HAVE used an undocumented opcode, and it would have run on it's target platforrm, an Atari 2600. It's entirely possible that an as-yet undiscovered Atari 2600 game or prototype exists that uses an illegal opcode. Stella wouldn't be a VCS emulator if that game ran on a VCS but not on the emulator. And if a programmer today should opt to take advantage of an illegal opcode and it works on the target platform, why should the emu refuse to honor the technique? It works today, it would have worked way back when. By not honoring the illegal opcode, the emu is indirectly enforcing an artificial constraint on the developer and on which games may and may not exist. The opcode, illegal though it is, existed before the emu. It's absurd for something which purports to be an emulator to deliberately choose not to emulate the full range of its target platform's original capabilities.

 

And who knows what the VCS on a chip will and will not do.

 

I don't know. But if it doesn't behave exactly like a VCS, then it's a different hardware platform. I don't see the logic in limiting your programming repertoir to meet the unknokwn specifications of futureware. I thought developing new Atari 2600 games was all about creating new pastware.

 

And as for all 2600s being the same, there are still occasional reports of problems with various games on certain 2600s.

 

And since none of those reports involve games which use illegal opcodes, how is this observation relavent?

 

Ben

Link to comment
Share on other sites

I would think that a specific microprocessor would execute any instruction, undocumented or not, the exact same way every time.

 

I've seen a document somewhere on the 'net done by some C64 programmers that attempted to document the behaviour of all the undocumented instructions. They were largely successful, but in some cases undocumented instructions can cause multiple sources to drive the same bus, which can cause unpredictable results due to manufacturing differences.

 

If Stella doesn't handle undocumented opcodes, I really see that as a failure on the part of that particular emulator.

 

I agree with you there. But note I specified MacStella, not Stella or StellaX. The problem is with that emulator only. (Not to say that all emulators are able to emulate all undocumented instructions either.)

 

As for VCS on a chip, it has a transistor budget too. And since it doesn't contain a transistor level copy of the 6507, each instruction (documented or not) has to be "programmed" individually.

 

My observation about some games not working on some VCS is meant to indicate that even with "identical" hardware, micro differences can and do exist which may have a much larger impact.

 

Goodness, what a discussion for what is supposed to be a beginner tutorial.

Link to comment
Share on other sites

And since it doesn't contain a transistor level copy of the 6507, each instruction (documented or not) has to be "programmed" individually.

Are you sure?

 

IIRC the illegal opcodes are the result of optimizing the design of the 650x. And all opcodes follow a certain pattern which reduces the number of transistors. So I would assume that anybody who designs a new 650x-compatible chip should get very similar results when optimizing.

 

Maybe we should simply ask the guy before speculating too much here. ;)

Link to comment
Share on other sites

And since it doesn't contain a transistor level copy of the 6507, each instruction (documented or not) has to be "programmed" individually.

Are you sure?

 

Unlike the TIA, there are no transistor level schematics available for the 6502. So any VHDL (the programming language for FPGAs) has to be based on functional documentation. And any transistor optimizations may be different becuase FPGAs are not simple NMOS devices.

Link to comment
Share on other sites

So any VHDL (the programming language for FPGAs) has to be based on functional documentation.  And any transistor optimizations may be different becuase FPGAs are not simple NMOS devices.

I (think I ;)) understand.

 

Anyway, here is a quote from the 2600 on-a-chip website:

Goals:

[*]Sync, digital luma and phase modulated chroma signals for generation of composite video through a weighted resistor network.

[*]Separate 4-bit audio channels ready for resistor digital-to-analog conversion and stereo output.

[*]Optional HMOVE bars supression.

[*]Design a synthesizable 6502-compatible core with the following specifications:

[*]100% compatible with original NMOS-6502 instructions (same instruction cycle count).

[*]Support for invalid opcodes.

[*]16-bit address bus.

[*]Design a synthesizable 6532 core.

[*]Implement an Atari 2600 VCS compatible system in a Xilinx Spartan II FPGA.

So no need to worry about illegal opcodes there. :)

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...