You are on page 1of 38

======================================================

Atari 2600 TIA Hardware Notes


(a small opus on the TIA)
======================================================
v1.0 6-March-2003
by Andrew Towers
mariofrog@bigpond.com
Copyright (C) Andrew Towers 2003
Preface

Following is a whole bunch of notes on the TIA I made while I was trying to work out how
the whole thing is put together. You'll need a copy of the TIA schematics to understand
the more complicated bits of this since I was looking at them when I wrote it. According
to my copy they were scanned in by Mark De Smet. They are now available for
download from AtariAge at:

http://www.atariage.com/2600/archives/schematics_tia/index.html

I started out searching through the stella archives for any info on triggering the players
more than once per scanline (at the time I wanted to draw more than the 12 copies
possible by flicker and 3-repeat) - and I came across Eckhard Stolberg's 'grid2' demo
from Oct 1998, followed by a long series of threads over several months discussing how
the technique actually manages to work =) From all the articles it looked like a complete
black art and no-one had a theory that would explain it fully.

Then I came across the TIA-1A schematics, and proceeded to spend the next 3-4 days
solid drinking copious amounts of coffee and taking very little sleep while I tried to figure
the whole mess out from the gate level up. (hey, the 2600 is a new hobby, I can splurge
;) In the end I found that as usual writing a 'few quick notes' turned into 'writing a tutorial'
or, 'a small opus on the TIA'. So, here we go.
Polynomial Counters, what the heck is this?

Almost all of the timing and counting within the TIA is implemented in the form of
"polynomial counters", so this seems a good place to start. If you've never come across
these before (I hadn't) they seem a really obscure way to go about counting things, but
they're very small and simple to implement and therefore cheap on silicon. They also
have the useful property that 'adding 1' takes linear time (unlike a ripple-carry
adder/counter) - as long as you don't want to know where you're up to in traditional
binary numeric form, they're perfect ;)

Actually, as the TIA designers pointed out, you can use a small lookup table to convert
from one to the other, and you can compare two counter states to see if you're up to the
same count without knowing the numeric values. But this is getting off track and hand-
wavery. If you want to know the maths behind polynomial counters I suggest you look
elsewhere, I'm no mathematician ;) These things seem to be used as pseudo-random
number generators or noise generators (see the TIA sound generator, ditto for the GBA)
more than anything else.

A polynomial counter (actually a form of "Linear Feedback Shift Register") consists of a


shift register, as the name suggests, with some sort of feedback logic - in this case a
single two- input XNOR gate obfuscated in NOR logic. They have the property that they
will step through up to (2^n)-1 unique states when optimally wired up, from any starting
state except for the illegal state (and of course it's possible to power-up in the illegal
state =) so for a 6-bit shift register there can be at most 63 valid states.

The TIA uses the same polynomial counter circuit for all of its horizontal counters - there
is an HSync counter, two Player Position and two Missile Position counters, and the Ball
Position counter. The sound generator has a more complex design involving another
polynomial counter or two - I haven't delved into the workings of this one yet.

Beside each counter there is a two-phase clock generator. This takes the incoming 3.58
MHz colour clock (CLK) and divides by 4 using a couple of flip-flops. Two AND gates are
then used to generate two independent clock signals thusly:
__ __ __
_| |_________| |_________| |_________ PHASE-1 (H@1)
__ __ __
_______| |_________| |_________| |___ PHASE-2 (H@2)

You'll need a thingo, fixed-spacing font, to make sense of that. The two clock lines are
used to perform a two-step increment of the counter, as well as being used
independently to move data through the supporting clocked logic.

This concept seems to come up a -lot- in the TIA, I think it's some sort of Zen NMOS
thing, it seems to go hand and hand with storing data in back of inverters all over the
place (a * is used in the TIA schematics to denote this), and building bit-shifting chains
into your data storage so you don't need addressing ;p If you've ever wondered why the
Playfield bit order is so obscure, now you know.

Each counter has a wired-AND counter state decode matrix (woo..) connected in parallel
with the shift register. In every case, the top line of the decoder on the schematics
checks for '111111' and forces a Reset if it is encountered. This is to prevent the counter
getting stuck in the illegal state when it powers up as mentioned earlier.

Also in every case, the next decoder line is the 'wrap-around' value - when this state
comes up, the counter does a self-reset to 000000 on the next phase-2 clock, and
usually does something useful like generating a START signal for graphics output.

The rest of the counter decodes depend entirely on which counter we're looking at, set
let's get into 'em.

Horizontal Sync Counter

The HSync counter counts from 0 to 56 once for every TV scan-line before wrapping
around, a period of 57 counts at 1/4 CLK (57*4=228 CLK). The counter decodes shown
below provide all the horizontal timing for the control lines used to construct a valid TV
signal.

This table shows the elapsed number of CLK, CPU cycles, Playfield (PF) bits and
Playfield pixels at the start of each counter state (i.e. when the counter changes to this
state on the rising edge of the H@2 clock). The decoded control lines are usually
clocked into other logic blocks during the next H@1-H@2 cycle (within 4 CLK).

Value HCount CLK CPU PF Pixel Control

000000 0 0 0
100000 1 4 1.3
110000 2 8 2.6
111000 3 12 4
111100 4 16 5.3 Set H-SYNC [SHS]
111110 5 20 6.6
011111 6 24 8
101111 7 28 9.3
110111 8 32 10.6 Reset H-SYNC [RHS]
111011 9 36 12
111101 10 40 13.3
011110 11 44 14.6
001111 12 48 16 ColourBurst [RCB]
100111 13 52 17.3
110011 14 56 18.6
111001 15 60 20
011100 16 64 21.3 Reset H-BLANK [RHB]
101110 17 68 22.6 0 0
010111 18 72 24 1 4 Late RHB [LRHB]
101011 19 76 25.3 2 8
110101 20 80 26.6 3 12
011010 21 84 28 4 16
001101 22 88 29.3 5 20
000110 23 92 30.6 6 24
000011 24 96 32 7 28
100001 25 100 33.3 8 32
010000 26 104 34.6 9 36
101000 27 108 36 10 40
110100 28 112 37.3 11 44
111010 29 116 38.6 12 48
011101 30 120 40 13 52
001110 31 124 41.3 14 56
000111 32 128 42.6 15 60
100011 33 132 44 16 64
110001 34 136 45.3 17 68
011000 35 140 46.6 18 72
101100 36 144 48 19 76 Center [CNT]
110110 37 148 49.3 20 80
011011 38 152 50.6 21 84
101101 39 156 52 22 88
010110 40 160 53.3 23 92
001011 41 164 54.6 24 96
100101 42 168 56 25 100
010010 43 172 57.3 26 104
001001 44 176 58.6 27 108
000100 45 180 60 28 112
100010 46 184 61.3 29 116
010001 47 188 62.6 30 120
001000 48 192 64 31 124
100100 49 196 65.3 32 128
110010 50 200 66.6 33 132
011001 51 204 68 34 136
001100 52 208 69.3 35 140
100110 53 212 70.6 36 144
010011 54 216 72 37 148
101001 55 220 73.3 38 152
010100 56 224 74.6 39 156 RESET, HBLANK [SHB]
101010 57 (228) (76) (40) (160) (already at 000000)
010101 58 232 - - -
001010 59 236 - - -
000101 60 240 - - -
000010 61 244 - - -
000001 62 248 - - -
000000 0 0 - - - (cycle)
111111 - - - - - ERROR (Reset to 000000)

Key:
SHS Turn on the TV HSYNC signal to start Horizontal flyback.
RHS Turn off the HSYNC signal, delayed 4 CLK.
RCB Reset Colour Burst, delayed 4 CLK latching [CB].
RHB Reset HBlank (enable output), delayed 4 CLK latching [HB].
LRHB Late RHB, used instead of RHB when [HMOVE] latch is set.
CNT Center screen, start copy/reflect PF, delayed 4 CLK for [CNTD].
SHB Start HBlank (disable output), Reset HCount to 000000.
The HSync counter resets itself after 57 counts; the decode on HCount=56 performs a
reset to 000000 delayed by 4 CLK, so HCount=57 becomes HCount=0. This gives a
period of 57 counts or 228 CLK.

Playfield pixels start on the [RHB] control line at CLK=64, but the first visible pixel won't
appear until CLK=68 due to the clocking on its output. The [CNT] control line either starts
the Playfield again as normal, or starts a reverse-shifted copy when reflect-playfield
[REF] is enabled.

RSYNC resets the two-phase clock for the HSync counter to the H@1 rising edge when
strobed. It looks like this could be used to move the HSync counter into phase with the
CPU on any cycle (although there is some auto-synchronisation between the two-phase
clock and the div-by-3 counter for the CPU clock, I haven't looked into this yet.) A full
H@1-H@2 cycle after RSYNC is strobed, the HSync counter is also reset to 000000 and
HBlank is turned on. This one requires more investigation.

Player 0 and Player 1 Horizontal Position Counters

There are two independent Player Horizontal Position Counters, one each for player 0
and player 1. The counters are identical; only one is drawn in the schematics. This
section describes only the player 0 counter.

The player position counter controls the position of the player graphics object (P0) on
each scanline. The player counter counts from 0 to 39 and then wraps around, giving a
period of 40 counts at 1/4 CLK (160 CLK) - also the number of visible pixels on a
scanline.

This table shows the elapsed number of CLK and CPU cycles at the beginning of each
counter state (the CPU column isn't particularly relevant). Each START decode is
delayed by 4 CLK in decoding, plus a further 1 CLK to latch the STARTat the graphics
scan counter. The START decodes are ANDed with flags from the NUSIZ register before
being latched, to determine whether to draw that copy. Actual graphics output is shown
in parentheses for non-stretched copies of the player.

Value PCount CPU CLK Event

000000 0 0 0 (draw -012)


100000 1 1.3 4 (draw 3456)
110000 2 2.6 8 (draw 7---)
111000 3 4 12 START DRAWING (NUSIZ=001,011)
111100 4 5.3 16 (draw -012)
111110 5 6.6 20 (draw 3456)
011111 6 8 24 (draw 7---)
101111 7 9.3 28 START DRAWING (NUSIZ=011,010,110)
110111 8 10.6 32 (draw -012)
111011 9 12 36 (draw 3456)
111101 10 13.3 40 (draw 7---)
011110 11 14.6 44
001111 12 16 48
100111 13 17.3 52
110011 14 18.6 56
111001 15 20 60 START DRAWING (NUSIZ=100,110)
011100 16 21.3 64 (draw -012)
101110 17 22.6 68 (draw 3456)
010111 18 24 72 (draw 7---)
101011 19 25.3 76
110101 20 26.6 80
011010 21 28 84
001101 22 29.3 88
000110 23 30.6 92
000011 24 32 96
100001 25 33.3 100
010000 26 34.6 104
101000 27 36 108
110100 28 37.3 112
111010 29 38.6 116
011101 30 40 120
001110 31 41.3 124
000111 32 42.6 128
100011 33 44 132
110001 34 45.3 136
011000 35 46.6 140
101100 36 48 144
110110 37 49.3 148
011011 38 50.6 152
101101 39 52 156 RESET, START DRAWING (always)
010110 40 53.3 160 (already at 000000)
001011 41 54.6
100101 42 56
010010 43 57.3
001001 44 58.6
000100 45 60
100010 46 61.3
010001 47 62.6
001000 48 64
100100 49 65.3
110010 50 66.6
011001 51 68
001100 52 69.3
100110 53 70.6
010011 54 72
101001 55 73.3
010100 56 74.6
101010 57 76
010101 58 -
001010 59 -
000101 60 -
000010 61 -
000001 62 -
000000 0 - (cycle)
111111 - - ERROR (Reset to 000000)
The graphics output for players contains some extra clocking logic not present for the
Playfield or other screen objects. It takes 1 additional CLK to latch the player START
signal. The rest of the clocking logic is in common with the other graphics objects;
therefore we can say that player graphics are delayed by 1 CLK (this is why the leftmost
possible start position for a RESP0 is pixel 1, not pixel 0. You can HMOVE the player
further left though, if necessary.)

The most important thing to note about the player counter is that it only receives CLK
signals during the visible part of each scanline, when HBlank is off; exactly 160 CLK per
scanline (except during HMOVE). During the other 68 CLK per line, the counter lies
dormant on the exact 1/4 phase it was up to. The [MOTCK] (motion clock?) line supplies
the CLK signals for all movable graphics objects during the visible part of the scanline. It
is an inverted (out of phase) CLK signal.

This arrangement means that resetting the player counter on any visible pixel will cause
the main copy of the player to appear at that same pixel position on the next and
subsequent scanlines. There are 5 CLK worth of clocking/latching to take into account,
so the actual position ends up 5 pixels to the right of the reset pixel (approx. 9 pixels
after the start of STA RESP0).

For better or worse, the manual 'reset' signal (RESP0) does not generate a START
signal for graphics output. This means that you must always do a 'reset' then wait for the
counter to wrap around (160 CLK later) before the main copy of the player will appear.
However, if you have any of the 'close', 'medium' or 'far' copies of the player enabled in
NUSIZ, these will be drawn on the current and subsequent scanlines as the appropriate
decodes are reached and generate their START signals.

Player 0 and Player 1 Graphics Scan Counters

The Player Graphics Scan Counters are 3-bit binary ripple counters attached to the
player objects, used to determine which pixel of the player is currently being drawn by
generating a 3-bit source pixel address. These are the only binary ripple counters in the
TIA.

The Scan Counters are never reset, so once the counter receives the Start signal it will
count fully from 0 to 7. Counting is only performed during the visible part of the scanline
since it is driven by the [MOTCK] line used to advance the Player Position Counter. This
gives rise to "sprite wrapping" whereby a player positioned so it ends past the right-hand
side of the screen will finish drawing at the beginning of the next scanline. Note that an
HMOVE can gobble up the wrapped player graphics - see below.

The count frequency is determined by the NUSIZ register for that player; this is used to
selectively mask off the clock signals to the Graphics Scan Counter. Depending on the
player stretch mode, one clock signal is allowed through every 1, 2 or 4 graphics CLK.
The stretched modes are derived from the two-phase clock; the H@2 phase allows 1 in
4 CLK through (4x stretch), both phases ORed together allow 1 in 2 CLK through (2x
stretch).
The NUSIZ register can be changed at any time in order to alter the counting frequency,
since it is read every graphics CLK. This should allow possible player graphics warp
effects etc.

Player Reflect bit - this is read every time a pixel is generated, and used to conditionally
invert the bits of the source pixel address. This has the effect of flipping the player image
drawn. This flag could potentially be changed during the rendering of the player, for
example this might be used to draw bits 01233210.

Player graphics registers - there are four 8-bit registers in the TIA for storing Player
graphics, two for each player. Only two of these are ever directly accessible; these are
labeled the "new" player graphics registers on the schematics. Unless the Player Vertical
Delay (VDELPn) is set, the "new" registers are always drawn.

Writes to GRP0 always modify the "new" P0 value, and the contents of the "new" P0 are
copied into "old" P0 whenever GRP1 is written. (Likewise, writes to GRP1 always modify
the "new" P1 value, and the contents of the "new" P1 are copied into "old" P1 whenever
GRP0 is written). It is safe to modify GRPn at any time, with immediate effect.

Vertical Delay bit - this is also read every time a pixel is generated and used to select
which of the "new" (0) or "old" (1) Player Graphics registers is used to generate the pixel.
(i.e. the pixel is retrieved from both registers in parallel, and this flag used to choose
between them at the graphics output). It is safe to modify VDELPn at any time, with
immediate effect.

Missile 0 and Missile 1 Horizontal Position Counters

There are also two individual Horizontal Position Counters for missile 0 and missile 1.
The counters are independent and identical.

These counters use exactly the same counter decodes as the players, but without the
extra 1 CLK delay to start writing out graphics.

Missiles use the same control lines as the player from the NUISZ register to determine
the number of copies drawn, although they ignore the player scaling options (you'll just
get a single copy for the scaled player modes).

Missile width is implemented in the same way as the ball width; it appears to be exactly
the same gate arrangement (see below).

The Missile-to-player reset is implemented by resetting the M0 counter when the P0


graphics scan counter is at %100 (in the middle of drawing the player graphics) AND the
main copy of P0 is being drawn (i.e. the missile counter will not be reset when a
subsequent copy is drawn, if any). This second condition is generated from a latch
outputting [FSTOB] that is reset when the P0 counter wraps around, and set when the
START signal is decoded for a 'close', 'medium' or 'far' copy of P0.
Ball Horizontal Position Counter

The ball position counter controls the position of the ball graphics object (BL) on each
scanline. The ball counter counts from 0 to 39 and then wraps around, giving a period of
40 counts at 1/4 CLK (160 CLK).

Ball width is given by combining clock signals of different widths based on the state of
the two size bits (the gates form an AND -> OR -> AND -> OR -> out arrangement, with
a hanger-on AND gate). See notes later for all the messy details ;p

It seems a shame to have a whole polynomial counter for the ball, and no special effects
aside from its size - except for one small detail.

If you look closely at the START signal for the ball, unlike all the other position counters -
the ball reset RESBL does send a START signal for graphics output! This makes the ball
incredibly useful since you can trigger it as many times as you like across the same
scanline and it will start drawing immediately each time :)

So it's good for cutting holes in things, drawing background details, clipping the edges off
things, etc. It can even be used to draw simple sprites, or used as the background colour
(because it's behind everything else) for a two-colour sprite.

Actually on my 2600jr (long rainbow), setting the ball size to 8 pixels results in solid
colour when it's reset every 9 pixels (this might just be colour bleeding, I'm not sure).
Value BCount CPU CLK Event

000000 0 0 0 (draw 0123)


100000 1 1.3 4 (draw 4567)
110000 2 2.6 8
111000 3 4 12
111100 4 5.3 16
111110 5 6.6 20
011111 6 8 24
101111 7 9.3 28
110111 8 10.6 32
111011 9 12 36
111101 10 13.3 40
011110 11 14.6 44
001111 12 16 48
100111 13 17.3 52
110011 14 18.6 56
111001 15 20 60
011100 16 21.3 64
101110 17 22.6 68
010111 18 24 72
101011 19 25.3 76
110101 20 26.6 80
011010 21 28 84
001101 22 29.3 88
000110 23 30.6 92
000011 24 32 96
100001 25 33.3 100
010000 26 34.6 104
101000 27 36 108
110100 28 37.3 112
111010 29 38.6 116
011101 30 40 120
001110 31 41.3 124
000111 32 42.6 128
100011 33 44 132
110001 34 45.3 136
011000 35 46.6 140
101100 36 48 144
110110 37 49.3 148
011011 38 50.6 152
101101 39 52 156 RESET, START DRAWING
010110 40 53.3
001011 41 54.6
100101 42 56
010010 43 57.3
001001 44 58.6
000100 45 60
100010 46 61.3
010001 47 62.6
001000 48 64
100100 49 65.3
110010 50 66.6
011001 51 68
001100 52 69.3
100110 53 70.6
010011 54 72
101001 55 73.3
010100 56 74.6
101010 57 76
010101 58 -
001010 59 -
000101 60 -
000010 61 -
000001 62 -
000000 0 - (cycle)
111111 - - ERROR (Reset to 000000)

Vertical Delay bit - the VDELBL control bit works in the same manner as the player
VDEL bits; the state of VDELBL is used every CLK to determine which of the "new" (0)
or "old" (1) ENABL values to use at the graphics output. Writes to ENABL always modify
the "new" value, and whenever GRP1 is written the "new" value is copied into the "old".
It is safe to modify VDELBL and ENABL at any time, with immediate effects.

Using the Horizontal Position Counters

The documented way to use a player position counter is to reset it with RESPn on any
CPU cycle divisible by 5 during the visible scanline (5 is a convenient number for DEX-
BNE loops), set up HMPn to adjust the position by +7 (left) to -8 (right) pixels, and hit
HMOVE immediately after the next WSYNC. Then configure NUSIZn for the number and
spacing of copies required, and let the hardware go about its business. Once this is set
up, you can just change the graphics in GRPn every scanline to get one, two or three
copies at fixed spacing.

In fact the hardware has hard-wired requirements for almost none of the above =) The
fixed spacing between copies is hard-wired and HMOVE is largely not negotiable, but
the rest is complete tosh.

The TIA renders each movable graphics object according to independent position
counters running at 1/4 CLK with a period of 40 increments, and synchronised to the last
RESPn/RESMn/RESBL strobe. Each and every time a counter wraps around, the 'main'
copy of the object starts to draw. Since it takes 4 CLK to reset the counter to zero and 4
CLK to increment the counter, the image can be expected to appear after exactly 40 full
counts, or 160 CLK.

The counters are normally only running during the 'visible' part of a scanline, unless
you're doing an HMOVE. Since the scanline has 160 visible pixels, this yields the
documented behavior that a RESPn/etc sets the position for the next scanline. It's out by
5 pixels when you set it, but who's counting?

Due to extra clocking logic for Player graphics output, the first player pixel won't appear
until 1 CLK later than for any other graphics object once rendering 'starts'. See the
HSync/Player Counter info above for an explanation of this.

During the horizontal blank (see the Horizontal Counter info above) the Player, Missile
and Ball counters stop receiving CLK signals, so they pause on the exact 1/4 CLK
they're up to and resume where they left off at the first visible pixel on the next scanline.
This gives rise to the 'wrap around' effect, to the point of splitting a copy of the player
image in half because it happened to start too near the right edge of the screen ;)

The object counters are running at the same 1/4 CLK rate as the HSync counter, but you
can set them out of phase with the HSync counter (and therefore the Playfield) by
resetting any of them on a CPU cycle that isn't divisible by 4. (If this were not the case,
there would only be 40 possible positions along the scanline and we could all go home
early). You can also use the HMOVE command, which is described below.

Playing with the HMOVE registers


In principle the operation of HMOVE is quite straight-forward; if an HMOVE is initiated
immediately after HBlank starts, which is the case when HMOVE is used as
documented, the [HMOVE] signal is latched and used to delay the end of the HBlank by
exactly 8 CLK, or two counts of the HSync Counter. This is achieved in the TIA by
resetting the HB (HBlank) latch on the [LRHB] (Late Reset H-Blank) counter decode
rather than the normal [RHB] (Reset H-Blank) decode.

The extra HBlank time shifts everything except the Playfield right by 8 pixels, because
the position counters will now resume counting 8 CLK later than they would have without
the HMOVE. This is also the source of the HMOVE 'comb' effect; the extended HBlank
hides the normal playfield output for the first 8 pixels of the line.

In order to move less than 8 pixels right the TIA performs 'clock stuffing' on the Player,
Missile and Ball position counters, whereby a number of clock pulses between 0 and 15
are sent to the counters during HMOVE. Each extra clock pulse eats up 1/4 count in the
object's horizontal position counter, and thereby moves the object left one pixel. This
must be done during HBlank because it is sending these extra clock pulses down the
same clock lines that usually receive [MOTCK] pulses during the visible part of the
scanline.

The Stella Programmer's Guide states that "the motion registers should not be modified
for at least 24 machine cycles after an HMOVE command". This is indeed for internal
hardware considerations, although perhaps not entirely mysterious. After several
attempts, I finally got my head around the heavily obfuscated logic in the schematics. It
turns out to be fairly simple, and quite elegant :)

The HMOVE values set by the programmer are stored in a matrix of 4-bit data latches
with built-in comparators - each latch effectively contains a wired-XOR gate, and the 4
latches for a given HMxx register are arranged in a wired-NOR formation to give a 4-bit
comparator.

Beside the matrix of HMxx latches is a 4-bit binary ripple counter. It begins at 15 and
decrements down to zero during the HMOVE at a rate of 1 decrement every 4 CLK (it's
built from 2-phase clocked logic). The counter is wired in parallel to the comparators in
all 5 HMxx registers.

At the beginning of the HMOVE, a latch is set for each movable object to indicate that it
requires more motion to the left. When the comparator for a given object detects that
none of the 4 bits match the bits in the counter state, it clears this latch (a clever
exercise in reverse logic!) Until this time, the output of the latch is sent through to the
movable object once every 4 CLK (on every H@1 signal from the HSync two-phase
clock) as an extra "stuffed" clock signal.

Since one extra CLK pulse is sent every 4 CLK, this takes at most 4*16=64 CLK
(including counter reset at the end), or 64/3=21 CPU cycles. It takes 3 CLK after the
HMOVE command is received to decode the [SEC] signal (at most 6 CLK depending on
the timing of STA HMOVE) and a further 4 CLK to set the "more movement required"
latches. So we need to wait at least 71/3=23.66 CPU cycles before the HMOVE
operation is complete. For a normal HMOVE after WSYNC, it might be safe by cycle 23
(this has not been tested).
The first compare (against 15) will be sampled 15 CLK after STA HMOVE begins and
every 4 CLK thereafter. The first counter decrement will happen at CLK 17, and every 4
CLK thereafter.

You may have noticed that the above discussion ignores the fact that HMxx values are
specified in the range +7 to -8.
In an odd twist, this was done purely for the convenience of the programmer! The
comparator for D7 in each HMxx latch is wired up in reverse, costing nothing in silicon
and effectively inverting this bit so that the value can be treated as a simple 0-15 count
for movement left. It might be easier to think of this as having D7 inverted when it is
stored in the first place.

In theory then the side effects of modifying the HMxx registers during HMOVE should be
quite straight-forward. If the internal counter has not yet reached the value in HMxx, a
new value greater than this (in 0-15 terms) will work normally. Conversely, if the counter
has already reached the value in HMxx, new values will have no effect because the latch
will have been cleared.

Much more interesting is this: if the counter has not yet reached the value in HMxx (or
has reached it but not yet committed the comparison) and a value with at least one bit in
common with all remaining internal counter states is written (zeros or ones), the stopping
condition will never be reached and the object will be moved a full 15 pixels left. In
addition to this, the HMOVE will complete without clearing the "more movement
required" latch, and so will continue to send an additional clock signal every 4 CLK
(during visible and non-visible parts of the scanline) until another HMOVE operation
clears the latch. The HMCLR command does not reset these latches.

The Cosmic Ark stars effect achieved this by writing the value $60 to HMM0, 21 cycles
after HMOVE starts. See this message in the archives: (Appendix C)

Following is how I believe it works: at 21 cycles in, the internal counter has just
decremented to %0000 and is about to test this against the HMxx registers (2 CLK from
now, if my timings are correct). If we flip the top bit of $60 as described above, we have
the binary pattern %1110. This pattern has at least one bit in common with the final
remaining state (the bottom zero bit), and also has bits in common with the default
counter state %1111 which will arise when the counter resets. This means the compare
will pass now and forever more :) For this to work, I expect that they must have set
HMM0 to $70 before using the trick (binary %0111, or %1111 with the bit flipped), but
after a cursory glance at Thomas' commented Cosmic Ark code I haven't found this.

Looking at the archives relating to Cosmic Arc and Rabbit Transit tricks, I also notice that
an HMCLR 20 cycles in, has the same effect. In this case it will be resetting HMxx to
%1000 (bit-flipped) which also obeys the rules for bypassing the stopping condition.

Also of note, the HMOVE latch used to extend the HBlank time is cleared when the
HSync Counter wraps around. This fact is exploited by the trick that involves hitting
HMOVE on the 74th CPU cycle of the scanline; the CLK stuffing will still take place
during the HBlank and the HSYNC latch will be set just before the counter wraps around.
It will then be cleared again immediately (and therefore ignored) when the counter
wraps, preventing the HMOVE comb effect. Since the extended HBlank is needed to
move all objects right 8 pixels, this has the limitation that objects can only be moved left,
and the normal HMOVE numbering no longer applies. Instead the HMOVE value is
interpreted as (8 + value) pixels to the left, i.e.:

-8 = 0 -4 = 4 0 = 8 4 = 12
-7 = 1 -3 = 5 1 = 9 5 = 13
-6 = 2 -2 = 6 2 = 10 6 = 14
-5 = 3 -1 = 7 3 = 11 7 = 15

This means that all objects will be moved 8 pixels left unless you set their HMxx value to
-8 for zero movement.

I've recently found a post in the Stella mailing list archives that gave these results by
exhaustive testing, posted by Brad Mott: (Appendix B)

Graphics Scan Counters during HMOVE

Since the Graphics Scan Counters are never reset, player graphics output can wrap
around as mentioned above.

An HMOVE 8 pixels right (-8 << 4), has no effect on the scan counter since it will
perform no "clock stuffing" of the player counters for that player (the extended HBlank
time moves everything right 8 pixels).

Any other HMOVE value will gobble up at least one pixel, or more proportional to the
HMOVE value. Since an HMOVE value really represents a count from 0 (for -8) to 15
(for +7) with the top bit inverted, this is the number of player pixels that will be gobbled
up by the HMOVE.

This means that an HMOVE of 0 will gobble up all remaining wrapped output for the non-
stretched player modes, since it sends 8 extra clocks to the player. (Note that this is only
true if HMOVE was actually strobed for the scanline, otherwise the configured HMxx
registers never have any effect). For the stretched player modes there could be some
output left - it takes 16 stuffed clocks to eat up a full 2X player, and 32 clocks to eat up a
full 4X player.

HMOVE during the visible scanline.

I mentioned above that HMOVE sends extra clock pulses down the same clock lines that
are usually used during the visible part of the scanline. In theory this means that
performing an HMOVE during the visible part of the scanline should have no effect.
However, looking at how the various clock signals interact, I suspect it is possible. I did
some preliminary experiments (on a 2600 Jr) at some point, and I seem to remember
having some success.

In this case the extra HMOVE clock pulses act to perform 'plugging' instead of the
normal 'stuffing'; by this I mean that the extra pulses plug up the gaps in the normal
[MOTCK] pulses, preventing them from counting as clock pulses. This only works
because the extra HMOVE pulses are derived from the two-phase clock on the HSync
counter, which is itself derived from CLK (the TIA colour clock input), whereas [MOTCK]
is an inverted CLK signal - so they are more or less precisely out of phase :)
I'm not sure how universal (or reliable!) this might turn out to be, but I haven't seen it
mentioned before. Also of note, this technique can only be used to effect a move to the
right, at a rate of 1 pixel every 4 CLK (since this is the rate that HMOVE generates the
extra clock pulses).

The Re-trigger Trick, and all that jazz

I've read some theories suggesting that re-triggering is a hack, possibly dependent on
chip revision, where you trick the TIA into rendering more than three copies by hitting
RESP0/RESP1 during the rendering of a 'legitimate' copy, or some other method to
confuse the poor chip. Through extensive coffee consumption, I have determined that
this is not the case. Perhaps peering at the TIA schematics for countless hours on end,
until I fell asleep (two days in a row), may have helped also.

The behaviour of the TIA positioning registers is quite predictable and completely
independent from its graphics output logic, as documented above. What remains are
issues involving the timing of RESPn commands, given that the TIA counts things at 1/4
clock and the CPU runs at 1/3 CLK =)

Following is a table of the cycle decodes for the Player counters, starting from CLK=0
when the counter resets. This is an excerpt from the Player Counter table listed
elsewhere in the document (I recommend you go have a look, the spacing between
events should look oddly familiar ;)

Value PCount CPU CLK Event

111000 3 4 12 START DRAWING (NUSIZ=001,011) Close


101111 7 9.3 28 START DRAWING (011,010,110) Medium
111001 15 20 60 START DRAWING (100,110) Far
101101 39 52 156 RESET, START DRAWING (always) Main

The columns from the left are: the polynomial counter state, (see notes above), the
decimal value that the player counter is up to, the number of CPU cycles since the
counter reset, and the number of CLKs elapsed since the counter reset.

You'll notice I'm now talking about everything relative to RESPn on the current scanline,
rather than the beginning of the scanline. This is because this is all that matters. You
should understand the following point:

If you hit RESPn at least twice on every scanline,


you will never see the 'main' copy of that player,
ever, on any scanline.

This is because the counter will always be reset before it manages to complete a full 40
counts (160 CLK), and so the 'main' copy will never start drawing.

This is tricky to test, especially if you don't reset a few things when you stop (e.g., for
VSync) - whenever you stop hitting RESPn, you will start to get the normal output on the
next and subsequent scanlines, including the 'main' copy. The very top visible scanline is
a perfectly valid 'subsequent scanline' after the very bottom visible scanline, once you
get past the first frame ;)

If you've set up NUSIZn for 3 copies close (011), you'll be getting four copies on every
scanline on which you hit RESPn twice, as long as they are far enough apart. This works
because it doesn't take a counter wrap-around to get to the 'close' and 'medium' copies
as shown in the table above. They will appear 4+12+1=17 and 4+28+1=33 pixels after
each RESPn CLK arrives in the TIA (it takes 4 extra CLK to reset the counter, and 1
extra CLK to start the graphics output).

It's important to note, that as long as the second RESPn on the line causes a reset after
the 'start' signal has been generated for the 'medium' copy of the first RESPn, you will
get four copies regardless of how far apart the RESPn hits are. If you do the second
RESPn too soon you'll end up with only three copies - the 'close' from the first RESPn,
followed by the 'close' and the 'medium' from the second RESPn. If you do the second
RESPn before the first 'close' copy, you'll only end up with the 'close' and 'medium' from
the second RESPn.

From this it follows that if you set NUSIZ0 to 011, hit RESPn and wait until the 'medium'
copy has started, then change NUSIZ0 to 100 or 110, you will get all of 'close', 'medium'
and 'far'.

Re-triggering after exactly 18, 33, 66 or 162 cycles

These are special cases only because resetting a Position Counter (RSPn, RESMn,
RESBL) also resets the two-phase clock attached to it, and this in turn affects the
clocked logic on the output of the counter decodes.

For the player counters, this affects the four decodes that produce the Start signal for
copies of the player graphics. These are generated 12, 28, 60, and 160 CLK after the
Position Counter has been reset, in order to trigger the 'close', 'medium', 'far' and 'main'
copies.

These decodes pass through a block of logic that requires a full cycle of the two-phase
clock (hence the normal 4 CLK delay before graphics output common to all movable
graphics objects). If the Position Counter and therefore the two-phase clock are reset
during this decoding process, the Start signal will either be lost or delayed up to 3 CLK
depending on exact timing.

This effect is most evident when attempting to re-trigger the player graphics over and
over again. For example, examine this re-triggering technique:

STA RESP0 ;3 reset P0, call this 0 CLK.


CMP $EA ;3 nop
STA RESP0 ;3 reset P0 again, after 18 CLK.
CMP $EA ;3 nop
STA RESP0 ;3 reset P0 again, after 18 CLK.

The visible result of this will be a 'close' copy of P0 shifted right by two pixels from the
expected position, followed by a second 'close' copy shifted right by two pixels, and
finally a third 'close' copy, not shifted right. There will be an 18 pixel gap between the
first two copies of P0, and only a 16 pixel gap before the third copy.

In order to fix up the spacing of the final copy, it is necessary to trigger P0 yet again
exactly 18 CLK later, but clear GRP0 in the mean time so nothing is drawn.

If the re-triggering will be continuing onto the next line there is no need to do this; just
ensure that the first re-trigger on the next line happens 18 visible pixels after the last
RESP0 on the previous line (i.e. 18 CLK later, minus HBlank time).

Notes on the Ball/Missile width en-clockifier

Just to reiterate, ball width is given by combining clock signals of different widths based
on the state of the two size bits (the gates form an AND -> OR -> AND -> OR -> out
arrangement, with a hanger-on AND gate).

The Enable (output) signal is built in two halves, arranged back- to-back at the final OR
gate.

The first half comes from one of three sources combined through the earlier OR gate
and then AND-ed with the Start signal:

(1) If D4 and D5 are both clear, one of the two-phase clock signals (active 1 in 4 colour
CLK) yields a single pixel of output. (2) If D4 is set, a line active 2 in every 4 colour CLK
is borrowed from the two-phase clock generator (this yields 2 pixels). (3) Finally D5 itself
is used directly - the Start signal is active for 4 CLK so this generates 4 pixels.

The second half is added if both D4 and D5 are set; a delayed copy of the Start signal (4
colour CLK wide again) is OR-ed into the Enable signal at the final OR gate.

I hope someone had as much fun building this little circuit as I had pulling it apart again.

CPU Clock to Player Pixel Table

The Player Position Counter can be reset to zero (with RESP0/1) on any CPU cycle as
shown below, and copies will appear at the pixel positions listed for 'close', 'medium'
and/or 'far' depending on the flags in NUSIZ; 1, 2, 3 or (if you change NUSIZ at the right
time) 4 copies at hard-wired positions after the reset. If the counter is allowed to wrap
around, the 'main' copy will appear on the next line.

Resetting the counter takes 4 CLK, decoding the 'start drawing' signal takes 4 CLK,
latching the 'start' takes a further 1 CLK giving a total 9 CLK delay after a RESP0/1.
Since the playfield takes 4 CLK to start drawing the player is visibly delayed by exactly 5
CLK - hence the magic '5' :)

NOTE: The player counter can be safely reset 18 CLK after the previous reset and the
previous copy will still be drawn. BUT the 'start' signal for the previous copy will be
delayed a further 2 CLK due to the 2- phase clock being reset before the 'start' signal
has been clocked through to the 'start' latch.
CPU CLK Pixel Main Close Medium Far PF

0 0 - 1 17 33 65 -
...
22 66 - 1 17 33 65 -
22.6 --------------------------------------------------------
23 69 1 6 22 38 70 0.25
24 72 4 9 25 41 73 1
25 75 7 12 28 44 76 1.75
26 78 10 15 31 47 79 2.5
27 81 13 18 34 50 82 3.25
28 84 16 21 37 53 85 3
29 87 19 24 40 56 88
30 90 22 27 43 59 91
31 93 25 30 46 62 94
32 96 28 33 49 65 97
33 99 31 36 52 68 100
34 102 34 39 55 71 103
35 105 37 42 58 74 106
36 108 40 45 61 77 109
37 111 43 48 64 80 112
38 114 46 51 67 83 115
39 117 49 54 70 86 118
40 120 52 57 73 89 121
41 123 55 60 76 92 124
42 126 58 63 79 95 127
43 129 61 66 82 98 130
44 132 64 69 85 101 133
45 135 67 72 88 104 136
46 138 70 75 91 107 139
47 141 73 78 94 110 142
48 144 76 81 97 113 145
49 147 79 84 100 116 148
50 150 82 87 103 119 151
51 153 85 90 106 122 154
52 156 88 93 109 125 157
53 159 91 96 112 128 0
54 162 94 99 115 131 3
55 165 97 102 118 134 6
56 168 100 105 121 137 9
57 171 103 108 124 140 12
58 174 106 111 127 143 15
59 177 109 114 130 146 18
60 180 112 117 133 149 21
61 183 115 120 136 152 24
62 186 118 123 139 155 27
63 189 121 126 142 158 30
64 192 124 129 145 1 33
65 195 127 132 148 4 36
66 198 130 135 151 7 39
67 201 133 138 154 10 42
68 204 136 141 157 13 45
69 207 139 144 0 16 48
70 210 142 147 3 19 51
71 213 145 150 6 22 54
72 216 148 153 9 25 57
73 219 151 156 12 28 60
74 222 154 159 15 31 63
75 225 157 2 18 34 66
76 228 0 5 21 37 69
----------------------------------------------------- Start HBLANK
Also note that hitting RESP0 before HBLANK has finished will reset the counter
immediately, but it will only start counting again when HBLANK goes off. Due to output
clocking, this will produce player graphics at playfield pixel 1.

The Venerable 6-digit Score Trick

The 6-digit score trick involves putting both players into 3-repeat mode (011 or 110 in
NUSIZ0/1) and resetting them such that all the player 2 images are positioned exactly
between all the player 1 images, ergo:

P1 P2
v v
1 2 1 2 1 2

Then you need to set the graphics up (GRP0/1) for the first two digits, and write some
very precise timing code to wait until the scan-line is just about to start drawing the first
copy of P1. While you're waiting, get the rest of the graphics loaded into the registers (A,
X, and Y).

At this point you need to start storing all the graphics you've loaded into GRP0 and
GRP1 as fast as you can - it will look like this because there's only one way to do it fast
enough:

STA GRP0 ; 3
STX GRP1 ; 3
STY GRP0 ; 3
ST? GRP1 ; 3 we've run out of registers!

Notice that each one takes 3 cycles to execute (which is 9 pixels) and makes the change
on the -end- of the 3rd cycle. We could use the stack pointer register (S) for the last one
and do a TSX, but that would take 5 cycles (that's 15 pixels) which is too long.

To get it working you need to turn on VDELP0/1 (vertical delay) which allows you to set
up the first 3 digits in the TIA's graphics registers before the beginning of the scanline,
and requires only the 3 remaining registers to hold the last 3 digits.

I've found a post in the Stellar archives that explains this technique in great detail, so I'll
stop here. (Appendix A)

Fine Print

Please note that these notes are my own, and are made available without any
warranties of any kind. They may include errors, omissions and much that is apocryphal;
use at your own risk.

Please let me know if you spot anything that is blatantly wrong and I'll update the
document. I'm also happy to answer any questions about this stuff.
Appendix A.
http://www.biglist.com/lists/stella/archives/199704/msg00137.html

The scores / 48-pixel high-res routine explained!


By Erik Mooney

In a burst of frustration at my invaders program (can't find the cycles to make the
invaders look like anything but Super Breakout rejects), I figured out the scores routine.
I don’t know if this has been done before, but the lack of comments on the code to draw
the text in Greg Troutman's game (everything else is highly commented) and in the 48-
pixel high-res routine in Okie Dokie's source indicate that nobody quite understands it
yet.

The key is the vertical delay registers, and the fact that they don't do vertical delay at all.
As the "big file" says, there are actually TWO graphics registers for each player.. I'm
going to call them GRP0, GRP0A, GRP1, and GRP1A. Again as the "big file" says, bit 0
of VDELP0 toggles between whether GRP0 or GRP0A is used for the player 0 display,
and VDELP1 toggles using GRP1 or GRP1A for the player 1 display. For this routine,
both vertical delay registers must be set, so GRPxA will always be used.

The tricky part lies in when GRP0A and GRP1A load. When GRP0 is written to (with
any value), GRP1A receives the contents of GRP1. When GRP1 is written to, GRP0A
becomes equal to GRP0.

You're "supposed" to write GRP0 and GRP1 at the beginning of alternating lines, so
when the vertical delay register is set, any write to GRP0 does not take effect until a
write to GRP1 occurs, which occurs on the next scanline.

Here's the code for it again (from Okie Dokie). GRHEIGHT is the number of scanlines
for which we want to do this. Player 0 has been set to pixel 123 (including horizontal
blank) and Player 1 has been set to pixel 131. So the digits begin at pixels 123, 131,
139, 147, 155, 163. A series of six 16-bit pointers to the digits reside at GRTABLE, in
RAM. In the table on the right, D# means digit number # of the score.
Cycles Pixel GRP0 GRP0A GRP1 GRP1A

loop2
ldy GRHEIGHT ;+3 63 189
lda (GRTABLE),y ;+5 68 204
sta GRP0 ;+3 71 213 D1 -- -- --
sta WSYNC ;go
lda (GRTABLE+$2),y ;+5 5 15
sta GRP1 ;+3 8 24 D1 D1 D2 --
lda (GRTABLE+$4),y ;+5 13 39
sta GRP0 ;+3 16 48 D3 D1 D2 D2
lda (GRTABLE+$6),y ;+5 21 63
sta TEMPVAR ;+3 24 72
lda (GRTABLE+$8),y ;+5 29 87
tax ;+2 31 93
lda (GRTABLE+$A),y ;+5 36 108
tay ;+2 38 114
lda TEMPVAR ;+3 41 123 !
sta GRP1 ;+3 44 132 D3 D3 D4 D2!
stx GRP0 ;+3 47 141 D5 D3! D4 D4
sty GRP1 ;+3 50 150 D5 D5 D6 D4!
sta GRP0 ;+3 53 159 D4* D5! D6 D6
dec GRHEIGHT ;+5 58 174 !
bpl loop2 ;+2 60 180

At the *, the value written to GRP0 does not matter.. what does matter is that this write
triggers GRP1A to receive new contents from GRP1. A "!" indicates that that register is
being used for displaying at that moment.

Looking at it a different way:


sta GRP0 ;digit 1 -> GRP0 WSYNC
sta GRP1 ;digit 2 -> GRP1, digit 1 = GRP0 -> GRP0A
sta GRP0 ;digit 3 -> GRP0, digit 2 = GRP1 -> GRP1A
*Digit 1 (GRP0A) begins displaying
*Digit 2 (GRP1A) begins. One pixel later, the next instruction completes.
sta GRP1 ;digit 4 -> GRP1, digit 3 = GRP0 -> GRP0A
*Digit 3 (GRP0A) begins. Two pixels later, the next instruction completes.
stx GRP0 ;digit 5 -> GRP0, digit 4 = GRP1 -> GRP1A
*Digit 4 (GRP1A) begins. 3 pixels later, the next instruction completes.
sty GRP1 ;digit 6 -> GRP1, digit 5 = GRP0 -> GRP0A
*Digit 5 (GRP0A) begins. 4 pixels later, the next instruction completes.
sta GRP0 ;any value -> GRP0, digit 6 = GRP1 -> GRP1A
*Digit 6 (GRP1A) begins.

The basic idea is that we preload GRP0A, GRP1A, and GRP0 for the first three digits
well ahead of time, then load the last three digits into our three registers and fire them off
in rapid succession at the correct time.. then one more GRP0 strobe is needed to make
GRP1A update one more time. Using the "A" registers lets us preload three digits
instead of two, so that we have enough registers to hold the rest of the digits. Note that
we always update GRP0A while GRP1A is being displayed, and vice versa.

My hat's off to the programmers who came up with this thing - "Wow. The above routine
just drew the score with no cycles to spare." Now that we know how it works (did it in
fact make sense to anyone?), it could be modified to display at different horizontal
positions.
Appendix B
http://www.biglist.com/lists/stella/archives/199804/msg00198.html

Games that do bad things to HMOVE


By Brad Mott

Well, I had some time to play around with the HMOVE stuff today and I've got all the
data for moving P0 and P1. The problem I wrote about P0 not being moved right was all
wrong. There was a bug in my program I loaded $0f into HMPX instead of $f0 :-)

So here's a table which summaries the information I gathered by hitting HMOVE at


various cycles after WSYNC. The table starts at cycle ten since that was the smallest
delay my program allowed, however, by wrapping around to 86 I end up with all of the
values :-)

I'm also including the program I used to gather the data. If anyone has some free time
feel free to build a table for the missiles and balls (hopefully it'll be the same).

Man, this was fun! Now I just need to look over the data to see if it makes any sense. It
looks like hitting HMOVE at cycle 73 or cycle 74 should be fairly useful in creating a
playfield where no HMOVE blanks occur.

Have Fun!
Brad
HMPx values
0 1 2 3 4 5 6 7 8 9 a b c d e f
Cyc
10 0 -1 -2 -2 -2 -2 -2 -2 8 7 6 5 4 3 2 1 ** HBLANK
11 0 -1 -1 -1 -1 -1 -1 -1 8 7 6 5 4 3 2 1 HBLANK
12 0 0 0 0 0 0 0 0 8 7 6 5 4 3 2 1 HBLANK
13 1 1 1 1 1 1 1 1 8 7 6 5 4 3 2 1 HBLANK
14 1 1 1 1 1 1 1 1 8 7 6 5 4 3 2 1 ** HBLANK
15 2 2 2 2 2 2 2 2 8 7 6 5 4 3 2 2 HBLANK
16 3 3 3 3 3 3 3 3 8 7 6 5 4 3 3 3 HBLANK
17 4 4 4 4 4 4 4 4 8 7 6 5 4 4 4 4 HBLANK
18 4 4 4 4 4 4 4 4 8 7 6 5 4 4 4 4 ** HBLANK
19 5 5 5 5 5 5 5 5 8 7 6 5 5 5 5 5 HBLANK
20 6 6 6 6 6 6 6 6 8 7 6 6 6 6 6 6 HBLANK
21 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
22 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
.
.
53 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
54 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
55 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0
56 0 0 0 0 0 0 -1 -2 0 0 0 0 0 0 0 0
57 0 0 0 0 0 -1 -2 -3 0 0 0 0 0 0 0 0
58 0 0 0 0 0 -1 -2 -3 0 0 0 0 0 0 0 0 **
59 0 0 0 0 -1 -2 -3 -4 0 0 0 0 0 0 0 0
60 0 0 0 -1 -2 -3 -4 -5 0 0 0 0 0 0 0 0
61 0 0 -1 -2 -3 -4 -5 -6 0 0 0 0 0 0 0 0
62 0 0 -1 -2 -3 -4 -5 -6 0 0 0 0 0 0 0 0 **
63 0 -1 -2 -3 -4 -5 -6 -7 0 0 0 0 0 0 0 0
64 -1 -2 -3 -4 -5 -6 -7 -8 0 0 0 0 0 0 0 0
65 -2 -3 -4 -5 -6 -7 -8 -9 0 0 0 0 0 0 0 -1
66 -2 -3 -4 -5 -6 -7 -8 -9 0 0 0 0 0 0 0 -1 **
67 -3 -4 -5 -6 -7 -8 -9 -10 0 0 0 0 0 0 -1 -2
68 -4 -5 -6 -7 -8 -9 -10 -11 0 0 0 0 0 -1 -2 -3
69 -5 -6 -7 -8 -9 -10 -11 -12 0 0 0 0 -1 -2 -3 -4
70 -5 -6 -7 -8 -9 -10 -11 -12 0 0 0 0 -1 -2 -3 -4 **
71 -6 -7 -8 -9 -10 -11 -12 -13 0 0 0 -1 -2 -3 -4 -5
72 -7 -8 -9 -10 -11 -12 -13 -14 0 0 -1 -2 -3 -4 -5 -6
73 -8 -9 -10 -11 -12 -13 -14 -15 0 -1 -2 -3 -4 -5 -6 -7
74 -8 -9 -10 -11 -12 -13 -14 -15 0 -1 -2 -3 -4 -5 -6 -7 **
75 0 -1 -2 -3 -4 -5 -6 -7 8 7 6 5 4 3 2 1 HBLANK
76 0 -1 -2 -3 -4 -5 -6 -7 8 7 6 5 4 3 2 1 HBLANK
77 0 -1 -2 -3 -4 -5 -6 -7 8 7 6 5 4 3 2 1 HBLANK
78 0 -1 -2 -3 -4 -5 -6 -7 8 7 6 5 4 3 2 1 HBLANK
79 0 -1 -2 -3 -4 -5 -6 -7 8 7 6 5 4 3 2 1 HBLANK
80 0 -1 -2 -3 -4 -5 -6 -6 8 7 6 5 4 3 2 1 HBLANK
81 0 -1 -2 -3 -4 -5 -5 -5 8 7 6 5 4 3 2 1 HBLANK
82 0 -1 -2 -3 -4 -5 -5 -5 8 7 6 5 4 3 2 1 ** HBLANK
83 0 -1 -2 -3 -4 -4 -4 -4 8 7 6 5 4 3 2 1 HBLANK
84 0 -1 -2 -3 -3 -3 -3 -3 8 7 6 5 4 3 2 1 HBLANK
85 0 -1 -2 -2 -2 -2 -2 -2 8 7 6 5 4 3 2 1 HBLANK
86 0 -1 -2 -2 -2 -2 -2 -2 8 7 6 5 4 3 2 1 ** HBLANK
87 0 -1 -1 -1 -1 -1 -1 -1 8 7 6 5 4 3 2 1 HBLANK
88 0 0 0 0 0 0 0 0 8 7 6 5 4 3 2 1 HBLANK
89 1 1 1 1 1 1 1 1 8 7 6 5 4 3 2 1 HBLANK
90 1 1 1 1 1 1 1 1 8 7 6 5 4 3 2 1 ** HBLANK
91 2 2 2 2 2 2 2 2 8 7 6 5 4 3 2 2 HBLANK
92 3 3 3 3 3 3 3 3 8 7 6 5 4 3 3 3 HBLANK
93 4 4 4 4 4 4 4 4 8 7 6 5 4 4 4 4 HBLANK

Notice that 10 + 76 = 86 and the table is repeating...

10 0 -1 -2 -2 -2 -2 -2 -2 8 7 6 5 4 3 2 1 ** HBLANK
11 0 -1 -1 -1 -1 -1 -1 -1 8 7 6 5 4 3 2 1 HBLANK
12 0 0 0 0 0 0 0 0 8 7 6 5 4 3 2 1 HBLANK
13 1 1 1 1 1 1 1 1 8 7 6 5 4 3 2 1 HBLANK
14 1 1 1 1 1 1 1 1 8 7 6 5 4 3 2 1 ** HBLANK
15 2 2 2 2 2 2 2 2 8 7 6 5 4 3 2 2 HBLANK
16 3 3 3 3 3 3 3 3 8 7 6 5 4 3 3 3 HBLANK
17 4 4 4 4 4 4 4 4 8 7 6 5 4 4 4 4 HBLANK

processor 6502

;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; TIA (Stella) write-only registers
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Vsync equ $00
Vblank equ $01
Wsync equ $02
Nusiz0 equ $04
Nusiz1 equ $05
ColuP0 equ $06
ColuP1 equ $07
ColuBK equ $09
Ctrlpf equ $0A
Resp0 equ $10
Resp1 equ $11
Grp0 equ $1b
Grp1 equ $1c
Hmp0 equ $20
Hmp1 equ $21
Hmove equ $2A
Hmclr equ $2B

;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; 6532 (RIOT) registers
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Swcha equ $0280
Swacnt equ $0281
Swchb equ $0282
Swbcnt equ $0283
Intim equ $0284
Tim64t equ $0296
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; ROM definitions
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RomStart equ $F000
RomEnd equ $FFFF
IntVectors equ $FFFA

delay equ $a0


oldjoy equ $a1

;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;; Program initialisation
;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
org $F000
MotionTable: dc.b $00, $10, $20, $30, $40, $50, $60, $70
dc.b $80, $90, $a0, $b0, $c0, $d0, $e0, $f0

Ziffern dc.b $77,$22,$77,$77,$55,$77,$44,$77,$77,$77,$77,$77,$77,$66,$77,$77


dc.b $55,$66,$11,$11,$55,$44,$44,$11,$55,$55,$55,$55,$44,$55,$44,$44
dc.b $55,$22,$77,$33,$77,$77,$77,$11,$77,$77,$77,$66,$44,$55,$66,$66
dc.b $55,$22,$44,$11,$11,$11,$55,$11,$55,$11,$55,$55,$44,$55,$44,$44
dc.b $77,$77,$77,$77,$11,$77,$77,$11,$77,$77,$55,$77,$77,$66,$77,$44
; 0 1 2 3 4 5 6 7 8 9 A B C D E F

org $F400
NUMDATA:
dc.b $77, $55, $55, $55, $77
dc.b $72, $56, $52, $52, $77
dc.b $77, $51, $57, $54, $77
dc.b $77, $51, $53, $51, $77
dc.b $75, $55, $57, $51, $71
dc.b $77, $54, $57, $51, $77
dc.b $74, $54, $57, $55, $77
dc.b $77, $51, $51, $51, $71
dc.b $77, $55, $57, $55, $77
dc.b $77, $55, $57, $51, $77
dc.b $77, $55, $57, $55, $75
dc.b $77, $55, $56, $55, $77
dc.b $77, $54, $54, $54, $77
dc.b $76, $55, $55, $55, $76
dc.b $77, $54, $56, $54, $77
dc.b $77, $54, $56, $54, $74
dc.b $27, $65, $25, $25, $77
dc.b $22, $66, $22, $22, $77
dc.b $27, $61, $27, $24, $77
dc.b $27, $61, $23, $21, $77
dc.b $25, $65, $27, $21, $71
dc.b $27, $64, $27, $21, $77
dc.b $24, $64, $27, $25, $77
dc.b $27, $61, $21, $21, $71
dc.b $27, $65, $27, $25, $77
dc.b $27, $65, $27, $21, $77
dc.b $27, $65, $27, $25, $75
dc.b $27, $65, $26, $25, $77
dc.b $27, $64, $24, $24, $77
dc.b $26, $65, $25, $25, $76
dc.b $27, $64, $26, $24, $77
dc.b $27, $64, $26, $24, $74
dc.b $77, $15, $75, $45, $77
dc.b $72, $16, $72, $42, $77
dc.b $77, $11, $77, $44, $77
dc.b $77, $11, $73, $41, $77
dc.b $75, $15, $77, $41, $71
dc.b $77, $14, $77, $41, $77
dc.b $74, $14, $77, $45, $77
dc.b $77, $11, $71, $41, $71
dc.b $77, $15, $77, $45, $77
dc.b $77, $15, $77, $41, $77
dc.b $77, $15, $77, $45, $75
dc.b $77, $15, $76, $45, $77
dc.b $77, $14, $74, $44, $77
dc.b $76, $15, $75, $45, $76
dc.b $77, $14, $76, $44, $77
dc.b $77, $14, $76, $44, $74
dc.b $77, $15, $35, $15, $77
dc.b $72, $16, $32, $12, $77
dc.b $77, $11, $37, $14, $77
dc.b $77, $11, $33, $11, $77
dc.b $75, $15, $37, $11, $71
dc.b $77, $14, $37, $11, $77
dc.b $74, $14, $37, $15, $77
dc.b $77, $11, $31, $11, $71
dc.b $77, $15, $37, $15, $77
dc.b $77, $15, $37, $11, $77
dc.b $77, $15, $37, $15, $75
dc.b $77, $15, $36, $15, $77
dc.b $77, $14, $34, $14, $77
dc.b $76, $15, $35, $15, $76
dc.b $77, $14, $36, $14, $77
dc.b $77, $14, $36, $14, $74
dc.b $57, $55, $75, $15, $17
dc.b $52, $56, $72, $12, $17
dc.b $57, $51, $77, $14, $17
dc.b $57, $51, $73, $11, $17
dc.b $55, $55, $77, $11, $11
dc.b $57, $54, $77, $11, $17
dc.b $54, $54, $77, $15, $17
dc.b $57, $51, $71, $11, $11
dc.b $57, $55, $77, $15, $17
dc.b $57, $55, $77, $11, $17
dc.b $57, $55, $77, $15, $15
dc.b $57, $55, $76, $15, $17
dc.b $57, $54, $74, $14, $17
dc.b $56, $55, $75, $15, $16
dc.b $57, $54, $76, $14, $17
dc.b $57, $54, $76, $14, $14
dc.b $77, $45, $75, $15, $77
dc.b $72, $46, $72, $12, $77
dc.b $77, $41, $77, $14, $77
dc.b $77, $41, $73, $11, $77
dc.b $75, $45, $77, $11, $71
dc.b $77, $44, $77, $11, $77
dc.b $74, $44, $77, $15, $77
dc.b $77, $41, $71, $11, $71
dc.b $77, $45, $77, $15, $77
dc.b $77, $45, $77, $11, $77
dc.b $77, $45, $77, $15, $75
dc.b $77, $45, $76, $15, $77
dc.b $77, $44, $74, $14, $77
dc.b $76, $45, $75, $15, $76
dc.b $77, $44, $76, $14, $77
dc.b $77, $44, $76, $14, $74
dc.b $47, $45, $75, $55, $77
dc.b $42, $46, $72, $52, $77
dc.b $47, $41, $77, $54, $77
dc.b $47, $41, $73, $51, $77
dc.b $45, $45, $77, $51, $71
dc.b $47, $44, $77, $51, $77
dc.b $44, $44, $77, $55, $77
dc.b $47, $41, $71, $51, $71
dc.b $47, $45, $77, $55, $77
dc.b $47, $45, $77, $51, $77
dc.b $47, $45, $77, $55, $75
dc.b $47, $45, $76, $55, $77
dc.b $47, $44, $74, $54, $77
dc.b $46, $45, $75, $55, $76
dc.b $47, $44, $76, $54, $77
dc.b $47, $44, $76, $54, $74
dc.b $77, $15, $15, $15, $17
dc.b $72, $16, $12, $12, $17
dc.b $77, $11, $17, $14, $17
dc.b $77, $11, $13, $11, $17
dc.b $75, $15, $17, $11, $11
dc.b $77, $14, $17, $11, $17
dc.b $74, $14, $17, $15, $17
dc.b $77, $11, $11, $11, $11
dc.b $77, $15, $17, $15, $17
dc.b $77, $15, $17, $11, $17
dc.b $77, $15, $17, $15, $15
dc.b $77, $15, $16, $15, $17
dc.b $77, $14, $14, $14, $17
dc.b $76, $15, $15, $15, $16
dc.b $77, $14, $16, $14, $17
dc.b $77, $14, $16, $14, $14
dc.b $77, $55, $75, $55, $77
dc.b $72, $56, $72, $52, $77
dc.b $77, $51, $77, $54, $77
dc.b $77, $51, $73, $51, $77
dc.b $75, $55, $77, $51, $71
dc.b $77, $54, $77, $51, $77
dc.b $74, $54, $77, $55, $77
dc.b $77, $51, $71, $51, $71
dc.b $77, $55, $77, $55, $77
dc.b $77, $55, $77, $51, $77
dc.b $77, $55, $77, $55, $75
dc.b $77, $55, $76, $55, $77
dc.b $77, $54, $74, $54, $77
dc.b $76, $55, $75, $55, $76
dc.b $77, $54, $76, $54, $77
dc.b $77, $54, $76, $54, $74
dc.b $77, $55, $75, $15, $77
dc.b $72, $56, $72, $12, $77
dc.b $77, $51, $77, $14, $77
dc.b $77, $51, $73, $11, $77
dc.b $75, $55, $77, $11, $71
dc.b $77, $54, $77, $11, $77
dc.b $74, $54, $77, $15, $77
dc.b $77, $51, $71, $11, $71
dc.b $77, $55, $77, $15, $77
dc.b $77, $55, $77, $11, $77
dc.b $77, $55, $77, $15, $75
dc.b $77, $55, $76, $15, $77
dc.b $77, $54, $74, $14, $77
dc.b $76, $55, $75, $15, $76
dc.b $77, $54, $76, $14, $77
dc.b $77, $54, $76, $14, $74
dc.b $77, $55, $75, $55, $57
dc.b $72, $56, $72, $52, $57
dc.b $77, $51, $77, $54, $57
dc.b $77, $51, $73, $51, $57
dc.b $75, $55, $77, $51, $51
dc.b $77, $54, $77, $51, $57
dc.b $74, $54, $77, $55, $57
dc.b $77, $51, $71, $51, $51
dc.b $77, $55, $77, $55, $57
dc.b $77, $55, $77, $51, $57
dc.b $77, $55, $77, $55, $55
dc.b $77, $55, $76, $55, $57
dc.b $77, $54, $74, $54, $57
dc.b $76, $55, $75, $55, $56
dc.b $77, $54, $76, $54, $57
dc.b $77, $54, $76, $54, $54
dc.b $77, $55, $65, $55, $77
dc.b $72, $56, $62, $52, $77
dc.b $77, $51, $67, $54, $77
dc.b $77, $51, $63, $51, $77
dc.b $75, $55, $67, $51, $71
dc.b $77, $54, $67, $51, $77
dc.b $74, $54, $67, $55, $77
dc.b $77, $51, $61, $51, $71
dc.b $77, $55, $67, $55, $77
dc.b $77, $55, $67, $51, $77
dc.b $77, $55, $67, $55, $75
dc.b $77, $55, $66, $55, $77
dc.b $77, $54, $64, $54, $77
dc.b $76, $55, $65, $55, $76
dc.b $77, $54, $66, $54, $77
dc.b $77, $54, $66, $54, $74
dc.b $77, $45, $45, $45, $77
dc.b $72, $46, $42, $42, $77
dc.b $77, $41, $47, $44, $77
dc.b $77, $41, $43, $41, $77
dc.b $75, $45, $47, $41, $71
dc.b $77, $44, $47, $41, $77
dc.b $74, $44, $47, $45, $77
dc.b $77, $41, $41, $41, $71
dc.b $77, $45, $47, $45, $77
dc.b $77, $45, $47, $41, $77
dc.b $77, $45, $47, $45, $75
dc.b $77, $45, $46, $45, $77
dc.b $77, $44, $44, $44, $77
dc.b $76, $45, $45, $45, $76
dc.b $77, $44, $46, $44, $77
dc.b $77, $44, $46, $44, $74
dc.b $67, $55, $55, $55, $67
dc.b $62, $56, $52, $52, $67
dc.b $67, $51, $57, $54, $67
dc.b $67, $51, $53, $51, $67
dc.b $65, $55, $57, $51, $61
dc.b $67, $54, $57, $51, $67
dc.b $64, $54, $57, $55, $67
dc.b $67, $51, $51, $51, $61
dc.b $67, $55, $57, $55, $67
dc.b $67, $55, $57, $51, $67
dc.b $67, $55, $57, $55, $65
dc.b $67, $55, $56, $55, $67
dc.b $67, $54, $54, $54, $67
dc.b $66, $55, $55, $55, $66
dc.b $67, $54, $56, $54, $67
dc.b $67, $54, $56, $54, $64
dc.b $77, $45, $65, $45, $77
dc.b $72, $46, $62, $42, $77
dc.b $77, $41, $67, $44, $77
dc.b $77, $41, $63, $41, $77
dc.b $75, $45, $67, $41, $71
dc.b $77, $44, $67, $41, $77
dc.b $74, $44, $67, $45, $77
dc.b $77, $41, $61, $41, $71
dc.b $77, $45, $67, $45, $77
dc.b $77, $45, $67, $41, $77
dc.b $77, $45, $67, $45, $75
dc.b $77, $45, $66, $45, $77
dc.b $77, $44, $64, $44, $77
dc.b $76, $45, $65, $45, $76
dc.b $77, $44, $66, $44, $77
dc.b $77, $44, $66, $44, $74
dc.b $77, $45, $65, $45, $47
dc.b $72, $46, $62, $42, $47
dc.b $77, $41, $67, $44, $47
dc.b $77, $41, $63, $41, $47
dc.b $75, $45, $67, $41, $41
dc.b $77, $44, $67, $41, $47
dc.b $74, $44, $67, $45, $47
dc.b $77, $41, $61, $41, $41
dc.b $77, $45, $67, $45, $47
dc.b $77, $45, $67, $41, $47
dc.b $77, $45, $67, $45, $45
dc.b $77, $45, $66, $45, $47
dc.b $77, $44, $64, $44, $47
dc.b $76, $45, $65, $45, $46
dc.b $77, $44, $66, $44, $47
dc.b $77, $44, $66, $44, $44
Cart_Init: SEI ; Disable interrupts.:
CLD ; Clear "decimal" mode.

LDX #$FF
TXS ; Clear the stack

Common_Init:
LDX #$28 ; Clear the TIA registers ($04-$2C)
LDA #$00
TIAClear:
STA $04,X
DEX
BPL TIAClear ; loop exits with X=$FF

LDX #$FF
RAMClear:
STA $00,X ; Clear the RAM ($FF-$80)
DEX
BMI RAMClear ; loop exits with X=$7F

LDX #$FF
TXS ; Reset the stack

IOClear:
STA Swbcnt ; console I/O always set to INPUT
STA Swacnt ; set controller I/O to INPUT

LDA $280
sta oldjoy

LDA #0
STA delay

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Main program loop
;
Start:
LDA #$02
STA Wsync ; Wait for horizontal sync
STA Vblank ; Turn on Vblank
STA Vsync ; Turn on Vsync
STA Wsync ; Leave Vsync on for 3 lines
STA Wsync
STA Wsync
LDA #$00
STA Vsync ; Turn Vsync off

LDA #43 ; Vblank for 37 lines


STA Tim64t ; 43*64intvls=2752=8256colclks=36.2lines

;; Look at joystick here and change delay value

LDA $280
EOR oldjoy
BEQ next

LDA $280
AND #$40
BNE right

dec delay
jmp out

right: LDA $280


AND #$80
BNE out
inc delay

out: lda $280


STA oldjoy

next:
lda #$00
sta $81
lda delay
clc
adc #10
sta $80

clc
rol $80
rol $81
rol $80
rol $81

adc $80
sta $80
lda $81
adc #0
sta $81

lda #<NUMDATA
adc $80
sta $80
lda #>NUMDATA
adc $81
sta $81

LDA #0
STA Nusiz0
STA Nusiz1
STA Grp0
STA Grp1

VblankLoop:
LDA Intim
BNE VblankLoop ; wait for vblank timer
STA Wsync ; finish waiting for the current line
STA Vblank ; turn off Vblank

LDA #$00 ;black


STA ColuBK
LDA #$0e ;white
STA ColuP0
STA ColuP1

LDA #$AA ; playfield color


STA $8

LDA #1 ; reflected playfield


STA Ctrlpf
LDA #$ff
STA $d

STA Wsync
LDY #$00
DigitLoop: LDA ($80),y
STA Grp0
sta Wsync
iny
cpy #$05
BNE DigitLoop

LDA #1
STA Nusiz0
STA Nusiz1

LDX #$00
MotionLoop: STA Wsync
LDA #0
STA Grp0
STA Grp1

LDA #$80
STA Hmp0
LDA MotionTable,X
STA Hmp1

LDA #$07
STA Wsync ; waste the rest of the line
NOP
TAY
pause1: DEY
BNE pause1
STA Resp0
STA Resp1

; The following loop does the HMOVE at ??

; STA Wsync
; STA Hmove
; JMP return

SEC
LDA #<delaybranch
SBC delay
STA $82
LDA #>delaybranch
SBC #0
STA $83
TXA

STA Wsync ;
JMP ($82) ; +5 5

return: TAX
LDA #$AA ; Turn graphics on
STA Grp0
STA Grp1

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Screen: LDY #6
B3: STA Wsync
DEY
BNE B3

INX
CPX #$10
BNE MotionLoop

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
STA Wsync

LDA #$80
STA Hmp0
LDA #$90
STA Hmp1

LDA #$07
STA Wsync ; waste the rest of the line
NOP
TAY
pause3: DEY
BNE pause3
STA Resp0
STA Resp1

STA Wsync
STA Hmove
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

LDY #40
B4: STA Wsync
DEY
BNE B4

LDA #$02
STA Vblank ;turn on Vblank
LDX #16
END: STA Wsync
DEX
BNE END
JMP Start

org $FD00
dodelay:
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9
dc.b $c9 ; +2 2
dc.b $c9
dc.b $c9 ; +2 2
dc.b $c9
dc.b $c9 ; +2 4
dc.b $c9
dc.b $c9 ; +2 6
dc.b $c9
dc.b $c9 ; +2 8
dc.b $c9
dc.b $c9 ; +2 10
dc.b $c9
dc.b $c9 ; +2 12
dc.b $c9
dc.b $c9 ; +2 14
dc.b $c9
dc.b $c9 ; +2 16
dc.b $c9
dc.b $c9 ; +2 18
dc.b $c9
dc.b $c9 ; +2 20
dc.b $c9
dc.b $c9 ; +2 22
dc.b $c9
dc.b $c9 ; +2 24
dc.b $c9
dc.b $c9 ; +2 26
dc.b $c9
dc.b $c9 ; +2 28
dc.b $c9
dc.b $c9 ; +2 30
dc.b $c9
dc.b $c9 ; +2 32
dc.b $c9
dc.b $c9 ; +2 34
dc.b $c9
dc.b $c9 ; +2 36
dc.b $c9
dc.b $c9 ; +2 38
dc.b $c9
dc.b $c9 ; +2 40
dc.b $c9
dc.b $c9 ; +2 42
dc.b $c9
dc.b $c9 ; +2 44
dc.b $c9
dc.b $c9 ; +2 46
dc.b $c9
dc.b $c9 ; +2 48
dc.b $c9
dc.b $c9 ; +2 50
dc.b $c9
dc.b $c9 ; +2 52
dc.b $c9
dc.b $c9 ; +2 54
dc.b $c9
dc.b $c9 ; +2 56
dc.b $c9
dc.b $c9 ; +2 58
dc.b $c9
dc.b $c9 ; +2 60
dc.b $c9
dc.b $c9 ; +2 62
dc.b $c9
dc.b $c9 ; +2 64
dc.b $c9
dc.b $c9 ; +2 66
dc.b $c9
dc.b $c9 ; +2 68
dc.b $c9
dc.b $c9 ; +2 70
dc.b $c5
delaybranch: dc.b $ea
STA Hmove ; +3 73
JMP return

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Set up the 6502 interrupt vector table
;
ORG IntVectors
NMI dc.w Cart_Init
Reset dc.w Cart_Init
IRQ dc.w Cart_Init

; END
Appendix C
http://www.biglist.com/lists/stella/archives/199705/msg00024.html

Cosmic Ark stars


By Eckhard Stolberg
OK, I cheated. I looked at the code, but it still took some experimentation to figure it out.

Actually the answer is right in the programming manual. It says:

'WARNING: These motion registers should not be modified during the 24 computer
cycles immediately following an HMOVE command. Unpredictable motion values may
result.'

And who would have predicted an effect like that. :-)

Cosmic Ark uses missile0 for this and I am not sure if it would work with other moving
object too. The trick is to do an HMOVE command, then wait 18 cycles and finally write a
different value into HMM0 again.

If you use the right values for both writes to HMM0, then the TIA gets confused, moves
the missile about 15 pixels away and keeps doing so in every line until you do another
HMOVE command, even if you change the motion registers before. My little demo
program uses $07 and $06 for this. Several other values, that I tried, had no effect.

The rest of the effect is just changing the initial position of missile0 every couple of
frames to get a moving star-pattern effect and changing the colour in every line to make
the stars sparkle.

This little program displays a similar star-pattern as cosmic ark does. It should work on
PAL as well as on NTSC. I hope it has enough comments to understand how it works.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; TIA (Stella) write-only registers
;
Vsync equ $00
Vblank equ $01
Wsync equ $02
Colup0 equ $06
Colubk equ $09
Ctrlpf equ $0A
Resm0 equ $12
Enam0 equ $1D
Hmm0 equ $22
Hmove equ $2A
Hmclr equ $2B
;
; RAM definitions
; Note: The system RAM maps in at 0080-00FF and also at 0180-01FF. It is
; used for variables and the system stack. The programmer must make sure
; the stack never grows so deep as to overwrite the variables.
;
RamStart equ $0080
RamEnd equ $00FF
StackBottom equ $00FF
StackTop equ $0080
;
; 6532 (RIOT) registers
;
Swcha equ $0280
Swacnt equ $0281
Swchb equ $0282
Swbcnt equ $0283
Intim equ $0284
Tim64t equ $0296
;
; ROM definitions
;
RomStart equ $F000
RomEnd equ $FFFF
IntVectors equ $FFFA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Ram definitions
;
ORG $80
tablePTR DS 1
colour DS 1
counter DS 1
colourcounter DS 1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Data Area
;
ORG $F800
table: DB 4,5,6,7,8,9,10,11,12,11,10,9,8,7,6,5
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Program initialisation
;

Cart_Init:
SEI ; Disable interrupts.:
CLD ; Clear "decimal" mode.

LDX #$FF
TXS ; Clear the stack

Common_Init:
LDX #$28 ; Clear the TIA registers ($04-$2C)
LDA #$00
TIAClear:
STA $04,X
DEX
BPL TIAClear ; loop exits with X=$FF

LDX #$FF
RAMClear:
STA $00,X ; Clear the RAM ($FF-$80)
DEX
BMI RAMClear ; loop exits with X=$7F

LDX #$FF
TXS ; Reset the stack

IOClear:
STA Swbcnt ; console I/O always set to INPUT
STA Swacnt ; set controller I/O to INPUT

StarsInit: LDA #$03


STA colour
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Main program loop
;
Start:
LDA #$02
STA Wsync ; Wait for horizontal sync
STA Vblank ; Turn on Vblank
STA Vsync ; Turn on Vsync
STA Wsync ; Leave Vsync on for 3 lines
STA Wsync
STA Wsync
LDA #$00
STA Vsync ; Turn Vsync off

LDA #43 ; Vblank for 37 lines


STA Tim64t ; 43*64intvls=2752=8256colclks=36.2lines

LDA #$00 ;black


STA ColuBK
LDA #$0e ;white
STA ColuP0
LDA #$02 ;turn missile0 on
STA ENAM0
LDX tablePTR ;reposition missile0 every couple of
;frames for starpattern movement
LDY counter
INY
CPY #10
BNE @1
LDY #0
INX
CPX #16
BNE @1
LDX #$00
@1: STX tablePTR
STY counter
LDA table,X
TAY
STA Wsync
@2: DEY
BPL @2
STA ResM0
LDA #$70 ;this value is important for the effect
STA HMM0

STA Wsync
STA HMOVE
JSR Trick ;waste 18 cycles and load move value
STA HMM0 ;this is the tricky part

LDX colourcounter ;change colours for spakling stars


INX
STX colourcounter
CPX #3
BNE VblankLoop
LDA colour
CLC
ADC #$10
STA colour

VblankLoop:
LDA Intim
BNE VblankLoop ; wait for vblank timer
STA Wsync ; finish waiting for the current line
STA Vblank ; turn off Vblank

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Screen: LDA colour
LDY #192
@1: STA Wsync ;change the colour every line for
;sparkling stars - otherwise do what
;you want here
CLC
ADC #$03
EOR #$A0
STA ColuP0
DEY
BNE @1
LDA #$02
STA Vblank ;turn on Vblank
LDX #30
@END: STA Wsync
DEX
BNE @END
JMP Start

Trick: NOP ;the tricky subroutine


NOP
LDA #$60
RTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Set up the 6502 interrupt vector table
;
ORG IntVectors
NMI dw Cart_Init
Reset dw Cart_Init
IRQ dw Cart_Init

END

You might also like