LIST OFF ; *** S P A C E J O C K E Y *** ; Copyright 1982 US Games Corporation ; Designer: Garry Kitchen ; Analyzed, labeled and commented ; by Dennis Debro ; Last Update: June 14, 2004 ; ; NTSC ROM usage stats ; ------------------------------------------- ; *** 110 BYTES OF RAM USED 18 BYTES FREE ; *** 4 BYTE OF ROM FREE ; ; PAL50 ROM usage stats ; ------------------------------------------- ; *** 110 BYTES OF RAM USED 18 BYTES FREE ; *** 2 BYTES OF ROM FREE ; ; ============================================================================== ; = THIS REVERSE-ENGINEERING PROJECT IS BEING SUPPLIED TO THE PUBLIC DOMAIN = ; = FOR EDUCATIONAL PURPOSES ONLY. THOUGH THE CODE WILL ASSEMBLE INTO THE = ; = EXACT GAME ROM, THE LABELS AND COMMENTS ARE THE INTERPRETATION OF MY OWN = ; = AND MAY NOT REPRESENT THE ORIGINAL VISION OF THE AUTHOR. = ; = = ; = THE ASSEMBLED CODE IS © 1982, US GAMES CORPORATION = ; = = ; ============================================================================== ; ; This was Garry's first Atari VCS game. Garry learned to program the VCS by ; reverse engineering the hardware and various games using his Apple II. This ; game was started by him reverse engineering Outlaw which was written by ; David Crane while at Atari. ; ; After writing Space Jockey he approached his boss and suggested selling the ; game to Activision. Instead the company decided to sell the game to US Games. ; So Space Jockey became US Games' first VCS game. ; ; Garry uses a horizontal position routine that *seems* to first appear here. ; This routine was modified over the years and has been seen in a number of ; games. ; ; The enemy attribute variable looks to hold data that isn't used in the game. ; There appears to be settings to know if the enemy is a flying enemy however ; Gary uses the enemy's id value to determine this in the released game (i.e. ; anything lower than the House's id is considered a flying object). Also D1 is ; set in the initial build of the enemy attributes but never used. I'm not sure ; what this value would've been used for. ; ; To produce the PAL listing I used the Carrere Video version. The PAL version ; adjusts the vertical blank time to make the game produce 314 scan lines. ; The colors were also adjusted but it seems they missed the place in the ; kernel where Garry colors some objects directly. The speeds and the sound ; frequencies were not adjusted for the PAL timing. processor 6502 ; ; Set the read address base so this runs on the real VCS and compiles to the ; exact ROM image. This must be done before including the vcs.h header file. ; TIA_BASE_READ_ADDRESS = $30 include "vcs.h" include "macro.h" include "tia_constants.h" ; ; Make sure we are using vcs.h version 1.05 or greater. ; IF VERSION_VCS < 105 echo "" echo "*** ERROR: vcs.h file *must* be version 1.05 or higher!" echo "*** Updates to this file, DASM, and associated tools are" echo "*** available at https://dasm-assembler.github.io/" echo "" err ENDIF ; ; Make sure we are using macro.h version 1.01 or greater. ; IF VERSION_MACRO < 101 echo "" echo "*** ERROR: macro.h file *must* be version 1.01 or higher!" echo "*** Updates to this file, DASM, and associated tools are" echo "*** available at https://dasm-assembler.github.io/" echo "" err ENDIF LIST ON ;=============================================================================== ; A S S E M B L E R - S W I T C H E S ;=============================================================================== NTSC = 0 PAL50 = 1 TRUE = 1 FALSE = 0 IFNCONST COMPILE_REGION COMPILE_REGION = NTSC ; change to compile for different regions ENDIF IF !(COMPILE_REGION = NTSC || COMPILE_REGION = PAL50) echo "" echo "*** ERROR: Invalid COMPILE_REGION value" echo "*** Valid values: NTSC = 0, PAL50 = 1" echo "" err ENDIF ;=============================================================================== ; F R A M E - T I M I N G S ;=============================================================================== VSYNC_TIME = 40 OVERSCAN_LINES = 28 ; number of scan lines in overscan IF COMPILE_REGION = NTSC VBLANK_TIME = 45 ELSE VBLANK_TIME = 106 ENDIF ;=============================================================================== ; C O L O R - C O N S T A N T S ;=============================================================================== BLACK = $00 WHITE = $0F IF COMPILE_REGION = NTSC YELLOW = $10 BRICK_RED = $30 RED = $40 PURPLE = $50 BLUE = $80 GREEN = $B0 BRIGHT_GREEN = $C0 GREEN_BROWN = $E0 BROWN = $F0 LT_BROWN = BROWN + 2 SIREN_LIGHT = BRICK_RED ELSE GREEN = $30 YELLOW = $40 GREEN_BROWN = YELLOW BRIGHT_GREEN = $50 BRICK_RED = $60 RED = $80 PURPLE = $A0 BLUE = $D0 BROWN = YELLOW LT_BROWN = BROWN + 4 SIREN_LIGHT = RED ENDIF ;=============================================================================== ; U S E R - C O N S T A N T S ;=============================================================================== ROM_BASE = $F000 ; objectType ids: ID_BALLOON = 0 ID_JET_PLANE = 1 ID_HELICOPTER = 2 ID_PROP_PLANE = 3 ID_HOUSE = 4 ID_TREE = 5 ID_TANK_0 = 6 ID_TANK_1 = 7 ; game selection values OPTION_PLAYER_MOVE_MISSILE = %01 OPTION_ENEMY_MOVE_VERT = %10 OPTION_PLAYER_MOVE_HORIZ = %100 OPTION_PLAYER_COLLISIONS = %1000 ; object score values (BCD) BALLOON_SCORE = $25 JET_PLANE_SCORE = $99 HELICOPTER_SCORE = $50 PROP_PLANE_SCORE = $99 HOUSE_SCORE = $20 TREE_SCORE = $20 TANK_SCORE = $99 ; kernel boundaries KERNEL_HEIGHT = 153 KERNEL_ZONE_HEIGHT = 52 SPACE_JOCKEY_XMIN = 16 SPACE_JOCKEY_XMAX = 131 SPACE_JOCKEY_YMIN = 21 SPACE_JOCKEY_YMAX = 153 ENEMY_XMAX = 152 MISSILE_XMIN = 1 MISSILE_XMAX = 162 SPACE_JOCKEY_MISSILE_OFFSET = 3 ENEMY_MISSILE_OFFSET = 8 INITIAL_START_COUNT = 32 NUMBER_OF_GROUPS = 3 ; number of enemy groups STARTING_LIVES = 2 ; number of lives at the start of a game MAX_LIVES = 6 ; maximum number of reserved lives MOUNTAIN_ROLL_RATE = 3 SELECT_DELAY = 30 ; SELECT switch frame delay LOGO_HEIGHT = 5 NUMBER_HEIGHT = 8 SPACE_JOCKEY_HEIGHT = 7 MISSILE_PIXEL_MOVEMENT = 3 ; enemy attribute values ENEMY_DESTROYED = %10000000 ENEMY_FLIES = %00001000 ENEMY_FIRES_SHOTS = %00000100 ENEMY_ACTIVE = %00000001 ;=============================================================================== ; Z P - V A R I A B L E S ;=============================================================================== SEG.U zpVars .org $80 spaceJockeyColors ds SPACE_JOCKEY_HEIGHT scoreColor ds 1 playfieldColor ds 1 backgroundColor ds 1 livesColor ds 1 spaceJockeyGraphics ds SPACE_JOCKEY_HEIGHT spaceJockeyVertPos ds 1 spaceJockeyHorzPos ds 1 pf0Data ds 3 pf1Data ds 3 pf2Data ds 3 enemyVerticalPos ds NUMBER_OF_GROUPS enemyLSB ds NUMBER_OF_GROUPS enemyGraphicPointer ds 2 enemyColorPointer ds 2 numberOfLives ds 1 playerScore ds 3 enemyAttributes ds NUMBER_OF_GROUPS digitPointer ds 12 deathAnimPointer ds 2 ;-------------------------------------- playerColors = deathAnimPointer ;-------------------------------------- tempCharHolder = playerColors ;-------------------------------------- allowedMotion = playerColors ;-------------------------------------- enemyVertDistance = playerColors ; used to calculate new enemy vertical pos loopCount = playerColors + 1 ;-------------------------------------- tempGRP0Graphic = loopCount enemyMotion ds NUMBER_OF_GROUPS startCount = enemyMotion objectIds ds NUMBER_OF_GROUPS enemyHorizPos ds NUMBER_OF_GROUPS enemyVelocity ds NUMBER_OF_GROUPS enemyDeathAnimRate ds NUMBER_OF_GROUPS enemyDeathSound ds NUMBER_OF_GROUPS enemyColorsLSB ds 2 zpUnused_01 ds 1 enemyScanline ds 1 zpUnused_02 ds 1 enemyMissileVert ds 1 enemyMissileHoriz ds 1 spaceJockeyMissileVert ds 1 spaceJockeyMissileHoriz ds 1 kernelZone ds 1 kernelZoneEnd ds 1 scanline ds 1 selectDebounce ds 1 mountainRollingRate ds 1 randomSeed ds 3 frameCount ds 1 enemyShotVolume ds 1 spaceJockeyShotFreq ds 1 ; sound frequency spaceJockDeathAnimRate ds 1 spaceJockeyDeathSound ds 1 colorCyclingModeTimer ds 1 showHighScore ds 1 ; show high score when set high (D7 = 1) spaceJockeyHit ds 1 enemyFiring ds 1 ; $FF = firing $00 = not firing spaceJockeyFiring ds 1 ; $FF = firing $00 = not firing gameState ds 1 ; D7 = 0 game over colorCyclingMode ds 1 clearGameRAM ds 1 ; set high to clear RAM highScore ds 3 gameSelection ds 1 echo "***",(*-$80 - 2)d, "BYTES OF RAM USED", ($100 - * + 2)d, "BYTES FREE" ;=============================================================================== ; R O M - C O D E (Part 1) ;=============================================================================== SEG Bank0 .org ROM_BASE Start ; ; Set up everything so the power up state is known. ; sei cld ; clear decimal mode ldx #$FF txs ; point the stack to the beginning inx ; x = 0 txa .clearLoop sta VSYNC,x inx bne .clearLoop jsr InitializeGame inc playerScore + 2 dec colorCyclingMode ; reduces on cart power up so its negative MainLoop lda backgroundColor sta COLUBK inc randomSeed dec randomSeed + 1 inc randomSeed + 2 VerticalSync ldy #$FF sty VSYNC ; start vertical sync (D1 = 1) sty VBLANK ; turn off TIA lda #VSYNC_TIME sta TIM8T ; set timer for VSYNC wait period inc frameCount ; increment frameCount each new frame bne .vsyncWaitTime lda colorCyclingMode ; cycle colors when value goes negative bpl .skipColorCycling ldx #<[spaceJockeyGraphics - spaceJockeyColors - 1] .cycleGameColors inc spaceJockeyColors,x dex bpl .cycleGameColors .skipColorCycling inc colorCyclingModeTimer bne .vsyncWaitTime sty colorCyclingMode ; color cycling mode now negative .vsyncWaitTime ldy INTIM bne .vsyncWaitTime sty WSYNC sty VSYNC ; end vertical sync VerticalBlank lda #VBLANK_TIME sta TIM64T ; set timer for VBLANK time lda SWCHB ; read the console switches and #SELECT_MASK | RESET_MASK ; mask the SELECT and RESET values cmp #SELECT_MASK | RESET_MASK ; see if SELECT or RESET is pressed bne .consoleSwitchDown ; one of the console switches is down lda INPT4 ; read left player fire button bmi .checkForGameReset ; if not pressed then check game reset .consoleSwitchDown sty colorCyclingMode ; reset color cycling mode sty colorCyclingModeTimer ; zero out ldx #<[livesColor - spaceJockeyColors] jsr InitLoop ; reset color values .checkForGameReset lda SWCHB ; read the console switches lsr ; shift RESET to carry bcs .skipReset ; skip game reset lda clearGameRAM ; if negative then clear game RAM bmi ClearGameRAM .setToClearGameRAM dec clearGameRAM ; show to clear RAM ClearGameRAM ldx #<[colorCyclingMode - (PF1 + 64)] lda #0 .clearRAM sta PF1 + 64,x ; clear RAM from PF2 to colorCyclingMode dex ; back to PF1 bne .clearRAM jsr InitializeGame bmi .convertDigits ; unconditional branch .skipReset ldx #0 lsr ; shift SELECT to carry bcs .skipSelect lda clearGameRAM bpl .setToClearGameRAM lda selectDebounce beq .incrementGameSelection dec selectDebounce bpl .skipIncrementGameSelection .incrementGameSelection stx playerScore + 1 ; clear the player's score (x = 0) stx playerScore inc gameSelection ; increase game selection lda gameSelection ; get the game selection and #$0F ; mask the upper nybble sta gameSelection ; store the value in the gameSelection clc adc #1 ; add 1 to the value cmp #10 ; show tens position value if than 10 bcc .setTensPosition sbc #10 ; subtract 10 from the value (carry set) ora #16 ; set the upper nybble to show tens value .setTensPosition sta playerScore + 2 ; store value to show the game selection ldx #SELECT_DELAY ; reset select debounce value .skipSelect stx selectDebounce .skipIncrementGameSelection lda gameState ; get the current game state bmi .skipAttactModeProcessing ; branch if game in progress lda INPT4 ; read left player fire button bmi .checkToShowHighScore ; skip game start if not pressed lda clearGameRAM bpl .setToClearGameRAM lda #$FF sta gameState ; show game is in progress inc clearGameRAM ; now positive sty playerScore + 2 sty showHighScore ; show the score beq .convertDigits ; unconditional branch .checkToShowHighScore lda SWCHA ; read the joystick values and #$F0 ; only concerned with left joystick port cmp #$F0 ; if the stick wasn't moved then beq .dontShowHighScore ; don't show the high score ldy #$FF ; joystick was moved so show high score .dontShowHighScore sty showHighScore ; set flag to trigger showing high score .convertDigits jmp BCD2DigitPtrs .skipAttactModeProcessing lda spaceJockeyHit ; check if the space jocky was hit bmi CheckGameCollisions ; skip joystick routine if true lda SWCHA ; read the joystick values sta allowedMotion ; save the value for later lda gameSelection ; get the current game selection and #OPTION_PLAYER_MOVE_HORIZ ; if the selection is divisible by 4 beq .checkVerticalMotion ; then skip horizontal movement lda allowedMotion ; get the player joystick value asl bmi .checkRightMotion ; ; player moving left ; inc randomSeed + 2 dec spaceJockeyHorzPos dec spaceJockeyHorzPos lda #SPACE_JOCKEY_XMIN ; make sure Space Jockey doesn't move cmp spaceJockeyHorzPos ; too far to the left bcc .checkRightMotion sta spaceJockeyHorzPos .checkRightMotion lda allowedMotion ; get the player joystick value bmi .checkVerticalMotion ; ; player moving right ; inc randomSeed inc spaceJockeyHorzPos inc spaceJockeyHorzPos lda #SPACE_JOCKEY_XMAX ; make sure Space Jockey doesn't move cmp spaceJockeyHorzPos ; too far to the right bcs .checkVerticalMotion sta spaceJockeyHorzPos .checkVerticalMotion lda allowedMotion ; get the player joystick value and #<~MOVE_DOWN bne .checkUpMotion ; ; player moving down ; inc randomSeed dec spaceJockeyVertPos dec spaceJockeyVertPos lda #SPACE_JOCKEY_YMIN ; make sure Space Jockey doesn't move cmp spaceJockeyVertPos ; too far down the screen bcc .checkUpMotion sta spaceJockeyVertPos .checkUpMotion lda allowedMotion ; get the player joystick value and #<~MOVE_UP bne CheckGameCollisions ; ; player moving up ; dec randomSeed + 1 inc spaceJockeyVertPos inc spaceJockeyVertPos lda #SPACE_JOCKEY_YMAX ; make sure Space Jockey doesn't move cmp spaceJockeyVertPos ; too far up the screen bcs CheckGameCollisions sta spaceJockeyVertPos CheckGameCollisions lda gameSelection and #OPTION_PLAYER_COLLISIONS ; game #8-16 check player/player collision beq .noPlayerCollisions lda CXPPMM ; read the player/missle collision bpl .noPlayerCollisions ; if the players hadn't collided then skip lda spaceJockeyVertPos sec sbc #SPACE_JOCKEY_HEIGHT - 2 bne .doneSpaceJockeyMissile .noPlayerCollisions lda spaceJockeyFiring ; get Space Jockey firing state bmi .moveSpaceJockeyMissile ; if firing then move the missile sta AUDV0 ldx spaceJockeyVertPos dex dex stx spaceJockeyMissileVert lda spaceJockeyHorzPos clc adc #16 sta spaceJockeyMissileHoriz lda INPT4 ; read left player fire button ora spaceJockeyHit ; or the value with space jockey hit state bmi RollingMountainAnimation ; skip the missile fire routine dec spaceJockeyFiring ; value now negative to show firing state dec spaceJockeyMissileVert .moveSpaceJockeyMissile inc randomSeed lda #8 sta AUDV0 sta AUDC0 lda spaceJockeyMissileHoriz ; get Space Jockey missile horizontal position clc adc #MISSILE_PIXEL_MOVEMENT ; Space Jockey missile pixel movement cmp #MISSILE_XMAX ; make sure the missile doesn't go out of bcc .setMissileHorizontalValue ; range lda #MISSILE_XMIN sta spaceJockeyMissileVert .setMissileHorizontalValue sta spaceJockeyMissileHoriz lda spaceJockeyShotFreq ; get the shot sound frequency clc adc #MISSILE_PIXEL_MOVEMENT ; increment it by the pixel movement cmp #SPACE_JOCKEY_XMAX - 1 bcc .setSpaceJockeyShotFrequency inc spaceJockeyFiring ; value positive to show not firing state lda #0 ; used to reset the shot sound frequency .setSpaceJockeyShotFrequency sta spaceJockeyShotFreq sta AUDF0 lda CXM1P bpl .moveSpaceJockeyMissileVert ; Space Jockey didn't shoot an enemy lda spaceJockeyMissileVert .doneSpaceJockeyMissile jsr FindKernelZone ; find the "zone" the space jockey is in lda enemyAttributes,x ; get the attributes value bmi .moveSpaceJockeyMissileVert ; branch if the object has been shot ora #ENEMY_DESTROYED ; show object is destroyed sta enemyAttributes,x lda #$15 sta AUDF0 ; set the object shot sound frequency sta spaceJockeyMissileVert lda #2 sta enemyDeathAnimRate,x ; death animation updated every 3 frames lda #0 sta enemyDeathSound,x sta spaceJockeyFiring ; clear the space jockey missile data sta spaceJockeyMissileHoriz sta spaceJockeyShotFreq jsr IncrementScore lda #$0F sta AUDC0 .moveSpaceJockeyMissileVert lda spaceJockeyFiring ; check to see if Space Jockey is firing bpl RollingMountainAnimation ; if not then skip to moutain rolling lda spaceJockeyMissileVert ; get the vertical position of the missile lsr ; if odd skip to mountain rolling (2LK) bcs RollingMountainAnimation lda gameSelection ; get the game selection lsr ; if even number skip to mountain rolling bcc RollingMountainAnimation lda spaceJockeyVertPos ; get Space Jockey vertical position sbc #SPACE_JOCKEY_MISSILE_OFFSET ; reduce the value by 3 for the new sta spaceJockeyMissileVert ; vertical position of the missile RollingMountainAnimation dec mountainRollingRate bpl .skipMountainRolling lda #MOUNTAIN_ROLL_RATE sta mountainRollingRate ldx #2 .rollMoutainLoop lda pf0Data,x and #$10 adc #$FE ror pf2Data,x rol pf1Data,x ror pf0Data,x dex bpl .rollMoutainLoop .skipMountainRolling lda spaceJockeyHit ; check to see if the space jockey was hit bpl .spaceJockeyAnimation ; branch if not dec spaceJockDeathAnimRate ; reduce the death animation frame count bpl .moveEnemies ; branch if not negative ldx #2 stx spaceJockDeathAnimRate ; reset the death animation frame rate ldx spaceJockeyDeathSound inx ; increment the death sound frequency cpx #16 bcc .determineDeathAnimation clc ldy #0 sty AUDC1 ; clear the channel (turn off death sound) ldx #NUMBER_OF_GROUPS - 1 .clearEnemiesLoop sty enemyVelocity,x ; set enemy velocity to 0 to move faster lda enemyAttributes,x ; get the enemy attribute value bcs .clearNextEnemy ; clear next enemy if one enemy found active lsr ; moves ENEMY_ACTIVE to carry .clearNextEnemy dex bpl .clearEnemiesLoop bcs .moveEnemies dec numberOfLives ; reduce the number of lives bpl .skipGameOver ; if still positive then game not over sty gameState ; show that game is over (y = 0) .skipGameOver sty spaceJockeyHit ; show Space Jockey not hit (D1 = 0) ldx #<[spaceJockeyHorzPos - spaceJockeyColors] jsr InitLoop sty AUDC1 ; clear audio channel (y = 0) bmi .moveEnemies ; unconditional branch .determineDeathAnimation cpx #5 bcs .playSpaceJockeyDeathSound ldy #SPACE_JOCKEY_HEIGHT - 2 lda #>SpaceJockeyDeathSprites sta deathAnimPointer + 1 lda SpaceJockeyDeathAnimTable - 1,x sta deathAnimPointer .storeDeathAnimationGraphics lda (deathAnimPointer),y sta spaceJockeyGraphics,y dey bpl .storeDeathAnimationGraphics .playSpaceJockeyDeathSound stx spaceJockeyDeathSound ; save the current death sound value txa sta AUDF1 ; set the death sound frequency eor #$FF sta AUDV1 ; set the death sound volume lda #8 sta AUDC1 ; set the death sound channel .moveEnemies jmp MoveEnemies .spaceJockeyAnimation ldy #%11010101 lda #8 and frameCount beq .setSpaceJockeyWindowGraphics ldy #%10101011 .setSpaceJockeyWindowGraphics sty spaceJockeyGraphics + 2 ldy #SIREN_LIGHT + 14 lda #$10 and frameCount beq .setSirenLightColor ldy #SIREN_LIGHT + 6 .setSirenLightColor sty spaceJockeyColors + 5 MoveEnemies ldx #NUMBER_OF_GROUPS - 1 .moveEnemyLoop lda enemyAttributes,x ; get the enemy's attributes lsr ; shift ENEMY_ACTIVE to carry bcs .determineEnemyMovement ; check to move enemy jmp CheckToLaunchNewAttack .determineEnemyMovement dec enemyMotion,x bpl .checkForEnemyShot lda enemyVelocity,x sta enemyMotion,x lda objectIds,x ; get the object id cmp #ID_HOUSE ; check to see if it can move vertically bcs .moveEnemyLeft ; if not...move object toward Space Jockey lda gameSelection ; get the game selection and #OPTION_ENEMY_MOVE_VERT ; check if the objects can move vertically beq .moveEnemyLeft ; ; enemy can move randomly up or down (game options 3, 4, 7, 8, 11, 12, 15, 16) ; lda randomSeed,x cmp #$D0 bcs .moveEnemyLeft bpl .moveEnemyUp .moveEnemyDown dec enemyVerticalPos,x lda EnemyVerticalFloor,x cmp enemyVerticalPos,x bcc .moveEnemyLeft bcs .setEnemyVerticalPosition .moveEnemyUp inc enemyVerticalPos,x lda EnemyVerticalCeiling,x cmp enemyVerticalPos,x bcs .moveEnemyLeft .setEnemyVerticalPosition sta enemyVerticalPos,x .moveEnemyLeft dec enemyHorizPos,x bne .checkForEnemyShot .removeEnemy lda #<[JetPlane - 1] ; point to any 0 byte on $F7 page sta enemyLSB,x ; store it to erase sprite lda enemyAttributes,x and #~(ENEMY_DESTROYED | ENEMY_ACTIVE) sta enemyAttributes,x ; show the enemy as INACTIVE lda #INITIAL_START_COUNT sta enemyMotion,x bne .nextEnemy ; unconditional branch .checkForEnemyShot lda enemyAttributes,x ; get the enemy attribute value bpl .setEnemyAnimation ; branch if enemy not destroyed dec enemyDeathAnimRate,x ; decrement death animation rate bpl .jumpNextEnemy ; keep current frame if positive lda #2 sta enemyDeathAnimRate,x inc enemyDeathSound,x ldy enemyDeathSound,x cpy #16 bcs .removeEnemy cpy #5 bcs .playEnemyDeathSound lda ExplosionAnimationTable - 1,y sta enemyLSB,x lda #NumberFonts sta digitPointer + 1,x dex dex lda playerScore,y ; get the value to display and #$0F ; mask the upper nybbles asl ; multiply the value by 8 asl asl adc #NumberFonts sta digitPointer + 1,x iny ; next digit dex dex bpl .bcd2DigitLoop ldx #8 ldy #