diff --git a/README.md b/README.md index 897df65..63536ad 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ mode, or UART/CTC setup must restore it before returning. prints the low 16 bits of the sum as four hexadecimal digits. The end address must be greater than or equal to the start address. +`D` prints 16 bytes per row followed by an ASCII column. Bytes outside the +printable ASCII range are shown as `.`. + Build the ROM and programmer-format images with: ```sh @@ -84,15 +87,15 @@ The outputs are: - `build/tinybasic/tinybasic.bin`: standalone relocated Tiny BASIC payload - `build/casette/tape_loader.bin`: standalone relocated tape-loader payload -The current compact image is 12,318 bytes. Its ROM layout is: +The current compact image is 12,967 bytes. Its ROM layout is: - `0000-007F`: reset/copy bootstrap -- `0080-1201`: packed BIOS+monitor image copied to RAM at `C000-D181` -- `1202-13FF`: unused space reserved for the resident image to grow +- `0080-12DD`: packed BIOS+monitor image copied to RAM at `C000-D25D` +- `12DE-13FF`: unused space reserved for the resident image to grow - `1400-1C0C`: Tiny BASIC, executed directly from ROM - `1C0D-2FFF`: unused ROM -- `3000-301D`: tape loader, executed directly from ROM -- `301E-7FFF`: unused space available for additional ROM content +- `3000-32A6`: tape loader, executed directly from ROM +- `32A7-7FFF`: unused space available for additional ROM content The resident image starts at `C000`, its IM 2 vector table is at `C040`, and linked code starts at `C080`. The bootstrap obtains the resident copy length @@ -103,8 +106,8 @@ The resulting RAM layout is: - `8000-AFFF`: Tiny BASIC workspace and BASIC program text - `B000-B077`: Tiny BASIC variables and input buffer - `B078-BFFF`: Tiny BASIC stack space -- `C000-D181`: resident BIOS, vectors, services, and monitor -- `D182-EFFF`: free space for resident-image growth +- `C000-D25D`: resident BIOS, vectors, services, and monitor +- `D25E-EFFF`: free space for resident-image growth - `F000-FFFF`: BIOS globals, UART buffers, and monitor stack The BIOS and monitor continue to work if low ROM is disabled. ROM-resident @@ -197,12 +200,14 @@ returns to the Tiny BASIC prompt. Tiny BASIC resets the stack pointer when it starts, so it does not currently return through the monitor's `G` call. Reset the machine to return to the -monitor. The tape loader occupies the later ROM slot at `3000-301D`; the first -unused byte after it is `301E`. +monitor. The tape loader occupies the later ROM slot at `3000-32A6`; the first +unused byte after it is `32A7`. ## Tape loader The tape-loader image is built from `apps/casette/tape_loader.asm`, linked at `3000`, and included in both ROM images. Start it with `T`, `TAPE`, or `G 3000`. The current image polls the comparator input and prints its state through the -resident BIOS console service. +resident BIOS console service. Signal acquisition is bounded; a missing, +unstable, or permanently high signal reports `TAPE SIGNAL TIMEOUT` and returns +to the monitor instead of waiting forever with interrupts disabled. diff --git a/apps/README.md b/apps/README.md index 910576f..610a19a 100644 --- a/apps/README.md +++ b/apps/README.md @@ -9,8 +9,8 @@ The current allocation is: - `1400-1C0C`: Tiny BASIC - `1C0D-2FFF`: available -- `3000-301D`: tape loader -- `301E-7FFF`: available for more applications (`3100` is the next convenient +- `3000-32A6`: tape loader +- `32A7-7FFF`: available for more applications (`3300` is the next convenient aligned origin) The resident BIOS and monitor own high RAM at `C000-FFFF`. Tiny BASIC owns diff --git a/apps/casette/encode.py b/apps/casette/encode.py index b29bc8d..93dfad6 100644 --- a/apps/casette/encode.py +++ b/apps/casette/encode.py @@ -163,7 +163,7 @@ def main() -> None: parser.add_argument("--low", type=float, default=2000.0, help="low tone in Hz") parser.add_argument("--high", type=float, default=4000.0, help="high tone in Hz") parser.add_argument("--sample-rate", type=int, default=48000) - parser.add_argument("--leader", type=float, default=1.0, help="leader seconds") + parser.add_argument("--leader", type=float, default=4.0, help="leader seconds") parser.add_argument("--preamble", type=int, default=64, help="number of 0x55 preamble bytes") parser.add_argument("--block-size", type=int, default=64) parser.add_argument("--amplitude", type=float, default=0.75) diff --git a/apps/casette/tape_loader.asm b/apps/casette/tape_loader.asm index 328a438..93463f2 100644 --- a/apps/casette/tape_loader.asm +++ b/apps/casette/tape_loader.asm @@ -3,48 +3,1053 @@ .area TAPELOAD (ABS) .org TAPE_LOADER_ROM_ADDRESS -COMP_PORT = 0x18 -WORK_BASE = 0x0B000 -LOAD_START = 0x08000 -LOAD_LIMIT_HI = 0x0B0 -STACK_TOP = 0x0BFFF +; ===================================================================== +; Cassette training diagnostic +; +; CPU: Z80 @ 1 MHz +; Comparator: port 0x18 +; +; Comparator register: +; D0 = comparator output +; D7..D1 = 0 +; +; This program DOES NOT decode data yet. +; +; It: +; 1. Finds a stable leader +; 2. Measures leader timing +; 3. Waits for 3 consecutive slower-carrier periods +; 4. Captures 128 raw comparator half-periods +; 5. Prints the raw values +; 6. Classifies them as H/L +; 7. Prints carrier run lengths +; +; All sampling happens with interrupts disabled. +; Printing happens only before or after sampling. +; ===================================================================== -OLD_SP = WORK_BASE+0x00 -start: - ld (OLD_SP), sp - ld sp, #STACK_TOP +COMP_PORT = 0x18 + +STACK_TOP = 0xBFFF + +WORK_BASE = 0xB000 + +OLD_SP = WORK_BASE + 0x00 ; word +OLD_IFF = WORK_BASE + 0x02 ; byte + +LEADER_REF = WORK_BASE + 0x03 ; byte +LEADER_AVG = WORK_BASE + 0x04 ; byte +LEADER_MAX = WORK_BASE + 0x05 ; byte + +TONE_THRESHOLD = WORK_BASE + 0x06 ; byte +LOW_TRIGGER = WORK_BASE + 0x07 ; byte + +RETURN_CODE = WORK_BASE + 0x08 ; byte + +CAPTURE_BUF = 0xB100 +CAPTURE_COUNT = 128 + + +; ===================================================================== +; Entry +; ===================================================================== + +tape_loader: + + ; ------------------------------------------------------------- + ; Save caller stack. + ; ------------------------------------------------------------- + + ld (OLD_SP),sp + + + ; ------------------------------------------------------------- + ; Save interrupt state. + ; + ; LD A,I copies IFF2 into P/V. + ; ------------------------------------------------------------- + + ld a,i + jp po,iff_was_off + + ld a,#1 + jr save_iff + +iff_was_off: + + xor a + +save_iff: + + ld (OLD_IFF),a + + + ; ------------------------------------------------------------- + ; Use our own stack. + ; ------------------------------------------------------------- di -poll_loop: + ld sp,#STACK_TOP - ; Read from the hardware port COMP_PORT - in a, (COMP_PORT) - out (COMP_PORT), a - - ; Save the raw port status in register B - ld b, a - ; Print a carriage return '\r' (ASCII 0x0D) - ld a, #0x0D + ; ------------------------------------------------------------- + ; Startup message. + ; + ; BIOS printing requires interrupts. + ; print_with_irqs returns with interrupts disabled. + ; ------------------------------------------------------------- + + ld hl,#msg_start + call print_with_irqs + + +; ===================================================================== +; Find stable leader +; +; The first measurement becomes our reference. +; +; Require another 31 measurements within +/-2 counts of that reference. +; +; At 1 MHz we only get a few loop counts per carrier half-period, so +; allowing +/-2 is intentionally generous. +; +; We also keep track of the maximum observed leader count. +; ===================================================================== + +find_leader: + + call measure_halfperiod + + + ld (LEADER_REF),a + ld (LEADER_MAX),a + + + ; HL = running sum. + + ld l,a + ld h,#0 + + + ; We already have one sample. + ; Need 31 more for 32 total. + + ld b,#31 + + +leader_stability_loop: + + call measure_halfperiod + + + ; E = measurement + + ld e,a + + + ; ------------------------------------------------------------- + ; Calculate: + ; + ; abs(sample - reference) + ; + ; without destroying E. + ; ------------------------------------------------------------- + + ld a,(LEADER_REF) + + cp e + jr nc,leader_ref_ge_sample + + + ; sample > reference + + ld a,e + ld c,a + + ld a,(LEADER_REF) + ld d,a + + ld a,c + sub d + + jr leader_diff_ready + + +leader_ref_ge_sample: + + ; A still contains reference. + + sub e + + +leader_diff_ready: + + ; Difference 0, 1, or 2 accepted. + ; + ; >= 3 means this isn't a stable leader. + ; Start over. + + cp #3 + jr nc,find_leader + + + ; ------------------------------------------------------------- + ; Update maximum leader period. + ; ------------------------------------------------------------- + + ld a,(LEADER_MAX) + + cp e + jr nc,leader_max_ok + + + ld a,e + ld (LEADER_MAX),a + + +leader_max_ok: + + ; ------------------------------------------------------------- + ; Add sample to 16-bit sum. + ; ------------------------------------------------------------- + + ld d,#0 + + add hl,de + + + djnz leader_stability_loop + + +; ===================================================================== +; Calculate leader average. +; +; 32 samples -> divide by 32. +; ===================================================================== + + srl h + rr l + + srl h + rr l + + srl h + rr l + + srl h + rr l + + srl h + rr l + + + ld a,l + + or a + jr z,find_leader + + + ld (LEADER_AVG),a + + +; ===================================================================== +; Initial carrier threshold. +; +; Everything up to LEADER_MAX is treated as the HIGH-frequency carrier. +; +; To decide that the LOW-frequency carrier has appeared, require: +; +; measurement >= LEADER_MAX + 1 +; +; AND require this to happen three times consecutively. +; +; Example from your previous capture: +; +; leader average ~= 02 +; leader max ~= 03 or 04 +; +; A single 04 can therefore no longer falsely trigger acquisition. +; ===================================================================== + + ld a,(LEADER_MAX) + + ld (TONE_THRESHOLD),a + + + inc a + + jr nz,low_trigger_ok + + + ; Protect against overflow, although this should never happen. + + ld a,#0xFF + + +low_trigger_ok: + + ld (LOW_TRIGGER),a + + +; ===================================================================== +; Wait for the beginning of the slower carrier. +; +; We require THREE consecutive periods >= LOW_TRIGGER. +; +; This is the important change from the previous diagnostic. +; ===================================================================== + +wait_for_low_restart: + + ld b,#3 + + +wait_for_low: + + call measure_halfperiod + + ld e,a + + + ld a,(LOW_TRIGGER) + + + ; We want: + ; + ; E >= LOW_TRIGGER + ; + ; CP does A - E. + ; + ; A < E -> carry + ; A = E -> zero + + cp e + + jr z,low_candidate + jr c,low_candidate + + + ; Too short: still leader/noise. + ; Restart the consecutive counter. + + jr wait_for_low_restart + + +low_candidate: + + djnz wait_for_low + + +; ===================================================================== +; We have now observed three consecutive slow periods. +; +; Capture 128 FRESH measurements. +; +; We deliberately do not print anything here because the tape stream +; has no gaps. +; ===================================================================== + + ld hl,#CAPTURE_BUF + + ld b,#CAPTURE_COUNT + + +capture_loop: + + call measure_halfperiod + + ld (hl),a + + inc hl + + djnz capture_loop + + +; ===================================================================== +; Capture complete. +; +; Timing no longer matters, so enable interrupts for BIOS output. +; ===================================================================== + + ei + + +; ===================================================================== +; Print calibration summary. +; ===================================================================== + + ld hl,#msg_leader_avg + call print_string + + ld a,(LEADER_AVG) + call print_hex8 + + call print_crlf + + + ld hl,#msg_leader_max + call print_string + + ld a,(LEADER_MAX) + call print_hex8 + + call print_crlf + + + ld hl,#msg_low_trigger + call print_string + + ld a,(LOW_TRIGGER) + call print_hex8 + + call print_crlf + + + ld hl,#msg_threshold + call print_string + + ld a,(TONE_THRESHOLD) + call print_hex8 + + call print_crlf + + call print_crlf + + +; ===================================================================== +; Print raw half-period measurements. +; +; 16 per line. +; ===================================================================== + + ld hl,#msg_raw + call print_string + + call print_crlf + + + ld hl,#CAPTURE_BUF + + ld b,#CAPTURE_COUNT + ld c,#0 + + +print_raw_loop: + + ld a,(hl) + call print_hex8 + + + ld a,#' ' call BIOS_PUTC - ; Restore the port status and isolate the relevant bit - ; (Assuming bit 0 determines 0 or 1; adjust 'and' mask if it is a different bit) - ld a, b - and #0x01 - ; Convert the binary 0 or 1 into ASCII '0' (0x30) or '1' (0x31) - add a, #0x30 - - ; Print the '0' or '1' character + inc hl + inc c + + + ; New line every 16 samples. + + ld a,c + and #0x0F + + jr nz,raw_no_newline + + + call print_crlf + + +raw_no_newline: + + djnz print_raw_loop + + + call print_crlf + + +; ===================================================================== +; Print H/L classification. +; +; H: +; sample <= TONE_THRESHOLD +; +; L: +; sample > TONE_THRESHOLD +; +; 64 characters per line. +; ===================================================================== + + ld hl,#msg_class + call print_string + + call print_crlf + + + ld hl,#CAPTURE_BUF + + ld b,#CAPTURE_COUNT + ld c,#0 + + +print_class_loop: + + ld a,(hl) + + call classify_sample + + + or a + jr z,class_print_low + + + ld a,#'H' + jr class_print_char + + +class_print_low: + + ld a,#'L' + + +class_print_char: + call BIOS_PUTC - ; Infinite loop: poll continuously - jr poll_loop - ld sp, (OLD_SP) + inc hl + inc c + + + ; New line every 64 classifications. + + ld a,c + and #0x3F + + jr nz,class_no_newline + + + call print_crlf + + +class_no_newline: + + djnz print_class_loop + + + call print_crlf + + +; ===================================================================== +; Print carrier run lengths. +; +; Example: +; +; L06 H0D L07 H0C ... +; +; This is the most interesting output for the 0x55 preamble. +; +; For nominal 2 kHz / 4 kHz at 600 baud we might expect something +; roughly around: +; +; LOW run: 6-7 comparator half-periods +; HIGH run: 12-14 comparator half-periods +; +; Exact values don't matter. +; Consistency is what matters. +; ===================================================================== + + ld hl,#msg_runs + call print_string + + call print_crlf + + + ld hl,#CAPTURE_BUF + + + ; ------------------------------------------------------------- + ; Establish first carrier. + ; ------------------------------------------------------------- + + ld a,(hl) + call classify_sample + + + ; D = current carrier + ; + ; 0 = LOW + ; 1 = HIGH + + ld d,a + + + ; C = current run length + + ld c,#1 + + + inc hl + + + ; 127 measurements remain. + + ld b,#127 + + +run_scan_loop: + + ld a,(hl) + + ; classify_sample preserves BC and DE. + + call classify_sample + + + cp d + jr nz,run_changed + + + ; Same carrier. + + inc c + + jr run_scan_next + + +run_changed: + + ; ------------------------------------------------------------- + ; A = new carrier. + ; + ; Save it while printing the old run. + ; ------------------------------------------------------------- + + ld e,a + + + ld a,d + call print_tone_letter + + + ld a,c + call print_hex8 + + + ld a,#' ' + call BIOS_PUTC + + + ; Start new run. + + ld d,e + ld c,#1 + + +run_scan_next: + + inc hl + + djnz run_scan_loop + + + ; ------------------------------------------------------------- + ; Print final run. + ; ------------------------------------------------------------- + + ld a,d + call print_tone_letter + + + ld a,c + call print_hex8 + + + call print_crlf + call print_crlf + + + ld hl,#msg_done + call print_string + + + xor a + ld (RETURN_CODE),a + + +; ===================================================================== +; Return to monitor / BIOS caller. +; ===================================================================== + +training_exit: + + di + + + ld sp,(OLD_SP) + + + ld a,(RETURN_CODE) + ld c,a + + + ; ------------------------------------------------------------- + ; Restore caller interrupt state. + ; ------------------------------------------------------------- + + ld a,(OLD_IFF) + + or a + + + ld a,c + + jr z,training_return_di + + + ; EI becomes active after the following instruction. + ; RET serves as that instruction. + ei ret + + +training_return_di: + + ret + + +; ===================================================================== +; measure_halfperiod +; +; 1. Wait for a comparator edge. +; 2. Starting exactly from that edge, count polling iterations until +; the next comparator edge. +; +; Returns: +; +; A = count, 1..255 +; +; If the interval is very long the counter saturates at FF instead of +; wrapping. +; +; +; At 1 MHz your observed leader currently gives roughly: +; +; 02 / 03 counts +; +; which is perfectly usable as long as the two carrier clusters remain +; distinct. +; ===================================================================== + +measure_halfperiod: + + in a,(#COMP_PORT) + + ld d,a + + +; --------------------------------------------------------------------- +; Wait for first edge. +; --------------------------------------------------------------------- + +mhp_wait_first_edge: + + in a,(#COMP_PORT) + + cp d + + jr z,mhp_wait_first_edge + + + ; Start measurement at this edge. + + ld d,a + + ld c,#0 + + +; --------------------------------------------------------------------- +; Measure to following edge. +; --------------------------------------------------------------------- + +mhp_measure_loop: + + inc c + + + in a,(#COMP_PORT) + + cp d + + jr nz,mhp_done + + + ; Saturate at FF. + + ld a,c + + cp #0xFF + + jr nz,mhp_measure_loop + + +; --------------------------------------------------------------------- +; Saturated: +; just wait for the edge without incrementing further. +; --------------------------------------------------------------------- + +mhp_saturated: + + in a,(#COMP_PORT) + + cp d + + jr z,mhp_saturated + + + ld a,#0xFF + + ret + + +mhp_done: + + ld a,c + + ret + + +; ===================================================================== +; classify_sample +; +; Input: +; +; A = measured half-period +; +; Output: +; +; A = 1 HIGH-frequency / short-period carrier +; A = 0 LOW-frequency / long-period carrier +; +; Preserves: +; +; BC +; DE +; HL +; ===================================================================== + +classify_sample: + + push bc + + + ld c,a + + + ld a,(TONE_THRESHOLD) + + cp c + + + ; threshold < sample -> LOW-frequency carrier + + jr c,class_sample_low + + + ld a,#1 + + pop bc + + ret + + +class_sample_low: + + xor a + + pop bc + + ret + + +; ===================================================================== +; print_tone_letter +; +; Input: +; +; A = 0 -> L +; A = 1 -> H +; ===================================================================== + +print_tone_letter: + + or a + + jr z,print_tone_low + + + ld a,#'H' + + jp BIOS_PUTC + + +print_tone_low: + + ld a,#'L' + + jp BIOS_PUTC + + +; ===================================================================== +; Printing helpers +; ===================================================================== + +print_with_irqs: + + ei + + call print_string + + di + + ret + + +; --------------------------------------------------------------------- + +print_string: + + ld a,(hl) + + or a + + ret z + + + call BIOS_PUTC + + + inc hl + + jr print_string + + +; --------------------------------------------------------------------- + +print_crlf: + + ld a,#13 + call BIOS_PUTC + + + ld a,#10 + jp BIOS_PUTC + + +; ===================================================================== +; print_hex8 +; +; A -> two hexadecimal characters. +; ===================================================================== + +print_hex8: + + push af + + + rrca + rrca + rrca + rrca + + and #0x0F + + call print_hex_nibble + + + pop af + + and #0x0F + + +; Fall through. + + +print_hex_nibble: + + cp #10 + + jr c,print_hex_digit + + + add a,#('A' - 10) + + jp BIOS_PUTC + + +print_hex_digit: + + add a,#'0' + + jp BIOS_PUTC + + +; ===================================================================== +; ROM strings +; ===================================================================== + +msg_start: + + .ascii "TAPE TRAINING - PLAY TAPE" + .db 13,10,0 + + +msg_leader_avg: + + .ascii "LEADER AVG = " + .db 0 + + +msg_leader_max: + + .ascii "LEADER MAX = " + .db 0 + + +msg_low_trigger: + + .ascii "LOW TRIGGER = " + .db 0 + + +msg_threshold: + + .ascii "H/L THRESHOLD = " + .db 0 + + +msg_raw: + + .ascii "RAW HALF-PERIOD COUNTS:" + .db 0 + + +msg_class: + + .ascii "CLASSIFICATION:" + .db 0 + + +msg_runs: + + .ascii "RUN LENGTHS:" + .db 0 + + +msg_done: + + .ascii "TRAINING CAPTURE DONE" + .db 13,10,0 \ No newline at end of file diff --git a/bios.c b/bios.c index 3d9d55e..b44e15f 100644 --- a/bios.c +++ b/bios.c @@ -5,6 +5,8 @@ #define UART_CTC_CARD_BASE 0x20 #define CTC_BASE (UART_CTC_CARD_BASE + 0x00) #define CTC_CHANNEL_0 (CTC_BASE + 0) +#define CTC_CHANNEL_1 (CTC_BASE + 1) +#define CTC_CHANNEL_2 (CTC_BASE + 2) #define CTC_CHANNEL_3 (CTC_BASE + 3) #define UART_BASE (UART_CTC_CARD_BASE + 0x04) #define UART_CONTROL (UART_BASE + 0) @@ -76,6 +78,16 @@ void bios_init(void) buffer_init(&rx_buf); buffer_init(&tx_buf); + /* A CPU-only reset does not necessarily reset the external CTC. Stop and + disable every channel before rebuilding the UART clock/IRQ path. */ + io_out(CTC_CHANNEL_0, 0x03); + io_out(CTC_CHANNEL_1, 0x03); + io_out(CTC_CHANNEL_2, 0x03); + io_out(CTC_CHANNEL_3, 0x03); + + /* The 6850 has no reset input; CR1:CR0 = 11 is its master reset. */ + io_out(UART_CONTROL, 0b00000011); + io_out(CTC_CHANNEL_0, 0x57); io_out(CTC_CHANNEL_0, 13); io_out(CTC_CHANNEL_3, 0b11001111); @@ -84,7 +96,6 @@ void bios_init(void) io_out(CTC_CHANNEL_0, 0x40); uart_tx_busy = 0; - io_out(UART_CONTROL, 0b00000011); io_out(UART_CONTROL, 0b10010101); enable_interrupts(); } diff --git a/boot.asm b/boot.asm index 1434ffd..f9f316e 100644 --- a/boot.asm +++ b/boot.asm @@ -113,6 +113,15 @@ init: di ld sp, #STACK_TOP + ; A reset may have interrupted a CTC service routine without executing + ; RETI. Release any stale daisy-chain in-service latches before the + ; channels are reinitialized. RETI acts as RET for each CALL while + ; also signalling end-of-interrupt to daisy-chained peripherals. + call release_daisy_chain + call release_daisy_chain + call release_daisy_chain + call release_daisy_chain + call gsinit ; Setup interrupt mode 2 @@ -125,6 +134,9 @@ init: hang: jr hang +release_daisy_chain: + reti + .area _GSINIT gsinit: ld bc, #l__INITIALIZER diff --git a/monitor.c b/monitor.c index 019d2d8..a5df9a5 100644 --- a/monitor.c +++ b/monitor.c @@ -131,6 +131,15 @@ static void command_dump(uint8_t argc, char **argv) print_hex8(*(volatile uint8_t *)(address + offset + column)); bios_putc(' '); } + for (; column < 16; ++column) { + bios_puts(" "); + } + bios_putc('|'); + for (column = 0; column < row_length; ++column) { + uint8_t value = *(volatile uint8_t *)(address + offset + column); + bios_putc(value >= ' ' && value <= '~' ? (char)value : '.'); + } + bios_putc('|'); bios_puts("\r\n"); offset += row_length; } diff --git a/test.wav b/test.wav index 2624c08..13fd33f 100644 Binary files a/test.wav and b/test.wav differ