reference: DUART counter/timer period when CTUR/CTLR unprogrammed -- datasheet does not settle it

This commit is contained in:
SDD committed 2026-10-16 15:00:20 +03:00
1 parent a6b6ae82ef
commit 7e65692b54
1 file changed
+312
+312
View File
@@ -0,0 +1,312 @@
# MAME `mc68681` counter/timer period when `CTUR`/`CTLR` are never programmed
Task: the VT1200 stub level-1 run wedges in a DUART1 counter-ready interrupt storm
because `ACR = 0x60` selects timer mode while `CTUR`/`CTLR` are never written.
MAME substitutes `std::max(CTR.w.l, 1)` and re-arms every tick. Determine from the
datasheet what the fitted part (Signetics/Philips **SCN2681**) actually does when the
counter is unprogrammed.
**Verdict: the datasheet does NOT settle it.** Details below; the reasoning is as
important as the quotes, and the conclusion is "stop, do not guess."
Sources (all local, in `reference/duart-datasheet/`): primary **SCN2681** Philips
1995-05-01 product specification (`SCN2681-transputer.pdf`/`.txt`) — the part fitted;
corroboration **SCC2681** Philips 2004-04-06 (`SCC2681-nxp`), **SCN68681** Signetics
1998-09-04 (`SCN68681-xdevs`), **MC68HC681** Motorola User's Manual
(`MC68681UM-nxp`). Page numbers are the printed page numbers (each page footer reads
`1995 May 1 <n>` for the SCN2681, etc.).
---
## 1. What the datasheet says
### 1a. There is no "0 means 65536" convention — the counter counts *through* 0
SCN2681, printed page 16 (also SCN68681 p. 17, SCC2681 p. 19):
> "In the counter mode, the C/T counts down the number of pulses loaded into CTUR and
> CTLR by the CPU. Counting begins upon receipt of a counter command. Upon reaching
> terminal count (000016), the counter ready interrupt bit (ISR[3]) is set. The counter
> continues counting past the terminal count until stopped by the CPU."
SCN68681, printed page 9, "Operation Control":
> "The counter operates as a down counter and sets its output bit in the ISR (Interrupt
> Status Register) **each time it passes through 0**."
Motorola MC68HC681 User's Manual, §3.5.1 (printed page 3-6):
> "The value (preload value) stored in the concatenation of the C/T upper register
> (CTUR) and the C/T lower register (CTLR) **can be from 000116 through FFFF16** …
> When the counter counts from 000116 to 000016 (terminal count), the C/T-ready bit in
> the interrupt status register (ISR[3]) is set. … **After 000016 the counter counts to
> FFFF16, and continues counting down from there.**"
So 0000 is the **terminal count**, not 65536; the counter saturates/reloads downward, it
does not wrap modulo 65536. **The "0 = 65536" hypothesis is refuted.**
### 1b. Zero is *below the minimum legal preload* — MAME's clamp to 1 is not derivable
SCN2681, printed page 16, "CTUR and CTLR – Counter/Timer Registers":
> "The CTUR and CTLR hold the eight MSBs and eight LSBs, respectively, of the value to
> be used by the counter/timer in either the counter or timer modes of operation.
> **The minimum value which may be loaded into the CTUR/CTLR registers is 000216.** Note
> that these registers are write-only and cannot be read by the CPU."
Motorola gives the same idea with a different floor: "can be from **000116** through
FFFF16" (MC68HC681 UM §3.5, p. 3-6). In timer mode the SCN2681 says the square-wave
period is "twice the value (in clock periods) of the CTUR and CTLR" — so a preload of 2
is the fastest legal half-period; a preload of 0 is not a legal state at all.
Consequence: MAME's `max(CTR.w.l, 1)` yields a half-period of **1** clock tick, which
**no legal preload can produce** (Philips floor = 2, Motorola floor = 1 but never 0).
It is a modelling choice, not a datasheet value.
### 1c. But the datasheet never says what an *unprogrammed* counter does
The datasheet defines the **legal** range and says nothing about 0. It also never
defines the **power-up/reset** value of CTUR/CTLR — and the reset description is
internally contradictory:
SCN2681, printed page 2, `RESET` pin description:
> "Reset: A High level clears internal registers (SRA, SRB, IMR, ISR, OPR, OPCR), puts
> OP0–OP7 in the High state, **stops the counter/timer**, and puts Channels A and B in
> the inactive state, with the TxDA and TxDB outputs in the mark (High) state."
Note that `CTUR`/`CTLR` are **not** in the list of registers reset clears.
SCN2681, printed page 16, "Counter/Timer" (the *same* datasheet):
> "**On power up and after reset, the timer/counter runs in timer mode and can only be
> restarted. Because it cannot be shut off or stopped, and runs continuously in timer
> mode**, it is recommended that at initialization, the output port (OP3) should be
> masked off through the OPCR[3:2] = 00 until the T/C is programmed to the desired
> operational state."
and, on the same page:
> "If new values have not been loaded, the previous count values are preserved and used
> for the next count cycle."
So on the fitted part: after reset the C/T **is running** in timer mode (the counter
section), yet the reset pin description says reset **stops** it. The datasheet does not
state the reset preload value, and the counter section's "runs continuously" statement
means the counter is armed with *whatever* value CTUR/CTLR holds — which at power-up is
not defined.
### 1d. The family differs on this exact point (as asked)
SCC2681 (2004), printed page 19:
> "On power up and after reset the timer/counter **comes up stopped** and in the timer
> mode. It will require a start counter command (a read at address 0xE) to start it.
> Because it cannot be shut off or stopped once started, and runs continuously in timer
> mode, it is recommended that at initialization, the output port (OP3) should be masked
> off through the OPCR[3:2] = 00 until the T/C is programmed to the desired operational
> state."
Motorola MC68HC681 UM, §3.5 (p. 3-6/3-7) is silent on "stopped at reset":
> "At power-up and after reset, the C/T operates in timer mode."
> "In timer mode, the C/T runs continuously; the CPU cannot start or stop it."
So the **fitted SCN2681 says the counter runs after reset**; the later **SCC2681 says it
comes up stopped** and needs a Start Counter command. This is a genuine family split
and, by itself, prevents any single unambiguous answer for "unprogrammed".
---
## 2. MAME's exact behaviour
File: `/home/arti/code/k-space/mame0289/src/devices/machine/mc68681.cpp`.
Reset (`duart_base_device::device_reset`, lines 275–289) zeroes the preload and quotes
the SCN2681 reset line:
```c++
275: void duart_base_device::device_reset()
276: {
277: ACR = 0; /* Interrupt Vector Register */
...
282: CTR.d = 0; /* Counter/Timer Preset Value */
283: // "reset clears internal registers (SRA, SRB, IMR, ISR, OPR, OPCR) puts OP0-7 in the high state, stops the counter/timer, and puts channels a/b in the inactive state"
```
The timer callback reloads the preload every half period with the clamp:
```c++
500: TIMER_CALLBACK_MEMBER(duart_base_device::duart_timer_callback)
501: {
502: if (ACR & 0x40)
503: {
504: // Timer mode
505: half_period ^= 1;
...
536: if (!half_period)
537: set_ISR_bits(INT_COUNTER_READY);
538:
539: int count = std::max(CTR.w.l, uint16_t(1)); // <-- clamp
540: start_ct(count);
541: }
542: else
543: {
...
548: // Counter mode
549: set_ISR_bits(INT_COUNTER_READY);
550: start_ct(0xffff);
551: }
552: }
```
The same clamp is reached on the two other arming paths — the **Start-Counter command**
(register `0x0e` read) and the **ACR write that enters timer mode**:
```c++
735: case 0x0e: /* Start counter command */
736: {
737: if (ACR & 0x40)
738: {
739: // Reset the timer
740: half_period = 0;
741: }
742:
743: int count = std::max(CTR.w.l, uint16_t(1)); // <-- clamp
744: start_ct(count);
745: break;
746: }
```
```c++
960: case 0x04: /* ACR */
961: {
962: uint8_t old_acr = ACR;
963: ACR = data;
...
967: if ((old_acr ^ data) & 0x40)
968: {
969: if (data & 0x40)
970: {
971: // Entering timer mode
972: uint16_t count = std::max(CTR.w.l, uint16_t(1)); // <-- clamp
973: half_period = 0;
974:
975: start_ct(count);
976: }
```
and the arming primitive itself (lines 494–498):
```c++
494: void duart_base_device::start_ct(int count)
495: {
496: uint32_t clock = get_ct_rate();
497: duart_timer->adjust(attotime::from_ticks(count, clock), 0);
498: }
```
Reading `CTUR`/`CTLR` (lines 999–1005) only updates `CTR`, so with no write `CTR.w.l`
stays 0, every arm uses count = 1, and the timer callback sets `ISR[3]` every second
callback — hence the ~229-CPU-cycle storm reported in
`harness/mame/LEVEL1_CONTINUE_REPORT.md`.
**Measured for the VT1200 run:** DUART1 register 4 (ACR) is written once, `0x60`, at PC
`3FA71CB0`; registers 6/7 (CTUR/CTLR) are **never written**. Confirmed from the harness
logs (`hp_board.log`/`board-hp.log` line 96: `W 06400004 pc=3FA71CB0 val=0060`; no
`06400006`/`06400007` write anywhere in the run). The clamp is therefore exactly the
line in play.
---
## 3. Verdict
**The datasheet does not settle it.**
What *is* settled:
* `0` does **not** mean 65536 — the C/T is a down-counter that passes through terminal
count `0000` and keeps counting down (SCN2681 p. 16; SCN68681 p. 9).
* `0` is **below the minimum legal preload** (SCN2681/SCC2681/SCN68681: `0002`;
MC68HC681: `0001`). MAME's clamp to `1` yields a period **no legal preload can
produce**, so it is a model invention, not a datasheet value.
What is **not** settled:
* The datasheet defines no behaviour for preload `0` (it is simply out of range).
* It does not define the power-up/reset value of `CTUR`/`CTLR` — reset does not clear
them (SCN2681 p. 2), and the reset state of the counter is self-contradictory in the
SCN2681 itself ("stops the counter/timer" p. 2 vs "runs in timer mode and can only be
restarted … cannot be shut off or stopped" p. 16).
* The family disagrees: SCN2681 (fitted) runs after reset; SCC2681 comes up stopped and
needs a Start Counter command.
So we cannot derive a trustworthy "correct" period from the datasheet. We can say only
that MAME's specific value is not datasheet-derived; every alternative (2, 1, "stopped
until Start") is likewise not settled by the fitted part's own contradicting text.
**No change is justified on datasheet grounds. Do not apply anything.** (The task called
this outcome in advance: a "does not settle it" result means stop rather than guess.)
Note on scope: the harness report `LEVEL1_CONTINUE_REPORT.md` §"The wall, named" already
described this as upstream `max(CTR,1)` behaviour and did not change MAME, which was
correct under this finding.
---
## 4. Minimal change — stated but **NOT applied**
Because the verdict is "does not settle it", there is **no datasheet-backed edit** to
propose, and none is made. For the record of what a *non*-datasheet-backed "make it
slower" patch would look like (so the owner can see the shape and reject it), the
candidate would be at three sites in `src/devices/machine/mc68681.cpp`:
```c++
// lines 539, 743, 972 — candidate only, NOT applied:
-int count = std::max(CTR.w.l, uint16_t(1));
+int count = std::max(CTR.w.l, uint16_t(2)); // or 0xffff, etc.
```
**Revert note for that candidate:** each of the three edits is a single-token change on
lines 539, 743, and 972; reverting is restoring `uint16_t(1)` on each. **Do not apply
this** — it is arbitrary: 2 matches the Philips floor but not the Motorola part, it still
invents behaviour for an illegal preload, and the datasheet gives no basis for choosing
it over "the counter stays stopped" (SCC2681) or "runs with an undefined preload"
(SCN2681). Choosing any value here would be exactly the "model quietly invents
behaviour" failure this check exists to catch.
---
## 5. Does the skip path hit the same fallback?
**Yes, the fallback is exercised — but it is harmless there, so this is not worth much
as a separate bug.**
* The skip path (`VT1200_SKIP_LEVEL1=1`) never runs the level-1 counter test, so the
`IMR = 0x08` (counter-ready only) write that creates the storm never happens.
* The OS *does* leave `CTUR`/`CTLR` unprogrammed on the DUARTs it configures: the OS
bring-up log shows `W 06200004=0060 ACR` and no write to `06200006`/`06200007`
(`harness/mame/OS_BRINGUP_REPORT.md` §8.3). And during the OS, the DUART1 ISR reads
back `0x0009` (`R 06400005 pc=3FB43900 val=0009`, §2) — bit 3 set, i.e. **the counter
fallback is running and setting `ISR[3]` on DUART1 as well**.
* It still does not storm because the OS never enables (or services) the counter-ready
interrupt: it programs `IMR = 0x33` on DUART2 (no bit 3; `OS_BRINGUP_REPORT.md` §4.1)
and its dispatcher masks the ISR with `0x66` (bits 1,2,5,6 — not bit 3). MAME's
`update_interrupts()` raises the IRQ only for masked bits, so `ISR[3]` being set is
invisible.
So: the skip path hits the *same model fallback* (unprogrammed preload in timer mode),
but only the **aborted level-1 test** turns it into a storm by enabling solely the
counter-ready interrupt (`IMR = 0x08`) on a dispatcher that handles bit 3. The storm is
a level-1-path symptom; the skip/OS path is unaffected. That lowers the value of chasing
this fallback — it is not what blocks the OS.
---
## Reproduction / provenance
* ROM `harness/mame/roms/vt1200/vt1200_v22.bin`, md5 `b46a3e442ccb86134d2356bd70d22fd2`
(unchanged; nothing in the MAME tree or `harness/mame/` was modified by this task).
* MAME source read-only: `/home/arti/code/k-space/mame0289/src/devices/machine/mc68681.cpp`
lines 275–289, 494–498, 500–553, 735–746, 960–976, 999–1005.
* Datasheet quotes from `reference/duart-datasheet/SCN2681-transputer.txt` (printed
pages 2 and 16), `SCN68681-xdevs.txt` (page 9), `SCC2681-nxp.txt` (page 19),
`MC68681UM-nxp.txt` (§3.5, pages 3-6/3-7).