Difference between revisions of "7800 HSC Technical Reference"

From 8BitDev.org - Atari 7800 Development Wiki
Jump to: navigation, search
(Created page with "= Atari 7800 High Score Cartridge (HSC) Technical Reference & Integration Guide = The '''Atari 7800 High Score Cartridge (HSC)''' is an expansion peripheral that provides non...")
 
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
−
= Atari 7800 High Score Cartridge (HSC) Technical Reference & Integration Guide =
+
= Atari 7800 High Score Cartridge (HSC) Technical Reference =
  
−
The '''Atari 7800 High Score Cartridge (HSC)''' is an expansion peripheral that provides non-volatile battery-backed RAM (NVRAM) and a standardized high-score interface for Atari 7800 games. It connects via a pass-through architecture: the HSC plugs into the 7800 cartridge slot, and the game cartridge plugs into the top of the HSC.
+
The '''Atari 7800 High Score Cartridge (HSC)''' is an expansion peripheral that provides battery-backed RAM (NVRAM) and a standard high score service for Atari 7800 games. It uses a pass-through design: the HSC plugs into the 7800 cartridge slot, and the game cartridge plugs into the top of the HSC.
 +
 
 +
This page covers both the game-facing interface and the internals of the ROM (<code>highscre.bin</code>, 4096 bytes, MD5 <code>c8a73288ab97226c52602204ab894286</code>). Internal addresses below refer to that ROM. The 2026 community disassembly ([[7800 HSC CART]]) reassembles byte-identical with DASM, and its labels are used here.
  
 
== System Architecture & Memory Map ==
 
== System Architecture & Memory Map ==
Line 10: Line 12:
 
! Address Range !! Size !! Device !! Description
 
! Address Range !! Size !! Device !! Description
 
|-
 
|-
−
| <code>$1000–$17FF</code> || 2 KB || Battery-Backed SRAM || 2048 bytes of non-volatile storage (directories, scores, owner name).
+
| <code>$1000–$17FF</code> || 2 KB || Battery-backed SRAM || Owner name, a 69-slot directory and 69 score tables.
 +
|-
 +
| <code>$3000–$3FFF</code> || 4 KB || HSC ROM || API, screen builders, font, strings and jump vectors.
 +
|}
 +
 
 +
The ROM has no 6502 reset or interrupt vectors of its own. It only runs when a game calls into it.
 +
 
 +
=== ROM Layout ===
 +
 
 +
{| class="wikitable"
 +
! Range !! Contents
 +
|-
 +
| <code>$3000–$38FF</code> || Code (vectors' bodies, initialization, ranking, display-list builders, text entry, menu)
 +
|-
 +
| <code>$3900–$3F2B</code> || Font: one 44-byte glyph row per page (<code>$3F00</code> = top row … <code>$3900</code> = bottom row). Offsets <code>$2C–$FF</code> of each page hold more code and data
 +
|-
 +
| <code>$3D2C</code> || Palette values for P4C2…P0C2 (<code>$9D $0A $96 $98 $FA</code>, stored in reverse)
 
|-
 
|-
−
| <code>$3000–$3FFF</code> || 4 KB || HSC ROM || High score BIOS, display list builders, font tiles, and API jump vectors.
+
| <code>$3D31–$3DCB</code> || Encoded DLL streams for each screen (3 bytes per zone; the middle byte is ignored and replaced by page N+1)
 +
|-
 +
| <code>$3E2C–$3EC6</code> || Display-list templates for the table, personalization and menu screens
 +
|-
 +
| <code>$3EC7–$3EDE</code> || Powers of ten, 10,000,000 down to 1, as 24-bit little-endian values
 +
|-
 +
| <code>$3F2C–$3FC3</code> || Strings: <code>CONGRATULATIONS  PLAYER 1</code>, <code>CARTRIDGE</code>/<code>PERSONALIZE</code>/<code>YOUR ATARI HIGH SCORE</code>, and the menu text (<code>WHAT WOULD YOU LIKE TO CLEAR?</code>, <code>THIS GAME</code>, <code>PERSONALIZATION</code>, <code>ALL GAMES</code>, <code>NOTHING</code>, <code>ARE YOU SURE?</code>, <code>YES</code>; "NO" is the first two letters of NOTHING)
 +
|-
 +
| <code>$3FEB–$3FFF</code> || Seven <code>JMP</code> vectors
 
|}
 
|}
  
 
=== Zero-Page Footprint ($80–$BC) ===
 
=== Zero-Page Footprint ($80–$BC) ===
  
−
The HSC uses zero-page locations <code>$80</code> through <code>$BC</code> (61 bytes total) for calculations, string manipulations, and pointers.
+
The HSC uses zero page <code>$80</code>–<code>$BC</code> (61 bytes) while a call runs.
  
−
* '''Automatic Zero-Page Preservation:''' Upon entry, the HSC backs up the caller's zero-page range <code>$80–$BC</code> into the game-provided RAM workspace at <code>UserRAM + $13</code> through <code>UserRAM + $4F</code>.
+
* '''Preservation:''' on entry, <code>HSCInitRoutine</code> (<code>$3483</code>) copies <code>$80–$BC</code> to the caller's page N at <code>+$13..+$4F</code>, in reverse order (<code>$BC</code> → <code>+$13</code> … <code>$80</code> → <code>+$4F</code>). <code>RestoreVarsAndRegsAndExit</code> (<code>$3C46</code>) copies it back.
−
* '''Restoration on Exit:''' Prior to executing <code>RTS</code>, the HSC completely restores <code>$80–$BC</code> from the backup area.
+
* '''$94/$95:''' these hold the HSC's pointer to page N during the call, so their original values are parked in NVRAM <code>$1000</code>/<code>$1001</code> before the backup and restored last.
−
* '''$94/$95 Preservation:''' Locations <code>$94</code> and <code>$95</code> are saved to and restored from dedicated NVRAM scratchpad bytes (<code>$1000</code> and <code>$1001</code>).
+
* The first 18 bytes, <code>$80–$91</code>, are a working copy of the argument block (<code>+$00..+$11</code>).
 +
 
 +
Game code that runs during a call (the sound hook, a custom controller driver, a DLI handler) sees the HSC's values in <code>$80–$BC</code>, so it must not write there.
  
 
=== Dual-Page RAM Requirement (512 Contiguous Bytes) ===
 
=== Dual-Page RAM Requirement (512 Contiguous Bytes) ===
  
−
When calling any HSC function, the game must provide a '''page-aligned 512-byte RAM workspace''' spanning two contiguous 256-byte pages: '''Page N''' and '''Page N+1'''.
+
Every call needs a page-aligned 512-byte RAM work area: '''page N''' (passed in <code>Y</code>) and '''page N+1'''.
  
 
<pre>
 
<pre>
−
Memory Layout of the 512-Byte Workspace:
+
Page N:
−
 
+
  +$00-$11  Argument block (inputs)         +$12      HSCSTAT result byte
−
Page N ($xx00 - $xxFF):
+
  +$13-$4F Zero-page backup ($BC..$80)
−
+-----------------------------------+ $xx00
+
  +$50-$8B Formatted table: 5 rows x 12 bytes (3 initials + 9 score characters)
−
| 18-Byte Argument Block ($00-$11) |
+
  +$8C-$A5 Game name (26)      +$A6-$BF Difficulty name (26)
−
+-----------------------------------+ $xx12
+
  +$C0-$D8  "CONGRATULATIONS  PLAYER n"    +$DA-$F9  Owner name (32)
−
| HSC Internal Scratch ($12)       |
+
  (the personalization and menu screens reuse +$8C onwards for their own strings)
−
+-----------------------------------+ $xx13
 
−
| Zero-Page Backup Area ($13-$4F) | <- Holds saved ZP $80-$BC (61 bytes)
 
−
+-----------------------------------+ $xx50
 
−
|  Formatted Display Workspace      | <- Initials and score string buffer
 
−
+-----------------------------------+ $xxFF
 
  
−
Page N+1 ($xx00 + 1 page):
+
Page N+1:
−
+-----------------------------------+ $(xx+1)00
+
  +$00-$86 Display lists (screen-specific templates, patched at run time)
−
| MARIA Display Lists (DL)         | <- HSC writes screen graphics DLs here
+
  +$87-... Display List List (DPPL = $87, DPPH = page N+1)
−
+-----------------------------------+ $(xx+1)87
 
−
| MARIA Display List List (DLL)    | <- DPPL=$87, DPPH=(Page N + 1)
 
−
+-----------------------------------+ $(xx+1)FF
 
 
</pre>
 
</pre>
  
 
<div style="background: #fff3cd; border-left: 5px solid #ffeeba; padding: 12px; margin: 12px 0;">
 
<div style="background: #fff3cd; border-left: 5px solid #ffeeba; padding: 12px; margin: 12px 0;">
−
'''CRITICAL ARCHITECTURAL WARNING:'''<br/>
+
'''Reserve both pages.''' Display calls build their DLs and DLL in page N+1 and point MARIA there. A game that reserves only 256 bytes gets the following page overwritten. HSCSTAT also overwrites page N <code>+$50..+$8B</code>.
−
The calling game must reserve both Page N and Page N+1. When the HSC renders score tables or the interactive initials screen, it points MARIA's Display List List pointer directly into Page N+1 (<code>DPPH = Page N + 1</code>, <code>DPPL = $87</code>). If only 256 bytes are allocated, the HSC display builder will overwrite program variables located in the subsequent page.
 
 
</div>
 
</div>
  
−
== Cartridge Detection & Handshake ==
+
== Cartridge Detection ==
  
−
To verify that the High Score Cartridge is physically installed before invoking API routines, inspect the two signature bytes located in the HSC font table:
+
Check the two signature bytes, which are the bottom rows of glyphs 'A' and 'E', before calling any vector:
  
 
<pre lang="asm">
 
<pre lang="asm">
−
; ==============================================================================
+
DetectHSC:                 ; carry set = HSC present
−
; DetectHSC: Checks for HSC ROM presence
+
     lda $3900
−
; Returns: Carry Flag Set (1) = HSC Present; Carry Flag Clear (0) = HSC Absent
+
     cmp #$C6
−
; ==============================================================================
+
     bne .no
−
DetectHSC:
+
     lda $3904
−
     LDA $3900               ; First signature byte in Font Row 6
+
     cmp #$FE
−
     CMP #$C6
+
     bne .no
−
     BNE .HscNotFound
+
     sec
−
     LDA $3904               ; Second signature byte in Font Row 6
+
     rts
−
     CMP #$FE
+
.no clc
−
     BNE .HscNotFound
+
     rts
−
     SEC                    ; HSC confirmed present
 
−
     RTS
 
−
.HscNotFound:
 
−
    CLC                    ; HSC not detected
 
−
     RTS
 
 
</pre>
 
</pre>
  
−
== Public Vector Table & API Specification ==
+
Variants seen in games: <code>lda #$C6 / cmp $3900</code> (Asteroids Deluxe), <code>lda $3900 / eor #$C6</code> (current 7800basic), and <code>lda $1002 / cmp #$68</code> (old 7800basic, which checks the NVRAM magic instead).
  
−
The HSC exports seven primary functions via a jump table located at <code>$3FEB–$3FFD</code>.
+
== Public Vector Table & API ==
  
 
{| class="wikitable"
 
{| class="wikitable"
−
! Vector Address !! Routine Name !! Description
+
! Vector !! Name !! Target !! Function ID (<code>$1007</code>) !! Description !! Result in A
 
|-
 
|-
−
| <code>$3FEB</code> || <code>HSCSETRS</code> || Configures MARIA registers (<code>CTRL</code>, <code>CHARBASE</code>, and <code>P0C2–P4C2</code>) for HSC font display.
+
| <code>$3FEB</code> || HSCSETRS || <code>$39A9</code> || — || Sets CTRL <code>$4B</code>, BACKGRND <code>$00</code>, P0C2–P4C2, CHARBASE <code>$39</code>. No argument block, no hold || undefined
 
|-
 
|-
−
| <code>$3FEE</code> || <code>HSCCPYFM</code> || Raw copy: transfers 25 bytes of score data from Cart NVRAM to the pointer in <code>($8F)</code>.
+
| <code>$3FEE</code> || HSCCPYFM || <code>$3000</code> || <code>$EF</code> || Copies the 25-byte table to the buffer pointed to by arg <code>+$0F/$10</code> || <code>$00</code>; <code>$EF</code> if no table (nothing copied)
 
|-
 
|-
−
| <code>$3FF1</code> || <code>HSCCPYTO</code> || Raw copy: transfers 25 bytes of score data from <code>($8F)</code> into Cart NVRAM.
+
| <code>$3FF1</code> || HSCCPYTO || <code>$3011</code> || <code>$04</code> || Copies 25 bytes from arg <code>+$0F/$10</code> into the table, creating it if needed || <code>$01</code>
 
|-
 
|-
−
| <code>$3FF4</code> || <code>HSCENTER2</code> || Headless entry: enters score without initials screen (initials passed in buffer).
+
| <code>$3FF4</code> || HSCENTER2 || <code>$3127</code> || <code>$01</code> || Like HSCENTER but takes the initials from arg <code>+$0F..+$11</code>. '''Not headless''': it still shows the congratulations table || <code>$01</code> if inserted, <code>$00</code> if not
 
|-
 
|-
−
| <code>$3FF7</code> || <code>HSCSTAT</code> || Status/rank query: allocates slot if missing; checks if score qualifies.
+
| <code>$3FF7</code> || HSCSTAT || <code>$3022</code> || <code>$02</code> || Query: rank, top score, number of tables. Never creates a table or updates LRU ages || <code>$00</code>
 
|-
 
|-
−
| <code>$3FFA</code> || <code>HSCATRCT</code> || Attract mode: renders and cycles high score tables for each difficulty level.
+
| <code>$3FFA</code> || HSCATRCT || <code>$30DA</code> || <code>$FF</code> (then <code>$FD</code>) || Shows one game/difficulty table (it doesn't cycle through difficulties) || <code>$00</code>; <code>$FF</code> if no table (returns at once)
 
|-
 
|-
−
| <code>$3FFD</code> || <code>HSCENTER</code> || Interactive entry: checks rank, displays initials screen, shifts table, and saves.
+
| <code>$3FFD</code> || HSCENTER || <code>$312B</code> || <code>$00</code> || Ranks the score; if it qualifies, inserts it and runs interactive initials entry || <code>$01</code> if inserted (also after a cancel), <code>$00</code> if not
 
|}
 
|}
  
−
*(Note: Some disassemblies mislabel the vector at <code>$3FEE</code> as <code>$3FFE</code>. The physical address is <code>$3FEE</code>.)*
+
=== Calling Conventions ===
  
−
=== Calling Conventions ===
+
* '''Y:''' high byte of page N.
 +
* '''X:''' ignored. <code>HSCInitRoutine</code> always uses offset <code>$00</code>.
 +
* '''On return:''' I flag set (<code>SEI</code> is never undone), D clear, stack balanced. X/Y undefined.
 +
* '''Return path:''' <code>HSCInitRoutine</code> slips two bytes, <code>$3C</code> and <code>$2B</code>, under its own return address. When the vector's body finishes with <code>RTS</code>, control goes to <code>$3C2C</code>, the common display hold. That code pushes the result, runs the hold, and leaves through <code>RestoreVarsAndRegsAndExit</code>, which pulls the result into A and returns to the game. So every vector except HSCSETRS ends in the hold, including HSCSTAT and the copy calls.
  
−
All HSC API routines require the following register state upon entry:
+
=== Common Entry Processing (<code>HSCInitRoutine</code>, $3483) ===
−
* '''<code>Y</code> Register:''' Must contain the '''Page Number (High Byte)''' of the argument block (e.g., <code>LDY #$25</code> for <code>$2500</code>).
 
−
* '''<code>X</code> Register:''' Must be set to <code>#$00</code>.
 
−
* '''Interrupts & Decimal Flag:''' Handled automatically by <code>HSCInitRoutine</code> (executes <code>SEI</code> and <code>CLD</code>).
 
−
* '''Stack Note:''' <code>HSCInitRoutine</code> pushes temporary frame marker bytes (<code>$3C</code>, <code>$2B</code>) to the stack. Routines called via standard <code>JSR</code> will return cleanly via <code>RTS</code>.
 
  
−
== User Argument Block Specification (Page N) ==
+
# Store the function ID at <code>$1007</code>, then <code>SEI</code>, <code>CLD</code>.
 +
# Display vectors (IDs other than <code>$02</code>, <code>$EF</code> and <code>$04</code>): CTRL = <code>$7F</code> (DMA off), AUDV0 = AUDV1 = 0.
 +
# Park <code>$94/$95</code> in <code>$1000/$1001</code> and back up the zero page.
 +
# Read SWCHB into <code>$BC</code> and write <code>$FF</code> to SWCHB, which drives any output bits high and forces one-button joystick mode. The saved value is written back on exit.
 +
# Copy argument bytes <code>+$00..+$11</code> to <code>$80..$91</code>. Player index = <code>+$02 & 3</code> → <code>$A9</code>.
 +
# If the name-pointer high byte '''and''' the logo-pointer high byte are both 0, overwrite <code>$80–$8E</code> with the defaults at <code>$39EF</code>: <code>$80 $01 $F1 $01 $79 $E9 $00 $1A $4C $00 $00 $00 $FF $3C $0A</code>. That's ID <code>$80/$01</code>, difficulty <code>$F0</code>, player 2, joystick, name at <code>$E979</code>, difficulty at <code>$1A00</code>, score at <code>$004C</code>, no logo, sound hook <code>$3CFF</code> (an <code>RTS</code>), 10-second hold. The purpose of this set is unknown.
 +
# Do the stack trick described above.
 +
# Display vectors: load the palette and CHARBASE (<code>HSCSETRS2</code>, <code>$39AD</code>).
 +
# Check the NVRAM magic (<code>$3512</code>). If it fails, check again (one retry), and if it still fails, format (<code>$3535</code>). Formatting writes the magic, sets every slot's difficulty and age to <code>$7F</code> (<code>$3FCF</code>), and then, unless the call is HSCCPYFM or HSCCPYTO, runs personalization (<code>$3C95</code>) and turns DMA off.
 +
# Convert the game's BCD score to binary (<code>$3A81</code>, see [[#Score conversion|Score conversion]]). This runs for '''every''' vector.
 +
# Look up the directory (<code>$3572</code>) and set <code>$A6/$A7</code> to the table address.
  
−
The argument block resides at <code>Page N + $00</code> through <code>Page N + $11</code> (18 bytes).
+
== User Argument Block (Page N) ==
  
 
{| class="wikitable"
 
{| class="wikitable"
−
! Offset !! Size !! Field Name !! Description
+
! Offset !! Size !! ZP copy !! Field !! Description
 
|-
 
|-
−
| <code>+$00</code> || 1 || <code>ArgGameID1</code> || Game ID byte 1 (Unique Atari Cart ID, e.g., <code>$8B</code> for Joust).
+
| <code>+$00</code> || 1 || <code>$80</code> || Game ID 1 || '''Bit 7 set:''' the game passes its score ÷ 10. The table shows an extra trailing 0 and the top score returned by HSCSTAT is in the game's units
 
|-
 
|-
−
| <code>+$01</code> || 1 || <code>ArgGameID2</code> || Game ID byte 2 (e.g., <code>$23</code> for Joust).
+
| <code>+$01</code> || 1 || <code>$81</code> || Game ID 2 ||
 
|-
 
|-
−
| <code>+$02</code> || 1 || <code>ArgGameDifficulty</code> || Bits 7–2: Difficulty setting (masked with <code>$FC</code>).<br/>Bits 1–0: Active controller index (0 = Player 1, 1 = Player 2).
+
| <code>+$02</code> || 1 || <code>$82</code> || Difficulty / player || Bits 7–2: difficulty (one table per value). Bits 1–0: player index. 0 = left port (SWCHA high nibble, INPT4); 1–3 = right port (SWCHA low nibble, INPT5). Shown as "PLAYER index+1"
 
|-
 
|-
−
| <code>+$03</code> || 1 || <code>ArgGameController</code> || Controller driver: <code>$00/$01</code> = Standard Joystick; <code>$02</code> = Custom Routine.
+
| <code>+$03</code> || 1 || <code>$83</code> || Controller type || <code>$02</code> = custom driver at the name pointer. Any other value = joystick (<code>$00</code> and <code>$01</code> behave the same)
 
|-
 
|-
−
| <code>+$04</code> || 2 || <code>ArgGameNamePtr</code> || Pointer (16-bit Lo/Hi) to Watkins-encoded Game Title string (or custom driver).
+
| <code>+$04</code> || 2 || <code>$84</code> || Game name pointer || '''Exactly 26''' characters, read raw and drawn centered (pad with <code>$1D</code>). With type <code>$02</code>, also the driver entry point. '''Overwritten by HSCSTAT'''
 
|-
 
|-
−
| <code>+$06</code> || 2 || <code>ArgDifficultyNamePtr</code> || Pointer (16-bit Lo/Hi) to Watkins-encoded Difficulty text string.
+
| <code>+$06</code> || 2 || <code>$86</code> || Difficulty name pointer || 26 characters. High byte 0 = no difficulty line (the 26 bytes are still copied from <code>$00xx</code>, see bugs)
 
|-
 
|-
−
| <code>+$08</code> || 2 || <code>ArgGameScorePtr</code> || Pointer (16-bit Lo/Hi) to 4-byte packed BCD score data in game RAM.
+
| <code>+$08</code> || 2 || <code>$88</code> || Score pointer || 4-byte packed BCD, big-endian (<code>1,234,560</code> = <code>$01 $23 $45 $60</code>). Read on every call, and written to by the clamp
 
|-
 
|-
−
| <code>+$0A</code> || 2 || <code>ArgLogoDllPtr</code> || Pointer (16-bit Lo/Hi) to Game Logo Display List List (DLL), or 0 if using text title.
+
| <code>+$0A</code> || 2 || <code>$8A</code> || Logo DLL pointer || High byte 0 = show the game name as text
 
|-
 
|-
−
| <code>+$0C</code> || 2 || <code>ArgSoundRoutinePtr</code> || Pointer (16-bit Lo/Hi) to sound callback executed during attract wait loops.
+
| <code>+$0C</code> || 2 || <code>$8C</code> || Sound hook || Called once per frame during the hold and during initials entry. Must be valid
 
|-
 
|-
−
| <code>+$0E</code> || 1 || <code>ArgHSDisplayTime</code> || Attract mode screen display timer multiplier.
+
| <code>+$0E</code> || 1 || <code>$8E</code> || Hold time || Seconds (60-frame units) of display hold before returning. 0 = none
 
|-
 
|-
−
| <code>+$0F</code> || 4 || <code>Returned Status</code> || Rank and checksum status bytes returned by <code>HSCSTAT</code> (offsets <code>+$0F–$12</code>).
+
| <code>+$0F</code> || 2 || <code>$8F</code> || Buffer pointer / initials || HSCCPYFM/HSCCPYTO: 25-byte buffer pointer. HSCENTER2: initials 1 and 2
 
|-
 
|-
−
| <code>+$13</code> || 61 || <code>ZP Preservation Area</code> || Scratchpad reserved for HSC to save/restore Zero-Page <code>$80–$BC</code>.
+
| <code>+$11</code> || 1 || <code>$91</code> || Initial 3 || HSCENTER2 only
 +
|-
 +
| <code>+$12</code> || 1 || — || (result) || Written by HSCSTAT
 
|}
 
|}
  
−
=== Field Specifications ===
+
=== HSCSTAT Results ===
  
−
; <code>ArgGameDifficulty</code> (Offset +$02)
+
{| class="wikitable"
−
: Bits 7..2 represent the game difficulty index. The HSC maintains separate 5-score tables for each difficulty level. Bits 1..0 specify which player's controller is polled during initials entry and attract screen skipping.
+
! Offset !! Result
 +
|-
 +
| <code>+$04</code> || Number of directory slots with this game's ID 1 and ID 2 and difficulty ≠ <code>$7F</code>, i.e. how many difficulty tables exist (0–69)
 +
|-
 +
| <code>+$05</code> || Rank: <code>$00</code> = 1st place … <code>$04</code> = 5th, <code>$FF</code> = doesn't qualify
 +
|-
 +
| <code>+$0F..+$12</code> || Current 1st-place score as 4-byte packed BCD, big-endian, in the game's units
 +
|}
 +
 
 +
* A score of 0 never qualifies (<code>IsScoreNonZero</code>, <code>$3DDC</code>).
 +
* A score '''equal''' to an existing entry ranks above it.
 +
* If the game/difficulty has no table, <code>+$05</code> is <code>$FF</code> for any score and <code>+$0F..+$12</code> are 0. So a game that calls HSCENTER only when HSCSTAT reports a qualifying rank can never create its first table.
 +
* How the top score is produced: HSCSTAT formats the table as text (<code>FormatAllScoresForDisplay</code>, <code>$3769</code>), then packs the eight digit characters of row 1 (<code>$30A9</code>) back into BCD, treating spaces as 0.
  
−
; <code>ArgGameController</code> (Offset +$03)
+
=== Sound Hook ===
−
: When set to <code>$00</code> or <code>$01</code>, the HSC directly reads the RIOT and TIA registers (<code>SWCHA</code>, <code>INPT4</code>, <code>INPT5</code>). When set to <code>$02</code>, the HSC executes an indirect jump to <code>(ArgGameNamePtr)</code> to poll custom controllers.
 
  
−
; <code>ArgGameScorePtr</code> (Offsets +$08, +$09)
+
The hook is reached by <code>JSR $36DB</code>, which does <code>JMP (SOUNDPTR)</code>, so the hook returns with <code>RTS</code>. It may clobber A, X and Y. It's called during the hold (but not while paused) and during initials entry. It isn't called during personalization or the menu.
−
: Pointer to a '''4-byte (8-digit) packed BCD score''' in Big-Endian order:
 
−
: <code>1,234,560 points = $01, $23, $45, $60</code>
 
−
: Maximum score value: <code>16,777,215</code> (<code>$16 $77 $72 $15</code> in BCD). Values higher than this will be clamped to <code>$FFFFFF</code>.
 
  
−
; <code>ArgSoundRoutinePtr</code> (Offsets +$0C, +$0D)
+
=== Custom Controller Driver ($03 = $02) ===
−
: Pointer to the game's audio driver. During attract mode display and initials entry loops, the HSC executes <code>JSR (ArgSoundRoutineLo)</code> once per frame.
 
  
−
; <code>Returned Rank Status</code> (Offset +$05 on return from HSCSTAT)
+
Reached by <code>JMP ($84)</code> (<code>$39A6</code>) from a subroutine call, with X = player index. It must return:
−
: * <code>$01–$05</code>: The submitted score qualifies for 1st through 5th place.
+
* A bits 7–4 = right, left, down, up (0 = pushed)
−
: * <code>$FF</code>: The submitted score did not qualify.
+
* Y bit 7 = fire (0 = pressed)
  
−
== Data Representations & Internal Encodings ==
+
Driver input is used for text entry, the hold's exit check and fire-release waits. The hold's raw checks (INPT4 and any SWCHA bit) are skipped for type <code>$02</code>. The screen saver (<code>$392C</code>) and the maintenance menu still read the real ports. Because the name pointer doubles as the driver address, a driver game should supply a logo, or its code bytes get drawn as the game name.
  
−
=== Watkins Character Set & Font Map ===
+
=== Logo DLL ===
  
−
The HSC does not use ASCII. Text strings must use the proprietary '''Watkins encoding''' matching the internal font table at <code>$3900–$3F00</code>.
+
The decoder (<code>$36DE</code>) copies the game's 3-byte DLL entries verbatim to the start of the DLL. It keeps a running total of zone heights (<code>(byte0 & $0F) + 1</code>) and stops after the entry that brings the total to at least '''65 lines''' (<code>$41</code>, when a difficulty name is present) or '''79 lines''' (<code>$4F</code>, when it isn't).
 +
 
 +
DLI handling:
 +
* If the '''first''' logo entry has its DLI bit set, <code>$B9</code> is set to <code>$40</code>.
 +
* <code>SetupDLL</code> (<code>$3715</code>) shifts <code>$B9</code> left for every HSC entry flagged with a DLI. The first such entry keeps its DLI bit; all later ones have it stripped.
 +
* Without a logo DLI, every HSC DLI bit is stripped.
 +
 
 +
The one kept DLI lets the game's NMI handler restore the HSC's CHARBASE and palette after a logo that changed them.
 +
 
 +
== Data Representations ==
 +
 
 +
=== Character Set & Font ===
 +
 
 +
The HSC uses its own character codes, often called '''Watkins encoding'''. Letters are <code>'X' - 'A'</code>.
  
 
{| class="wikitable" style="text-align: center;"
 
{| class="wikitable" style="text-align: center;"
−
! Hex !! Dec !! Glyph !! Hex !! Dec !! Glyph !! Hex !! Dec !! Glyph
+
! Code !! Glyph !! Code !! Glyph !! Code !! Glyph
 
|-
 
|-
−
| <code>$00</code> || 0 || '''A''' || <code>$0F</code> || 15 || '''P''' || <code>$1E</code> || 30 || '''''&#39;''''' (Apostrophe)
+
| <code>$00–$19</code> || '''A'''–'''Z''' || <code>$1D</code> || (space) || <code>$21–$2A</code> || '''0'''–'''9'''
 
|-
 
|-
−
| <code>$01</code> || 1 || '''B''' || <code>$10</code> || 16 || '''Q''' || <code>$1F</code> || 31 || '''&#x2190;''' (Left / Erase)
+
| <code>$1A</code> || '''_''' (underscore: full-width bar on the bottom row) || <code>$1E</code> || ''' ' ''' (apostrophe) || <code>$2B</code> || '''?'''
 
|-
 
|-
−
| <code>$02</code> || 2 || '''C''' || <code>$11</code> || 17 || '''R''' || <code>$20</code> || 32 || '''&#x2588;''' (Block Cursor)
+
| <code>$1B</code> || '''.''' (period) || <code>$1F</code> || '''█''' (solid block: END in text entry) || ||
 +
|-
 +
| <code>$1C</code> || '''-''' (hyphen) || <code>$20</code> || '''←''' (left arrow: BACKSPACE in text entry) || ||
 +
|}
 +
 
 +
There's '''no colon'''.
 +
 
 +
Glyphs are 8×7 pixels, drawn in 320A mode with CHARBASE <code>$39</code>. Row r of glyph c (r = 0 at the top) is at <code>$3900 + (6−r)×$100 + c</code>.
 +
 
 +
DASM macro:
 +
<pre lang="asm">
 +
    mac WATKINS
 +
        dc.b {1}-"A"
 +
    endm
 +
</pre>
 +
 
 +
=== Packed Initials (15 bits) ===
 +
 
 +
<pre>
 +
Byte 0: [ C1.4 | C1.3 | C1.2 | C0.4 | C0.3 | C0.2 | C0.1 | C0.0 ]
 +
Byte 1: [ C1.1 | C1.0 |  0  | C2.4 | C2.3 | C2.2 | C2.1 | C2.0 ]
 +
 
 +
byte0 = ((c1 >> 2) << 5) | c0        c0 = byte0 & $1F
 +
byte1 = ((c1 & 3) << 6) | c2        c1 = ((byte0 >> 5) << 2) | (byte1 >> 6)
 +
                                    c2 = byte1 & $1F
 +
</pre>
 +
 
 +
* Packing: <code>EncodeAndSaveInitialsToTable</code>, <code>$31D7</code>. Unpacking: <code>$3AD6</code>.
 +
* Empty entries hold <code>___</code> (three underscores, <code>$1A $1A $1A</code>), packed as <code>$DA $9A</code>.
 +
* Other examples: <code>ABC</code> = <code>$00 $42</code>, <code>XYZ</code> = <code>$D7 $19</code>.
 +
 
 +
=== Score conversion ===
 +
 
 +
Games pass 8-digit packed BCD. NVRAM holds a '''24-bit binary''' value, little-endian.
 +
 
 +
# '''Clamp''' (<code>$3A83</code>): the score is compared byte by byte with <code>$16 $77 $72 $15</code> (16,777,215). If it's larger, the maximum is '''written back into the game's score''' and used instead. The comparison is signed, see [[#Bugs and quirks in the ROM|bugs]].
 +
# '''Convert''' (<code>$3AA8</code>): for each nibble, the value is multiplied by 10 (<code>$3A5E</code>, a full 24-bit ×10) and the nibble is added. The result goes in <code>$AD</code> (LSB), <code>$AE</code>, <code>$AF</code> (MSB). The nibble add has a carry bug, see [[#Bugs and quirks in the ROM|bugs]].
 +
# '''Display''' (<code>$37BC</code>): the value is turned into decimal by repeated subtraction of the powers of ten at <code>$3EC7</code>, giving a 9-character field:
 +
#* ID1 bit 7 clear: a leading space, then 8 digits with leading zeros blanked. The last digit always shows.
 +
#* ID1 bit 7 set: 8 digits with leading zeros blanked, then a final <code>0</code>.
 +
 
 +
== Cartridge NVRAM Layout ($1000–$17FF) ==
 +
 
 +
{| class="wikitable"
 +
! Address !! Size !! Disassembly label !! Contents
 
|-
 
|-
−
| <code>$03</code> || 3 || '''D''' || <code>$12</code> || 18 || '''S''' || <code>$21</code> || 33 || '''0'''
+
| <code>$1000–$1001</code> || 2 || <code>CartRAMSaveZ94/Z95</code> || Caller's <code>$94/$95</code> during a call
 
|-
 
|-
−
| <code>$04</code> || 4 || '''E''' || <code>$13</code> || 19 || '''T''' || <code>$22</code> || 34 || '''1'''
+
| <code>$1002–$1006</code> || 5 || <code>CartRAMMagic1..5</code> || <code>$68 $83 $AA $55 $9C</code> = formatted
 
|-
 
|-
−
| <code>$05</code> || 5 || '''F''' || <code>$14</code> || 20 || '''U''' || <code>$23</code> || 35 || '''2'''
+
| <code>$1007</code> || 1 || <code>CartRAMEntryID</code> || Function ID of the call in progress (<code>$FD</code>/<code>$FE</code>/<code>$FF</code> during attract-style displays)
 
|-
 
|-
−
| <code>$06</code> || 6 || '''G''' || <code>$15</code> || 21 || '''V''' || <code>$24</code> || 36 || '''3'''
+
| <code>$1008–$1027</code> || 32 || <code>CartRAMOwnerName</code> || Owner name. Bytes after the length are leftover; the END code <code>$1F</code> usually follows the last character
 
|-
 
|-
−
| <code>$07</code> || 7 || '''H''' || <code>$16</code> || 22 || '''W''' || <code>$25</code> || 37 || '''4'''
+
| <code>$1028</code> || 1 || <code>CartRAMOwnerNameLength</code> || 1–32
 
|-
 
|-
−
| <code>$08</code> || 8 || '''I''' || <code>$17</code> || 23 || '''X''' || <code>$26</code> || 38 || '''5'''
+
| <code>$1029–$106D</code> || 69 || <code>CartRAMGameID1Table</code> || Game ID 1 per slot
 
|-
 
|-
−
| <code>$09</code> || 9 || '''J''' || <code>$18</code> || 24 || '''Y''' || <code>$27</code> || 39 || '''6'''
+
| <code>$106E–$10B2</code> || 69 || <code>CartRAMGameID2Table</code> || Game ID 2 per slot
 
|-
 
|-
−
| <code>$0A</code> || 10 || '''K''' || <code>$19</code> || 25 || '''Z''' || <code>$28</code> || 40 || '''7'''
+
| <code>$10B3–$10F7</code> || 69 || <code>CartRAMGameDifficultyTable</code> || Difficulty (<code>& $FC</code>) per slot. <code>$7F</code> = free
 
|-
 
|-
−
| <code>$0B</code> || 11 || '''L''' || <code>$1A</code> || 26 || '''.''' (Period) || <code>$29</code> || 41 || '''8'''
+
| <code>$10F8–$113C</code> || 69 || <code>CartRAMGameAgeTable</code> || LRU age per slot. <code>$7F</code> = free
 
|-
 
|-
−
| <code>$0C</code> || 12 || '''M''' || <code>$1B</code> || 27 || ''':''' (Colon) || <code>$2A</code> || 42 || '''9'''
+
| <code>$113D–$17F9</code> || 1725 || — || 69 tables × 25 bytes; slot s at <code>$113D + 25·s</code>
 
|-
 
|-
−
| <code>$0D</code> || 13 || '''N''' || <code>$1C</code> || 28 || '''-''' (Hyphen) || <code>$2B</code> || 43 || '''?''' (Question Mark)
+
| <code>$17FA–$17FE</code> || 5 || <code>CartRAMSavedEntry</code> || Copy of the displaced 5th-place entry during HSCENTER, used to undo a cancel
 
|-
 
|-
−
| <code>$0E</code> || 14 || '''O''' || <code>$1D</code> || 29 || ''(Space)'' || colspan="3" |
+
| <code>$17FF</code> || 1 || — || Unused
 
|}
 
|}
  
−
DASM Watkins macro:
+
Formatting only writes the magic and the <code>$7F</code> markers. IDs, tables and the owner name keep whatever was there.
−
<pre lang="asm">
+
 
−
    mac WATKINS
+
=== Score Tables ===
−
        dc.b {1}-"A"
+
 
−
    endm
+
Each table holds 5 entries of 5 bytes, 1st place first: packed initials (2 bytes), then the score as 24-bit little-endian binary (3 bytes). A new table is 5 × (<code>$DA $9A $00 $00 $00</code>).
−
</pre>
+
 
 +
=== Directory Lookup & LRU ===
 +
 
 +
'''Lookup''' (<code>$3572</code>):
 +
* Scans slots 68 down to 0 for a slot whose ID 1, ID 2 and difficulty (<code>arg+$02 & $FC</code>) all match. A free slot can't match, because <code>$7F</code> has its low bits set.
 +
* While scanning, it tracks the slot with the largest age (signed compare, strictly greater). That slot is the eviction victim: free slots (<code>$7F</code>) win, and on ties the highest slot number wins.
 +
 
 +
'''Touch''' (<code>$35A1</code>), done by every call except HSCSTAT:
 +
* Let old = the slot's age. Set its age to 0.
 +
* Then increment every age that is less than old (signed), the touched slot included.
 +
* So the most recently used slot ends with age '''1''', not 0. In-use ages stay distinct from 1 upward, and free slots stay <code>$7F</code>.
 +
 
 +
'''Not found''' (<code>$35E0</code>):
 +
* Negative function IDs (HSCATRCT <code>$FF</code>, HSCCPYFM <code>$EF</code>) drop the call's return address, store the ID in the hold timer, and return the ID straight away, with no hold. DMA was already turned off and audio muted for HSCATRCT.
 +
* HSCSTAT reports "no table".
 +
* Everything else creates a table (<code>$35F4</code>) in the victim slot, so when all 69 slots are in use the least recently used game is evicted.
 +
 
 +
== Interactive Behavior ==
 +
 
 +
=== Display Hold ($3C2C) ===
 +
 
 +
* The hold counts down <code>arg+$0E</code> seconds. Each second is 60 passes, and each pass waits one vertical blank, checks input, and calls the sound hook.
 +
* The input check (<code>$3C63</code>) ends the hold on any of:
 +
** P1 fire, or any SWCHA bit (both of these are skipped for driver games)
 +
** RESET or SELECT
 +
** the selected controller (joystick or driver)
 +
* '''Pause''' (<code>$3B51</code>), triggered by SWCHB bit 3 low on two reads two frames apart:
 +
** Wait for release, mute audio, and stop calling the hook.
 +
** While paused, the exit inputs above still work. Another PAUSE press resumes the countdown.
 +
** Holding PAUSE and pressing SELECT (<code>$3B71</code>) opens the maintenance menu.
 +
** After about 233×256 frames (roughly 16 minutes) paused, the screen saver (<code>$392C</code>) turns DMA off until any input.
 +
 
 +
=== Initials Entry ($3242) ===
 +
 
 +
'''Setup.''' The new score has already been inserted: lower entries shifted down, and the old 5th place saved at <code>$17FA</code>. The inserted row still shows the displaced entry's initials.
 +
 
 +
'''The cursor''' is a separate one-character DL object on the row. It blinks every 30 frames by toggling palette bit 2 (P0 ↔ P4). It starts at position 0, showing the character already there.
 +
 
 +
'''Stepping through characters.''' The stick is sampled every 10 frames:
 +
* '''Up/Right''' = next: A…Z, _, ., -, space, ', █, ←, then A. At position 0 the ← is skipped.
 +
* '''Down/Left''' = previous, the same cycle backwards.
 +
* Pressing Next on <code>_</code> as the first move at a position jumps straight to 'A'.
 +
* Position 3 offers only █ (END) and ← (BACKSPACE).
 +
 
 +
'''Fire''' is checked every frame and acts after release:
 +
* On a character: accept it and move right.
 +
* On ←: move left. The ← glyph stays drawn in the position left behind.
 +
* On █: finish.
 +
* RESET or SELECT finishes at the current position.
 +
 
 +
'''Finishing:'''
 +
* At position 0, finishing cancels: the table is shifted back and the old 5th place restored from <code>$17FA</code>. The routine still falls through into the packing code, which rewrites the restored entry's initials unchanged.
 +
* At positions 1–2, the remaining initials are filled with spaces.
 +
 
 +
There's no time-out. A screen saver kicks in after about 15 minutes (233×233 frames) without input.
  
−
=== 15-Bit Packed Initials Format ===
+
=== Personalization ($3C95) ===
  
−
Initials consist of three 5-bit characters (<code>$00–$1F</code>) packed into '''2 consecutive bytes''':
+
Shown on first use and from the menu. The screen reads PERSONALIZE / YOUR ATARI HIGH SCORE / CARTRIDGE, and an entry line underneath stays centered as it grows. Entry rules:
 +
* Up to 32 characters, same character cycle as the initials, no digits.
 +
* It always uses the left joystick and has no console abort.
 +
* Each new position starts on the previous character.
 +
* █ (END) finishes. An empty name becomes one space.
 +
* Before entry starts, the owner name in NVRAM is set to <code>A T A R I</code> (length 9) in case power is lost mid-entry.
  
−
<pre>
+
=== Maintenance Menu ($3B7F) ===
−
Character 0: C0 [4..0]
 
−
Character 1: C1 [4..0]
 
−
Character 2: C2 [4..0]
 
  
−
Byte 0: [ C1.4 | C1.3 | C1.2 | C0.4 | C0.3 | C0.2 | C0.1 | C0.0 ]
+
The menu reads WHAT WOULD YOU LIKE TO CLEAR?, with four choices: THIS GAME, ALL GAMES, PERSONALIZATION, NOTHING. The selection starts on NOTHING, and Up/Down move it, wrapping around. Fire brings up ARE YOU SURE? YES NO: Left/Right choose, starting on NO, and NO returns to the menu.
−
Byte 1: [ C1.1 | C1.0 |  0  | C2.4 | C2.3 | C2.2 | C2.1 | C2.0 ]
 
−
</pre>
 
  
−
==== Packing Algorithm ====
+
What each choice does after YES (dispatcher <code>$39C8</code>):
−
<pre lang="asm">
+
* '''THIS GAME''': frees the current game/difficulty slot only (<code>$3FC4</code>). The IDs and score bytes stay behind.
−
; Inputs: Initials in ZP buffer (z98) at offsets 0, 1, 2
+
* '''ALL GAMES''': frees all 69 slots. The owner name is kept.
−
; Outputs: A = Packed Byte 0, Y = Packed Byte 1
+
* '''PERSONALIZATION''': runs the name entry.
−
PackInitials:
+
* '''NOTHING''': leaves the HSC, returning the original result to the game.
−
    LDY #$01
 
−
    LDA (z98),Y        ; Character 1
 
−
    AND #$1C            ; Mask bits 4, 3, 2
 
−
    ASL
 
−
    ASL
 
−
    ASL                ; Shift to bits 7, 6, 5
 
−
    DEY
 
−
    ORA (z98),Y        ; Combine with Character 0 (bits 4..0)
 
−
    PHA                ; Save Byte 0
 
−
    INY
 
−
    LDA (z98),Y        ; Character 1
 
−
    AND #$03            ; Mask bits 1, 0
 
−
    CLC
 
−
    ROR
 
−
    ROR
 
−
    ROR                ; Rotate bits 1, 0 into bits 7, 6
 
−
    INY
 
−
    ORA (z98),Y        ; Combine with Character 2 (bits 4..0)
 
−
    TAY                ; Y = Byte 1
 
−
    PLA                ; A = Byte 0
 
−
    RTS
 
−
</pre>
 
  
−
==== Unpacking Algorithm ====
+
The first three return to the menu afterwards. The menu has no time-out, no console exit and no sound hook, and it always uses the left joystick.
−
<pre lang="asm">
 
−
; Inputs: A = Byte 0, X = Byte 1
 
−
; Outputs: A = Char 0, X = Char 1, Y = Char 2
 
−
UnpackInitials:
 
−
    PHA
 
−
    AND #$E0            ; Mask bits 7..5 of Byte 0
 
−
    LSR
 
−
    LSR
 
−
    LSR
 
−
    STA zB1            ; zB1 = C1 bits 4..2
 
−
    TXA
 
−
    AND #$1F
 
−
    TAY                ; Y = Char 2 (bits 4..0 of Byte 1)
 
−
    TXA
 
−
    AND #$C0            ; Mask bits 7..6 of Byte 1
 
−
    CLC
 
−
    ROL
 
−
    ROL
 
−
    ROL                ; Rotate to bits 1..0
 
−
    ORA zB1
 
−
    TAX                ; X = Char 1
 
−
    PLA
 
−
    AND #$1F            ; A = Char 0
 
−
    RTS
 
−
</pre>
 
  
−
* '''Default Initials:''' The placeholder initials <code>"..."</code> (Period, Period, Period = <code>26, 26, 26</code> / <code>$1A, $1A, $1A</code>) pack into <code>Byte 0 = $DA</code>, <code>Byte 1 = $9A</code>.
+
=== Screen Layout & Colors ===
  
−
=== Score Conversion: BCD to 24-Bit Binary ===
+
Colors: BACKGRND <code>$00</code>, P0C2 <code>$FA</code> (headers, cursor), P1C2 <code>$98</code> (scores and initials of rows 2–5), P2C2 <code>$96</code> (rank digits 2–5), P3C2 <code>$0A</code> (game, difficulty and owner names), P4C2 <code>$9D</code> (1st-place row, cursor blink).
  
−
While games supply a 4-byte packed BCD score pointer, '''the HSC converts the score into a 24-bit binary integer for ranking and Cart NVRAM storage''':
+
The table screen, top to bottom:
 +
# '''Header:''' 10+16+16 blank lines, the game name (26 characters at x = 56 in 320 mode), then 16 blank lines. A game logo replaces this.
 +
# '''Difficulty''' (optional): 7 blank lines, then the difficulty name.
 +
# '''Entry layout only:''' 16 blank lines, <code>CONGRATULATIONS</code>, 7 blank lines, <code>PLAYER n</code>.
 +
# '''Table:''' 16 blank lines, the owner name (centered), then five rows. Each row is a rank digit at x = 100, the 9-character score at x = 116 and the initials at x = 196, separated by 7-line gaps.
 +
# Blank 7-line zones pad the DLL to at least 243 lines.
  
−
# '''Unpack and Multiply:''' Routine <code>e3AA8</code> converts the 4-byte BCD digits into a 24-bit unsigned binary integer stored across three zero-page registers: <code>zAD</code> (LSB), <code>zAE</code> (MID), and <code>zAF</code> (MSB).
+
The community "PAL hack" (<code>PALHACK</code> in the disassembly) only lengthens the gap zones.
−
# '''NVRAM Storage:''' The score is saved as a '''little-endian 24-bit integer''' occupying 3 bytes:
 
−
#* <code>Offset +0:</code> Initials Byte 0
 
−
#* <code>Offset +1:</code> Initials Byte 1
 
−
#* <code>Offset +2:</code> Score LSB (bits 0–7)
 
−
#* <code>Offset +3:</code> Score MID (bits 8–15)
 
−
#* <code>Offset +4:</code> Score MSB (bits 16–23)
 
−
# '''Display Formatting:''' When drawing score tables, routine <code>e37BC</code> converts the 24-bit binary integer back into decimal characters using a table of powers of 10 (<code>$989680</code> = 10,000,000 down to <code>$000001</code> = 1).
 
  
−
== Cartridge NVRAM Layout ($1000–$17FF) ==
+
== Bugs and Quirks in the HSC ROM ==
  
 
{| class="wikitable"
 
{| class="wikitable"
−
! Address Range !! Size !! Field Identifier !! Description
+
! # !! Where !! Behavior !! Effect
 
|-
 
|-
−
| <code>$1000–$1001</code> || 2 || <code>CartRAMSaveZ94/Z95</code> || Storage for caller's zero-page <code>$94/$95</code>.
+
| 1 || <code>$3ABB</code>, <code>$3ACC</code> || '''Carry lost in BCD→binary conversion.''' After each nibble is added to the low byte, the carry goes into the middle byte with <code>ADC #$00</code> but never on into the high byte (<code>$AF</code>). || Whenever the running value crosses a multiple of 65,536 during a nibble add, 65,536 is lost. 65,536 → 0, 131,072 → 65,536, '''655,360 → 0''', which can then never rank. 14 multiples of 10 below 2,000,000 are affected.
 
|-
 
|-
−
| <code>$1002–$1006</code> || 5 || <code>CartRAMMagic</code> || Magic bytes (<code>$68, $83, $AA, $55, $9C</code>).
+
| 2 || <code>$3A8B–$3A91</code> || '''Signed clamp compare.''' <code>CMP</code> then <code>BMI/BPL</code> tests the sign of (limit − score) instead of the carry. || First byte <code>$97–$99</code> (scores ≥ 97,000,000) isn't clamped, and neither is <code>$16 $77 $72</code> followed by <code>$96–$99</code>. Those values wrap modulo 2^24.
 
|-
 
|-
−
| <code>$1007</code> || 1 || <code>CartRAMEntryID</code> || ID of entry point currently executing.
+
| 3 || <code>$3A93</code> || The clamp writes the maximum back into the game's score bytes, on every vector, including HSCATRCT and the copy calls. || A game showing its score after an HSC call shows 16,777,215.
 
|-
 
|-
−
| <code>$1008–$1027</code> || 32 || <code>CartRAMOwnerName</code> || Cartridge owner string (Watkins encoding).
+
| 4 || <code>$31D7</code> || HSCENTER2 initials aren't masked to 5 bits. || A value above <code>$1F</code> in initial 1 or 3 corrupts initial 2's packed bits.
 
|-
 
|-
−
| <code>$1028</code> || 1 || <code>CartRAMOwnerNameLength</code> || Length of owner name string.
+
| 5 || <code>$36AB</code> || The 26-byte difficulty-name copy runs even when its pointer's high byte is 0. || Reads zero page and TIA/MARIA addresses. Harmless on stock hardware.
 
|-
 
|-
−
| <code>$1029–$106D</code> || 69 || <code>CartRAMGameID1Table</code> || Game ID 1 table (up to 69 game configurations).
+
| 6 || <code>$3C2D</code> || The hold timer is decremented and tested with <code>BMI</code>. || Hold values <code>$81–$FF</code> behave like 0; <code>$80</code> holds for 128 s.
 
|-
 
|-
−
| <code>$106E–$10B2</code> || 69 || <code>CartRAMGameID2Table</code> || Game ID 2 table.
+
| 7 || <code>$3318</code> || BACKSPACE stores the ← glyph in the position it leaves. || The ← stays on screen, and moving right again starts on ←.  
 
|-
 
|-
−
| <code>$10B3–$10F7</code> || 69 || <code>CartRAMGameDifficultyTable</code> || Game Difficulty table (difficulty masked with <code>$FC</code>).
+
| 8 || <code>$392C</code> || The screen-saver wake-up reads the raw ports. || Driver-based controllers can't wake the screen.
 
|-
 
|-
−
| <code>$10F8–$113C</code> || 69 || <code>CartRAMGameIndexTable</code> || LRU aging table (prefilled with <code>$7F</code>).
+
| 9 || <code>$392C</code> || The screen saver ends with SWACNT = SWBCNT = 0. || A game's two-button-mode setup is lost after a long idle period.
 
|-
 
|-
−
| <code>$113D–$17F9</code> || 1725 || <code>Score Data Blocks</code> || 69 tables &times; 25 bytes (5 scores per table).
+
| 10 || <code>$3022</code> || HSCSTAT reports <code>$FF</code> when no table exists, even for scores HSCENTER would accept. || Games must call HSCENTER for any nonzero score to get their first table.
 
|-
 
|-
−
| <code>$17FA–$17FE</code> || 5 || <code>CartRAMTempScore</code> || Scratch buffer used when shifting scores down.
+
| 11 || <code>$35A1</code> || The touch loop also increments the slot it just zeroed. || The most recently used age is 1, not 0.
 
|-
 
|-
−
| <code>$17FF</code> || 1 || <code>Unused</code> || Unused byte.
+
| 12 || <code>$3C95</code> || First-use formatting during HSCSTAT runs the personalization screen and returns with DMA off. || Games that call HSCSTAT at boot show a naming screen on a fresh cart.
 +
|-
 +
| 13 || <code>$3242</code> || Fire isn't required to be released before entry starts. || A button still held from gameplay accepts the first initial on release.
 +
|-
 +
| 14 || <code>$36DE</code>, <code>$3715</code> || <code>$B9</code> is both the logo-DLI flag and the screen-saver timer. || Harmless; worth knowing when reading the code.
 +
|-
 +
| 15 || <code>$3F6E</code> || A stray letter 'A' between two strings is never referenced. || None.
 
|}
 
|}
  
−
=== Directory Lookup & LRU Allocation ===
 
−
 
−
* '''Capacity:''' Up to 69 distinct (Game ID 1 + Game ID 2 + Difficulty) entries.
 
−
* '''Lookup:''' <code>HSCInitRoutine</code> searches backwards (indices 68 down to 0) for matching ID and difficulty bytes.
 
−
* '''LRU Eviction:''' If all 69 slots are occupied and a new game or difficulty is registered, the slot with the highest aging index in <code>CartRAMGameIndexTable</code> is purged and replaced.
 
−
* '''Table Address Formula:'''
 
−
:<math>\text{TableAddress} = \$113D + (\text{SlotIndex} \times 25)</math>
 
−
* Each 25-byte block stores 5 entries of 5 bytes each, ordered from 1st place (offset <code>+$00</code>) to 5th place (offset <code>+$14</code>).
 
  
 
== 7800 Assembly Integration Example ==
 
== 7800 Assembly Integration Example ==
Line 335: Line 427:
  
 
<pre lang="asm">
 
<pre lang="asm">
−
; ==============================================================================
 
−
; ROM Argument Template
 
−
; ==============================================================================
 
 
HscRomTemplate:
 
HscRomTemplate:
−
     .byte $8B, $23          ; Game ID 1 and 2
+
     .byte $8B, $23          ; game ID 1 and 2
−
     .byte $00              ; Difficulty (Bits 7..2) | Controller (Bits 1..0)
+
     .byte $00              ; difficulty << 2 | player (filled in at run time)
−
     .byte $00              ; Controller Type: $00 = Standard Joystick
+
     .byte $00              ; controller type: joystick
−
     .word HscTitleText      ; Pointer to Watkins Game Name string
+
     .word HscTitleText      ; 26-character game name
−
     .word HscDiffText      ; Pointer to Watkins Difficulty string
+
     .word HscDiffText      ; 26-character difficulty name (or high byte 0)
−
     .word GameScoreBcd      ; Pointer to 4-byte packed BCD score in RAM
+
     .word GameScoreBcd      ; 4-byte packed BCD score in RAM
−
     .word 0                ; Logo DLL Pointer (0 = Use standard text title)
+
     .word 0                ; no logo: show the name as text
−
     .word GameSoundCallback ; Sound playback callback routine
+
     .word GameSoundCallback ; must be valid; point at an RTS if unused
−
     .byte $04               ; Display Duration Multiplier (Attract speed)
+
     .byte $05               ; hold time in seconds
  
−
HscTitleText:
+
HscTitleText:               ; exactly 26 characters, padded to center "MY GAME"
 +
    .byte $1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D
 
     WATKINS "M"
 
     WATKINS "M"
 
     WATKINS "Y"
 
     WATKINS "Y"
−
     .byte $1D               ; Watkins space
+
     .byte $1D
 
     WATKINS "G"
 
     WATKINS "G"
 
     WATKINS "A"
 
     WATKINS "A"
 
     WATKINS "M"
 
     WATKINS "M"
 
     WATKINS "E"
 
     WATKINS "E"
−
     .byte $1D
+
     .byte $1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D
−
 
 
−
HscDiffText:
 
−
    WATKINS "E"
 
−
    WATKINS "A"
 
−
    WATKINS "S"
 
−
    WATKINS "Y"
 
−
    .byte $1D
 
 
</pre>
 
</pre>
  
−
=== Attract Mode Screen Display ===
+
=== Attract Mode ===
  
 
<pre lang="asm">
 
<pre lang="asm">
−
; ==============================================================================
 
−
; ShowHighScores: Displays high score table during attract loop
 
−
; ==============================================================================
 
 
ShowHighScores:
 
ShowHighScores:
−
     JSR DetectHSC
+
     jsr DetectHSC
−
     BCC .ExitNoHsc          ; Skip if HSC is not present
+
     bcc .done
 +
    jsr CopyArgs            ; copy the 15-byte template to $2500 (page N = $25)
 +
    lda CurrentDifficulty
 +
    asl
 +
    asl
 +
    sta $2502              ; player index 0
 +
    ldy #$25
 +
    jsr $3FFA              ; HSCATRCT: A = $FF if this difficulty has no table
 +
    jsr RestoreGameScreen  ; DMA, DPP, CHARBASE, palettes, controller ports
 +
.done
 +
    rts
 +
</pre>
  
−
    ; Copy configuration template to Page N RAM ($2500)
+
=== Game Over ===
−
    LDX #14
 
−
.CopyArgs:
 
−
    LDA HscRomTemplate,X
 
−
    STA $2500,X
 
−
    DEX
 
−
    BPL .CopyArgs
 
  
−
    ; Set desired difficulty (Bits 7..2)
+
Don't gate HSCENTER on HSCSTAT: HSCSTAT reports <code>$FF</code> until a table exists (bug 10). HSCENTER does its own ranking and simply shows the table when the score doesn't qualify.
−
    LDA CurrentDifficulty  ; e.g., 0, 1, 2
 
−
    ASL
 
−
    ASL
 
−
    STA $2502
 
−
 
 
−
    ; Call HSC Attract Mode Vector
 
−
    LDX #$00
 
−
    LDY #$25                ; Y = Page $25 ($2500 argument block)
 
−
    JSR $3FFA              ; HSCATRCT
 
−
 
 
−
    ; Restore game video registers and display list pointers
 
−
    JSR RestoreGameScreen
 
−
.ExitNoHsc:
 
−
    RTS
 
−
</pre>
 
−
 
 
−
=== Game Over Score Submission ===
 
  
 
<pre lang="asm">
 
<pre lang="asm">
−
; ==============================================================================
 
−
; SubmitScore: Checks rank qualification and launches initials entry screen
 
−
; ==============================================================================
 
 
SubmitScore:
 
SubmitScore:
−
     JSR DetectHSC
+
     jsr DetectHSC
−
     BCC .ExitNoHsc
+
     bcc .done
−
 
+
     jsr CopyArgs
−
    ; Setup argument block at $2500
+
     lda CurrentDifficulty
−
    LDX #14
+
     asl
−
.SetupArgs:
+
     asl
−
    LDA HscRomTemplate,X
+
     ora CurrentPlayer      ; 0 or 1
−
    STA $2500,X
+
     sta $2502
−
     DEX
+
     ldy #$25
−
     BPL .SetupArgs
+
     jsr $3FFD               ; HSCENTER: A = $01 if inserted
−
 
+
     jsr RestoreGameScreen
−
    LDA CurrentDifficulty
+
.done
−
     ASL
+
     rts
−
     ASL
 
−
    STA $2502
 
−
 
 
−
     ; Check if score qualifies
 
−
     LDX #$00
 
−
     LDY #$25
 
−
     JSR $3FF7               ; HSCSTAT
 
−
 
 
−
    LDA $2505              ; Read returned rank
 
−
     CMP #$FF                ; $FF = Did not qualify
 
−
    BEQ .DidNotQualify
 
−
 
 
−
     ; Score qualifies! Run interactive initials entry screen and save
 
−
    LDX #$00
 
−
    LDY #$25
 
−
    JSR $3FFD              ; HSCENTER
 
  
−
     JSR RestoreGameScreen
+
ReadTopScore:              ; e.g. for an on-screen "HI" display
−
.DidNotQualify:
+
    jsr CopyArgs
−
.ExitNoHsc:
+
    lda #0
−
     RTS
+
    sta $250E              ; no hold, or the game's frame freezes
 +
     ldy #$25
 +
    jsr $3FF7              ; HSCSTAT
 +
    ; $250F-$2512 = top score in BCD; $2504/$2505 overwritten, so rebuild the block before the next call
 +
     rts
 
</pre>
 
</pre>

Latest revision as of 18:54, 2 October 2026

Atari 7800 High Score Cartridge (HSC) Technical Reference

The Atari 7800 High Score Cartridge (HSC) is an expansion peripheral that provides battery-backed RAM (NVRAM) and a standard high score service for Atari 7800 games. It uses a pass-through design: the HSC plugs into the 7800 cartridge slot, and the game cartridge plugs into the top of the HSC.

This page covers both the game-facing interface and the internals of the ROM (highscre.bin, 4096 bytes, MD5 c8a73288ab97226c52602204ab894286). Internal addresses below refer to that ROM. The 2026 community disassembly (7800 HSC CART) reassembles byte-identical with DASM, and its labels are used here.

System Architecture & Memory Map

Hardware Memory Map

Address Range Size Device Description
$1000–$17FF 2 KB Battery-backed SRAM Owner name, a 69-slot directory and 69 score tables.
$3000–$3FFF 4 KB HSC ROM API, screen builders, font, strings and jump vectors.

The ROM has no 6502 reset or interrupt vectors of its own. It only runs when a game calls into it.

ROM Layout

Range Contents
$3000–$38FF Code (vectors' bodies, initialization, ranking, display-list builders, text entry, menu)
$3900–$3F2B Font: one 44-byte glyph row per page ($3F00 = top row … $3900 = bottom row). Offsets $2C–$FF of each page hold more code and data
$3D2C Palette values for P4C2…P0C2 ($9D $0A $96 $98 $FA, stored in reverse)
$3D31–$3DCB Encoded DLL streams for each screen (3 bytes per zone; the middle byte is ignored and replaced by page N+1)
$3E2C–$3EC6 Display-list templates for the table, personalization and menu screens
$3EC7–$3EDE Powers of ten, 10,000,000 down to 1, as 24-bit little-endian values
$3F2C–$3FC3 Strings: CONGRATULATIONS PLAYER 1, CARTRIDGE/PERSONALIZE/YOUR ATARI HIGH SCORE, and the menu text (WHAT WOULD YOU LIKE TO CLEAR?, THIS GAME, PERSONALIZATION, ALL GAMES, NOTHING, ARE YOU SURE?, YES; "NO" is the first two letters of NOTHING)
$3FEB–$3FFF Seven JMP vectors

Zero-Page Footprint ($80–$BC)

The HSC uses zero page $80–$BC (61 bytes) while a call runs.

  • Preservation: on entry, HSCInitRoutine ($3483) copies $80–$BC to the caller's page N at +$13..+$4F, in reverse order ($BC → +$13 … $80 → +$4F). RestoreVarsAndRegsAndExit ($3C46) copies it back.
  • $94/$95: these hold the HSC's pointer to page N during the call, so their original values are parked in NVRAM $1000/$1001 before the backup and restored last.
  • The first 18 bytes, $80–$91, are a working copy of the argument block (+$00..+$11).

Game code that runs during a call (the sound hook, a custom controller driver, a DLI handler) sees the HSC's values in $80–$BC, so it must not write there.

Dual-Page RAM Requirement (512 Contiguous Bytes)

Every call needs a page-aligned 512-byte RAM work area: page N (passed in Y) and page N+1.

Page N:
  +$00-$11  Argument block (inputs)          +$12      HSCSTAT result byte
  +$13-$4F  Zero-page backup ($BC..$80)
  +$50-$8B  Formatted table: 5 rows x 12 bytes (3 initials + 9 score characters)
  +$8C-$A5  Game name (26)       +$A6-$BF  Difficulty name (26)
  +$C0-$D8  "CONGRATULATIONS  PLAYER n"    +$DA-$F9  Owner name (32)
  (the personalization and menu screens reuse +$8C onwards for their own strings)

Page N+1:
  +$00-$86  Display lists (screen-specific templates, patched at run time)
  +$87-...  Display List List (DPPL = $87, DPPH = page N+1)

Reserve both pages. Display calls build their DLs and DLL in page N+1 and point MARIA there. A game that reserves only 256 bytes gets the following page overwritten. HSCSTAT also overwrites page N +$50..+$8B.

Cartridge Detection

Check the two signature bytes, which are the bottom rows of glyphs 'A' and 'E', before calling any vector:

DetectHSC:                  ; carry set = HSC present
    lda $3900
    cmp #$C6
    bne .no
    lda $3904
    cmp #$FE
    bne .no
    sec
    rts
.no clc
    rts

Variants seen in games: lda #$C6 / cmp $3900 (Asteroids Deluxe), lda $3900 / eor #$C6 (current 7800basic), and lda $1002 / cmp #$68 (old 7800basic, which checks the NVRAM magic instead).

Public Vector Table & API

Vector Name Target Function ID ($1007) Description Result in A
$3FEB HSCSETRS $39A9 — Sets CTRL $4B, BACKGRND $00, P0C2–P4C2, CHARBASE $39. No argument block, no hold undefined
$3FEE HSCCPYFM $3000 $EF Copies the 25-byte table to the buffer pointed to by arg +$0F/$10 $00; $EF if no table (nothing copied)
$3FF1 HSCCPYTO $3011 $04 Copies 25 bytes from arg +$0F/$10 into the table, creating it if needed $01
$3FF4 HSCENTER2 $3127 $01 Like HSCENTER but takes the initials from arg +$0F..+$11. Not headless: it still shows the congratulations table $01 if inserted, $00 if not
$3FF7 HSCSTAT $3022 $02 Query: rank, top score, number of tables. Never creates a table or updates LRU ages $00
$3FFA HSCATRCT $30DA $FF (then $FD) Shows one game/difficulty table (it doesn't cycle through difficulties) $00; $FF if no table (returns at once)
$3FFD HSCENTER $312B $00 Ranks the score; if it qualifies, inserts it and runs interactive initials entry $01 if inserted (also after a cancel), $00 if not

Calling Conventions

  • Y: high byte of page N.
  • X: ignored. HSCInitRoutine always uses offset $00.
  • On return: I flag set (SEI is never undone), D clear, stack balanced. X/Y undefined.
  • Return path: HSCInitRoutine slips two bytes, $3C and $2B, under its own return address. When the vector's body finishes with RTS, control goes to $3C2C, the common display hold. That code pushes the result, runs the hold, and leaves through RestoreVarsAndRegsAndExit, which pulls the result into A and returns to the game. So every vector except HSCSETRS ends in the hold, including HSCSTAT and the copy calls.

Common Entry Processing (HSCInitRoutine, $3483)

  1. Store the function ID at $1007, then SEI, CLD.
  2. Display vectors (IDs other than $02, $EF and $04): CTRL = $7F (DMA off), AUDV0 = AUDV1 = 0.
  3. Park $94/$95 in $1000/$1001 and back up the zero page.
  4. Read SWCHB into $BC and write $FF to SWCHB, which drives any output bits high and forces one-button joystick mode. The saved value is written back on exit.
  5. Copy argument bytes +$00..+$11 to $80..$91. Player index = +$02 & 3 → $A9.
  6. If the name-pointer high byte and the logo-pointer high byte are both 0, overwrite $80–$8E with the defaults at $39EF: $80 $01 $F1 $01 $79 $E9 $00 $1A $4C $00 $00 $00 $FF $3C $0A. That's ID $80/$01, difficulty $F0, player 2, joystick, name at $E979, difficulty at $1A00, score at $004C, no logo, sound hook $3CFF (an RTS), 10-second hold. The purpose of this set is unknown.
  7. Do the stack trick described above.
  8. Display vectors: load the palette and CHARBASE (HSCSETRS2, $39AD).
  9. Check the NVRAM magic ($3512). If it fails, check again (one retry), and if it still fails, format ($3535). Formatting writes the magic, sets every slot's difficulty and age to $7F ($3FCF), and then, unless the call is HSCCPYFM or HSCCPYTO, runs personalization ($3C95) and turns DMA off.
  10. Convert the game's BCD score to binary ($3A81, see Score conversion). This runs for every vector.
  11. Look up the directory ($3572) and set $A6/$A7 to the table address.

User Argument Block (Page N)

Offset Size ZP copy Field Description
+$00 1 $80 Game ID 1 Bit 7 set: the game passes its score ÷ 10. The table shows an extra trailing 0 and the top score returned by HSCSTAT is in the game's units
+$01 1 $81 Game ID 2
+$02 1 $82 Difficulty / player Bits 7–2: difficulty (one table per value). Bits 1–0: player index. 0 = left port (SWCHA high nibble, INPT4); 1–3 = right port (SWCHA low nibble, INPT5). Shown as "PLAYER index+1"
+$03 1 $83 Controller type $02 = custom driver at the name pointer. Any other value = joystick ($00 and $01 behave the same)
+$04 2 $84 Game name pointer Exactly 26 characters, read raw and drawn centered (pad with $1D). With type $02, also the driver entry point. Overwritten by HSCSTAT
+$06 2 $86 Difficulty name pointer 26 characters. High byte 0 = no difficulty line (the 26 bytes are still copied from $00xx, see bugs)
+$08 2 $88 Score pointer 4-byte packed BCD, big-endian (1,234,560 = $01 $23 $45 $60). Read on every call, and written to by the clamp
+$0A 2 $8A Logo DLL pointer High byte 0 = show the game name as text
+$0C 2 $8C Sound hook Called once per frame during the hold and during initials entry. Must be valid
+$0E 1 $8E Hold time Seconds (60-frame units) of display hold before returning. 0 = none
+$0F 2 $8F Buffer pointer / initials HSCCPYFM/HSCCPYTO: 25-byte buffer pointer. HSCENTER2: initials 1 and 2
+$11 1 $91 Initial 3 HSCENTER2 only
+$12 1 — (result) Written by HSCSTAT

HSCSTAT Results

Offset Result
+$04 Number of directory slots with this game's ID 1 and ID 2 and difficulty ≠ $7F, i.e. how many difficulty tables exist (0–69)
+$05 Rank: $00 = 1st place … $04 = 5th, $FF = doesn't qualify
+$0F..+$12 Current 1st-place score as 4-byte packed BCD, big-endian, in the game's units
  • A score of 0 never qualifies (IsScoreNonZero, $3DDC).
  • A score equal to an existing entry ranks above it.
  • If the game/difficulty has no table, +$05 is $FF for any score and +$0F..+$12 are 0. So a game that calls HSCENTER only when HSCSTAT reports a qualifying rank can never create its first table.
  • How the top score is produced: HSCSTAT formats the table as text (FormatAllScoresForDisplay, $3769), then packs the eight digit characters of row 1 ($30A9) back into BCD, treating spaces as 0.

Sound Hook

The hook is reached by JSR $36DB, which does JMP (SOUNDPTR), so the hook returns with RTS. It may clobber A, X and Y. It's called during the hold (but not while paused) and during initials entry. It isn't called during personalization or the menu.

Custom Controller Driver ($03 = $02)

Reached by JMP ($84) ($39A6) from a subroutine call, with X = player index. It must return:

  • A bits 7–4 = right, left, down, up (0 = pushed)
  • Y bit 7 = fire (0 = pressed)

Driver input is used for text entry, the hold's exit check and fire-release waits. The hold's raw checks (INPT4 and any SWCHA bit) are skipped for type $02. The screen saver ($392C) and the maintenance menu still read the real ports. Because the name pointer doubles as the driver address, a driver game should supply a logo, or its code bytes get drawn as the game name.

Logo DLL

The decoder ($36DE) copies the game's 3-byte DLL entries verbatim to the start of the DLL. It keeps a running total of zone heights ((byte0 & $0F) + 1) and stops after the entry that brings the total to at least 65 lines ($41, when a difficulty name is present) or 79 lines ($4F, when it isn't).

DLI handling:

  • If the first logo entry has its DLI bit set, $B9 is set to $40.
  • SetupDLL ($3715) shifts $B9 left for every HSC entry flagged with a DLI. The first such entry keeps its DLI bit; all later ones have it stripped.
  • Without a logo DLI, every HSC DLI bit is stripped.

The one kept DLI lets the game's NMI handler restore the HSC's CHARBASE and palette after a logo that changed them.

Data Representations

Character Set & Font

The HSC uses its own character codes, often called Watkins encoding. Letters are 'X' - 'A'.

Code Glyph Code Glyph Code Glyph
$00–$19 A–Z $1D (space) $21–$2A 0–9
$1A _ (underscore: full-width bar on the bottom row) $1E ' (apostrophe) $2B ?
$1B . (period) $1F █ (solid block: END in text entry)
$1C - (hyphen) $20 ← (left arrow: BACKSPACE in text entry)

There's no colon.

Glyphs are 8×7 pixels, drawn in 320A mode with CHARBASE $39. Row r of glyph c (r = 0 at the top) is at $3900 + (6−r)×$100 + c.

DASM macro:

    mac WATKINS
        dc.b {1}-"A"
    endm

Packed Initials (15 bits)

Byte 0: [ C1.4 | C1.3 | C1.2 | C0.4 | C0.3 | C0.2 | C0.1 | C0.0 ]
Byte 1: [ C1.1 | C1.0 |   0  | C2.4 | C2.3 | C2.2 | C2.1 | C2.0 ]

byte0 = ((c1 >> 2) << 5) | c0        c0 = byte0 & $1F
byte1 = ((c1 & 3) << 6) | c2         c1 = ((byte0 >> 5) << 2) | (byte1 >> 6)
                                     c2 = byte1 & $1F
  • Packing: EncodeAndSaveInitialsToTable, $31D7. Unpacking: $3AD6.
  • Empty entries hold ___ (three underscores, $1A $1A $1A), packed as $DA $9A.
  • Other examples: ABC = $00 $42, XYZ = $D7 $19.

Score conversion

Games pass 8-digit packed BCD. NVRAM holds a 24-bit binary value, little-endian.

  1. Clamp ($3A83): the score is compared byte by byte with $16 $77 $72 $15 (16,777,215). If it's larger, the maximum is written back into the game's score and used instead. The comparison is signed, see bugs.
  2. Convert ($3AA8): for each nibble, the value is multiplied by 10 ($3A5E, a full 24-bit ×10) and the nibble is added. The result goes in $AD (LSB), $AE, $AF (MSB). The nibble add has a carry bug, see bugs.
  3. Display ($37BC): the value is turned into decimal by repeated subtraction of the powers of ten at $3EC7, giving a 9-character field:
    • ID1 bit 7 clear: a leading space, then 8 digits with leading zeros blanked. The last digit always shows.
    • ID1 bit 7 set: 8 digits with leading zeros blanked, then a final 0.

Cartridge NVRAM Layout ($1000–$17FF)

Address Size Disassembly label Contents
$1000–$1001 2 CartRAMSaveZ94/Z95 Caller's $94/$95 during a call
$1002–$1006 5 CartRAMMagic1..5 $68 $83 $AA $55 $9C = formatted
$1007 1 CartRAMEntryID Function ID of the call in progress ($FD/$FE/$FF during attract-style displays)
$1008–$1027 32 CartRAMOwnerName Owner name. Bytes after the length are leftover; the END code $1F usually follows the last character
$1028 1 CartRAMOwnerNameLength 1–32
$1029–$106D 69 CartRAMGameID1Table Game ID 1 per slot
$106E–$10B2 69 CartRAMGameID2Table Game ID 2 per slot
$10B3–$10F7 69 CartRAMGameDifficultyTable Difficulty (& $FC) per slot. $7F = free
$10F8–$113C 69 CartRAMGameAgeTable LRU age per slot. $7F = free
$113D–$17F9 1725 — 69 tables × 25 bytes; slot s at $113D + 25·s
$17FA–$17FE 5 CartRAMSavedEntry Copy of the displaced 5th-place entry during HSCENTER, used to undo a cancel
$17FF 1 — Unused

Formatting only writes the magic and the $7F markers. IDs, tables and the owner name keep whatever was there.

Score Tables

Each table holds 5 entries of 5 bytes, 1st place first: packed initials (2 bytes), then the score as 24-bit little-endian binary (3 bytes). A new table is 5 × ($DA $9A $00 $00 $00).

Directory Lookup & LRU

Lookup ($3572):

  • Scans slots 68 down to 0 for a slot whose ID 1, ID 2 and difficulty (arg+$02 & $FC) all match. A free slot can't match, because $7F has its low bits set.
  • While scanning, it tracks the slot with the largest age (signed compare, strictly greater). That slot is the eviction victim: free slots ($7F) win, and on ties the highest slot number wins.

Touch ($35A1), done by every call except HSCSTAT:

  • Let old = the slot's age. Set its age to 0.
  • Then increment every age that is less than old (signed), the touched slot included.
  • So the most recently used slot ends with age 1, not 0. In-use ages stay distinct from 1 upward, and free slots stay $7F.

Not found ($35E0):

  • Negative function IDs (HSCATRCT $FF, HSCCPYFM $EF) drop the call's return address, store the ID in the hold timer, and return the ID straight away, with no hold. DMA was already turned off and audio muted for HSCATRCT.
  • HSCSTAT reports "no table".
  • Everything else creates a table ($35F4) in the victim slot, so when all 69 slots are in use the least recently used game is evicted.

Interactive Behavior

Display Hold ($3C2C)

  • The hold counts down arg+$0E seconds. Each second is 60 passes, and each pass waits one vertical blank, checks input, and calls the sound hook.
  • The input check ($3C63) ends the hold on any of:
    • P1 fire, or any SWCHA bit (both of these are skipped for driver games)
    • RESET or SELECT
    • the selected controller (joystick or driver)
  • Pause ($3B51), triggered by SWCHB bit 3 low on two reads two frames apart:
    • Wait for release, mute audio, and stop calling the hook.
    • While paused, the exit inputs above still work. Another PAUSE press resumes the countdown.
    • Holding PAUSE and pressing SELECT ($3B71) opens the maintenance menu.
    • After about 233×256 frames (roughly 16 minutes) paused, the screen saver ($392C) turns DMA off until any input.

Initials Entry ($3242)

Setup. The new score has already been inserted: lower entries shifted down, and the old 5th place saved at $17FA. The inserted row still shows the displaced entry's initials.

The cursor is a separate one-character DL object on the row. It blinks every 30 frames by toggling palette bit 2 (P0 ↔ P4). It starts at position 0, showing the character already there.

Stepping through characters. The stick is sampled every 10 frames:

  • Up/Right = next: A…Z, _, ., -, space, ', █, ←, then A. At position 0 the ← is skipped.
  • Down/Left = previous, the same cycle backwards.
  • Pressing Next on _ as the first move at a position jumps straight to 'A'.
  • Position 3 offers only █ (END) and ← (BACKSPACE).

Fire is checked every frame and acts after release:

  • On a character: accept it and move right.
  • On ←: move left. The ← glyph stays drawn in the position left behind.
  • On █: finish.
  • RESET or SELECT finishes at the current position.

Finishing:

  • At position 0, finishing cancels: the table is shifted back and the old 5th place restored from $17FA. The routine still falls through into the packing code, which rewrites the restored entry's initials unchanged.
  • At positions 1–2, the remaining initials are filled with spaces.

There's no time-out. A screen saver kicks in after about 15 minutes (233×233 frames) without input.

Personalization ($3C95)

Shown on first use and from the menu. The screen reads PERSONALIZE / YOUR ATARI HIGH SCORE / CARTRIDGE, and an entry line underneath stays centered as it grows. Entry rules:

  • Up to 32 characters, same character cycle as the initials, no digits.
  • It always uses the left joystick and has no console abort.
  • Each new position starts on the previous character.
  • █ (END) finishes. An empty name becomes one space.
  • Before entry starts, the owner name in NVRAM is set to A T A R I (length 9) in case power is lost mid-entry.

Maintenance Menu ($3B7F)

The menu reads WHAT WOULD YOU LIKE TO CLEAR?, with four choices: THIS GAME, ALL GAMES, PERSONALIZATION, NOTHING. The selection starts on NOTHING, and Up/Down move it, wrapping around. Fire brings up ARE YOU SURE? YES NO: Left/Right choose, starting on NO, and NO returns to the menu.

What each choice does after YES (dispatcher $39C8):

  • THIS GAME: frees the current game/difficulty slot only ($3FC4). The IDs and score bytes stay behind.
  • ALL GAMES: frees all 69 slots. The owner name is kept.
  • PERSONALIZATION: runs the name entry.
  • NOTHING: leaves the HSC, returning the original result to the game.

The first three return to the menu afterwards. The menu has no time-out, no console exit and no sound hook, and it always uses the left joystick.

Screen Layout & Colors

Colors: BACKGRND $00, P0C2 $FA (headers, cursor), P1C2 $98 (scores and initials of rows 2–5), P2C2 $96 (rank digits 2–5), P3C2 $0A (game, difficulty and owner names), P4C2 $9D (1st-place row, cursor blink).

The table screen, top to bottom:

  1. Header: 10+16+16 blank lines, the game name (26 characters at x = 56 in 320 mode), then 16 blank lines. A game logo replaces this.
  2. Difficulty (optional): 7 blank lines, then the difficulty name.
  3. Entry layout only: 16 blank lines, CONGRATULATIONS, 7 blank lines, PLAYER n.
  4. Table: 16 blank lines, the owner name (centered), then five rows. Each row is a rank digit at x = 100, the 9-character score at x = 116 and the initials at x = 196, separated by 7-line gaps.
  5. Blank 7-line zones pad the DLL to at least 243 lines.

The community "PAL hack" (PALHACK in the disassembly) only lengthens the gap zones.

Bugs and Quirks in the HSC ROM

# Where Behavior Effect
1 $3ABB, $3ACC Carry lost in BCD→binary conversion. After each nibble is added to the low byte, the carry goes into the middle byte with ADC #$00 but never on into the high byte ($AF). Whenever the running value crosses a multiple of 65,536 during a nibble add, 65,536 is lost. 65,536 → 0, 131,072 → 65,536, 655,360 → 0, which can then never rank. 14 multiples of 10 below 2,000,000 are affected.
2 $3A8B–$3A91 Signed clamp compare. CMP then BMI/BPL tests the sign of (limit − score) instead of the carry. First byte $97–$99 (scores ≥ 97,000,000) isn't clamped, and neither is $16 $77 $72 followed by $96–$99. Those values wrap modulo 2^24.
3 $3A93 The clamp writes the maximum back into the game's score bytes, on every vector, including HSCATRCT and the copy calls. A game showing its score after an HSC call shows 16,777,215.
4 $31D7 HSCENTER2 initials aren't masked to 5 bits. A value above $1F in initial 1 or 3 corrupts initial 2's packed bits.
5 $36AB The 26-byte difficulty-name copy runs even when its pointer's high byte is 0. Reads zero page and TIA/MARIA addresses. Harmless on stock hardware.
6 $3C2D The hold timer is decremented and tested with BMI. Hold values $81–$FF behave like 0; $80 holds for 128 s.
7 $3318 BACKSPACE stores the ← glyph in the position it leaves. The ← stays on screen, and moving right again starts on ←.
8 $392C The screen-saver wake-up reads the raw ports. Driver-based controllers can't wake the screen.
9 $392C The screen saver ends with SWACNT = SWBCNT = 0. A game's two-button-mode setup is lost after a long idle period.
10 $3022 HSCSTAT reports $FF when no table exists, even for scores HSCENTER would accept. Games must call HSCENTER for any nonzero score to get their first table.
11 $35A1 The touch loop also increments the slot it just zeroed. The most recently used age is 1, not 0.
12 $3C95 First-use formatting during HSCSTAT runs the personalization screen and returns with DMA off. Games that call HSCSTAT at boot show a naming screen on a fresh cart.
13 $3242 Fire isn't required to be released before entry starts. A button still held from gameplay accepts the first initial on release.
14 $36DE, $3715 $B9 is both the logo-DLI flag and the screen-saver timer. Harmless; worth knowing when reading the code.
15 $3F6E A stray letter 'A' between two strings is never referenced. None.


7800 Assembly Integration Example

Argument Block Template

HscRomTemplate:
    .byte $8B, $23          ; game ID 1 and 2
    .byte $00               ; difficulty << 2 | player (filled in at run time)
    .byte $00               ; controller type: joystick
    .word HscTitleText      ; 26-character game name
    .word HscDiffText       ; 26-character difficulty name (or high byte 0)
    .word GameScoreBcd      ; 4-byte packed BCD score in RAM
    .word 0                 ; no logo: show the name as text
    .word GameSoundCallback ; must be valid; point at an RTS if unused
    .byte $05               ; hold time in seconds

HscTitleText:               ; exactly 26 characters, padded to center "MY GAME"
    .byte $1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D
    WATKINS "M"
    WATKINS "Y"
    .byte $1D
    WATKINS "G"
    WATKINS "A"
    WATKINS "M"
    WATKINS "E"
    .byte $1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D,$1D

Attract Mode

ShowHighScores:
    jsr DetectHSC
    bcc .done
    jsr CopyArgs            ; copy the 15-byte template to $2500 (page N = $25)
    lda CurrentDifficulty
    asl
    asl
    sta $2502               ; player index 0
    ldy #$25
    jsr $3FFA               ; HSCATRCT: A = $FF if this difficulty has no table
    jsr RestoreGameScreen   ; DMA, DPP, CHARBASE, palettes, controller ports
.done
    rts

Game Over

Don't gate HSCENTER on HSCSTAT: HSCSTAT reports $FF until a table exists (bug 10). HSCENTER does its own ranking and simply shows the table when the score doesn't qualify.

SubmitScore:
    jsr DetectHSC
    bcc .done
    jsr CopyArgs
    lda CurrentDifficulty
    asl
    asl
    ora CurrentPlayer       ; 0 or 1
    sta $2502
    ldy #$25
    jsr $3FFD               ; HSCENTER: A = $01 if inserted
    jsr RestoreGameScreen
.done
    rts

ReadTopScore:               ; e.g. for an on-screen "HI" display
    jsr CopyArgs
    lda #0
    sta $250E               ; no hold, or the game's frame freezes
    ldy #$25
    jsr $3FF7               ; HSCSTAT
    ; $250F-$2512 = top score in BCD; $2504/$2505 overwritten, so rebuild the block before the next call
    rts