; ============================================================================
;  thermo.asm  —  NES open-bus "thermometer" test ROM   (NROM / mapper 0)
;
;  A teaching / demonstration ROM for the AprVisual study
;  "Can the NES tell you the room temperature?" (WebSite/s1a/nes-thermometer.html).
;  It reads the PPU open-bus decay time and prints the temperature in whole
;  degrees Celsius over 0–100 °C, e.g.  "25 DEGREE CELSIUS".
;
;  ---------------------------------------------------------------------------
;  WHY THIS WORKS (the physics)
;  ---------------------------------------------------------------------------
;  The PPU I/O "open bus" latch — the value you read back from a write-only PPU
;  register — is not driven by anything once you stop writing to it. It is held
;  only by a tiny parasitic capacitance, and it slowly bleeds to 0 through
;  reverse-bias junction leakage. That leakage current is exponential in
;  temperature (Arrhenius), so the survival time is a thermometer. We prime the
;  latch to all-ones and count how many times a tight polling loop runs before
;  it decays: warmer -> faster decay -> smaller count; colder -> bigger count.
;
;  ---------------------------------------------------------------------------
;  THE ONE TRICK: read the PPU latch, NOT the CPU bus
;  ---------------------------------------------------------------------------
;  "Open bus" IS the data bus itself, not separate storage. You cannot poll a CPU
;  open-bus address: the polling instructions travel over that same bus, so each
;  fetch drives its own opcode bytes onto it and OVERWRITES whatever you wrote.
;  The PPU's internal I/O latch is on the other die and CPU fetches never touch it.
;  We seed and read it the idiomatic way: WRITE $2002 (a read-only register — the
;  write is ignored as a register op but still fills the PPU I/O latch, no side
;  effect), then READ $2001 (a write-only register — reading it returns the full
;  8-bit open-bus latch). With thanks to a NESdev reviewer for these corrections.
;
;  ---------------------------------------------------------------------------
;  COUNT -> CELSIUS (integer, 0–100 °C, no 6502 multiply / divide / float)
;  ---------------------------------------------------------------------------
;  The count -> temperature inversion (an Arrhenius log) is precomputed on the PC
;  (build.py) as a 128-entry lookup table: thr[i] = the count you'd measure at
;  (i - 0.5) °C, for i = 0..100 (indices 101..127 are 0 padding). The table
;  decreases (cold = big count), so the answer is "the largest index i whose
;  threshold is still >= our count" — a 7-step power-of-two binary search, no
;  division. Then print i as a decimal number.
;
;  Honest limits (a technical demo, not a precision instrument):
;   * Integer only — 0.1 °C would be false precision. Across 0–100 °C every whole
;     degree maps to a DISTINCT decay count (587× span, strictly monotonic), so the
;     round-trip is per-degree exact end to end; the only residual error is the
;     ±0.5 °C of rounding an analogue temperature to an integer display. (An earlier
;     build blurred above ~44 °C — that was a non-monotonic host clock in the
;     emulator, since fixed; see the study.) The absolute method ceiling is far
;     hotter, where the decay shrinks to one loop iteration.
;   * Below 0 °C clamps to 0; above 100 °C clamps to 100; an emulator whose open
;     bus never decays makes the count run away, and the ROM shows "--" instead.
;   * The count depends on the emulator's exact loop timing, so the table is
;     calibrated to THIS build (tools/aprnes) — a per-model calibration.
; ============================================================================

.MEMORYMAP
DEFAULTSLOT 0
SLOT 0 $C000 $4000
.ENDME
.ROMBANKSIZE $4000
.ROMBANKS 1

.EMPTYFILL $00

; ------------------------------------------------------------------ zero page
.DEFINE cnt0 $10      ; measured decay count, 24-bit little-endian
.DEFINE cnt1 $11
.DEFINE cnt2 $12
.DEFINE idx  $13      ; binary-search result = temperature in whole °C (0..100)
.DEFINE stp  $14      ; search step (64,32,...,1)
.DEFINE tst  $15      ; candidate index = idx + step (0..127)
.DEFINE tblL $16      ; thr_*[tst] fetched here (24-bit)
.DEFINE tblM $17
.DEFINE tblH $18
.DEFINE hund $19      ; hundreds digit (0 or 1)
.DEFINE tensd $1A     ; tens digit
.DEFINE onesd $1B     ; ones digit
.DEFINE ptr  $1C      ; 16-bit pointer for the suffix string ($1C/$1D)

.BANK 0 SLOT 0
.ORG $0000

; Read-only data first, so the code below sees these as 16-bit addresses
; (a forward reference would make WLA guess zero-page and mis-size the opcode).
suffix_str:
    .db " DEGREE CELSIUS", $00

; the 128x3 count->temperature lookup table (generated by build.py)
.INCLUDE "thermo_table.inc"

; ============================================================================
;  RESET
; ============================================================================
RESET:
    sei
    cld
    ldx #$FF
    txs
    lda #$00
    sta $2000            ; NMI off
    sta $2001            ; rendering off

vwait1:
    bit $2002
    bpl vwait1

    lda #$00             ; clear the 2KB internal RAM
    tax
clrram:
    sta $0000,x
    sta $0100,x
    sta $0200,x
    sta $0300,x
    sta $0400,x
    sta $0500,x
    sta $0600,x
    sta $0700,x
    inx
    bne clrram

vwait2:
    bit $2002
    bpl vwait2

    ; ---- palette: bg = black, colour 1 = white ----
    lda #$3F
    sta $2006
    lda #$00
    sta $2006
    lda #$0F
    sta $2007
    lda #$30
    sta $2007

    ; ---- clear name table $2000-$23FF to the blank tile ($20 = space) ----
    lda #$20
    sta $2006
    lda #$00
    sta $2006
    lda #$20
    ldx #$04
    ldy #$00
ntclr:
    sta $2007
    dey
    bne ntclr
    dex
    bne ntclr

    ; ---- clear the attribute table $23C0-$23FF to palette 0 ----
    ; (undefined power-on palette RAM differs per emulator; force palette 0 so text
    ;  never lands on an uninitialised palette and turns invisible).
    lda #$23
    sta $2006
    lda #$C0
    sta $2006
    lda #$00
    ldx #$40
attrclr:
    sta $2007
    dex
    bne attrclr

; ============================================================================
;  MEASURE  —  time the open-bus decay as a loop count
; ============================================================================
    ; Prime the latch to $FF by writing $2002 (read-only -> no side effect).
    lda #$FF
    sta $2002

    lda #$00
    sta cnt0
    sta cnt1
    sta cnt2
measure:
    lda $2001            ; read a write-only register -> full 8-bit open-bus latch
    cmp #$FF             ; still all-ones?
    bne measured         ; a bit dropped -> stop timing
    inc cnt0             ; 24-bit increment
    bne measure
    inc cnt1
    bne measure
    inc cnt2
    lda cnt2             ; timeout: ~2M loops with no decay = this emulator doesn't
    cmp #$20             ; model open-bus decay -> bail out instead of hanging
    bcs no_decay
    jmp measure

no_decay:
    lda #$21             ; show "--" (no valid measurement)
    sta $2006
    lda #$C6
    sta $2006
    lda #$2D
    sta $2007
    sta $2007
    jmp put_suffix

measured:

; ============================================================================
;  CONVERT  —  count -> °C via a 7-step power-of-two binary search
;  thr[] decreases with index; find the largest index i where thr[i] >= count.
;  128-entry table, index 0..127 (0..100 real, rest padding), so no bound check.
; ============================================================================
    lda #$00
    sta idx
    lda #$40             ; step = 64
    sta stp
rx_loop:
    lda idx              ; test = idx + step
    clc
    adc stp
    sta tst
    jsr read_thr         ; tblH:tblM:tblL = thr[tst]
    lda tblL             ; if thr[tst] >= count -> keep this bit
    cmp cnt0
    lda tblM
    sbc cnt1
    lda tblH
    sbc cnt2
    bcc rx_next          ; borrow => thr < count => don't set the bit
    lda tst
    sta idx
rx_next:
    lsr stp              ; step >>= 1
    bne rx_loop
    ; idx (0..100) = temperature in whole degrees C

; ============================================================================
;  FORMAT  —  idx (0..100) -> hundreds / tens / ones  (repeated subtraction)
; ============================================================================
    lda idx
    ldx #$00
    cmp #100
    bcc fmt_h
    sec
    sbc #100
    inx                  ; hundreds = 1 (only value 100)
fmt_h:
    stx hund
    ; A = 0..99
    ldy #$FF
fmt_t:
    iny
    sec
    sbc #10
    bcs fmt_t
    adc #10              ; undo overshoot -> ones digit
    sta onesd
    sty tensd

; ============================================================================
;  DISPLAY  —  "<digits> DEGREE CELSIUS" with leading-zero blanking
; ============================================================================
    lda #$21             ; name table row 14, col 6 = $21C6
    sta $2006
    lda #$C6
    sta $2006
    lda hund
    beq disp_tens        ; no hundreds digit for < 100
    ora #$30
    sta $2007
    lda tensd            ; hundreds present -> tens is not a leading zero
    ora #$30
    sta $2007
    jmp disp_ones
disp_tens:
    lda tensd
    beq disp_ones        ; blank a leading-zero tens
    ora #$30
    sta $2007
disp_ones:
    lda onesd
    ora #$30
    sta $2007

; ----------------------------------------------------------------------------
;  put_suffix: print " DEGREE CELSIUS", then turn rendering on and idle
; ----------------------------------------------------------------------------
put_suffix:
    lda #<suffix_str
    sta ptr
    lda #>suffix_str
    sta ptr+1
    ldy #$00
sfx:
    lda (ptr),y
    beq sfx_done
    sta $2007
    iny
    bne sfx
sfx_done:
    bit $2002            ; reset the $2005/$2006 write toggle
    lda #$00
    sta $2005
    sta $2005
    sta $2000            ; NMI off, BG pattern table 0
    lda #$0A
    sta $2001            ; show background
forever:
    jmp forever

; ----------------------------------------------------------------------------
;  read_thr: tblH:tblM:tblL = thr[tst].  The three 128-byte SoA arrays are
;  addressed directly (absolute,Y) because they're defined before this code.
; ----------------------------------------------------------------------------
read_thr:
    ldy tst
    lda thr_lo,y
    sta tblL
    lda thr_mid,y
    sta tblM
    lda thr_hi,y
    sta tblH
    rts

nmi_isr:
irq_isr:
    rti

; ---- CPU vectors at $FFFA/$FFFC/$FFFE ----
.ORG $3FFA
.dw nmi_isr
.dw RESET
.dw irq_isr
