changeset 2705:ab2d916380bf

WIP uPD78K/II CPU core
author Michael Pavone <pavone@retrodev.com>
date Sun, 06 Jul 2025 15:20:46 -0700
parents c5dce4284e69
children 0bd48217941a
files Makefile upd78k2.cpu upd78k2_util.c upd78k2run.c
diffstat 4 files changed, 1004 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Jul 06 15:18:02 2025 -0700
+++ b/Makefile	Sun Jul 06 15:20:46 2025 -0700
@@ -326,6 +326,7 @@
 MTESTOBJS:=trans.o serialize.o $(M68KOBJS) $(TRANSOBJS) util.o
 ZTESTOBJS:=ztestrun.o serialize.o $(Z80OBJS) $(TRANSOBJS) util.o
 CPMOBJS:=blastcpm.o util.o serialize.o $(Z80OBJS) $(TRANSOBJS)
+UPD78K2RUNOBJS:=upd78k2.o upd78k2run.o util.o backend.o tern.o
 
 LIBCFLAGS=$(CFLAGS) -fpic -DIS_LIB -DDISABLE_ZLIB
 
@@ -390,6 +391,9 @@
 vos_prog_info : $(OBJDIR)/vos_prog_info.o $(OBJDIR)/vos_program_module.o
 	$(CC) -o $@ $^ $(OPT)
 
+upd78k2run : $(UPD78K2RUNOBJS:%.o=$(OBJDIR)/%.o)
+	$(CC) -o $@ $^ $(OPT)
+
 .PRECIOUS: %.c
 %.c %.h : %.cpu cpu_dsl.py
 	./cpu_dsl.py -d $(shell echo $@ | sed -E -e "s/^z80.*$$/$(Z80_DISPATCH)/" -e '/^goto/! s/^.*$$/call/') $< > $(shell echo $@ | sed -E 's/\.[ch]$$/./')c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/upd78k2.cpu	Sun Jul 06 15:20:46 2025 -0700
@@ -0,0 +1,853 @@
+#"Minimum instruction cycle is 333 ns for internal ROM 500 ns for external at 12 MHz"
+#That's 4 clks for internal ROM, 6 clocks external
+#Above seems to be defined based on external clock input, but there's an internal divider
+#Instruction timing tables seem to specified in terms of this divided clock
+info
+	prefix upd78k2_
+	opcode_size 8
+	header upd78k2.h
+	body upd78k2_run_op
+	extra_tables sfr bit1 bit2 muldiv base sfrbit spmov indexed regind alt_base alt_indexed alt_regind mov_reg
+	include upd78k2_util.c
+regs
+	main 8 x a c b e d l h
+	psw 8
+	pc 16
+	sp 16
+	rbs 8
+	int_enable 8
+	int_priority_flag 8
+	chflags 8
+	zflag 8
+	scratch1 32
+	scratch2 32
+	mem_pointers ptr8 4
+	port_data 8 8
+	port_mode 8 8
+	mm 8
+	iram 8 256
+flags
+	register psw
+	I 7 none int_enable
+	Z 6 zero zflag
+	R 5 none rbs.1
+	A 4 half-carry chflags.3
+	B 3 none rbs.0
+	P 1 none int_priority_flag
+	C 0 carry chflags.7
+declare
+	uint8_t upd78237_sfr_read(uint32_t address, void *context);
+	void *upd78237_sfr_write(uint32_t address, void *context, uint8_t value);
+	void init_upd78k2_opts(upd78k2_options *opts, memmap_chunk const *chunks, uint32_t num_chunks);
+	upd78k2_context *init_upd78k2_context(upd78k2_options *opts);
+
+#Prefix bytes
+# 0000 0001 -> saddr becomes sfr, mem becomes &mem
+# 0000 0010 -> bit instructions/ conditional branches
+# 0000 0011 -> more bit instructions / conditional branches
+# 0000 0110 -> base mode
+# 0000 1000 -> saddr/sfr bit instructions / conditional branches
+# 0000 1001 -> mov !addr16/stbc
+# 0000 1010 -> indexed mode
+# 0001 0110 -> register indirect
+
+sfr_read
+	arg offset 8
+	scratch1 = offset + 0xFF00
+	ocall read_8
+
+sfr_write
+	arg offset 8
+	arg value 8
+	scratch2 = offset + 0xFF00
+	scratch1 = value
+	ocall write_8
+	
+iram_read
+	arg offset 8
+	local normal_iram 8
+	normal_iram = 1
+	if offset >=U 0xE0
+		#register bank area
+		switch rbs
+		case 0
+			if offset >=U 0xF8
+				normal_iram = 0
+			end
+		case 1
+			if offset >=U 0xF0
+				if offset >=U 0xF8
+				else
+					normal_iram = 0
+				end
+			end
+		case 2
+			if offset >=U 0xE8
+				if offset >=U 0xF0
+				else
+					normal_iram = 0
+				end
+			end
+		case 3
+			if offset >=U 0xE8
+			else
+				normal_iram = 0
+			end
+		end
+	end
+	if normal_iram
+		scratch1 = iram.offset
+	else
+		scratch1 = offset & 7
+		scratch1 = main.scratch1
+	end
+
+iram_write
+	arg offset 8
+	arg value 8
+	local normal_iram 8
+	local regnum 8
+	normal_iram = 1
+	if offset >=U 0xE0
+		#register bank area
+		switch rbs
+		case 0
+			if offset >=U 0xF8
+				normal_iram = 0
+			end
+		case 1
+			if offset >=U 0xF0
+				if offset >=U 0xF8
+				else
+					normal_iram = 0
+				end
+			end
+		case 2
+			if offset >=U 0xE8
+				if offset >=U 0xF0
+				else
+					normal_iram = 0
+				end
+			end
+		case 3
+			if offset >=U 0xE8
+			else
+				normal_iram = 0
+			end
+	end
+	if normal_iram
+		iram.offset = value
+	else
+		regnum = offset & 8
+		main.regnum = value
+	end
+
+mem_read_no_exp
+	arg addr 32
+	local offset 8
+	if addr >=U 0xFE00
+		if addr >=U 0xFF00
+			offset = addr
+			sfr_read offset
+		else
+			offset = addr
+			iram_read offset
+		end
+	else
+		scratch1 = addr
+		ocall read_8
+	end
+
+mem_write_no_exp
+	arg addr 32
+	arg value 8
+	local offset 8
+	if addr >=U 0xFE00
+		if addr >=U 0xFF00
+			offset = addr
+			sfr_write offset value
+		else
+			offset = addr
+			iram_write offset value
+		end
+	else
+		scratch2 = addr
+		scratch1 = value
+		ocall write_8
+	end
+
+upd78k2_op_fetch
+	mem_read_no_exp pc
+	pc += 1
+
+upd78k2_op_fetch_word
+	local tmp 8
+	upd78k2_op_fetch
+	tmp = scratch1
+	upd78k2_op_fetch
+	scratch1 <<= 8
+	scratch1 |= tmp
+
+upd78k2_run_op
+	upd78k2_op_fetch
+	dispatch scratch1
+
+saddr_read
+	arg offset 8
+	local tmp 8
+	if offset >=U 0xE0
+		#sfr area
+		tmp = offset - 0xE0
+		sfr_read tmp
+	else
+		tmp = offset + 0x20
+		iram_read tmp
+	end
+
+saddr_write
+	arg offset 8
+	arg value 8
+	local tmp 8
+	if offset >=U 0xE0
+		#sfr area
+		tmp = offset - 0xE0
+		sfr_write tmp value
+	else
+		tmp = offset + 0x20
+		iram_write tmp value
+	end
+
+mem_read
+	arg address 16
+	arg alt_bank 8
+	local full_address 32
+	local meg_enable 8
+	meg_enable = mm & 0x40
+	if meg_enable
+		if alt_bank
+			full_address = port_data.6 & 0xF
+		else
+			full_address = port_mode.6 & 0xF
+		end
+		full_address <<= 16
+		full_address |= address
+	else
+		full_address = address
+	end
+	mem_read_no_exp full_address
+
+mem_write
+	arg address 16
+	arg value 8
+	arg alt_bank 8
+	local full_address 32
+	local meg_enable 8
+	meg_enable = mm & 0x40
+	if meg_enable
+		if alt_bank
+			full_address = port_data.6 & 0xF
+		else
+			full_address = port_mode.6 & 0xF
+		end
+		full_address <<= 16
+		full_address |= address
+	else
+		full_address = address
+	end
+	mem_write_no_exp full_address value
+
+push_word
+	arg value 16
+	local tmp 8
+	tmp = value >> 8
+	sp -= 1
+	mem_write_no_exp sp tmp
+	tmp = value
+	sp -= 1
+	mem_write_no_exp sp tmp
+
+pop_word
+	mem_read_no_exp sp
+	dst = scratch1
+	sp += 1
+	mem_read_no_exp sp
+	scratch1 <<= 8
+	dst |= scratch1
+	sp += 1
+
+00000000 nop
+	cycles 2 #minimum cycle time appears to be 4 (ignoring internal divider)
+
+00000001 sfr_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 sfr
+
+00000010 bit1_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 bit1
+
+00000011 bit2_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 bit2
+
+00000101 muldiv_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 muldiv
+
+00000110 base_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 base
+
+00001000 sfrbit_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 sfrbit
+
+00001001 spmov_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 spmov
+
+00001010 indexed_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 indexed
+
+00010110 regind_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 regind
+
+00100100 mov_reg_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 mov_reg
+
+sfr 00000110 alt_base_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 alt_base
+
+sfr 00001010 alt_indexed_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 alt_indexed
+
+sfr 00010110 alt_regind_prefix
+	upd78k2_op_fetch
+	dispatch scratch1 alt_regind
+
+aluop
+	arg op 8
+	arg src 8
+	switch op
+	case 0
+		dst += src
+		update_flags ZAC
+	case 1
+		adc dst src dst
+		update_flags ZAC
+	case 2
+		dst -= src
+		update_flags ZAC
+	case 3
+		sbc dst src dst
+		update_flags ZAC
+	case 4
+		dst &= src
+		update_flags Z
+	case 5
+		dst ^= src
+		update_flags Z
+	case 6
+		dst |= src
+		update_flags Z
+	case 7
+		cmp dst src
+		update_flags ZAC
+	end
+
+aluop16
+	arg op 8
+	arg src 16
+	switch op
+	case 0
+		dst += src
+		update_flags ZAC
+	case 2
+		dst -= src
+		update_flags ZAC
+	case 7
+		dst -= src
+		update_flags ZAC
+	default
+		#TODO: what happens in these invalid cases
+	end
+
+10001PPP alu_r_r
+	local tmp_src 16
+	local tmp_dst 16
+	upd78k2_op_fetch
+	scratch2 = scratch1 >> 4
+	if scratch2 >=U 7
+		#TODO: is MSB just ignored or is this treated as some kind of illegal instruction or nop?
+	else
+		scratch1 &= 0xF
+		if scratch1 >=U 7
+			scratch1 &= 7
+			meta dst tmp_dst
+			tmp_dst = a << 8
+			tmp_dst |= x
+			switch scratch1
+			case 0
+				tmp_src = a << 8
+				tmp_src |= x
+			case 2
+				tmp_src = b << 8
+				tmp_src |= c
+			case 4
+				tmp_src = d << 8
+				tmp_src |= e
+			case 6
+				tmp_src = h << 8
+				tmp_src |= l
+			default
+				#TODO: what happens in these invalid cases
+			end
+			aluop16 P tmp_src
+			x = tmp_dst
+			a = tmp_dst >> 8
+		else
+			meta dst main.scratch2
+			cycles 2 #penalty for extra IRAM access since regs are technically stored there?
+			aluop P main.scratch1
+		end
+	end
+
+10101PPP alu_immed
+	upd78k2_op_fetch
+	meta dst a
+	aluop P scratch1
+	
+calc_addr_base
+	arg regpair 8
+	local tmp 16
+	upd78k2_op_fetch
+	switch regpair
+	case 0
+		#[de+byte]
+		tmp = d << 8
+		tmp |= e
+		adst += tmp
+	case 1
+		#[sp+byte]
+		adst += sp
+	case 2
+		#[hl+byte]
+		tmp = h << 8
+		tmp |= l
+		adst += tmp
+	end
+
+read_base_mode
+	arg regpair 8
+	arg alt_bank 8
+	meta adst scratch1
+	calc_addr_base regpair
+	mem_read scratch1 alt_bank
+
+write_base_mode
+	arg regpair 8
+	arg value 8
+	arg alt_bank 8
+	meta adst scratch2
+	calc_addr_base regpair
+	mem_write scratch2 value alt_bank
+	
+calc_addr_regind
+	arg regpair 8
+	local tmp 16
+	switch regpair
+	case 0
+		#[de+]
+		adst = d << 8
+		adst |= e
+		tmp = adst + 1
+		e = tmp
+		d = tmp >> 8
+	case 1
+		#[hl+]
+		adst = h << 8
+		adst |= l
+		tmp = adst + 1
+		l = tmp
+		h = tmp >> 8
+	case 2
+		#[de-]
+		adst = d << 8
+		adst |= e
+		tmp = adst - 1
+		e = tmp
+		d = tmp >> 8
+	case 3
+		#[hl-]
+		adst = h << 8
+		adst |= l
+		tmp = adst - 1
+		l = tmp
+		h = tmp >> 8
+	case 4
+		#[de]
+		adst = d << 8
+		adst |= e
+	case 5
+		#[hl]
+		adst = h << 8
+		adst |= l
+	end
+
+read_regind_mode
+	arg regpair 8
+	arg alt_bank 8
+	meta adst scratch1
+	calc_addr_regind regpair
+	mem_read scratch1 alt_bank
+
+write_regind_mode
+	arg regpair 8
+	arg value 8
+	arg alt_bank 8
+	meta adst scratch2
+	calc_addr_regind regpair
+	mem_write scratch2 value alt_bank
+
+calc_addr_indexed
+	arg index_reg 8
+	local tmp 16
+	upd78k2_op_fetch_word
+	switch index_reg
+	case 0
+		tmp = d << 8
+		tmp |= e
+		adst += tmp
+	case 1
+		adst += a
+	case 2
+		tmp = h << 8
+		tmp |= l
+		adst += tmp
+	case 3
+		adst += b
+	end
+
+read_indexed_mode
+	arg index_reg 8
+	arg alt_bank 8
+	meta adst scratch1
+	calc_addr_indexed index_reg
+	mem_read scratch1 alt_bank
+
+write_indexed_mode
+	arg index_reg 8
+	arg value 8
+	arg alt_bank 8
+	meta adst scratch2
+	calc_addr_indexed index_reg
+	mem_write scratch2 value alt_bank
+
+base 00RR1PPP alu_base
+	invalid R 3
+	read_base_mode R 0
+	meta dst a
+	aluop P scratch1
+
+alt_base 00RR1PPP alu_alt_base
+	invalid R 3
+	read_base_mode R 1
+	meta dst a
+	aluop P scratch1
+
+base 00RR0000 mov_a_base
+	invalid R 3
+	read_base_mode R 0
+	a = scratch1
+	
+alt_base 00RR0000 alt_mov_a_base
+	invalid R 3
+	read_base_mode R 1
+	a = scratch1
+
+base 10RR0000 mov_base_a
+	invalid R 3
+	write_base_mode R a 0
+
+alt_base 10RR0000 alt_mov_base_a
+	invalid R 3
+	write_base_mode R a 1
+
+regind 0RRR1PPP alu_reg_indirect
+	invalid R 6
+	invalid R 7
+	read_regind_mode R 0
+	meta dst a
+	aluop P scratch1
+
+alt_regind 0RRR1PPP alu_alt_reg_indirect
+	invalid R 6
+	invalid R 7
+	read_regind_mode R 1
+	meta dst a
+	aluop P scratch1
+
+regind 0RRR0000 mov_a_reg_indirect
+	invalid R 6
+	invalid R 7
+	read_regind_mode R 0
+	a = scratch1
+
+alt_regind 0RRR0000 alt_mov_a_reg_indirect
+	invalid R 6
+	invalid R 7
+	read_regind_mode R 1
+	a = scratch1
+	
+regind 1RRR0000 mov_reg_indirect_a
+	invalid R 6
+	invalid R 7
+	write_regind_mode R a 0
+
+alt_regind 1RRR0000 alt_mov_reg_indirect_a
+	invalid R 6
+	invalid R 7
+	write_regind_mode R a 1
+
+01010RRR mov_reg_indirect_a_short
+	invalid R 6
+	invalid R 7
+	write_regind_mode R a 0
+
+01011RRR mov_a_reg_indirect_short
+	invalid R 6
+	invalid R 7
+	read_regind_mode R 0
+	a = scratch1
+
+indexed 00RR1PPP alu_indexed
+	read_indexed_mode R 0
+	meta dst a
+	aluop P scratch1
+
+alt_indexed 00RR1PPP alu_alt_indexed
+	read_indexed_mode R 1
+	meta dst a
+	aluop P scratch1
+
+indexed 00RR0000 mov_a_indexed
+	read_indexed_mode R 0
+	a = scratch1
+
+alt_indexed 00RR0000 alt_mov_a_indexed
+	read_indexed_mode R 1
+	a = scratch1
+
+indexed 10RR0000 mov_indexed_a
+	write_indexed_mode R a 0
+
+alt_indexed 10RR0000 alt_mov_indexed_a
+	write_indexed_mode R a 1
+
+mov_reg 0DD01SS0 movw_rp_rp
+	local dst 8
+	local src 8
+	dst = D << 1
+	src = S << 1
+	main.dst = main.src
+	dst += 1
+	src += 1
+	main.dst = main.src
+
+mov_reg 0DDD0SSS mov_r_r
+	main.D = main.S
+
+01100PP0 movw_rp_immed
+	local dst 8
+	dst = P << 1
+	upd78k2_op_fetch
+	main.dst = scratch1
+	dst += 1
+	upd78k2_op_fetch
+	main.dst = scratch1
+
+11000RRR inc_r
+	cycles 2
+	main.R += 1
+	update_flags ZA
+
+11001RRR dec_r
+	cycles 2
+	main.R -= 1
+	update_flags ZA
+
+11010RRR mov_a_r
+	a = main.R
+
+muldiv 01001PP0 br_rp
+	local reg 8
+	local tmp 16
+	reg = P << 1
+	pc = main.reg
+	reg += 1
+	tmp = main.reg << 8
+	pc |= tmp
+
+00010100 br_rel
+	upd78k2_op_fetch
+	sext 16 scratch1 scratch1
+	pc += scratch1
+
+00101100 br_abs
+	upd78k2_op_fetch_word
+	pc = scratch1
+
+100000FS bcc
+	local flag 8
+	upd78k2_op_fetch
+	if F
+		flag = chflags >> 7
+	else
+		flag = zflag
+	end
+	if flag = S
+		sext 16 scratch1 scratch1
+		pc += scratch1
+	end
+
+muldiv 01011PP0 call_rp
+	local reg 8
+	local tmp 16
+	push_word pc
+	reg = P << 1
+	pc = main.reg
+	reg += 1
+	tmp = main.reg << 8
+	pc |= tmp
+
+00101000 call_long
+	local address 16
+	upd78k2_op_fetch_word
+	address = scratch1
+	push_word pc
+	pc = address
+
+10010AAA call_short
+	local address 16
+	address = A << 8
+	address |= 0x800
+	upd78k2_op_fetch
+	address |= scratch1
+	push_word pc
+	pc = address
+
+111TTTTT call_table
+	local address 16
+	mem_read_no_exp T
+	address = scratch1
+	scratch1 = T + 1
+	mem_read_no_exp scratch1
+	scratch1 <<= 8
+	address |= 8
+	push_word pc
+	pc = address
+
+01010110 ret
+	meta dst pc
+	pop_word
+
+001101PP pop_rp
+	local rp 8
+	local word 16
+	meta dst word
+	pop_word
+	main.rp = word
+	rp += 1
+	main.rp = word >> 8
+	
+001111PP push_rp
+	local rp 8
+	local word 16
+	rp = P << 1
+	rp += 1
+	word = main.rp << 8
+	rp -= 1
+	word |= main.rp
+	push_word word
+
+muldiv 101010NN sel
+	local offset 8
+	if N != rbs
+		offset = rbs << 3
+		offset = 0xF8 - offset
+		iram.offset = x
+		offset += 1
+		iram.offset = a
+		offset += 1
+		iram.offset = c
+		offset += 1
+		iram.offset = b
+		offset += 1
+		iram.offset = e
+		offset += 1
+		iram.offset = d
+		offset += 1
+		iram.offset = l
+		offset += 1
+		iram.offset = h
+		offset = N << 3
+		offset = 0xF8 - offset
+		x = iram.offset
+		offset += 1
+		a = iram.offset
+		offset += 1
+		c = iram.offset
+		offset += 1
+		b = iram.offset
+		offset += 1
+		e = iram.offset
+		offset += 1
+		d = iram.offset
+		offset += 1
+		l = iram.offset
+		offset += 1
+		h = iram.offset
+		rbs = N
+	end
+
+muldiv 11001000 incw_sp
+	sp += 1
+
+muldiv 11001001 decw_sp
+	sp -= 1
+
+00001011 movw_sfrp_immed
+	upd78k2_op_fetch
+	if scratch1 = 0xFC
+		#unclear if SP is actually mapped in the SFR space
+		#or if this is special cased, but the docs don't
+		#suggest you can write to SP with other SFR-targeting
+		#instructions so I'm assuming the latter.
+		upd78k2_op_fetch
+		sp = scratch1
+		upd78k2_op_fetch
+		scratch1 <<= 8
+		sp |= scratch1
+	else
+		scratch2 = scratch1
+		upd78k2_op_fetch
+		sfr_write scratch2 scratch1
+		scratch2 += 1
+		upd78k2_op_fetch
+		sfr_write scratch2 scratch1
+	end
+
+00001100 movw_sadrp_word
+	local offset 8
+	upd78k2_op_fetch
+	offset = scratch1
+	upd78k2_op_fetch
+	saddr_write offset scratch1
+	offset += 1
+	upd78k2_op_fetch
+	saddr_write offset scratch1
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/upd78k2_util.c	Sun Jul 06 15:20:46 2025 -0700
@@ -0,0 +1,77 @@
+#include <string.h>
+
+void upd78k2_read_8(upd78k2_context *upd)
+{
+	uint32_t tmp = upd->scratch1;
+	upd->scratch1 = read_byte(upd->scratch1, (void **)upd->mem_pointers, &upd->opts->gen, upd);
+	if (tmp == upd->pc) {
+		printf("uPD78K/II fetch %04X: %02X, AX=%02X%02X BC=%02X%02X DE=%02X%02X HL=%02X%02X SP=%04X\n", tmp, upd->scratch1,
+			upd->main[1], upd->main[0], upd->main[3], upd->main[2], upd->main[5], upd->main[4], upd->main[7], upd->main[6], upd->sp);
+	}
+}
+
+void upd78k2_write_8(upd78k2_context *upd)
+{
+	write_byte(upd->scratch2, upd->scratch1, (void **)upd->mem_pointers, &upd->opts->gen, upd);
+}
+
+uint8_t upd78237_sfr_read(uint32_t address, void *context)
+{
+	upd78k2_context *upd = context;
+	if (address < 8) {
+		return upd->port_data[address];
+	}
+	switch (address)
+	{
+	case 0x21:
+	case 0x26:
+		return upd->port_mode[address & 0x7];
+	case 0xC4:
+		return upd->mm;
+	default:
+		fprintf(stderr, "Unhandled uPD78237 SFR read %02X\n", address);
+		return 0xFF;
+	}
+}
+
+void *upd78237_sfr_write(uint32_t address, void *context, uint8_t value)
+{
+	upd78k2_context *upd = context;
+	if (address < 8 && address != 2 && address != 7) {
+		upd->port_data[address] = value;
+	} else {
+		switch (address)
+		{
+		case 0x20:
+		case 0x23:
+		case 0x25:
+		case 0x26:
+			upd->port_mode[address & 7] = value;
+			break;
+		case 0xC4:
+			upd->mm = value;
+			break;
+		default:
+			fprintf(stderr, "Unhandled uPD78237 SFR write %02X: %02X\n", address, value);
+			break;
+		}
+	}
+	return context;
+}
+
+void init_upd78k2_opts(upd78k2_options *opts, memmap_chunk const *chunks, uint32_t num_chunks)
+{
+	memset(opts, 0, sizeof(*opts));
+	opts->gen.memmap = chunks;
+	opts->gen.memmap_chunks = num_chunks;
+	opts->gen.address_mask = 0xFFFFF;
+	opts->gen.max_address = 0xFFFFF;
+}
+
+upd78k2_context *init_upd78k2_context(upd78k2_options *opts)
+{
+	upd78k2_context *context = calloc(1, sizeof(upd78k2_context));
+	context->opts = opts;
+	return context;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/upd78k2run.c	Sun Jul 06 15:20:46 2025 -0700
@@ -0,0 +1,70 @@
+#include <stdlib.h>
+#include "upd78k2.h"
+
+int headless = 1;
+void render_errorbox(char * title, char * buf)
+{
+}
+
+void render_infobox(char * title, char * buf)
+{
+}
+
+uint8_t pram[384];
+uint8_t rom[0xFC80];
+
+const memmap_chunk upd_map[] = {
+	{ 0x0000, 0xFC80, 0xFFFFF, .flags = MMAP_READ, .buffer = rom},
+	{ 0xFC80, 0xFD00, 0x7F, .flags = MMAP_READ | MMAP_WRITE | MMAP_CODE, .buffer = pram},
+	{ 0xFD00, 0xFE00, 0xFF, .flags = MMAP_READ | MMAP_WRITE | MMAP_CODE, .buffer = pram + 128},
+	{ 0xFF00, 0xFFFF, 0xFF, .read_8 = upd78237_sfr_read, .write_8 = upd78237_sfr_write}
+};
+
+int main(int argc, char **argv)
+{
+	long filesize;
+	uint8_t *filebuf;
+	upd78k2_options opts;
+	upd78k2_context *upd;
+	char *fname = NULL;
+	uint8_t retranslate = 0;
+	for (int i = 1; i < argc; i++)
+	{
+		if (argv[i][0] == '-') {
+			switch(argv[i][1])
+			{
+			case 'r':
+				retranslate = 1;
+				break;
+			default:
+				fprintf(stderr, "Unrecognized switch -%c\n", argv[i][1]);
+				exit(1);
+			}
+		} else if (!fname) {
+			fname = argv[i];
+		}
+	}
+	if (!fname) {
+		fputs("usage: ztestrun zrom [cartrom]\n", stderr);
+		exit(1);
+	}
+	FILE * f = fopen(fname, "rb");
+	if (!f) {
+		fprintf(stderr, "unable to open file %s\n", fname);
+		exit(1);
+	}
+	fseek(f, 0, SEEK_END);
+	filesize = ftell(f);
+	fseek(f, 0, SEEK_SET);
+	filesize = filesize < sizeof(rom) ? filesize : sizeof(rom);
+	if (fread(rom, 1, filesize, f) != filesize) {
+		fprintf(stderr, "error reading %s\n",fname);
+		exit(1);
+	}
+	fclose(f);
+	init_upd78k2_opts(&opts, upd_map, 4);
+	upd = init_upd78k2_context(&opts);
+	upd->pc = rom[0] | rom[1] << 8;
+	upd78k2_execute(upd, 10000);
+	return 0;
+}
\ No newline at end of file