Jump to content
  • entries
    657
  • comments
    2,689
  • views
    897,748

Step 1 - Generate a Stable Display


SpiceWare

5,432 views

First things first - head over to MiniDig - best of stella and download the Stella Programmer's Guide from the docs page. I've also attached it to this blog entry, but you should still check out what's available over at MiniDig.

Also, for this tutorial you'll need to have DASM to compile the code as well as Stella and/or a real 2600 with a Harmony cart to run your code.

The heart of the Atari is TIA, the Television Interface Adaptor. It's the video chip, sound generator, and also handles some of the controller input. As a video chip, TIA is very unusual. Most video game systems have memory that holds the current state of the display. Their video chip reads that memory and uses that information to generate the display. But not TIA - memory was very expensive at the time, so TIA was designed with a handful of registers that contain just the information needed to draw a single scanline. It's up to our program to change those registers in realtime so that each scanline shows what its supposed to. It's also up to our program to generate a "sync signal" that tells the TV when its time to start generating a new image.

Turn to page 2 of the programmer's guide. You'll find the following diagram, which I've slightly modified:


blogentry-3056-0-31557200-1403654736_thumb.png



The Horizontal Blank is part of each scanline, so we don't need to worry about generating it. Everything else though is up to us! We need to generate a sync signal over 3 scanlines, after which we need to wait 37 scanlines before we tell TIA to "turn on" the image output. After that we need to update TIA so each of the 192 scanlines that comprise visible portion of the display show what they're supposed to. Once that's done, we "turn off" the image output and wait 30 scanlines before we start all over again.

In the source code, available below, you can see the Main loop which follows the diagram:

Main:
        jsr VerticalSync    ; Jump to SubRoutine VerticalSync
        jsr VerticalBlank   ; Jump to SubRoutine VerticalBlank
        jsr Kernel          ; Jump to SubRoutine Kernel
        jsr OverScan        ; Jump to SubRoutine OverScan
        jmp Main            ; JuMP to Main
 



Each of the subroutines handles what's needed, such as this section which generates the sync signal:

VerticalSync:
        lda #2      ; LoaD Accumulator with 2 so D1=1
        sta WSYNC   ; Wait for SYNC (halts CPU until end of scanline)
        sta VSYNC   ; Accumulator D1=1, turns on Vertical Sync signal
        sta WSYNC   ; Wait for Sync - halts CPU until end of 1st scanline of VSYNC
        sta WSYNC   ; wait until end of 2nd scanline of VSYNC
        lda #0      ; LoaD Accumulator with 0 so D1=0
        sta WSYNC   ; wait until end of 3rd scanline of VSYNC
        sta VSYNC   ; Accumulator D1=0, turns off Vertical Sync signal
        rts         ; ReTurn from Subroutine
 



Currently there's no game logic, so the VerticalBlank just waits for the 37 scanlines to pass:

VerticalBlank:    
        ldx #37         ; LoaD X with 37
vbLoop:        
        sta WSYNC       ; Wait for SYNC (halts CPU until end of scanline)
        dex             ; DEcrement X by 1
        bne vbLoop      ; Branch if Not Equal to 0
        rts             ; ReTurn from Subroutine
 



The Kernel is the section of code that draws the screen.

Kernel:            
    ; turn on the display
        sta WSYNC       ; Wait for SYNC (halts CPU until end of scanline)
        lda #0          ; LoaD Accumulator with 0 so D1=0
        sta VBLANK      ; Accumulator D1=1, turns off Vertical Blank signal (image output on)
        
    ; draw the screen        
        ldx #192        ; Load X with 192
KernelLoop:   
        sta WSYNC       ; Wait for SYNC (halts CPU until end of scanline)
        stx COLUBK      ; STore X into TIA's background color register
        dex             ; DEcrement X by 1
        bne KernelLoop  ; Branch if Not Equal to 0
        rts             ; ReTurn from Subroutine
 



For this initial build it just changes the background color so we can see that we're generating a stable picture:

blogentry-3056-0-83147300-1403652756_thumb.png


Like Vertical Blank, OverScan doesn't have anything to do besides turning off the image output, so it just waits for enough scanlines to pass so that the total scanline count is 262.

OverScan:
        sta WSYNC   ; Wait for SYNC (halts CPU until end of scanline)
        lda #2      ; LoaD Accumulator with 2 so D1=1
        sta VBLANK  ; STore Accumulator to VBLANK, D1=1 turns image output off
    
        ldx #27     ; LoaD X with 27
osLoop:
        sta WSYNC   ; Wait for SYNC (halts CPU until end of scanline)
        dex         ; DEcrement X by 1
        bne osLoop  ; Branch if Not Equal to 0
        rts         ; ReTurn from Subroutine
 



Anyway, download the source and take a look - there's comments galore.


ROM

collect_20140624.bin

 

Source

Collect_20140624.zip

 

Stella Programmer's Guide

Stella Programmers Guide.pdf

 

Addendum on Keynote - what the audience sees:


blogentry-3056-0-56497000-1403721646_thumb.jpg

 

What I see on the iPad:

blogentry-3056-0-84378500-1403721660_thumb.png

 

COLLECT TUTORIAL NAVIGATION

<PREVIOUS> <INDEX> <NEXT>

  • Like 6
  • Thanks 1

10 Comments


Recommended Comments

You may want to mention that 192 is a guideline and was based upon the display capability of TVs at the time. Games can (and do) draw more lines. PAL & SECAM games will draw more lines as those standards display more lines per screen (at a lower refresh rate).

Link to comment

Yep - I planned to mention that and use Medieval Mayhem's 200 visible scanlines as an example. Things like this I put in the presenter notes. The iCloud version of Keynote doesn't let you see them yet, it's still a beta release. I've added a couple photos at the end of the blog entry which show what the audience sees vs what I see on the iPad. And yes, I practice giving my presentations using my HDTV. I connect to the Mac mini which drives my TV using AirServer, which emulates the AirPlay feature of the Apple TV. I covered AirServer in one of my DVR-Project blog entries. (as an aside - I see they now have a version for Windows).

 

Another thing I plan to mention is that the liberal use of JSR/RTS I'm doing in this example is only for legibility. Eliminating the JSR/RTS will save RAM (by reducing stack requirements), ROM (save 4 bytes for each JSR/RTS removed) and processing time (12 cycles per JSR/RTS). I'll most likely create a final version of Collect in order to show the savings.

Link to comment

Newbie here. I'm having trouble understanding why you are waiting for scanlines to finish at the beginning of VerticalSync, Kernel, and Overscan. From what I understand vsync takes 3 scanlines, vblank 37, the image 192 and then overscan 30. In your code the vsync subroutine waits for 1 + 3 scanlines, vblank 37, kernel 1 + 192 and overscan 1 + 27. Why is this? Isn't the image/kernel starting 2 scanlines later than it should?

 

Thanks!

  • Like 1
Link to comment

Great questions :thumbsup:

For VerticalSync, STA VSYNC needs to be done at the start of the scanline. Since we don't know the exact cycle* we're at when we get to VerticalSync, the STA WSYNC is done to insure that VSYNC is set at the proper time. An advanced trick is to set VSYNC at different times on the scanline in order to create an interlaced display. More info on that here.

For the Kernel, we have the same problem - we don't know the exact cycle* that we'll get to Kernel, so we use a STA WSYNC to make sure we turn on video output (STA VBLANK) before the scanline starts to be drawn. In Stella it's not noticeable if VBLANK is set later in the scanline, but on a real Atari it can be. You can see that in this blog entry, scroll down to the two photos of Frantic - the setting of VBLANK in the middle of the scanline is readily apparent in the second photo.

For the Overscan, same thing - we don't know the exact cycle* we'll get there and we want to turn off video output (using STA VBLANK again) before the scanline starts.

As far as starting a couple scanlines later - that's not a problem as the television is very forgiving. There's two things we need to be concerned with when creating the display:

1) The total number of scanlines for every frame is the same. If it's not, the television will jitter and/or roll. While we typically use 262 for NTSC, it's possible to use more or less. For PAL we typically use 312. It's possible to use more or less, though PAL does require that the number is even - if the television receives an odd number of scanlines the color information will be lost and the image will be displayed in black & white.

2) The scanline the Kernel begins at should be the same for every frame. If it's not, the picture will jitter up/down.

*The reality is for this simple program we can calculate the cycles - but as we make changes to the program we'd have to remember to go back and recalculate, then readjust code so the timing of STA VSYNC and STA VBLANK is correct. Using STA WSYNC means we don't have to worry about doing that.

  • Thanks 1
Link to comment

It took me a little while to figure out what "D1" in these comments meant.

 

lda #0 ; LoaD Accumulator with 0 so D1=0

lda #2 ; LoaD Accumulator with 2 so D1=1

 

From the Stella Programmer's Guide and Kirk Israel's tutorial here:

 

http://www.atariage.com/2600/programming/2600_101/index.html

 

...well I'll just quote from the bottom of here:

 

http://www.atariage.com/2600/programming/2600_101/03first.html

 

; If you read your Stella Programmer's Guide,
; you'll learn that bit "D1" of VSYNC needs to be
; set to 1 to turn on the VSYNC, and then later
; you set the same bit to zero to turn it off.
; bits are numbered from right to left, starting
; with zero...that means VSYNC needs to be set with something
; like 0010 , or any other pattern where "D1" (i.e. second
; bit from the right) is set to 1. 0010 in binary
; is two in decimal, so let's just do that:
;
LDA #2
STA VSYNC ; Sync it up you damn dirty television!

 

 

Humble Suggestion - change the comment on the 2 lines at top of this comment to:

 

lda #0 ; LoaD Accumulator with 0 so we can set D1=0 with the following line's sta

lda #2 ; LoaD Accumulator with 2 so we can set D1=1 with the following line's sta

 

Otherwise the original comments read to novices (such as me) that loading the accumulator is what sets the VBLANK and VSYNC rather than the following lines' sta operand. This is probably obvious if you already are familiar with assembly language and know what D1 refers to (unlike moi).

 

Also, I got confused because in the collect.asm file on lines 135 & 136 I found this:

 

lda #0 ; LoaD Accumulator with 0 so D1=0
sta VBLANK ; Accumulator D1=1, turns off Vertical Blank signal (image output on)

 

Possibly the 2nd line's comment should show "; Accumulator D1=0, turns off..."

 

GREAT FIRST LESSON !!!

Link to comment

I've been coding in assembly since the early '80s so it's hard to remember what wasn't familiar when I started. I normally use "bit x" when referencing the bits within a byte, where the value of x is 0-7 and arranged as 76543210(so if just bit 7 is on the value of the byte would be $80 or 128). Bit x notation seems a lot clearer to me, but Stella Programmer's Guide used the D notation so that's what I used for this code so it would match the documentation.

 

Yep, that would be an error, they do happen. I probably copied/pasted the lines of code and forgot to fix the comment in the second line. It's probably wrong in steps 2-14 as well.

 

Thanks!

Link to comment

The resulting display here is _very_ clean.

 

That is to say, it is nicely centered on both CRT displays, as well as upscalers and CRT displays that try to make the best of a signal that is only "somewhat" compliant.

 

See picture here: CJiEuxA.jpg

 

And the vertical blank signals are turned off, NOT ONLY during VBLANK, but during the overscan. Why is this important? Because when a video signal is being generated, you see artifacts of various things, including the signal coming out of the horizontal blank (HBLANK) and the last little chirp of the colorburst signal. In my case, since I am using a display converter/upscaler to HDMI, I see the entire overscan area, including a chirp of the colorburst and a bit of noise in the back porch signal (which show up as very faint vertical bars along the left and right borders of the display)...

 

Many games take liberties with the vertical timings, even though they generate a stable 262/263 (sometimes more, sometimes less) display, which causes the display to shift on an upscaler. And I've seen more than a few games that forget to turn off the vertical blank during overscan, leaving the artifacts I've mentioned above.

 

-Thom

  • Like 1
Link to comment

Thank you for the tutorials!  They've been incredibly helpful so far!  I just have a few questions, and I thought I'd ask them in the posts / lessons where they cropped up.

 

You made a comment in your code about how the last six bytes / three words were reserved for the 6507 interrupt vectors, but what exactly are those?  I can't find any mention of them in the official documentation.

 

All three are pointing to the InitSystem subroutine, and they have names like Reset and IRQ, so I'm assuming they're tied to some kind of system-wide event.

Link to comment

The NMI and IRQ vectors are described in any and all documentation about the 6502. These are addresses that are jumped to, when these events occur.

 

The 6507 does not connect the NMI or IRQ lines to anything meaningful, so other than the BRK instruction in code (which generates an IRQ), the NMI and IRQ vectors are meaningless. The only one of any merit on the VCS is RESET, which is the address the 6507 jumps to when the processor is reset.

 

-Thom

  • Like 3
Link to comment
On 7/21/2023 at 11:09 PM, KainXavier said:

Thank you for the tutorials!

 

You're welcome!

 

On 7/21/2023 at 11:09 PM, KainXavier said:

You made a comment in your code about how the last six bytes / three words were reserved for the 6507 interrupt vectors, but what exactly are those?

 

The documentation to look at for those 6 bytes is for the 6507 processor.  The 6507 is a member of the 6500 family of CPUs, so you can find that at 6502.org.

  1. Click on Documents in the top-right corner
  2. Click on Books
  3. Download mcs6500_family_hardware_manual.pd
  4. Download mcs6500_family_programming_manual.pdf

The documentation is quite detailed, so I'll try to summarize it.

 

If an external event occurs the CPU needs to be informed about it so the event can be handled. An example would be if you were calling a BBS with a modem.  The chip that talks to the modem, known as a UART, had limited resources so after it receives a byte of data it must hand it over to the CPU right away before the next byte comes in. It does this by flagging an interrupt to the CPU. Some chips in the system can have a timer, such as RIOT in the 2600.  When the timer runs out an interrupt will be flagged so the CPU knows about it.

 

FFFA-FFFB = NMI vector

 

These 2 bytes point to the routine that handles a Non-Maskable Interrupt. The CPU cannot ignore an NMI, you'd use this for the UART because if the CPU ignored the byte of data it could be lost by being overwritten by the next byte that's received. When an NMI occurs your program is temporarily halted while the CPU runs the NMI routine.

 

FFFC-FFFD = RESET vector

 

These 2 bytes point to the routine that handles a Reset. This occurs when the system is initially powered on because the state of the hardware will be unknown, such as the Accumulator in the CPU will contain a random value between 0-255. The reset routine is used to initialize everything to a known state. The reset can also be triggered on systems that have a dedicated reset button*

 

FFFE-FFFF = IRQ vector

 

These 2 bytes point to the routine that handles Interrupt ReQuest. The CPU can ignore an IRQ, you'd use this for things like the timer as it's not critical that the timer is handled right away because the CPU can check if the timer ran out at any time.  When an IRQ occurs your program is temporarily halted while the CPU runs the IRQ routine.  This can also be triggered by your program via a BRK instruction.

 

 

The 6507 is a 6502 that's put into a reduced package, it only has 28 pins instead of 40. This reduces the cost of the CPU. Some of the missing pins include those needed to signal the NMI and the IRQ, so those cannot be triggered on the 2600 by external events.  The IRQ can still be triggered by a BRK instruction as that happens inside the CPU. I had that occur on accident in Medieval Mayhem.  BRK can be used as a space saving way to call subroutines, though this is an advanced technique. @Thomas Jentzsch explained this in the old Stella mailing list.

 

 

 

the red button on my Mega-Cart is an example of a reset button.  After playing a game I can press that button to reset my VIC20. This lets me select a different game to play without having to power cycle my VIC-20.

IMG_3688.thumb.jpeg.494e26b4a4c4986fd83848cadfe17219.jpeg

 

 

 

  • Like 1
  • Thanks 1
Link to comment
Guest
Add a comment...

×   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...