Skip to content

Instantly share code, notes, and snippets.

@ped7g
Created May 4, 2023 10:31
Show Gist options
  • Save ped7g/48b076cbb2c6d514a9cd87f88ca6ef0d to your computer and use it in GitHub Desktop.
Save ped7g/48b076cbb2c6d514a9cd87f88ca6ef0d to your computer and use it in GitHub Desktop.
Failed attempt to create band-aid tool to make ZX Next HDMI 50Hz mode timing good-enough for multicolor titles
; ----- Colour palette (ULA)
BLACK equ 0
BLUE equ 1
RED equ 2
MAGENTA equ 3
GREEN equ 4
CYAN equ 5
YELLOW equ 6
WHITE equ 7
P_BLACK equ 0
P_BLUE equ 1<<3
P_RED equ 2<<3
P_MAGENTA equ 3<<3
P_GREEN equ 4<<3
P_CYAN equ 5<<3
P_YELLOW equ 6<<3
P_WHITE equ 7<<3
; ----- Attribs
A_FLASH equ 128
A_BRIGHT equ 64
;----------------------------------------------
BIT_UP equ 4 ; 16
BIT_DOWN equ 5 ; 32
BIT_LEFT equ 6 ; 64
BIT_RIGHT equ 7 ; 128
DIR_NONE equ %00000000
DIR_UP equ %00010000
DIR_DOWN equ %00100000
DIR_LEFT equ %01000000
DIR_RIGHT equ %10000000
DIR_UP_I equ %11101111
DIR_DOWN_I equ %11011111
DIR_LEFT_I equ %10111111
DIR_RIGHT_I equ %01111111
;-----------------------------------------------------------------------------
;-- I/O ports - ZX Spectrum classic (48, 128, Timex, Pentagon, ...) ports
ULA_P_FE equ $FE ; BORDER + MIC + BEEP + read Keyboard
TIMEX_P_FF equ $FF ; Timex video control port
ZX128_MEMORY_P_7FFD equ $7FFD ; ZX Spectrum 128 ports
ZX128_MEMORY_P_DFFD equ $DFFD
ZX128P3_MEMORY_P_1FFD equ $1FFD
AY_REG_P_FFFD equ $FFFD
AY_DATA_P_BFFD equ $BFFD
Z80_DMA_PORT_DATAGEAR equ $6B ; on ZXN the zxnDMA handles this in zxnDMA mode
Z80_DMA_PORT_MB02 equ $0B ; on ZXN the zxnDMA handles this in Zilog mode
DIVMMC_CONTROL_P_E3 equ $E3
SPI_CS_P_E7 equ $E7
SPI_DATA_P_EB equ $EB
KEMPSTON_MOUSE_X_P_FBDF equ $FBDF
KEMPSTON_MOUSE_Y_P_FFDF equ $FFDF
KEMPSTON_MOUSE_B_P_FADF equ $FADF ; kempston mouse wheel+buttons
KEMPSTON_JOY1_P_1F equ $1F
KEMPSTON_JOY2_P_37 equ $37
;-----------------------------------------------------------------------------
;-- I/O ports - ZX Spectrum NEXT specific ports
TBBLUE_REGISTER_SELECT_P_243B equ $243B
; -- port $243B = 9275 Read+Write (detection bitmask: %0010_0100_0011_1011)
; -- selects NextREG mapped at port TBBLUE_REGISTER_ACCESS_P_253B
TBBLUE_REGISTER_ACCESS_P_253B equ $253B
; -- port $253B = 9531 Read?+Write? (detection bitmask: %0010_0101_0011_1011)
; -- data for selected NextREG (read/write depends on the register selected)
; indexes into DAC_CHANNEL_* def-arrays, depending on the type of DAC you want to use
DAC_GS_COVOX_INDEX equ 1
DAC_PENTAGON_ATM_INDEX equ 2
DAC_SPECDRUM_INDEX equ 3
DAC_SOUNDRIVE1_INDEX equ 4
DAC_SOUNDRIVE2_INDEX equ 5
DAC_COVOX_INDEX equ 6
DAC_PROFI_COVOX_INDEX equ 7
; -- enable 8bit DACs with PERIPHERAL_3_NR_08, use DAC_*_INDEX to access particular set of ports
DEFARRAY DAC_CHANNEL_A @@, @@, $FB, $DF, $1F, $F1, @@, $3F
DEFARRAY DAC_CHANNEL_B @@, $B3, @@, @@, $0F, $F3, $0F, @@
DEFARRAY DAC_CHANNEL_C @@, $B3, @@, @@, $4F, $F9, $4F, @@
DEFARRAY DAC_CHANNEL_D @@, @@, $FB, $DF, $5F, $FB, @@, $5F
; -- like for example: ld bc,DAC_CHANNEL_B[DAC_PROFI_COVOX_INDEX]
I2C_SCL_P_103B equ $103B ; i2c bus port (clock) (write only?)
I2C_SDA_P_113B equ $113B ; i2c bus port (data) (read+write)
UART_TX_P_133B equ $133B ; UART tx port (read+write)
UART_RX_P_143B equ $143B ; UART rx port (read+write)
UART_CTRL_P_153B equ $153B ; UART control port (read+write)
ZILOG_DMA_P_0B equ $0B
ZXN_DMA_P_6B equ $6B
; -- port $6B = 107 Read+Write (detection bitmask: %xxxx_xxxx_0110_1011)
; - The zxnDMA is mostly compatible with Zilog DMA chip (Z8410) (at least
; as far as old ZX apps are concerned), but has many modifications.
; - core3.1.1 update - Zilog/zxnDMA mode is now selected by port number, not PERIPHERAL_2_NR_06!
; - core3.0 update - (REMOVED) specific behaviour details can be selected (PERIPHERAL_2_NR_06)
LAYER2_ACCESS_P_123B equ $123B
; -- port $123B = 4667 Read+Write (detection bitmask: %0001_0010_0011_1011)
; - see ports.txt or wiki for details (has become a bit more complex over time)
LAYER2_ACCESS_WRITE_OVER_ROM equ $01 ; map Layer2 bank into ROM area (0000..3FFF) for WRITE-only (reads as ROM)
LAYER2_ACCESS_L2_ENABLED equ $02 ; enable Layer2 (make banks form nextreg $12 visible)
LAYER2_ACCESS_READ_OVER_ROM equ $04 ; map Layer2 bank into ROM area (0000..3FFF) for READ-only
LAYER2_ACCESS_SHADOW_OVER_ROM equ $08 ; bank selected by bits 6-7 is from "shadow Layer 2" banks range (nextreg $13)
LAYER2_ACCESS_BANK_OFFSET equ $10 ; bit 2-0 is bank offset for current active mapping +0..+7 (other bits are reserved, use 0)
LAYER2_ACCESS_OVER_ROM_BANK_M equ $C0 ; (mask of) value 0..3 selecting bank mapped for R/W (Nextreg $12 or $13)
LAYER2_ACCESS_OVER_ROM_BANK_0 equ $00 ; screen lines 0..63 (256x192) or columns 0..63 (320x256) or columns 0..127 (640x256)
LAYER2_ACCESS_OVER_ROM_BANK_1 equ $40 ; screen lines 64..127 (256x192) or columns 64..127 (320x256) or columns 128..255 (640x256)
LAYER2_ACCESS_OVER_ROM_BANK_2 equ $80 ; screen lines 128..191 (256x192) or columns 128..191 (320x256) or columns 256..383 (640x256)
LAYER2_ACCESS_OVER_ROM_48K equ $C0 ; maps all 0..191 lines into $0000..$BFFF region (256x192) or 2/3 of columns in 320x256/640x256
SPRITE_STATUS_SLOT_SELECT_P_303B equ $303B
; -- port $303B = 12347 Read+Write (detection bitmask: %0011_0000_0011_1011)
; -- write:
; - sets both "sprite slot" (0..63) and "pattern slot" (0..63 +128)
; - once the sprite/pattern slots are set, they act independently and
; each port ($xx57 and $xx5B) will auto-increment its own slot index
; (to resync one can write to this port again).
; - the +128 flag will make the pattern upload start at byte 128 of pattern
; slot (second half of slot)
; - The sprite-slot (sprite-attributes) may be optionally interlinked with
; NextReg $34 (feature controlled by NextReg $34)
; - auto-increments of slot position from value 63 are officially
; "undefined behaviour", wrap to 0 is not guaranteed. (only setting slots
; explicitly back to valid 0..63 will make your code future-proof)
; -- read (will also reset both collision and max-sprites flags):
; - bit 1 = maximum sprites per line hit (set when sprite renderer ran
; out of time when preparing next scanline)
; - bit 0 = collision flag (set when any sprites draw non-transparent
; pixel at the same location)
; Both flags contain values for current scanline already at the beginning
; of scanline (sprite engine renders one line ahead into buffer and updates
; flags progressively as it renders the sprites)
SPRITE_STATUS_MAXIMUM_SPRITES equ $02
SPRITE_STATUS_COLLISION equ $01
SPRITE_SLOT_SELECT_PATTERN_HALF equ 128 ; add it to 0..63 index to make pattern upload start at second half of pattern
SPRITE_ATTRIBUTE_P_57 equ $57
; -- port $xx57 = 87 write-only (detection bitmask: %xxxx_xxxx_0101_0111)
; - writing 4 or 5 bytes long structures to control particular sprite
; - after 4/5 bytes block the sprite slot index is auto-incremented
; - for detailed documentation check official docs or wiki (too long)
SPRITE_PATTERN_P_5B equ $5B
; -- port $xx5B = 91 write-only (detection bitmask: %xxxx_xxxx_0101_1011)
; - each pattern slot is 256 bytes long = one 16x16 pattern of 8-bit pixels
; or two 16x16 patterns of 4-bit pixels.
; - Patterns are uploaded in "English" order (left to right, top to bottom),
; one byte encodes single pixel in 8 bit mode and two pixels in 4 bit
; mode (bits 7-4 are "left" pixel, 3-0 are "right" pixel)
; - pixels are offset (index) into active sprite palette
TURBO_SOUND_CONTROL_P_FFFD equ $FFFD ; write with bit 7 = 1 (port shared with AY)
;-----------------------------------------------------------------------------
;-- NEXT HW Registers (NextReg)
MACHINE_ID_NR_00 equ $00
NEXT_VERSION_NR_01 equ $01
NEXT_RESET_NR_02 equ $02
MACHINE_TYPE_NR_03 equ $03
ROM_MAPPING_NR_04 equ $04 ;In config mode, allows RAM to be mapped to ROM area.
PERIPHERAL_1_NR_05 equ $05 ;Sets joystick mode, video frequency and Scandoubler.
PERIPHERAL_2_NR_06 equ $06 ;Enables turbo/50Hz/60Hz keys, DivMMC, Multiface and audio (beep/AY)
TURBO_CONTROL_NR_07 equ $07
PERIPHERAL_3_NR_08 equ $08 ;ABC/ACB Stereo, Internal Speaker, SpecDrum, Timex Video Modes, Turbo Sound Next, RAM contention and [un]lock 128k paging.
PERIPHERAL_4_NR_09 equ $09 ;Sets scanlines, AY mono output, Sprite-id lockstep, disables Kempston and divMMC ports.
PERIPHERAL_5_NR_0A equ $0A ;Mouse buttons and DPI settings (core 3.1.5)
NEXT_VERSION_MINOR_NR_0E equ $0E
ANTI_BRICK_NR_10 equ $10
VIDEO_TIMING_NR_11 equ $11
LAYER2_RAM_BANK_NR_12 equ $12 ;bank number where visible Layer 2 video memory begins.
LAYER2_RAM_SHADOW_BANK_NR_13 equ $13 ;bank number for "shadow" write-over-rom mapping
GLOBAL_TRANSPARENCY_NR_14 equ $14 ;Sets the color treated as transparent for ULA/Layer2/LoRes
SPRITE_CONTROL_NR_15 equ $15 ;LoRes mode, Sprites configuration, layers priority
; bit 7: enable LoRes mode
; bit 6: sprite rendering (1=sprite 0 on top of other, 0=sprite 0 at bottom)
; bit 5: If 1, the clipping works even in "over border" mode
; 4-2: layers priority: 000=SLU, 001=LSU, 010=SUL, 011=LUS, 100=USL, 101=ULS, 110=S,mix(U+L), 111=S,mix(U+L-5)
; bit 1: enable sprites over border, bit 0: show sprites
LAYER2_XOFFSET_NR_16 equ $16
LAYER2_YOFFSET_NR_17 equ $17
CLIP_LAYER2_NR_18 equ $18
CLIP_SPRITE_NR_19 equ $19
CLIP_ULA_LORES_NR_1A equ $1A
CLIP_TILEMAP_NR_1B equ $1B
CLIP_WINDOW_CONTROL_NR_1C equ $1C ;set to 15 to reset all clip-window indices to 0
VIDEO_LINE_MSB_NR_1E equ $1E
VIDEO_LINE_LSB_NR_1F equ $1F
VIDEO_INTERUPT_CONTROL_NR_22 equ $22 ;Controls the timing of raster interrupts and the ULA frame interrupt.
VIDEO_INTERUPT_VALUE_NR_23 equ $23
ULA_XOFFSET_NR_26 equ $26 ;since core 3.0
ULA_YOFFSET_NR_27 equ $27 ;since core 3.0
HIGH_ADRESS_KEYMAP_NR_28 equ $28 ;reads first 8b part of value written to $44 (even unfinished 16b write)
LOW_ADRESS_KEYMAP_NR_29 equ $29
HIGH_DATA_TO_KEYMAP_NR_2A equ $2A
LOW_DATA_TO_KEYMAP_NR_2B equ $2B
DAC_B_MIRROR_NR_2C equ $2C ;reads as MSB of Pi I2S left side sample, LSB waits at $2D
DAC_AD_MIRROR_NR_2D equ $2D ;another alias for $2D, reads LSB of value initiated by $2C or $2E read
SOUNDDRIVE_DF_MIRROR_NR_2D equ $2D ;Nextreg port-mirror of port 0xDF
DAC_C_MIRROR_NR_2E equ $2E ;reads as MSB of Pi I2S right side sample, LSB waits at $2D
TILEMAP_XOFFSET_MSB_NR_2F equ $2F
TILEMAP_XOFFSET_LSB_NR_30 equ $30
TILEMAP_YOFFSET_NR_31 equ $31
LORES_XOFFSET_NR_32 equ $32
LORES_YOFFSET_NR_33 equ $33
SPRITE_ATTR_SLOT_SEL_NR_34 equ $34 ;Sprite-attribute slot index for $35-$39/$75-$79 port $57 mirrors
SPRITE_ATTR0_NR_35 equ $35 ;port $57 mirror in nextreg space (accessible to copper)
SPRITE_ATTR1_NR_36 equ $36
SPRITE_ATTR2_NR_37 equ $37
SPRITE_ATTR3_NR_38 equ $38
SPRITE_ATTR4_NR_39 equ $39
PALETTE_INDEX_NR_40 equ $40 ;Chooses a ULANext palette number to configure.
PALETTE_VALUE_NR_41 equ $41 ;Used to upload 8-bit colors to the ULANext palette.
PALETTE_FORMAT_NR_42 equ $42 ;ink-mask for ULANext modes
PALETTE_CONTROL_NR_43 equ $43 ;Enables or disables ULANext interpretation of attribute values and toggles active palette.
PALETTE_VALUE_9BIT_NR_44 equ $44 ;Holds the additional blue color bit for RGB333 color selection.
TRANSPARENCY_FALLBACK_COL_NR_4A equ $4A ;8-bit colour to be drawn when all layers are transparent
SPRITE_TRANSPARENCY_I_NR_4B equ $4B ;index of transparent colour in sprite palette (only bottom 4 bits for 4-bit patterns)
TILEMAP_TRANSPARENCY_I_NR_4C equ $4C ;index of transparent colour in tilemap graphics (only bottom 4 bits)
MMU0_0000_NR_50 equ $50 ;Set a Spectrum RAM page at position 0x0000 to 0x1FFF
MMU1_2000_NR_51 equ $51 ;Set a Spectrum RAM page at position 0x2000 to 0x3FFF
MMU2_4000_NR_52 equ $52 ;Set a Spectrum RAM page at position 0x4000 to 0x5FFF
MMU3_6000_NR_53 equ $53 ;Set a Spectrum RAM page at position 0x6000 to 0x7FFF
MMU4_8000_NR_54 equ $54 ;Set a Spectrum RAM page at position 0x8000 to 0x9FFF
MMU5_A000_NR_55 equ $55 ;Set a Spectrum RAM page at position 0xA000 to 0xBFFF
MMU6_C000_NR_56 equ $56 ;Set a Spectrum RAM page at position 0xC000 to 0xDFFF
MMU7_E000_NR_57 equ $57 ;Set a Spectrum RAM page at position 0xE000 to 0xFFFF
COPPER_DATA_NR_60 equ $60
COPPER_CONTROL_LO_NR_61 equ $61
COPPER_CONTROL_HI_NR_62 equ $62
COPPER_DATA_16B_NR_63 equ $63 ; same as $60, but waits for full 16b before write
VIDEO_LINE_OFFSET_NR_64 equ $64 ; (core 3.1.5)
ULA_CONTROL_NR_68 equ $68
DISPLAY_CONTROL_NR_69 equ $69
LORES_CONTROL_NR_6A equ $6A
TILEMAP_CONTROL_NR_6B equ $6B
TILEMAP_DEFAULT_ATTR_NR_6C equ $6C
TILEMAP_BASE_ADR_NR_6E equ $6E ;Tilemap base address of map
TILEMAP_GFX_ADR_NR_6F equ $6F ;Tilemap definitions (graphics of tiles)
LAYER2_CONTROL_NR_70 equ $70
LAYER2_XOFFSET_MSB_NR_71 equ $71 ; for 320x256 and 640x256 L2 modes (core 3.0.6+)
SPRITE_ATTR0_INC_NR_75 equ $75 ;port $57 mirror in nextreg space (accessible to copper) (slot index++)
SPRITE_ATTR1_INC_NR_76 equ $76
SPRITE_ATTR2_INC_NR_77 equ $77
SPRITE_ATTR3_INC_NR_78 equ $78
SPRITE_ATTR4_INC_NR_79 equ $79
USER_STORAGE_0_NR_7F equ $7F
EXPANSION_BUS_ENABLE_NR_80 equ $80
EXPANSION_BUS_CONTROL_NR_81 equ $81
INTERNAL_PORT_DECODING_0_NR_82 equ $82 ;bits 0-7
INTERNAL_PORT_DECODING_1_NR_83 equ $83 ;bits 8-15
INTERNAL_PORT_DECODING_2_NR_84 equ $84 ;bits 16-23
INTERNAL_PORT_DECODING_3_NR_85 equ $85 ;bits 24-31
EXPANSION_BUS_DECODING_0_NR_86 equ $86 ;bits 0-7 mask
EXPANSION_BUS_DECODING_1_NR_87 equ $87 ;bits 8-15 mask
EXPANSION_BUS_DECODING_2_NR_88 equ $88 ;bits 16-23 mask
EXPANSION_BUS_DECODING_3_NR_89 equ $89 ;bits 24-31 mask
EXPANSION_BUS_PROPAGATE_NR_8A equ $8A ;Monitoring internal I/O or adding external keyboard
ALTERNATE_ROM_NR_8C equ $8C ;Enable alternate ROM or lock 48k ROM
ZX_MEM_MAPPING_NR_8E equ $8E ;shortcut to set classic zx128+3 memory model at one place
PI_GPIO_OUT_ENABLE_0_NR_90 equ $90 ;pins 0-7
PI_GPIO_OUT_ENABLE_1_NR_91 equ $91 ;pins 8-15
PI_GPIO_OUT_ENABLE_2_NR_92 equ $92 ;pins 16-23
PI_GPIO_OUT_ENABLE_3_NR_93 equ $93 ;pins 24-27
PI_GPIO_0_NR_98 equ $98 ;pins 0-7
PI_GPIO_1_NR_99 equ $99 ;pins 8-15
PI_GPIO_2_NR_9A equ $9A ;pins 16-23
PI_GPIO_3_NR_9B equ $9B ;pins 24-27
PI_PERIPHERALS_ENABLE_NR_A0 equ $A0
PI_I2S_AUDIO_CONTROL_NR_A2 equ $A2
;PI_I2S_CLOCK_DIVIDE_NR_A3 equ $A3 ; REMOVED in core 3.1.5 (no more master-mode)
ESP_WIFI_GPIO_OUTPUT_NR_A8 equ $A8
ESP_WIFI_GPIO_NR_A9 equ $A9
EXTENDED_KEYS_0_NR_B0 equ $B0 ;read Next compound keys as standalone keys (outside of zx48 matrix)
EXTENDED_KEYS_1_NR_B1 equ $B1 ;read Next compound keys as standalone keys (outside of zx48 matrix)
;DIVMMC_TRAP_ENABLE_1_NR_B2 equ $B2 ; NOT IMPLEMENTED in core yet (as of 3.1.4), may happen in future
;DIVMMC_TRAP_ENABLE_2_NR_B4 equ $B4 ; NOT IMPLEMENTED in core yet (as of 3.1.4), may happen in future
DEBUG_LED_CONTROL_NR_FF equ $FF ;Turns debug LEDs on and off on TBBlue implementations that have them.
;-----------------------------------------------------------------------------
;-- common memory addresses
MEM_ROM_CHARS_3C00 equ $3C00 ; actual chars start at $3D00 with space
MEM_ZX_SCREEN_4000 equ $4000
MEM_ZX_ATTRIB_5800 equ $5800
MEM_LORES0_4000 equ $4000
MEM_LORES1_6000 equ $6000
MEM_TIMEX_SCR0_4000 equ $4000
MEM_TIMEX_SCR1_6000 equ $6000
;-----------------------------------------------------------------------------
;-- Copper commands
COPPER_NOOP equ %00000000
COPPER_WAIT_H equ %1'000000'0
COPPER_HALT_B equ $FF ; 2x $FF = wait for (511,63) = infinite wait
;-----------------------------------------------------------------------------
; DMA (Register 6)
DMA_RESET equ $C3
DMA_RESET_PORT_A_TIMING equ $C7
DMA_RESET_PORT_B_TIMING equ $CB
DMA_LOAD equ $CF
DMA_CONTINUE equ $D3
DMA_DISABLE_INTERUPTS equ $AF
DMA_ENABLE_INTERUPTS equ $AB
DMA_RESET_DISABLE_INTERUPTS equ $A3
DMA_ENABLE_AFTER_RETI equ $B7
DMA_READ_STATUS_BYTE equ $BF
DMA_REINIT_STATUS_BYTE equ $8B
DMA_START_READ_SEQUENCE equ $A7
DMA_FORCE_READY equ $B3
DMA_DISABLE equ $83
DMA_ENABLE equ $87
DMA_READ_MASK_FOLLOWS equ $BB
; About UART<->ESP baund rate from AA:
;
; It's very easy to compute the prescalar.
; 1. Read nextreg 0x11 to find out the video timing the system is using 0-7
; 2. Take the associated actual system clock from this table:define(__CLK_28_0', 28000000)
; define(CLK_28_1', 28571429)
; define(`CLK_28_2', 29464286)
; define(__CLK_28_3', 30000000)
; define(CLK_28_4', 31000000)
; define(`CLK_28_5', 32000000)
; define(__CLK_28_6', 33000000)
; define(__CLK_28_7', 27000000)
; 3. Divide the clock by the baud rate you want to find the 14-bit prescalar value.
; That's it.
;=====================================================================================
; ********* does NOT WORK as expected *******
;=====================================================================================
; explained by AA (ZX Next FPGA core maintainer):
; The copper is always running at 28 MHz. The cpu change happens when the cpu control signals
; are idle :
; - on the rising edge of the cpu clock as seen in z80 timing diagrams with m1, mreq and iorq all high.
; The cpu speed is not changed until the cpu is in the idle state. That's to avoid runt pulses
; in the timing. But even after the cpu speed is change, the selection is through a hardware
; module BUFGMUX_1 which is a mux selecting the appropriate clock. This is a special piece
; of hardware that makes sure the clock is changed in a way that runt pulses don't happen
; so there could be a cycle or two at the slower clock speed before that allows a change too.
; * So the change can happen on any rising edge of the cpu's clock but usually not
; in the middle of memory or i/o cycles.
; If the cpu is at 28 MHz, the BUFGMUX might postpone changing to a 3.5 MHz clock until the 3.5 MHz
; clock goes low then high. And that's a contended clock so there are other reasons for that clock
; to be delayed. IIRC the contended clock is held high while contended.
;
;=====================================================================================
; Ped7g: so what happens with this prototype - the short 8px spans of CPU throttled to 7 or 14MHz
; becomes usually longer than 8px spans (looks it's statistically easier to go from 3.5MHz
; to 7/14MHz than to go down to 3.5MHz from 7/14MHz), and instead of boosting 216T scanline to
; correct 224T, the timing does fluctuate and does average higher than 224T per line, making
; this impossible as general band-aid for HDMI mode vs general multicolor titles.
;
; There is hypothetical possibility to tune this precisely against robust and stable engine with
; some margins in required precisions, like maybe nirvana+ engine, customising every copper line
; boost to fit the running code, but it's not clear/guaranteed the fluctuations will be stable
; across frames even with fixed engine code, and would require tedious manual tuning re-running
; the code on real Next with HDMI output all the time.
;
; As I don't have working HDMI display myself, I'm abandoning this project here, posting the crude
; prototype code with these comments, just in case somebody finds this interesting or even wants
; to try to tune the code for particular multicolor game.
;
;=====================================================================================
; dot7gFX "HDMI timing change" v0.1 2023-04-27, copper code to throttle CPU
; © Peter Helcmanovsky 2023, license: https://opensource.org/licenses/MIT
; requires ZX Spectrum Next with core3.1.5+
;
; Assembles with sjasmplus - https://github.com/z00m128/sjasmplus (v1.20.2+)
; The Makefile has build commands, but it should be enough to just run:
; sjasmplus hdmit.asm --raw=hdmit.dot
;
; TODO:
; - missing lot of parts as the main thing proved to not work, abandoning this
;
;=====================================================================================
; copper.txt timings:
;
; Table 3.1: Vertical line counts and dot clock combinations
;
; 48K VGA 50Hz 312 lines 224.0 * 4 = 896 dot clocks
; 128K VGA 50Hz 311 lines 228.0 * 4 = 912 dot clocks
; PENTAGON VGA 50Hz 320 lines 224.0 * 4 = 896 dot clocks
;
; 48K VGA 60Hz 262 lines 224.0 * 4 = 896 dot clocks
; 128K VGA 60Hz 261 lines 228.0 * 4 = 912 dot clocks
;
; HDMI 50Hz 312 lines 216.0 * 4 = 864 dot clocks
; HDMI 60Hz 262 lines 214.5 * 4 = 858 dot clocks
;
; Table 3.2: Dot clocks per second
;
; 48K VGA 50Hz 312 lines 13,977,600 clocks 14.0Mhz (28Mhz)
; 128K VGA 50Hz 311 lines 14,181,600 clocks 14.2Mhz (28Mhz)
; PENTAGON VGA 50Hz 320 lines 14,336,000 clocks 14.3Mhz (28Mhz)
;
; 48K VGA 60Hz 262 lines 14,085,120 clocks 14.1Mhz (28Mhz) (! obsolete !)
; 128K VGA 60Hz 261 lines 14,281,920 clocks 14.3Mhz (28Mhz) (! obsolete !)
;
; HDMI 50Hz 312 lines 13,478,400 clocks 13.5Mhz (27Mhz)
; HDMI 60Hz 262 lines 13,487,760 clocks 13.5Mhz (27Mhz)
;
; 16 dot clocks = 8 pixels in standard 256x192 resolution
; 16 dot clocks = 16 pixels in Timex HIRES 512x192 resolution
;
; Table 3.5: Slack dot clocks after maximum line compare
;
; 858 dot clocks per line (52 * 16 = 832) SLACK = 26 dot clocks
; 864 dot clocks per line (52 * 16 = 832) SLACK = 32 dot clocks
; 896 dot clocks per line (54 * 16 = 864) SLACK = 32 dot clocks
; 912 dot clocks per line (55 * 16 = 880) SLACK = 32 dot clocks
;=====================================================================================
; core 3.1.10 VHDL timings:
; 48K VGA 50Hz 312 lines 224.0 * 4 = 896 dot clocks int 248:0
; 128K VGA 50Hz 311 lines 228.0 * 4 = 912 dot clocks 128 int 248:4 / +3 int 248:2
; PENTAGON VGA 50Hz 320 lines 224.0 * 4 = 896 dot clocks int 239:323
;
; 48K VGA 60Hz 264 lines 224.0 * 4 = 896 dot clocks (! line diff !) int 224:0
; 128K VGA 60Hz 264 lines 228.0 * 4 = 912 dot clocks (! line diff !) 128 int 224:4 / +3 int 224:2
;
; HDMI 50Hz 312 lines 216.0 * 4 = 864 dot clocks int 256:4
; HDMI 60Hz 262 lines 214.5 * 4 = 858 dot clocks int 235:4
;
;=====================================================================================
; designing timing fix
; HDMI 50Hz -> ZX48 50Hz
; - same lines
; - each line has 216T instead of 224T = -8T
; - total frame difference 67392 - 69888 = -2496T
; HDMI 50Hz -> ZX128 50Hz
; - +1 line
; - each line has 216T instead of 228T = -12T
; - total frame difference 67392 - 70908 = -3516T
;
;=====================================================================================
; HDMI 50Hz -> ZX48 50Hz
; 3.5 3.5
; at 3.5MHz 1T -> 1T, 4T -> 4T
; at 7MHz 1T -> 2T, 4T -> 8T
; at 14MHz 1T -> 4T, 4T -> 16T (dot clock)
; at 28MHz 1T -> 8T, 4T -> 32T
; copper wait is per 8pix = 16 dot clocks (16 hires pixels), 4T in 3.5MHz
; WAIT 1 clock = 0.5dot, MOVE is 2 clocks = 1dot, NOP is 1 clock = 0.5dot
;
; to meet zx48 do two 8px strides at 7MHz, that's 2x doing 8T instead of 2x 4T = 16-8 = +8T
; at each line .. *ouch*
;
; wait line,39 ; 312px (256 + 56)
; move 7,1 ; 7MHz
; wait line,41 ; 328px (256 + 72)
; move 7,0 ; 3.5MHz
; -- 4 instructions is slot-code
; -- 1024 instructions / 4 = 256 slots
; ->missing room for 312-256 = 56 lines
;
; new slot allocation layout approach dealing with interupt as well,
; reconfiguring it to line interrupt at 248:128T (0x23 = 249 !)
; lines:
; 248..279: 32 lines, adding 12T per line, per four lines, -128T by late interrupt
; = 32 * (216 + 12) - 128 = 7168 T vs ZX48 32 * 224 = 7168 T
; - 4 segments at 14MHz = 16*4-4*4 = +48T (48/4 = +12T) at lines 248, 252, 256, 260, 264, 268, 272, 276
; 280..291: 12 lines, adding 8T per line, per two lines
; = 12 * (216 + 8) = 2688 T (same as ZX48)
; - 4 segments at 7MHz = 8*4-4*4 = +16T (16/2 = +8T) at lines 280, 282, .., 290
; 292..311: 20 lines, adding 8T every line
; = 20 * (216 + 8) = 4480 T (same as ZX48)
; - 2 segments at 7MHz = 8*2-4*2 = +8T at each line
; 0..211: 192+20 lines, adding 8T every line
; = 212 * (216 + 8) = 47488 T (same as ZX48)
; - 2 segments at 7MHz = 8*2-4*2 = +8T at each line
; 212..223: 12 lines, adding 8T per line, per two lines
; = 12 * (216 + 8) = 2688 T (same as ZX48)
; - 4 segments at 7MHz = 8*4-4*4 = +16T (16/2 = +8T) at lines 212, 214, .., 222
; 224..231: 8 lines, adding 8T per line, per four lines
; = 8 * (216 + 8) = 1792 T (same as ZX48)
; - 8 segments at 7MHz = 8*8-4*8 = +32T (32/4 = +8T) at lines 224, 228
; 232..247: 16 lines per 216T, no copper code
; = 16 * 216 = 3456 T vs ZX48 3584 T -> 128T missing
; 248 : extra +128T until line interrupt vs ZX48 0T -> fixing total frame time 69888 T
; total copper slots: = 8+6+20+192+20+6+2 = 254
; one free slot remaining in copper = 4 NOOP to deal with that
;
;=====================================================================================
; HDMI 50Hz -> ZX128 50Hz
;
; to meet zx128 do one 8px stride at 14MHz, that's 1x doing 16T instead of 1x 4T = 16-4 = +12T
;
; wait line,39 ; 312px (256 + 56)
; move 7,2 ; 14MHz
; wait line,40 ; 320px (256 + 64)
; move 7,0 ; 3.5MHz
; -- 4 instructions is slot-code
;
; possible layout:
; 192 pixel lines, top/bottom border: 20/21 lines per 1, 12 per 2
; =192+20+21+6+6 = 245 slots (11 to go), =192+20+21+12+12 = 257 lines (55 to go in HDMI, but 54 in zx128)
; 55 * 216T = 11880T
; 54 * 228T = 12312T
; = 432T difference
; 36 lines can use throttle to keep 228T
; 19 lines per 216T will throw away 228T to fix total lines as if there were 311 lines
; ^ this must happen before interrupt at end
; 55 lines per 5 = 11 slots
; 293 lines to throttle by +12T
;
OPT reset --zxnext --syntax=abf
INCLUDE "constants.i.asm"
ORG $2000
STRUCT S_VARS
vLinesCount WORD
intLine WORD
ENDS
;----------------------------------------------------------------------------------------------------------
startDot:
; parse command line options
call parseCommandLine
ld hl,txtCopy
call printMsg
call analyseCurrentMode
cfgAnalyse or a ; to be modified to `scf` by parseCommandLine
call c,displayModeAnalysis ; -a
cfgStopCu or a ; to be modified to `scf` by parseCommandLine
jp c,stopCopper ; -s to stop copper and restore interrupts (exits with CF=0)
ld hl,txtHelp
cfgHelp or a ; to be modified to `scf` by parseCommandLine
jp c,printMsg ; -h or invalid args (exits with CF=0)
; stop any copper code currently running and restore interrupts config
call stopCopper
; select copper code to generate (in this prototype only HDMI 50Hz -> ZX48 50Hz exists)
cfgMode ld a,0 ;FIXME all ; to be modified by parseCommandLine
; generate copper code to adjust timings
ld bc,TBBLUE_REGISTER_SELECT_P_243B
ld a,COPPER_DATA_16B_NR_63
out (c),a
inc b
getZx48:
;FIXME generate copper dynamically
; 248..279: 32 lines, adding 12T per line, per four lines, -128T by late interrupt
; = 32 * (216 + 12) - 128 = 7168 T vs ZX48 32 * 224 = 7168 T
; - 4 segments at 14MHz = 16*4-4*4 = +48T (48/4 = +12T) at lines 248, 252, 256, 260, 264, 268, 272, 276
ld a,$80|(39<<1) ; copper WAIT, h=39
ld de,$0700+248
ld hl,$0200|((39^43)<<1) ; h=2 means 14MHz, throttle is 4 segments => +48T => +12T per line
exa
ld a,8 ; line 248..279 (248, 252, 256, 260, 264, 268, 272, 276)
.gen_l1:
exa
out (c),a,,(c),e ; CWAIT line, 39
out (c),d,,(c),h ; CMOVE 7, fast CPU
xor l
out (c),a,,(c),e ; CWAIT line, 43
xor l
out (c),d,,(c),0 ; CMOVE 7, 3.5MHz ; out0-ok
.4 inc e
jr nz,$+3
inc a ; handle +256 overflow in line number
exa
dec a
jr nz,.gen_l1
; 280..291: 12 lines, adding 8T per line, per two lines
; = 12 * (216 + 8) = 2688 T (same as ZX48)
; - 4 segments at 7MHz = 8*4-4*4 = +16T (16/2 = +8T) at lines 280, 282, .., 290
ld a,6 ; line 280..291 (+2)
dec h ; h = 1 as 7MHz, throttle 4 segments => +16T => +8T per line
.gen_l2:
exa
out (c),a,,(c),e ; CWAIT line, 39
out (c),d,,(c),h ; CMOVE 7, fast CPU
xor l
out (c),a,,(c),e ; CWAIT line, 43
xor l
out (c),d,,(c),0 ; CMOVE 7, 3.5MHz ; out0-ok
.2 inc e
exa
dec a
jr nz,.gen_l2
; 292..311: 20 lines, adding 8T every line
; = 20 * (216 + 8) = 4480 T (same as ZX48)
; - 2 segments at 7MHz = 8*2-4*2 = +8T at each line
ld a,20 ; line 292..311
ld l,(39^41)<<1 ; throttle 2 segments => +8T per line
.gen_l3:
exa
out (c),a,,(c),e ; CWAIT line, 39
out (c),d,,(c),h ; CMOVE 7, fast CPU
xor l
out (c),a,,(c),e ; CWAIT line, 41
xor l
out (c),d,,(c),0 ; CMOVE 7, 3.5MHz ; out0-ok
inc e
exa
dec a
jr nz,.gen_l3
; 0..211: 192+20 lines, adding 8T every line
; = 212 * (216 + 8) = 47488 T (same as ZX48)
; - 2 segments at 7MHz = 8*2-4*2 = +8T at each line
exa
dec a ; restore MSB for line 0
exa
ld e,a ; line 0
ld a,192+20 ; line 0..211
.gen_l4:
exa
out (c),a,,(c),e ; CWAIT line, 39
out (c),d,,(c),h ; CMOVE 7, fast CPU
xor l
out (c),a,,(c),e ; CWAIT line, 41
xor l
out (c),d,,(c),0 ; CMOVE 7, 3.5MHz ; out0-ok
inc e
exa
dec a
jr nz,.gen_l4
; 212..223: 12 lines, adding 8T per line, per two lines
; = 12 * (216 + 8) = 2688 T (same as ZX48)
; - 4 segments at 7MHz = 8*4-4*4 = +16T (16/2 = +8T) at lines 212, 214, .., 222
ld a,6 ; line 212..223 (+2)
ld l,(39^43)<<1 ; throttle 4 segments => +16T => +8T per line
.gen_l5:
exa
out (c),a,,(c),e ; CWAIT line, 39
out (c),d,,(c),h ; CMOVE 7, fast CPU
xor l
out (c),a,,(c),e ; CWAIT line, 43
xor l
out (c),d,,(c),0 ; CMOVE 7, 3.5MHz ; out0-ok
.2 inc e
exa
dec a
jr nz,.gen_l5
; 224..231: 8 lines, adding 8T per line, per four lines
; = 8 * (216 + 8) = 1792 T (same as ZX48)
; - 8 segments at 7MHz = 8*8-4*8 = +32T (32/4 = +8T) at lines 224, 228
ld a,2 ; line 224..231 (+4) (224, 228)
ld l,(39^47)<<1 ; throttle 8 segments => +32T => +8T per line
.gen_l6:
exa
out (c),a,,(c),e ; CWAIT line, 39
out (c),d,,(c),h ; CMOVE 7, fast CPU
xor l
out (c),a,,(c),e ; CWAIT line, 47
xor l
out (c),d,,(c),0 ; CMOVE 7, 3.5MHz ; out0-ok
.4 inc e
exa
dec a
jr nz,.gen_l6
; 232..247: 16 lines per 216T, no copper code
; = 16 * 216 = 3456 T vs ZX48 3584 T -> 128T missing
; 248 : extra +128T until line interrupt vs ZX48 0T -> fixing total frame time 69888 T
; total copper slots: = 8+6+20+192+20+6+2 = 254
; two free slots remaining in copper = 2x4 NOOP to deal with that
ld a,2*4*2
.gen_l7: out (c),0 ; CNOOP ; out0-ok
dec a
jr nz,.gen_l7
; setup line interrupt
nextreg VIDEO_INTERUPT_VALUE_NR_23,249 ; line 249 to trigger at end of line 248:128T
nextreg VIDEO_INTERUPT_CONTROL_NR_22,%110 ; disable ULA int, enabled line int, MSB=0
; start the copper code
nextreg COPPER_CONTROL_HI_NR_62,%01'00'0000 ; reset CPC to zero, start copper
; exit back to NextZXOS
exit:
or a ; clear CF to not signal error to NextZXOS
ret
;----------------------------------------------------------------------------------------------------------
ReadNextReg:
; reads nextreg in A into A (does modify currently selected NextReg on I/O port)
ld bc,TBBLUE_REGISTER_SELECT_P_243B
out (c),a
inc b ; bc = TBBLUE_REGISTER_ACCESS_P_253B
in a,(c) ; read desired NextReg state
ret
;----------------------------------------------------------------------------------------------------------
stopCopper:
; stop any copper code currently running and restore interrupts configuration to ULA int
xor a ; A = 0, CF = 0
nextreg COPPER_CONTROL_LO_NR_61,a
nextreg COPPER_CONTROL_HI_NR_62,a ; stop copper and set write-index to 0
nextreg VIDEO_INTERUPT_CONTROL_NR_22,a ; restore interrupts back to ULA int
ret
;----------------------------------------------------------------------------------------------------------
analyseCurrentMode:
; read current video mode properties
call findVLinesCount
ld (vars.vLinesCount),hl ; total lines of video mode
; check interrupt line, assume the dot command is run with IM1 mode with enabled OS interrupts
; (other option is to read nextreg 0x22 in tight loop, which will most likely freeze emus)
di
ld hl,im2tab
ld de,im2tab+1
ld bc,im2isr-im2tab
ld a,high(im2tab)
ld i,a
im 2
inc a ; A = high(im2isr)
ld (hl),a
ldir
; build: `jp im2real_isr` at im2isr address
ld (hl),$C3
inc l
ld (hl),low(.isr_get_line)
inc l
ld (hl),high(.isr_get_line)
ei
halt ; jumps to following instruction with DI
.isr_get_line:
pop hl ; throw away return address
ld a,VIDEO_LINE_LSB_NR_1F
call ReadNextReg
ld l,a
ld a,VIDEO_LINE_MSB_NR_1E
call ReadNextReg
ld h,a
ld (vars.intLine),hl
; check line duration
;FIXME all, seems one will have to sample reading video line to not tamper with line interrupts
; restore IM 1 OS interrupts
im 1
ld a,$3F
ld i,a
ei
;FIXME all
ret
; returns in HL the video-lines count for current mode (ie. 0x137 for VGA ZX128)
; modifies: AF, BC, HL
; This algorithm works correctly only for modes with 258..511 video lines
; core 3.1.5 and 3.1.10 conforms to this for all VGA/HDMI modes in any variant (min 261)
findVLinesCount:
ld bc,TBBLUE_REGISTER_SELECT_P_243B
ld hl,$0100 + VIDEO_LINE_LSB_NR_1F ; H = 1, L = VIDEO_LINE_LSB_NR_1F
out (c),l
inc b
.waitForNon255MaxLsb:
ld a,255 ; non-255 max not found yet
.waitForZeroLsb:
ld l,a
in a,(c) ; L = last non-zero LSB, A = fresh LSB (may be zero upon wrap)
jr nz,.waitForZeroLsb
inc l ; check if L is non-255 when line LSB wraps to 0 => max LSB found
jr z,.waitForNon255MaxLsb
ret ; here HL is then equal to lines count (H=1 already, L=line.LSB+1)
;----------------------------------------------------------------------------------------------------------
displayModeAnalysis:
; display analysis of video mode properties
;FIXME all
ret
;----------------------------------------------------------------------------------------------------------
printMsg:
; print zero terminated string from HL by using `rst $10` (to be compatible with any user mode)
ld a,(hl)
inc hl
and $7F ; clear 7th bit
ret z ; exit if terminator ($00 or $80)
rst $10
jr printMsg
;----------------------------------------------------------------------------------------------------------
isWhiteOrEol:
; returns:
; ZF=1, CF=0 : the char in A is space or EOL-like
; ZF=0, CF=0 : other char
cp ' '
ret z
.eolOnly:
cp ':'
ret z
cp 13
ret z
cp 10
ret z
or a
ret
;----------------------------------------------------------------------------------------------------------
skipWhite:
; returns:
; CF=0 : A = non-whitespace char, HL points after it
; CF=1 : end of line was reached without any non-whitespace char
.loop: ; skip through spaces
ld a,(hl)
inc hl
cp ' '
jr z,.loop
; check for EOLs: 0, 10, 13, ':'
call isWhiteOrEol.eolOnly
ret nz ; non-white char, return with CF=0 and char in A
scf
ret ; signal EOL by CF=1
letterPal: DB "ulst"
;----------------------------------------------------------------------------------------------------------
parseCommandLine:
; FIXME all
or a
ret
/*
ld a,h
or l
jr z,displayHelp ; 0 == HL -> empty command line, display help
; some command line is available, try to parse it
call skipWhite
jr c,displayHelp ; encountering end of line too soon
; select palette by the letter
or $20 ; lowercase it
ex de,hl
ld hl,letterPal
ld bc,5 ; +1 length to run beyond the buffer in case of mismatch
cpir
ld a,l
sub 1 + low letterPal ; A = 0,1,2,3,4 for [u,l,s,t,<other>]
cp 4
jr nc,displayHelp ; invalid palette letter, display help
ex de,hl
ld c,a ; C = 0,1,2,3 -> palette select (will become bits 5-4 for NR $43)
; check if there is optional digit 0/1 to select first/second palette
ld a,(hl)
sub '0'
jr c,.not_a_01digit
cp 2
jr nc,.not_a_01digit
; A = 0/1 depending on the digit 0/1 in command line
inc hl ; '0'/'1' char accepted
.2 add a,a ; A<<2
or c
ld c,a ; C = palette select with future bit 6 (first/second palette)
.not_a_01digit:
; check if there is whitespace or EOL after palette option (otherwise display help)
ld a,(hl)
call isWhiteOrEol
jr nz,displayHelp
; convert the palette select value to final form and store it in variable
ld a,c
swapnib
ld (palette),a
; check if there is optional <delay> argument
call skipWhite
ccf
ret nc ; that was all, done, return with CF=0 signalling OK
; non-white char in A, parse it as integer
ld e,0
.parseDelay:
sub '0'
jr c,displayHelp ; non-digit char, display help
ld d,10
cp d
jr nc,displayHelp ; non-digit char, display help
mul de ; E *= 10
add de,a ; DE += digit
ld a,d
or a
jr nz,displayHelp ; integer overflow, display help
; parse next char, should be digit or white/eol
ld a,(hl)
inc hl
call isWhiteOrEol
jr nz,.parseDelay ; some char, could be digit, check it
dec hl ; space or EOL, revert ++HL for final check
; <delay> parsed, store it
ld a,e
or a
jr z,displayHelp ; only values 1..255 are valid
ld (delay),a
; check if EOL can be reached
call skipWhite
jr nc,displayHelp ; something unexpected on the remaining line, display help
; all parsed, all OK, clear CF -> run the effect
or a
ret
*/
;----------------------------------------------------------------------------------------------------------
;12345678901234567890123456789012; 32chars width
txtCopy: DB "dot7gFX HDMI timing change v0.2\r"
DB "by Ped7g, installs copper code\r\r"
DB 0
txtHelp: DB "TODO SYNOPSIS:\r"
DB " ../HDMIT.DOT <?> [<?>]\r"
DB "TODO ARGUMENTS:\r"
DB " ? = <type u|l|s|t>[0|1]\r"
DB " ? = <type 1..255>\r\r"
DB "TODO EXAMPLE:\r"
DB " ../HDMIT.DOT ? ?\r"
DB " blah blah\r"
DB 0
;----------------------------------------------------------------------------------------------------------
; initialised data (with default values)
;----------------------------------------------------------------------------------------------------------
; uninitialised data -> not part of the binary
vars S_VARS = $
im2tab equ (vars + S_VARS + 255) & -256
im2isr equ im2tab + high(im2tab) + $0101
; for debugging in CSpect (with full card image)
DEVICE ZXSPECTRUM48 : CSPECTMAP "hdmit.map"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment