Put the data phase on the DMAC, and find auto-request is charged by time
ROADMAP P4a. src/player/dma.i programs HD63450 channel 1 and takes the SCSI DATA IN phase off the CPU; src/player/dmagate.s reads the same 2,048 B at LBA 1000 three ways -- PIO, the channel with the bus held, the channel stealing cycles -- and all three are byte-exact against the host's copy of the volume. The evidence never reads $EA0015, because 57.3 established that it cannot: with the DMAC's OWN asserted MAME cannot tell a CPU-driven byte there from a DMAC-driven one. The discriminator is the CPU's own progress. MTC is sampled by the INSTRUCTION AFTER the one that starts the channel, and held it reads 0 of 2,048 -- the whole transfer happened between two instructions, because the 68000 did not execute in between -- against the full count and 426 loop trips for the stealing configuration. Put the stealing registers in the held slot and every byte still arrives and tools/bench/dma_run.sh goes RED, which is what says the counter can come out different; 58.3's vacuous "UNDERRUNS: 0/120" is the trap being avoided. tools/analysis/27_dmac_config.py decodes the four register bytes out of the player's own source, with the MC68450 field tables now in one copy (tools/analysis/mc68450.py) shared with 21_iplrom_dmac.py, so the player's configuration and the IPL ROM's 16..19 clk/B one are the same decoding. Three bounds on the apparatus, read out of MAME 0.277 rather than inferred: the CZ-6BS1 has NO request line to the DMAC (its flow control is DTACK), so external request cannot be run; single address cannot be run either, because only channel 0 has device callbacks; and only burst is modelled as held. Of the four rows of the W ladder exactly one -- dual address held -- has a code path here, and it is the one demonstrated. W did not move by one clock, for the third session running. What outlives the emulator is the currency. Every W in this project is clocks per DELIVERED byte, which presumes the device asks; an auto-requested channel spends its share of the bus whether or not a byte is there, so a record costs what it costs to ARRIVE -- halve the delivery rate and the CPU cost of the same record doubles. tools/analysis/28_autorequest_cost.py prices it from MC68450 3.8 and 5.2.3.3.2, gating its formulas against Table 5-3's sixteen rows first. At 37,405 B and an explicit 460 KB/s: max rate costs the whole 95.3% of a frame the record takes to land, and of the GCR's four bus shares only BR=00, 50%, carries the rate -- 10.61 clk/B, 47.6% of a frame, against 40.4% for the W=9 row and 391.8% measured for PIO. The GCR is a design lever nothing in this tree had named. 59.4 changes what is left. sc_in_data now REFUSES a windowed read when the data phase is the channel's (SCE_WINDOW), because a channel writes a contiguous run and cannot drop the 300 B in front of a record. 117 of 120 records need one, so sector-aligned records have gone from a preference in ROADMAP's re-encode bundle to the precondition the transport enforces -- and that bundle is now the only thing between this tree and M2. One collision, recorded because the procedure is the finding: DM_USE first sat at $18300, which is ring.i's XF_SLOT mailbox, and the P4b stage -- untouched by this work -- went red on a run that never reached its snapshot. check.sh was ALL GREEN before any of this, which is what made that red unambiguous. ALL GREEN after too, with one new stage. decode.bin is unchanged at 1,296 B and the same MD5. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -31,6 +31,7 @@ registers itself. It is evidence about what Sharp's engineers could get the
|
||||
board to do, from the vendor, for these exact devices.
|
||||
"""
|
||||
import sys, os, argparse, hashlib
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
BASE = 0xFE0000 # where the IPL ROM is mapped (and its 0xFF0000 alias)
|
||||
|
||||
@@ -41,50 +42,7 @@ KNOWN = {
|
||||
"IPL 1.0 (MAME x68000 -bios ipl10), 131,072 B",
|
||||
}
|
||||
|
||||
# --- MC68450 register map, by offset inside a channel's 0x40 block ----------
|
||||
REG = {0x00: "CSR", 0x01: "CER", 0x04: "DCR", 0x05: "OCR", 0x06: "SCR",
|
||||
0x07: "CCR", 0x0A: "MTC", 0x0C: "MAR", 0x14: "DAR", 0x1A: "BTC",
|
||||
0x1C: "BAR", 0x25: "NIV", 0x27: "EIV", 0x29: "MFC", 0x2D: "CPR",
|
||||
0x31: "DFC", 0x39: "BFC"}
|
||||
|
||||
XRM = {0: "burst",
|
||||
1: "UNDEFINED",
|
||||
2: "cycle steal WITHOUT hold (bus released between operands)",
|
||||
3: "cycle steal with hold"}
|
||||
DTYP = {0: "68000-compatible, EXPLICITLY addressed -> DUAL ADDRESS",
|
||||
1: "6800-compatible, EXPLICITLY addressed -> DUAL ADDRESS",
|
||||
2: "device with ACK, implicitly addressed -> SINGLE ADDRESS",
|
||||
3: "device with ACK and RDY, implicit -> SINGLE ADDRESS"}
|
||||
DPS = {0: "8-bit port", 1: "16-bit port"}
|
||||
PCL = {0: "status input", 1: "status input with interrupt",
|
||||
2: "start pulse", 3: "abort input"}
|
||||
SIZE = {0: "byte", 1: "word", 2: "long word", 3: "byte, unpacked"}
|
||||
CHAIN= {0: "none", 1: "UNDEFINED", 2: "array", 3: "linked array"}
|
||||
REQG = {0: "auto-request at limited rate", 1: "auto-request at max rate",
|
||||
2: "EXTERNAL request (one operand per device request)",
|
||||
3: "auto-request first operand, external thereafter"}
|
||||
|
||||
|
||||
def dcr(v):
|
||||
return [f"XRM = {v>>6&3:02b} {XRM[v>>6&3]}",
|
||||
f"DTYP = {v>>4&3:02b} {DTYP[v>>4&3]}",
|
||||
f"DPS = {v>>3&1:b} {DPS[v>>3&1]}",
|
||||
f"PCL = {v&3:02b} {PCL[v&3]}"]
|
||||
|
||||
|
||||
def ocr(v):
|
||||
return [f"DIR = {v>>7&1:b} " +
|
||||
("device -> memory (read)" if v & 0x80 else "memory -> device (write)"),
|
||||
f"SIZE = {v>>4&3:02b} {SIZE[v>>4&3]}",
|
||||
f"CHAIN= {v>>2&3:02b} {CHAIN[v>>2&3]}",
|
||||
f"REQG = {v&3:02b} {REQG[v&3]}"]
|
||||
|
||||
|
||||
def scr(v):
|
||||
m = {0: "no count", 1: "increment", 2: "decrement", 3: "UNDEFINED"}
|
||||
return [f"MAC = {v>>2&3:02b} memory address {m[v>>2&3]}",
|
||||
f"DAC = {v&3:02b} device address {m[v&3]}"]
|
||||
|
||||
from mc68450 import REG, XRM, DTYP, DPS, PCL, SIZE, CHAIN, REQG, dcr, ocr, scr
|
||||
|
||||
# --- the evidence ----------------------------------------------------------
|
||||
# (address, expected bytes, one-line description). Every register value quoted
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""What the PLAYER programs into the DMAC -- read out of the assembler source.
|
||||
|
||||
python3 tools/analysis/27_dmac_config.py [src/player/dma.i]
|
||||
|
||||
ROADMAP P4a asks for a DMAC configuration that HOLDS THE BUS, and the whole
|
||||
weight of the claim is in four register bytes. tools/analysis/21_iplrom_dmac.py
|
||||
already reads the IPL ROM's four channels the same way, out of the shipping
|
||||
image, and found Sharp's own disk channel at 16..19 clocks a byte (FINDINGS
|
||||
52.5) -- above the entire bracket this project costs P4 in. This is the other
|
||||
half of that comparison: the same MC68450 field tables (tools/analysis/
|
||||
mc68450.py, one copy) applied to the bytes src/player/dma.i actually programs.
|
||||
|
||||
IT PARSES THE SOURCE RATHER THAN RESTATING IT. A constant typed into this file
|
||||
would be a claim about the player that the player could quietly stop honouring;
|
||||
the equates are read out of src/player/dma.i, so a change there changes what is
|
||||
printed here and a mismatch between the two is not expressible.
|
||||
|
||||
IT IS A GATE. Each configuration is checked against what it is FOR -- held
|
||||
must decode as a mode that keeps the bus, stealing must decode as one that does
|
||||
not -- and a disagreement exits non-zero rather than printing a paragraph.
|
||||
|
||||
WHAT IT IS NOT: a rate. Nothing here is a measurement of anything. It says
|
||||
which mode the player asks the chip for; tools/bench/dma_run.sh shows the
|
||||
machine doing it, and `W` -- the clocks it costs on real silicon -- remains the
|
||||
project's largest open number (ROADMAP B1/B3).
|
||||
"""
|
||||
import sys, os, re
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from mc68450 import dcr, ocr, scr, XRM, DTYP, REQG
|
||||
|
||||
src = sys.argv[1] if len(sys.argv) > 1 else "src/player/dma.i"
|
||||
if not os.path.exists(src):
|
||||
sys.exit(f"missing {src} -- run from the repo root.")
|
||||
text = open(src).read()
|
||||
|
||||
def equ(name):
|
||||
m = re.search(rf"^{name}\s*=\s*\$([0-9A-Fa-f]+)", text, re.M)
|
||||
if not m:
|
||||
sys.exit(f"{src} no longer defines {name}. This script reads the "
|
||||
f"player's own equates; it does not keep a copy of them.")
|
||||
return int(m.group(1), 16)
|
||||
|
||||
# The SCR the channel is given is written inline rather than equated, because it
|
||||
# is the same for both configurations and there is nothing to choose about it.
|
||||
m = re.search(r"move\.b\s+#\$([0-9A-Fa-f]+),DM_SCR", text)
|
||||
if not m:
|
||||
sys.exit(f"{src} no longer writes DM_SCR with a literal.")
|
||||
SCR = int(m.group(1), 16)
|
||||
|
||||
CFG = [("BUS HELD", "DM_HELD_DCR", "DM_HELD_OCR"),
|
||||
("CYCLE STEALING", "DM_STEAL_DCR", "DM_STEAL_OCR")]
|
||||
|
||||
print(f"WHAT src/player/dma.i PROGRAMS -- decoded from {src}\n")
|
||||
bad = 0
|
||||
for label, dn, on in CFG:
|
||||
D, O = equ(dn), equ(on)
|
||||
print(f" {label} DCR = ${D:02X} OCR = ${O:02X} SCR = ${SCR:02X}")
|
||||
for line in dcr(D):
|
||||
print(f" {line}")
|
||||
for line in ocr(O):
|
||||
print(f" {line}")
|
||||
for line in scr(SCR):
|
||||
print(f" {line}")
|
||||
holds = (D >> 6 & 3) in (0, 3) # burst, or cycle steal WITH hold
|
||||
dual = (D >> 4 & 3) in (0, 1)
|
||||
tomem = bool(O & 0x80)
|
||||
checks = [
|
||||
(dual, "DTYP must be explicitly addressed: only channel 0 has device "
|
||||
"callbacks in this machine, so an implicit-address DTYP on "
|
||||
"channel 1 falls through to the dual-address path anyway"),
|
||||
(tomem, "OCR DIR must be device -> memory; this is a READ"),
|
||||
((O >> 4 & 3) == 0, "OCR SIZE must be byte: the SPC's port is 8 bits"),
|
||||
((O >> 2 & 3) == 0, "OCR CHAIN must be none until P5a picks a chaining "
|
||||
"scheme for the two-deep request queue (FINDINGS 55.3)"),
|
||||
((SCR & 3) == 0, "SCR DAC must not count: the device address is a "
|
||||
"REGISTER at $EA0015 and must not walk off it"),
|
||||
((SCR >> 2 & 3) == 1, "SCR MAC must increment: the record is contiguous"),
|
||||
((O & 3) in (0, 1), "OCR REQG must be an AUTO-request mode: the "
|
||||
"expansion slot has no request line to the DMAC in "
|
||||
"this machine, so external request cannot be run"),
|
||||
]
|
||||
if label == "BUS HELD":
|
||||
checks.append((holds, "the held configuration must decode as a mode "
|
||||
"that KEEPS the bus between operands"))
|
||||
checks.append(((O & 3) == 1, "and as max-rate auto-request: MAME models "
|
||||
"a held bus only for burst + REQG 01"))
|
||||
else:
|
||||
checks.append((not holds, "the stealing configuration must decode as a "
|
||||
"mode that RELEASES the bus between operands "
|
||||
"-- otherwise the two have no contrast"))
|
||||
for ok, why in checks:
|
||||
if not ok:
|
||||
print(f" FAIL: {why}")
|
||||
bad += 1
|
||||
print()
|
||||
|
||||
print("""AGAINST THE MACHINE'S OWN DISK CHANNEL (21_iplrom_dmac.py, FINDINGS 52.5)
|
||||
|
||||
IPL ROM ch1, SASI DCR $80 OCR $B2 dual address, 8-bit port, cycle steal
|
||||
WITHOUT hold, EXTERNAL request
|
||||
-> a full arbitration per byte, 16..19
|
||||
player, held DCR $00 OCR $81 dual address, 8-bit port, BURST,
|
||||
auto-request at max rate
|
||||
-> the ladder's dual-address held row, 9
|
||||
|
||||
Sharp's own configuration and the player's differ in exactly the field that
|
||||
decides the project. That is 52.5's finding read the other way round: a cheaper
|
||||
configuration IS reachable for an explicitly-addressed 8-bit port, and what it
|
||||
costs on real silicon is still ROADMAP B3's question and not this file's.""")
|
||||
sys.exit(1 if bad else 0)
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""What AUTO-REQUEST DMA costs the 68000, when there is no request line.
|
||||
|
||||
python3 tools/analysis/28_autorequest_cost.py --kbps 460 [--record 37405]
|
||||
|
||||
WHY THIS EXISTS. The project's per-byte ladder -- W = 5 single-address held, 9
|
||||
dual held, 12 single arbitrated, 16..19 dual arbitrated (FINDINGS 42.4, 52.5) --
|
||||
prices a transfer that the DEVICE asks for: one external request, one operand,
|
||||
a known number of stolen clocks per delivered byte. Session 27 found that the
|
||||
CZ-6BS1 as MAME models it has NO REQUEST LINE to the DMAC at all (FINDINGS
|
||||
59.2): the card's flow control is DTACK, and every configuration that can be run
|
||||
against it is AUTO-REQUEST, where the channel transfers because its own counter
|
||||
says so and not because a byte has arrived.
|
||||
|
||||
THAT CHANGES THE CURRENCY, and it is the reason this file is not a line in
|
||||
another one. An externally requested transfer is charged PER DELIVERED BYTE.
|
||||
An auto-requested one is charged PER UNIT OF TIME THE CHANNEL IS ACTIVE, because
|
||||
the channel has no way to know the device is not ready: it takes its allotted
|
||||
share of the bus and spends it whether or not a byte comes back. So the cost of
|
||||
delivering a record depends on HOW LONG THE RECORD TAKES TO ARRIVE -- i.e. on
|
||||
the delivery rate, the figure this tree deliberately has no default for (FINDINGS
|
||||
50) -- and the tool REQUIRES one rather than assuming it.
|
||||
|
||||
SOURCED: MC68450 Direct Memory Access Controller, Motorola, Jul 1989
|
||||
(bitsavers), sections 3.8 and 5.2.3.3, the same document buscost.py's transfer
|
||||
timings come from. Section 5.2.3.3.1: under maximum-rate auto-request "all
|
||||
operands in the data block will be transferred in one burst, so that the DMAC
|
||||
will use 100% of the available bus bandwidth" -- which is the datasheet saying,
|
||||
in its own words, what session 27 measured MAME's model doing when it HALTED the
|
||||
68000 for the whole data phase (FINDINGS 59.1).
|
||||
|
||||
THE ONE LOAD-BEARING ASSUMPTION, stated because the whole table rests on it:
|
||||
that the channel SPENDS its allotted share whether or not the device has a byte.
|
||||
Under auto-request a request is pending until MTC is exhausted, so the DMAC
|
||||
takes the bus during every burst window it is entitled to; when the device is
|
||||
not ready the cycle is stretched by wait states (a real CZ-6BS1 negating DTACK)
|
||||
or retried later (MAME's model discards the operand), and either way the window
|
||||
is gone from the CPU's point of view. If a real card instead lets the DMAC off
|
||||
the bus early when no byte is there, these figures are UPPER BOUNDS. That is a
|
||||
board question and it is ROADMAP B3's.
|
||||
|
||||
NOT A MEASUREMENT. Every figure below is arithmetic over datasheet constants
|
||||
and an explicit rate. `W` is still unmeasured and still wants a board.
|
||||
"""
|
||||
import sys, os, argparse
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from buscost import DMA_DUAL_BYTE_CLK, DMA_READ_CLK, DMA_WRITE_CLK
|
||||
|
||||
CPU_HZ = 10_000_000.0 # the X68000 the whole tree is costed against
|
||||
FPS = 12.0
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--kbps", type=float, required=True,
|
||||
help="delivery rate in KB/s. REQUIRED: this tree has no default "
|
||||
"rate and the whole answer scales with it (FINDINGS 50).")
|
||||
ap.add_argument("--record", type=int, default=37405,
|
||||
help="mean record size in bytes (default: the gate container's)")
|
||||
a = ap.parse_args()
|
||||
RATE = a.kbps * 1024.0
|
||||
|
||||
# --- 3.8 GENERAL CONTROL REGISTER, decoded from the formulas in 5.2.3.3.2 ---
|
||||
# burst time = 2^(BT + 4) clocks
|
||||
# sample period = 2^(BT + BR + 5) clocks
|
||||
# DMAC's share = 2^-(BR + 1)
|
||||
# and Table 5-3 prints all sixteen combinations, so the formulas are GATED
|
||||
# against the table rather than trusted.
|
||||
TABLE = { # (BR, BT): (burst, MPU period, share, sample period)
|
||||
(0,0):(16,16,.5,32), (0,1):(32,32,.5,64), (0,2):(64,64,.5,128), (0,3):(128,128,.5,256),
|
||||
(1,0):(16,48,.25,64), (1,1):(32,96,.25,128), (1,2):(64,192,.25,256),(1,3):(128,384,.25,512),
|
||||
(2,0):(16,112,.125,128),(2,1):(32,224,.125,256),(2,2):(64,448,.125,512),(2,3):(128,896,.125,1024),
|
||||
(3,0):(16,240,.0625,256),(3,1):(32,480,.0625,512),(3,2):(64,960,.0625,1024),
|
||||
(3,3):(128,1920,.0625,2048),
|
||||
}
|
||||
bad = 0
|
||||
for (br, bt), (burst, mpu, share, sample) in sorted(TABLE.items()):
|
||||
f_burst, f_sample, f_share = 2**(bt+4), 2**(bt+br+5), 2.0**-(br+1)
|
||||
for got, want, what in ((f_burst, burst, "burst time"),
|
||||
(f_sample, sample, "sample period"),
|
||||
(f_share, share, "bandwidth share"),
|
||||
(f_sample - f_burst, mpu, "MPU period")):
|
||||
if got != want:
|
||||
print(f" FAIL BR={br:02b} BT={bt:02b} {what}: formula {got}, "
|
||||
f"Table 5-3 {want}")
|
||||
bad += 1
|
||||
if bad:
|
||||
sys.exit(f"\n{bad} disagreements between 5.2.3.3.2's formulas and Table 5-3. "
|
||||
"Everything below\nis those formulas, so it is not printed.")
|
||||
print(f"MC68450 5.2.3.3.2's formulas reproduce all 16 rows of Table 5-3.\n")
|
||||
|
||||
BYTE_CLK = DMA_DUAL_BYTE_CLK # dual address, 8-bit port: a 4-clock read of
|
||||
# $EA0015 and a 5-clock write to the ring
|
||||
frame_clk = CPU_HZ / FPS
|
||||
wire_s = a.record / RATE # how long the record takes to land
|
||||
wire_clk = wire_s * CPU_HZ # ...in 68000 clocks
|
||||
per_byte_wire = wire_clk / a.record # clocks of wall time per byte
|
||||
|
||||
print(f"THE RECORD: {a.record:,} B at {a.kbps:g} KB/s = {wire_s*1000:.2f} ms "
|
||||
f"= {wire_clk:,.0f} clocks = {100*wire_clk/frame_clk:.1f}% of a "
|
||||
f"{FPS:g} fps frame")
|
||||
print(f" one byte of WIRE TIME is {per_byte_wire:.2f} clocks; one byte of DMAC "
|
||||
f"WORK is {BYTE_CLK} ({DMA_READ_CLK} read + {DMA_WRITE_CLK} write, "
|
||||
f"buscost.py)\n")
|
||||
|
||||
print("REQG 01, AUTO-REQUEST AT MAXIMUM RATE -- what session 27 demonstrated")
|
||||
print(f" The channel holds the bus until MTC is exhausted (5.2.3.3.1: 100% of "
|
||||
f"the\n bandwidth), so the CPU gets NOTHING for the whole delivery:")
|
||||
print(f" cost to the 68000 = the whole {100*wire_clk/frame_clk:.1f}% of a "
|
||||
f"frame, or {per_byte_wire:.2f} clk/B")
|
||||
print(f" It is the cheapest configuration per BYTE MOVED and the dearest per "
|
||||
f"byte\n DELIVERED, and the gap between those is the device's own "
|
||||
f"slowness:\n {BYTE_CLK} clocks of work in {per_byte_wire:.1f} clocks "
|
||||
f"of waiting = {100*BYTE_CLK/per_byte_wire:.1f}% of the held bus does "
|
||||
f"anything.\n")
|
||||
|
||||
print("REQG 00, LIMITED-RATE AUTO-REQUEST -- the lever the GCR actually gives")
|
||||
print(" The DMAC takes its programmed share of the bus and spends it whether "
|
||||
"or not\n a byte is there, so the CPU pays the SHARE for the WHOLE "
|
||||
"delivery -- and the\n share must also be big enough to carry the rate. "
|
||||
"Both, or it does not fit.\n")
|
||||
print(" BR share sustains clk/B charged % of a frame fits "
|
||||
f"{a.kbps:g} KB/s?")
|
||||
fits_any = []
|
||||
for br in range(4):
|
||||
burst, mpu, share, sample = TABLE[(br, 3)] # BT=11, the longest burst
|
||||
# bytes the channel can move inside one burst window, and how often that
|
||||
# window comes round
|
||||
bytes_per_burst = burst // BYTE_CLK
|
||||
sustains = bytes_per_burst * CPU_HZ / sample
|
||||
charged = share * per_byte_wire # clocks the CPU loses per
|
||||
# DELIVERED byte
|
||||
pct = 100 * share * wire_clk / frame_clk
|
||||
ok = sustains >= RATE
|
||||
if ok:
|
||||
fits_any.append((br, share, charged, pct))
|
||||
print(f" {br:02b} {share*100:5.2f}% {sustains/1024:7.1f} KB/s "
|
||||
f"{charged:9.2f} {pct:8.1f}% {'yes' if ok else 'NO'}")
|
||||
print(f"\n (BT = 11 throughout: the longest burst, 128 clocks, which is the "
|
||||
f"most\n favourable row -- a shorter burst moves fewer bytes per window "
|
||||
f"at the same\n share and sustains proportionally less.)")
|
||||
|
||||
if not fits_any:
|
||||
print(f"\n NOTHING FITS. At {a.kbps:g} KB/s no limited-rate share can "
|
||||
f"carry the record,\n so the only auto-request configuration that "
|
||||
f"delivers is maximum rate --\n and that one stops the CPU for the "
|
||||
f"whole {100*wire_clk/frame_clk:.1f}% of a frame the record takes.")
|
||||
else:
|
||||
br, share, charged, pct = fits_any[0]
|
||||
print(f"\n CHEAPEST THAT FITS: BR = {br:02b}, {share*100:g}% of the bus, "
|
||||
f"{charged:.2f} clk/B charged to the\n 68000 -- {pct:.1f}% of a frame "
|
||||
f"per record.")
|
||||
print(f" Against the ladder: W=5 held costs {5*a.record/frame_clk*100:.1f}%, "
|
||||
f"W=9 dual held {9*a.record/frame_clk*100:.1f}%,\n W=19 the IPL ROM's "
|
||||
f"own {19*a.record/frame_clk*100:.1f}%, and PIO measured "
|
||||
f"{87.28*a.record/frame_clk*100:.1f}% (FINDINGS 58.2).")
|
||||
|
||||
print(f"""
|
||||
WHAT THIS SETTLES, AND WHAT IT DOES NOT
|
||||
|
||||
1. AUTO-REQUEST IS CHARGED BY TIME, NOT BY BYTE. Every W in this project is
|
||||
clocks per DELIVERED byte, which presumes the device asks. With no request
|
||||
line the channel spends its share of the bus at a rate it was told, so the
|
||||
record's cost scales with how long the disc takes -- halve the delivery rate
|
||||
and the CPU cost of the same record DOUBLES. No W does that.
|
||||
|
||||
2. THE GCR IS A DESIGN LEVER NOBODY HAD NAMED. BT and BR are two bits each and
|
||||
they set what fraction of the bus the player gives away. That is the same
|
||||
kind of choice as `aligned` vs `split` and it belongs in the same list.
|
||||
|
||||
3. IT IS STILL NOT A MEASUREMENT. These are datasheet constants and an explicit
|
||||
rate. Whether the real CZ-6BS1 drives #EXREQ (pin B36 exists on the slot, and
|
||||
MAME's model simply does not connect it) is ROADMAP B3's question, and if it
|
||||
does, the ladder applies and this file is the fallback rather than the plan.""")
|
||||
@@ -0,0 +1,59 @@
|
||||
"""MC68450 / HD63450 register field layouts, in ONE copy.
|
||||
|
||||
Read by tools/analysis/21_iplrom_dmac.py, which decodes what the X68000's IPL
|
||||
ROM programs into the DMAC, and by tools/analysis/27_dmac_config.py, which
|
||||
decodes what src/player/dma.i programs into it. The two exist to be COMPARED
|
||||
-- the ROM's own disk channel costs 16..19 clocks a byte (FINDINGS 52.5) and
|
||||
the player's job is to be cheaper -- and a comparison between two decodings
|
||||
that used two copies of these tables would not be one. This tree has already
|
||||
paid twice for a transform with two copies of itself (FINDINGS 49.7.5).
|
||||
|
||||
SOURCED: MC68450 Direct Memory Access Controller, Motorola, Jul 1989
|
||||
(bitsavers) -- the same document FINDINGS 39 cites for the transfer timings in
|
||||
tools/analysis/buscost.py.
|
||||
"""
|
||||
# --- MC68450 register map, by offset inside a channel's 0x40 block ----------
|
||||
REG = {0x00: "CSR", 0x01: "CER", 0x04: "DCR", 0x05: "OCR", 0x06: "SCR",
|
||||
0x07: "CCR", 0x0A: "MTC", 0x0C: "MAR", 0x14: "DAR", 0x1A: "BTC",
|
||||
0x1C: "BAR", 0x25: "NIV", 0x27: "EIV", 0x29: "MFC", 0x2D: "CPR",
|
||||
0x31: "DFC", 0x39: "BFC"}
|
||||
|
||||
XRM = {0: "burst",
|
||||
1: "UNDEFINED",
|
||||
2: "cycle steal WITHOUT hold (bus released between operands)",
|
||||
3: "cycle steal with hold"}
|
||||
DTYP = {0: "68000-compatible, EXPLICITLY addressed -> DUAL ADDRESS",
|
||||
1: "6800-compatible, EXPLICITLY addressed -> DUAL ADDRESS",
|
||||
2: "device with ACK, implicitly addressed -> SINGLE ADDRESS",
|
||||
3: "device with ACK and RDY, implicit -> SINGLE ADDRESS"}
|
||||
DPS = {0: "8-bit port", 1: "16-bit port"}
|
||||
PCL = {0: "status input", 1: "status input with interrupt",
|
||||
2: "start pulse", 3: "abort input"}
|
||||
SIZE = {0: "byte", 1: "word", 2: "long word", 3: "byte, unpacked"}
|
||||
CHAIN= {0: "none", 1: "UNDEFINED", 2: "array", 3: "linked array"}
|
||||
REQG = {0: "auto-request at limited rate", 1: "auto-request at max rate",
|
||||
2: "EXTERNAL request (one operand per device request)",
|
||||
3: "auto-request first operand, external thereafter"}
|
||||
|
||||
|
||||
def dcr(v):
|
||||
return [f"XRM = {v>>6&3:02b} {XRM[v>>6&3]}",
|
||||
f"DTYP = {v>>4&3:02b} {DTYP[v>>4&3]}",
|
||||
f"DPS = {v>>3&1:b} {DPS[v>>3&1]}",
|
||||
f"PCL = {v&3:02b} {PCL[v&3]}"]
|
||||
|
||||
|
||||
def ocr(v):
|
||||
return [f"DIR = {v>>7&1:b} " +
|
||||
("device -> memory (read)" if v & 0x80 else "memory -> device (write)"),
|
||||
f"SIZE = {v>>4&3:02b} {SIZE[v>>4&3]}",
|
||||
f"CHAIN= {v>>2&3:02b} {CHAIN[v>>2&3]}",
|
||||
f"REQG = {v&3:02b} {REQG[v&3]}"]
|
||||
|
||||
|
||||
def scr(v):
|
||||
m = {0: "no count", 1: "increment", 2: "decrement", 3: "UNDEFINED"}
|
||||
return [f"MAC = {v>>2&3:02b} memory address {m[v>>2&3]}",
|
||||
f"DAC = {v&3:02b} device address {m[v&3]}"]
|
||||
|
||||
|
||||
@@ -504,6 +504,50 @@ else
|
||||
echo " SKIPPED: no chdman (ships with mame-tools) -- cannot build the volume"
|
||||
fi
|
||||
|
||||
echo "--- session 27: the DMAC drives the data phase, and holds the bus (FINDINGS 59) ---"
|
||||
# ROADMAP P4a, the last item before M2. The two stages above have the CPU moving
|
||||
# every byte itself, at the 87.28 clocks per delivered byte FINDINGS 58.2
|
||||
# measured -- 391.8% of a 12 fps frame. This one hands the DATA IN phase to the
|
||||
# HD63450 and gates on the thing 57.3 said would be hard to show: that the DMAC,
|
||||
# and not the CPU, is driving it.
|
||||
#
|
||||
# IT IS GATED WITHOUT LOOKING AT $EA0015, and that is the design. With the
|
||||
# DMAC's OWN asserted -- which it is at idle here -- MAME cannot distinguish a
|
||||
# CPU-driven byte at that address from a DMAC-driven one, so watching it proves
|
||||
# nothing. What is gated instead is THE CPU'S OWN PROGRESS:
|
||||
# * the same 2,048 B off the disc three ways -- PIO, held, stealing -- all
|
||||
# three byte-exact against the host's copy, so the configuration is being
|
||||
# compared against a delivery that works and not against nothing;
|
||||
# * MTC sampled by the INSTRUCTION AFTER the one that starts the channel: zero
|
||||
# in the held configuration (the whole transfer happened between two
|
||||
# instructions, because the 68000 did not execute in between) and the full
|
||||
# count in the stealing one;
|
||||
# * the CPU's own trip count round its wait loop: 1 against hundreds. A
|
||||
# counter that CANNOT come out different is 58.3's vacuous "UNDERRUNS: 0/120"
|
||||
# again, so the run asserts the contrast and not just the held value;
|
||||
# * the channel's own CSR/CER/MTC/MAR, which must say it moved every byte
|
||||
# without error;
|
||||
# * and a WINDOWED read through the channel REFUSED. 117 of 120 records start
|
||||
# part way into a sector (58.3); a channel writes a contiguous run and cannot
|
||||
# drop the bytes in front of one, so it would write the neighbouring records
|
||||
# into the ring with no bounds check to catch it (49.2). The refusal is what
|
||||
# makes "sector-aligned container" a precondition the transport states.
|
||||
#
|
||||
# NOT GATED ON RATE, and it cannot be: MAME's DMAC runs on wall-clock attotimes
|
||||
# (42.5) and models a held bus by HALTING the CPU rather than by charging it
|
||||
# cycles per operand. `W` is untouched. tools/analysis/28_autorequest_cost.py
|
||||
# prices what this configuration costs, from the datasheet and an explicit rate.
|
||||
# Skipped rather than failed when chdman is absent.
|
||||
if command -v chdman > /dev/null; then
|
||||
bash tools/bench/dma_run.sh "$DLX" > tmp/dma_gate.log 2>&1 || {
|
||||
echo "FAIL: the DMAC did not drive the SCSI data phase."
|
||||
tail -16 tmp/dma_gate.log; exit 1; }
|
||||
grep -aE "BYTES OK|MTC one instruction|trips round|REFUSED" tmp/dma_gate.log \
|
||||
| sed 's/^ *//;s/^/ /'
|
||||
else
|
||||
echo " SKIPPED: no chdman (ships with mame-tools) -- cannot build the volume"
|
||||
fi
|
||||
|
||||
echo "--- session 24: the scene graph, and the gap between branch points (FINDINGS 56) ---"
|
||||
# The arcade scene graph is not in this repo and is not redistributable from
|
||||
# here. tools/import/scenegraph.py is the ONE file in the tree that knows the
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
-- Drive src/player/dmagate.s: does the HD63450 drive the SCSI data phase, and
|
||||
-- does it HOLD THE BUS? (ROADMAP P4a)
|
||||
--
|
||||
-- THE APPARATUS is tools/bench/scsi_run.sh's, unchanged and stated again
|
||||
-- because it is two substitutions deep: `x68000 -exp1 cz6bs1` (the board 42.5
|
||||
-- says to benchmark, never x68ksupr, whose internal SCSI is PIO-only in MAME),
|
||||
-- and a ZERO-FILLED scsiexrom.bin on a private rompath, which is honest only
|
||||
-- because the player drives the SPC registers directly and never executes a
|
||||
-- byte of that ROM.
|
||||
--
|
||||
-- WHAT THIS RIG DOES NOT DO, and it is the point of the whole design: it never
|
||||
-- looks at $EA0015. 57.3 showed that address cannot answer the question --
|
||||
-- with the DMAC's OWN asserted MAME cannot tell a CPU-driven byte there from a
|
||||
-- DMAC-driven one. What separates the two configurations below is whether the
|
||||
-- 68000 EXECUTED ANYTHING while the bytes were arriving, which is a fact about
|
||||
-- the CPU and is read out of the DMAC's own registers plus a counter the
|
||||
-- machine incremented itself.
|
||||
--
|
||||
-- AND IT IS NOT A RATE. MAME's DMAC is configured in wall-clock attotimes
|
||||
-- (42.5); its burst mode halts the CPU outright rather than charging it cycles
|
||||
-- per operand. `W` is untouched here and still wants a board.
|
||||
local M = manager.machine
|
||||
local SP = M.devices[":maincpu"].spaces["program"]
|
||||
local function P(s) print("[DMA] "..s) end
|
||||
local function T() local t=M.time; return t.seconds + t.attoseconds/1e18 end
|
||||
|
||||
local DGFLAG, DGREC, DGREC_SZ = 0x18600, 0x18610, 32
|
||||
local DGWIN, DGWERR = 0x18680, 0x18684
|
||||
local DGLBA, DGBLK = 1000, 4
|
||||
local DST = {0x20000, 0x24000, 0x28000}
|
||||
local NAME = {"PIO (the path FINDINGS 58 measured)",
|
||||
"DMA, BUS HELD (DCR $00 burst, OCR $81 max rate)",
|
||||
"DMA, STEALING (DCR $80 cycle steal, OCR $80 limited)"}
|
||||
local SHORT = {"pio", "held", "steal"}
|
||||
local ERRNAME = {[0]="OK", "SELECTION TIMEOUT -- no target answered",
|
||||
"UNEXPECTED PHASE", "POLL TIMEOUT -- a phase never arrived",
|
||||
"NON-ZERO SCSI STATUS",
|
||||
"WINDOWED READ REFUSED -- a channel cannot drop bytes"}
|
||||
local DISK = os.getenv("DLX_SCSI_IMG") or "dlxdisk.img"
|
||||
|
||||
local code do local f=io.open("dmagate.bin","rb"); code=f:read("a"); f:close() end
|
||||
|
||||
-- the disc's own bytes, once, for all three comparisons
|
||||
local want do
|
||||
local f = io.open(DISK, "rb")
|
||||
if f then f:seek("set", DGLBA*512); want = f:read(DGBLK*512); f:close() end
|
||||
end
|
||||
|
||||
local st = "boot"
|
||||
SUB = emu.add_machine_frame_notifier(function()
|
||||
local ok, err = pcall(function()
|
||||
if st == "boot" then
|
||||
if T() < 3.0 then return end
|
||||
for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end
|
||||
SP:write_u32(DGFLAG, 0)
|
||||
local cpu = M.devices[":maincpu"]
|
||||
cpu.state["SR"].value = 0x2700
|
||||
cpu.state["SP"].value = 0x8000
|
||||
cpu.state["PC"].value = 0x10000
|
||||
P(string.format("dmagate.bin=%d B loaded at $10000; reading LBA %d, %d B, "
|
||||
.."three ways", #code, DGLBA, DGBLK*512))
|
||||
st = "wait"; return
|
||||
end
|
||||
if st == "wait" then
|
||||
if SP:read_u32(DGFLAG) ~= 1 then
|
||||
if T() > 60 then P("TIMEOUT: the gate never finished"); P("done"); M:exit() end
|
||||
return
|
||||
end
|
||||
if not want then P("no "..DISK.." to check against"); P("done"); M:exit(); return end
|
||||
local LEN = DGBLK*512
|
||||
for i = 0, 2 do
|
||||
local b = DGREC + i*DGREC_SZ
|
||||
local rc = SP:read_u32(b)
|
||||
local e = SP:read_u32(b+4)
|
||||
local mtc0 = SP:read_u32(b+8)
|
||||
local spin = SP:read_u32(b+12)
|
||||
local csr = SP:read_u32(b+16)
|
||||
local cer = SP:read_u32(b+20)
|
||||
local mtcf = SP:read_u32(b+24)
|
||||
local marf = SP:read_u32(b+28)
|
||||
P(NAME[i+1])
|
||||
if rc ~= 0 then
|
||||
P(string.format(" FAILED: err=%d (%s)", e, ERRNAME[e] or "?"))
|
||||
else
|
||||
local bad, first = 0, nil
|
||||
for k = 1, LEN do
|
||||
if SP:read_u8(DST[i+1]+k-1) ~= string.byte(want, k) then
|
||||
bad = bad + 1; first = first or (k-1)
|
||||
end
|
||||
end
|
||||
if bad == 0 then
|
||||
P(string.format(" BYTES OK: %d B from LBA %d match %s byte for byte "
|
||||
.."[%s]", LEN, DGLBA, DISK, SHORT[i+1]))
|
||||
else
|
||||
P(string.format(" BYTES WRONG [%s]: %d of %d differ, first at +%d",
|
||||
SHORT[i+1], bad, LEN, first))
|
||||
end
|
||||
end
|
||||
if i > 0 then
|
||||
-- THE DISCRIMINATOR. MTC as the instruction after START saw it, and
|
||||
-- the number of times the CPU went round its own wait loop.
|
||||
P(string.format(" MTC one instruction after START: %d of %d -> the "
|
||||
.."CPU %s while the transfer ran [%s]",
|
||||
mtc0, LEN,
|
||||
(mtc0 == 0) and "NEVER EXECUTED" or "kept executing",
|
||||
SHORT[i+1]))
|
||||
P(string.format(" CPU trips round the wait loop: %d [%s]", spin, SHORT[i+1]))
|
||||
P(string.format(" channel: CSR=$%02X (%s%s%s) CER=$%02X MTC=%d "
|
||||
.."MAR=$%06X (+%d) [%s]",
|
||||
csr,
|
||||
((csr & 0x80) ~= 0) and "COC " or "",
|
||||
((csr & 0x10) ~= 0) and "ERR " or "",
|
||||
((csr & 0x08) ~= 0) and "ACT" or "idle",
|
||||
cer, mtcf, marf, marf - DST[i+1], SHORT[i+1]))
|
||||
end
|
||||
end
|
||||
-- The refusal. Expected to fail, and the run is only green if it did.
|
||||
local w, we = SP:read_u32(DGWIN), SP:read_u32(DGWERR)
|
||||
if w == 0xFFFFFFFF and we == 5 then
|
||||
P("WINDOWED DMA READ REFUSED, as it must be: a channel writes a "
|
||||
.."contiguous run and cannot drop the 300 B in front of the record "
|
||||
.."(58.3). P4a's precondition is a SECTOR-ALIGNED container.")
|
||||
else
|
||||
P(string.format("WINDOW NOT REFUSED: rc=%d err=%d -- the transport would "
|
||||
.."have written the neighbours' bytes into the ring.", w, we))
|
||||
end
|
||||
P("done"); M:exit(); return
|
||||
end
|
||||
end)
|
||||
if not ok then P("LUA ERROR: "..tostring(err)); P("done"); M:exit() end
|
||||
end)
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# One HD63450 data-phase run: does the DMAC drive the SCSI data phase, and does
|
||||
# it HOLD THE BUS? (ROADMAP P4a, the last item before M2.)
|
||||
#
|
||||
# tools/bench/dma_run.sh [container.dlx]
|
||||
#
|
||||
# The apparatus is tools/bench/scsi_run.sh's -- `x68000 -exp1 cz6bs1` and a
|
||||
# zero-filled scsiexrom.bin on a private rompath -- and the volume is
|
||||
# tools/bench/mkvol.sh's, the same bytes the host-file ring rig reads.
|
||||
#
|
||||
# WHAT A GREEN RUN MEANS: the same 2,048 B came off the disc three ways -- PIO,
|
||||
# the channel with the bus held, the channel stealing cycles -- all three
|
||||
# byte-exact against the host's copy; and in the held configuration THE WHOLE
|
||||
# TRANSFER HAPPENED BETWEEN TWO INSTRUCTIONS, which is what holding the bus
|
||||
# means and is not a claim about $EA0015 (57.3).
|
||||
#
|
||||
# WHAT IT DOES NOT MEAN: anything about `W`. MAME's DMAC runs on wall-clock
|
||||
# attotimes (42.5) and models a held bus by HALTING the CPU rather than by
|
||||
# charging it cycles per operand. This settles which configuration works.
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
DLX=${1:-tmp/rc_fr_singe_scsi_span.dlx}
|
||||
|
||||
bash tools/bench/mkvol.sh "$DLX"
|
||||
|
||||
tools/vasm/vasmm68k_mot -Fbin -o tmp/dmagate.bin src/player/dmagate.s > /dev/null
|
||||
|
||||
# What the player will program, decoded out of the same constants it programs.
|
||||
python3 tools/analysis/27_dmac_config.py
|
||||
|
||||
# stdbuf -oL: without it a long MAME run is unobservable until it exits, and a
|
||||
# run that is merely finishing looks exactly like one that is wedged (34.1).
|
||||
( cd tmp && SDL_VIDEODRIVER=dummy stdbuf -oL timeout -k 5 300 \
|
||||
mame x68000 -bios ipl10 -exp1 cz6bs1 \
|
||||
-rompath "$HOME/mame/roms;./p4roms" -hard dlxdisk.chd \
|
||||
-ramsize 2M -video soft -window -sound none -nothrottle -plugins \
|
||||
-autoboot_script ../tools/bench/dma.lua \
|
||||
-seconds_to_run 90 > dma_run.log 2>&1 )
|
||||
grep -aq "^\[DMA\] done" tmp/dma_run.log || {
|
||||
echo "FAIL: the DMA gate did not finish -- no completion marker."
|
||||
tail -8 tmp/dma_run.log; exit 1; }
|
||||
grep -a "^\[DMA\]" tmp/dma_run.log | sed 's/^\[DMA\] / /'
|
||||
|
||||
# THE ASSERTIONS. Printing a result and gating on it are different things.
|
||||
fail() { echo "FAIL: $1"; exit 1; }
|
||||
grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[pio\]" tmp/dma_run.log || \
|
||||
fail "the PIO reference read did not match -- nothing below is about the DMAC."
|
||||
grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[held\]" tmp/dma_run.log || \
|
||||
fail "the bus-held DMA read did not deliver the disc's bytes."
|
||||
grep -aq "BYTES OK: 2048 B from LBA 1000 .*\[steal\]" tmp/dma_run.log || \
|
||||
fail "the cycle-stealing DMA read did not deliver the disc's bytes."
|
||||
grep -aq "MTC one instruction after START: 0 of 2048 .*NEVER EXECUTED .*\[held\]" \
|
||||
tmp/dma_run.log || \
|
||||
fail "the bus was NOT held: the CPU executed while the channel ran, so this is
|
||||
not the configuration ROADMAP P4a asks for. That MTC is the whole of the
|
||||
evidence that does not come from watching \$EA0015 (57.3)."
|
||||
grep -aq "CPU trips round the wait loop: 1 \[held\]" tmp/dma_run.log || \
|
||||
fail "the held configuration's CPU went round its wait loop more than once --
|
||||
it was running, so the bus was not held for the whole transfer."
|
||||
# A NEGATIVE ASSERTION IS WRITTEN AS AN `if`, not as `grep ... && fail`: under
|
||||
# `set -e` a failing grep in an AND-list takes the whole script's exit status
|
||||
# with it, so the run would report the failure it was looking for as a pass.
|
||||
SPIN=$(sed -n 's/.*CPU trips round the wait loop: \([0-9]*\) \[steal\].*/\1/p' \
|
||||
tmp/dma_run.log)
|
||||
[ -n "$SPIN" ] && [ "$SPIN" -ge 100 ] || \
|
||||
fail "the cycle-stealing configuration did not leave the CPU running (spin
|
||||
= ${SPIN:-none}) -- the two configurations are meant to DIFFER in exactly
|
||||
that, and a contrast of one against one is not a contrast."
|
||||
if grep -aq "MTC one instruction after START: 0 of 2048 .*\[steal\]" tmp/dma_run.log
|
||||
then
|
||||
fail "the cycle-stealing configuration also finished between two instructions,
|
||||
so the comparison has no contrast in it and the discriminator is measuring
|
||||
something other than bus ownership."
|
||||
fi
|
||||
grep -aq "COC .*CER=\$00 MTC=0 .*(+2048) \[held\]" tmp/dma_run.log || \
|
||||
fail "the held channel did not report a clean completion of every byte."
|
||||
grep -aq "COC .*CER=\$00 MTC=0 .*(+2048) \[steal\]" tmp/dma_run.log || \
|
||||
fail "the stealing channel did not report a clean completion of every byte."
|
||||
grep -aq "WINDOWED DMA READ REFUSED" tmp/dma_run.log || \
|
||||
fail "a WINDOWED read through the channel was not refused. 117 of 120 records
|
||||
start part way into a sector (58.3), and a channel cannot drop the bytes
|
||||
in front of one -- so it would write the neighbouring records into the
|
||||
ring, over data the decoder has not finished with, with no bounds check
|
||||
to catch it (49.2)."
|
||||
exit 0
|
||||
Reference in New Issue
Block a user