Compare commits

..
7 Commits
Author SHA1 Message Date
maddiebaka 2a077bd1e7 Add activity spinner 2026-08-23 21:08:52 -04:00
maddiebaka cb936d97b8 Fix serial and keyboard input to be non-blocking 2026-08-23 20:36:30 -04:00
maddiebaka 0b4cb9f0b8 Add 'press Q to exit' function 2026-08-23 20:31:56 -04:00
maddiebaka 33fe56dade Add conversion to hPa 2026-08-23 20:26:52 -04:00
maddiebaka 014689916d Add character counting guard 2026-08-23 20:14:43 -04:00
maddiebaka b8a3647ae5 Reformat with z80fmt 2026-08-16 06:52:47 -04:00
maddiebaka adce930a17 Add z80fmt formatting tool 2026-08-16 06:52:30 -04:00
2 changed files with 572 additions and 226 deletions
+331 -226
View File
@@ -1,226 +1,331 @@
org 100h org 100h
siob_data equ 083h siob_data equ 083h
siob_ctrl equ 082h siob_ctrl equ 082h
lcd_data equ 0dbh lcd_data equ 0dbh
lcd_ctrl equ 0dah lcd_ctrl equ 0dah
celcius_offset equ 08ah celcius_offset equ 08ah
pressure_offset equ 0cah pressure_offset equ 0cah
humid_offset equ 09eh humid_offset equ 09eh
spinner_offset equ 0e7h ; line 4, column 20 on a 20x4 panel
bdos equ 0005h
conout equ 02h bs_char equ 01h ; cgram slot holding our backslash
conin equ 01h bs_cgram equ 048h ; set cgram address, slot 1 row 0
char_idx equ 10h bdos equ 0005h
conout equ 02h
start: conin equ 01h
call init_lcd dconio equ 06h
call init_siob printstr equ 09h
call lcd_delay
ld a,celcius_offset ; write celcius label start:
out (lcd_ctrl),a ld c,printstr ; tell the operator how to quit
call lcd_delay ld de,quitmsg
ld hl,clabel call bdos
call print_str_to_lcd
call init_lcd
ld a,pressure_offset ; write pressure label call init_siob
out (lcd_ctrl),a call lcd_delay
call lcd_delay
ld hl,plabel call load_glyphs
call print_str_to_lcd
ld a,celcius_offset ; write celcius label
ld a,humid_offset ; write humidity label out (lcd_ctrl),a
out (lcd_ctrl),a call lcd_delay
call lcd_delay ld hl,clabel
ld hl,hlabel call print_str_to_lcd
call print_str_to_lcd
ld a,pressure_offset ; write pressure label
main: out (lcd_ctrl),a
call read_char_b call lcd_delay
call parse_char ld hl,plabel
;call lcd_delay call print_str_to_lcd
;call print_char_to_lcd
ld c,11 ld a,humid_offset ; write humidity label
call bdos out (lcd_ctrl),a
or a call lcd_delay
jr z,main ld hl,hlabel
ret call print_str_to_lcd
init_siob: main:
ld a,018h ; channel reset call read_char_b ; never blocks now
out (siob_ctrl),a jr nc,check_quit ; wire is idle, go look at the keyboard
ld hl,rx_seen
ld a,04h ld (hl),0ffh ; note traffic, a holds the char untouched
out (siob_ctrl),a ; wr4 baud settings call parse_char
ld a,0c4h jr main ; drain the sio while it still has data
out (siob_ctrl),a
check_quit:
ld a,03h call spin_tick ; keep the spinner turning while idle
out (siob_ctrl),a ; rx enable ld c,dconio ; direct console i/o, no echo
ld a,0c1h ld e,0ffh ; ff polls, returning zero if no key
out (siob_ctrl),a call bdos
cp a,051h ; Q
ld a,05h ret z
out (siob_ctrl),a ; tx settings cp a,071h ; q
ld a,0eah ret z
out (siob_ctrl),a jr main ; no key, or a key we ignore
ret init_siob:
ld a,018h ; channel reset
init_lcd: out (siob_ctrl),a
ld a,38h
out (lcd_ctrl),a ; function 8 bit, 2 lines, 5x8 dot font ld a,04h
call lcd_delay out (siob_ctrl),a ; wr4 baud settings
ld a,0ch ld a,0c4h
out (lcd_ctrl),a ; display on, cursor off, no blink out (siob_ctrl),a
call lcd_delay
ld a,01h ld a,03h
out (lcd_ctrl),a ; clear display out (siob_ctrl),a ; rx enable
ld a,0c1h
ret out (siob_ctrl),a
lcd_delay: ld a,05h
ld bc,500 out (siob_ctrl),a ; tx settings
delay1: ld a,0eah
dec bc out (siob_ctrl),a
ld a,b
or c ret
jr nz,delay1
ret init_lcd:
ld a,38h
convert_to_hpa: out (lcd_ctrl),a ; function 8 bit, 2 lines, 5x8 dot font
ld a,(pressure) call lcd_delay
ret ld a,0ch
out (lcd_ctrl),a ; display on, cursor off, no blink
read_char_b: call lcd_delay
wait: ld a,01h
in a,(siob_ctrl) out (lcd_ctrl),a ; clear display
bit 0,a
jr z,wait ret
in a,(siob_data) load_glyphs: ; the a00 rom has yen at 5ch, so draw our own backslash
ret ld a,bs_cgram
out (lcd_ctrl),a
parse_char: ; char should be in register a call lcd_delay
cp a,25h ; percent sign (end of transmission) ld hl,glyph_bs
jr z,print_telem_to_lcd ld e,08h ; eight rows, and e survives lcd_delay
cp a,0dh ; carriage return glyph_row:
jr z,null_term ld a,(hl)
cp a,0ah ; new line out (lcd_data),a ; cgram address auto increments
jr z,parse_char_done call lcd_delay
cp a,054h ; T character inc hl
jr z,set_char_idx_t ; point the char_idx to the temperature buffer dec e
cp a,050h ; P character jr nz,glyph_row
jr z,set_char_idx_p ; point the char_idx to the pressure buffer ret
cp a,048h ; H character
jr z,set_char_idx_h ; point the char_idx to the humidity buffer lcd_delay:
ld bc,500
ld bc,(char_idx) ; get address stored in char_idx delay1:
ld (bc),a ; deref that address and store char dec bc
inc bc ld a,b
ld (char_idx),bc ; store incremented address in char_idx or c
jr nz,delay1
;out (lcd_data),a ret
parse_char_done:
ret convert_to_hpa: ; shift the decimal point in (pressure) two places left
ld hl,pressure
print_telem_to_lcd: ld b,0 ; count of digits ahead of the point
call move_to_tempaddr find_point:
ld hl,temperature ld a,(hl)
call print_str_to_lcd or a
ret z ; no point in the string, leave it alone
call move_to_presaddr cp a,2eh ; period
ld hl,pressure jr z,found_point
call print_str_to_lcd inc hl
inc b
call move_to_humidaddr jr find_point
ld hl,humidity found_point:
call print_str_to_lcd ld a,b
cp 3 ; need three whole digits to shift by two
ld a,1h ret c
out (0),a dec hl ; step back over the ones digit
ret dec hl ; hl now holds the new tenths digit
ld a,(hl)
null_term: ; null terminate end of parsed string in memory ld (hl),2eh ; drop the point in two places left
ld a,00h inc hl
ld bc,(char_idx) ld (hl),a ; and the saved digit after it
ld (bc),a inc hl
jr parse_char_done ld (hl),00h ; null terminate, discarding the rest
ret
set_char_idx_t: read_char_b: ; carry set with the char in a, carry clear if nothing waiting
ld bc,temperature in a,(siob_ctrl)
ld (char_idx),bc bit 0,a
ret jr z,no_char
in a,(siob_data)
set_char_idx_p: scf ; flag a as a real character
ld bc,pressure ret
ld (char_idx),bc no_char:
ret or a ; clears carry, a holds status not data
ret
set_char_idx_h:
ld bc,humidity parse_char: ; char should be in register a
ld (char_idx),bc cp a,25h ; percent sign (end of transmission)
ret jr z,print_telem_to_lcd
cp a,0dh ; carriage return
jr z,null_term
print_char_to_lcd: cp a,0ah ; new line
cp a,00h ; null byte jr z,parse_char_done
jr z,lcd_done cp a,054h ; T character
cp a,0dh ; carriage return jr z,set_char_idx_t ; point the char_idx to the temperature buffer
jr z,lcd_done cp a,050h ; P character
cp a,0ah ; new line jr z,set_char_idx_p ; point the char_idx to the pressure buffer
jr z,lcd_done cp a,048h ; H character
cp a,054h ; T character jr z,set_char_idx_h ; point the char_idx to the humidity buffer
call z,move_to_tempaddr
cp a,050h ; P character push af
call z,move_to_presaddr ld a,(char_cnt) ; get number stored in char_cnt
cp a,048h ; H character cp char_max ; compare it to maximum character count
call z,move_to_humidaddr jr nc,store_full ; if no carry, the buffer is full
out (lcd_data),a inc a ; otherwise increment
lcd_done: ld (char_cnt),a ; and store back in char_cnt
ret pop af
print_str_to_lcd: ; place memory address in hl first ld bc,(char_idx) ; get address stored in char_idx
ld a,(hl) ld (bc),a ; deref that address and store char
or a inc bc
jr z,print_done ld (char_idx),bc ; store incremented address in char_idx
out (lcd_data),a parse_char_done:
call lcd_delay ret
inc hl store_full:
jr print_str_to_lcd pop af
print_done: ret
ret
print_telem_to_lcd:
move_to_tempaddr: call move_to_tempaddr
ld c,a ld hl,temperature
ld a,85h call print_str_to_lcd
out (lcd_ctrl),a
ld a,c call convert_to_hpa
call lcd_delay call move_to_presaddr
ret ld hl,pressure
call print_str_to_lcd
move_to_presaddr:
ld a,0c1h call move_to_humidaddr
out (lcd_ctrl),a ld hl,humidity
call lcd_delay call print_str_to_lcd
ret
ld a,1h
move_to_humidaddr: ret
ld a,99h
out (lcd_ctrl),a null_term: ; null terminate end of parsed string in memory
call lcd_delay ld a,00h
ret ld bc,(char_idx)
ld (bc),a
clabel: DB 'degree C',0 jr parse_char_done
plabel: DB 'pascal',0
hlabel: DB '% RH',0
set_char_idx_t:
temperature: DS 16 ld bc,temperature
humidity: DS 16 ld (char_idx),bc
pressure: DS 16 xor a ; reset char_cnt to zero
ld (char_cnt),a
ret
set_char_idx_p:
ld bc,pressure
ld (char_idx),bc
xor a ; reset char_cnt to zero
ld (char_cnt),a
ret
set_char_idx_h:
ld bc,humidity
ld (char_idx),bc
xor a ; reset char_cnt to zero
ld (char_cnt),a
ret
print_str_to_lcd: ; place memory address in hl first
ld a,(hl)
or a
jr z,print_done
out (lcd_data),a
call lcd_delay
inc hl
jr print_str_to_lcd
print_done:
ret
spin_tick: ; count down, then step the spinner one frame
ld hl,(spin_cnt)
dec hl
ld (spin_cnt),hl
ld a,h ; dec hl leaves the flags alone
or l
ret nz ; not time yet
ld hl,spin_ticks ; reload, so the interval stays regular
ld (spin_cnt),hl
ld a,(rx_seen) ; did anything arrive this interval?
or a
ret z ; no, hold the frame and stop spinning
xor a
ld (rx_seen),a ; clear it for the next interval
ld a,(spin_idx)
inc a
and 03h ; four frames, then wrap
ld (spin_idx),a
ld hl,spinner
ld d,00h
ld e,a
add hl,de ; hl now points at the frame character
ld a,spinner_offset
out (lcd_ctrl),a ; park the cursor bottom right
call lcd_delay
ld a,(hl) ; fetch late, lcd_delay wrecks a and bc but not hl
out (lcd_data),a
call lcd_delay
ret
move_to_tempaddr:
ld a,85h
out (lcd_ctrl),a
call lcd_delay
ret
move_to_presaddr:
ld a,0c3h
out (lcd_ctrl),a
call lcd_delay
ret
move_to_humidaddr:
ld a,99h
out (lcd_ctrl),a
call lcd_delay
ret
clabel: DB 'degree C',0
plabel: DB 'hPa',0
hlabel: DB '% RH',0
quitmsg: DB 'Press Q to quit',0dh,0ah,'$'
spinner: DB '-','/','|',bs_char
glyph_bs: DB 00h,10h,08h,04h,02h,01h,00h,00h
spin_ticks equ 3000 ; idle polls per frame, tune on real hardware
spin_idx DB 03h ; wraps to the first frame on the next tick
spin_cnt DW spin_ticks
rx_seen DB 00h ; set on every byte off the sio
char_max equ 15
char_cnt DB 0
char_idx DW scratch
scratch: DS 16
temperature: DS 16
humidity: DS 16
pressure: DS 16
Executable
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
z80fmt - column formatter for Z80 assembly source (z88dk z80asm flavour).
Splits each line into label | mnemonic | operands | ;comment and pads the
first three into fixed-width columns with spaces. String and character
literals are parsed properly, so semicolons inside DEFM data are never
mistaken for comments, and EX AF,AF' does not open a quote.
Usage:
z80fmt.py file.z80 [more.z80 ...] # write formatted source to stdout
z80fmt.py -i file.z80 # rewrite in place
z80fmt.py --check *.z80 # exit 1 if any file needs formatting
cat file.z80 | z80fmt.py # filter mode
"""
import argparse
import sys
MNEMONICS = {
"adc", "add", "and", "bit", "call", "ccf", "cp", "cpd", "cpdr", "cpi",
"cpir", "cpl", "daa", "dec", "di", "djnz", "ei", "ex", "exx", "halt",
"im", "in", "inc", "ind", "indr", "ini", "inir", "jp", "jr", "ld",
"ldd", "lddr", "ldi", "ldir", "neg", "nop", "or", "otdr", "otir", "out",
"outd", "outi", "pop", "push", "res", "ret", "reti", "retn", "rl",
"rla", "rlc", "rlca", "rld", "rr", "rra", "rrc", "rrca", "rrd", "rst",
"sbc", "scf", "set", "sla", "sll", "sli", "sra", "srl", "sub", "xor",
# Z180 extras
"in0", "out0", "mlt", "tst", "tstio", "slp", "otim", "otdm", "otimr",
"otdmr",
}
DIRECTIVES = {
"org", "defb", "defw", "defm", "defs", "defc", "equ", "include",
"binary", "incbin", "public", "extern", "global", "module", "section",
"align", "defvars", "if", "ifdef", "ifndef", "else", "elif", "endif",
"end", "db", "dw", "ds", "dm", "dc", "byte", "word", "ascii", "asciz",
"asciiz", "macro", "endm", "rept", "endr", "local", "title", "line",
"defgroup", "defarray",
}
def split_comment(line):
"""Return (code, comment). Quote-aware so ';' inside literals is kept."""
quote = None
i, n = 0, len(line)
while i < n:
c = line[i]
if quote:
if c == "\\" and quote == '"' and i + 1 < n:
i += 2
continue
if c == quote:
quote = None
elif c == '"':
quote = '"'
elif c == "'":
# A quote right after an identifier char is a register prime
# (EX AF,AF' / HL'), not the start of a character literal.
prev = line[i - 1] if i else ""
if not (prev.isalnum() or prev == "_"):
quote = "'"
elif c == ";":
return line[:i], line[i:]
i += 1
return line, ""
def is_opcode(token):
base = token.rstrip(":").lstrip(".").lower()
return base in MNEMONICS or base in DIRECTIVES
def split_code(code):
"""Return (label, mnemonic, operands) from the comment-stripped code."""
if not code.strip():
return "", "", ""
label = ""
if code[0] not in " \t":
first = code.split(None, 1)[0]
rest = code.split(None, 1)[1] if len(code.split(None, 1)) > 1 else ""
if first.endswith(":") or not is_opcode(first):
label, code = first, rest
rest = code.strip()
if not rest:
return label, "", ""
parts = rest.split(None, 1)
mnemonic = parts[0]
operands = parts[1].strip() if len(parts) > 1 else ""
return label, mnemonic, operands
def normalize_commas(operands, sep):
"""Rewrite comma spacing outside of literals."""
out, quote, i, n = [], None, 0, len(operands)
while i < n:
c = operands[i]
if quote:
out.append(c)
if c == "\\" and quote == '"' and i + 1 < n:
out.append(operands[i + 1])
i += 2
continue
if c == quote:
quote = None
elif c == '"':
quote = '"'
out.append(c)
elif c == "'":
prev = operands[i - 1] if i else ""
if not (prev.isalnum() or prev == "_"):
quote = "'"
out.append(c)
elif c == ",":
while out and out[-1] == " ":
out.pop()
out.append("," + sep)
j = i + 1
while j < n and operands[j] in " \t":
j += 1
i = j
continue
else:
out.append(c)
i += 1
return "".join(out).rstrip()
def pad(text, width, gap=1):
return text.ljust(width) if len(text) < width else text + " " * gap
def format_line(line, opt):
raw = line.rstrip("\n").rstrip()
if not raw.strip():
return [""]
stripped = raw.lstrip()
# Preprocessor lines pass through untouched.
if stripped.startswith("#"):
return [stripped]
# Whole-line comments: keep column-0 banners at column 0, indent the
# rest to the mnemonic column.
if stripped.startswith(";"):
if raw[0] in " \t":
return [" " * opt.label_width + stripped]
return [stripped]
code, comment = split_comment(raw)
label, mnemonic, operands = split_code(code)
if opt.case == "upper":
mnemonic = mnemonic.upper()
elif opt.case == "lower":
mnemonic = mnemonic.lower()
if opt.comma_space and operands:
operands = normalize_commas(operands, " ")
elif operands:
operands = normalize_commas(operands, "")
lines = []
has_code = bool(mnemonic or operands or comment)
if label and has_code and opt.wrap_long_labels and len(label) >= opt.label_width:
lines.append(label)
label = ""
if not has_code:
lines.append(label)
return lines
out = pad(label, opt.label_width) if label else " " * opt.label_width
out += pad(mnemonic, opt.mnemonic_width) if mnemonic else " " * opt.mnemonic_width
if comment:
out += pad(operands, opt.operand_width) if operands else " " * opt.operand_width
out += comment
else:
out += operands
lines.append(out.rstrip())
return lines
def format_text(text, opt):
out = []
for line in text.splitlines():
out.extend(format_line(line, opt))
return "\n".join(out) + ("\n" if text.endswith("\n") or text else "")
def main():
p = argparse.ArgumentParser(description="Format Z80 assembly into columns.")
p.add_argument("files", nargs="*", help="source files (default: stdin)")
p.add_argument("-i", "--in-place", action="store_true")
p.add_argument("--check", action="store_true",
help="exit 1 if any file would change; write nothing")
p.add_argument("--label-width", type=int, default=16)
p.add_argument("--mnemonic-width", type=int, default=8)
p.add_argument("--operand-width", type=int, default=24)
p.add_argument("--comma-space", action="store_true",
help="put one space after operand commas (default: none)")
p.add_argument("--case", choices=["keep", "upper", "lower"], default="keep",
help="case of mnemonics only; symbols are left alone")
p.add_argument("--wrap-long-labels", action="store_true",
help="put over-long labels on their own line")
opt = p.parse_args()
if not opt.files:
sys.stdout.write(format_text(sys.stdin.read(), opt))
return 0
changed = False
for path in opt.files:
with open(path, "r", encoding="utf-8", errors="surrogateescape") as fh:
original = fh.read()
formatted = format_text(original, opt)
if formatted != original:
changed = True
if opt.check:
print(f"would reformat: {path}", file=sys.stderr)
if opt.check:
continue
if opt.in_place:
if formatted != original:
with open(path, "w", encoding="utf-8",
errors="surrogateescape") as fh:
fh.write(formatted)
else:
sys.stdout.write(formatted)
return 1 if (opt.check and changed) else 0
if __name__ == "__main__":
sys.exit(main())