Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ The same address and data bus continues from the 65C02 to the HM62256 RAM (orang
| 65C02 pin 34 (RWB) | RAM pin 27 (WE#) | CPU write-enable to RAM |
| Pico pin 32 (GP27) | 65C02 pin 40 (RESB) | Reset control |
| Pico pin 34 (GP28) | 65C02 pin 37 (PHI2) | Clock at 1 kHz (default) |
| Pico GP23 | 65C02 pin 34 (RWB) | Read/write sense for bus monitor |
| *(optional)* Pico GP23 | 65C02 pin 34 (RWB) | Not used for protocol `rw` on Pico 2; firmware infers from A15 |
| RAM pin 22 (OE#) | +3.3 V | Outputs disabled (writes only), avoids bus contention |

### Pull-up resistors (6 × 10 kΩ, all to +3.3 V)
Expand Down Expand Up @@ -276,6 +276,8 @@ A captured bus cycle looks like:
{"v":1,"type":"event","event":"cycle","seq":1,"addr":"8000","data":"18","rw":0}
```

`rw` is **0 = read**, **1 = write**. On this build it is **inferred from A15** (ROM `$8000–$FFFF` → read, RAM `$0000–$7FFF` → write) because Pico 2 GP23 is not a usable header pin for CPU RWB. CPU RWB still drives RAM `WE#` for real writes. Store cycles should show `rw=1`; ROM fetches (including STP `$DB`) show `rw=0`.

## Testing

Automated tests use the JSON bus-cycle stream. End the ROM with a `STP` (`0xDB`) instruction so capture stops deterministically:
Expand All @@ -286,7 +288,7 @@ uv run romulan program.txt --build --upload # demo program ends in S
uv run romulan hardware capture --until stp --port /dev/ttyACM0
```

`read_until_stp()` captures one frame per PHI2 rising edge until the CPU fetches `STP` or `max_cycles` is reached. At the default **1 kHz** clock, host polling keeps up via a multi-slot firmware queue.
`read_until_stp()` captures one frame per bus cycle until the CPU fetches `STP` or `max_cycles` is reached. Address (and ROM read data) are sampled on the PHI2 rising edge; **write data** is sampled repeatedly while PHI2 stays high (last sample before the falling edge) so STA bytes stay valid on both Pico W and Pico 2 W. At the default **1 kHz** clock, host polling keeps up via a multi-slot firmware queue.

## Deployment

Expand Down
4 changes: 4 additions & 0 deletions docs/hardware-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ Captures bus activity as JSON. Streams one event per PHI2 rising edge until the
{"v":1,"type":"event","event":"cycle","seq":1,"addr":"8000","data":"18","rw":0}
```

`rw` is **0 = read**, **1 = write**. On this build it is **inferred from A15** (ROM
`$8000–$FFFF` → read, RAM `$0000–$7FFF` → write) because Pico 2 GP23 cannot sense CPU
RWB on the header. STP stop still uses a ROM read of `$DB`.

Final event:

```json
Expand Down
5 changes: 3 additions & 2 deletions docs/hardware/pinout.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ The full per-chip pin maps. For the condensed bus/connection tables, see below:
- **GP26 (A15):** also drives the RAM's `CE#`. When A15 = 1 (ROM region) the RAM is
deselected and the Pico drives data; when A15 = 0 (RAM region) the Pico stays Hi-Z and
the RAM drives data. Same wire, both purposes.
- **GP23 (RWB):** the Pico reads this to distinguish CPU read cycles (`RWB high → 0`) from
write cycles (`RWB low → 1`) in the bus monitor.
- **GP23 (RWB):** not used for protocol `rw` on Pico 2 (GP23 is not a usable header
pin). Capture/monitor **infer** read vs write from **A15** (ROM = read, RAM = write).
CPU RWB still ties to RAM `WE#` for real writes.
- **GP27 (RESET):** configured as INPUT to release reset (10 kΩ pull-up runs the CPU),
OUTPUT LOW to assert reset.
- **GP28 (PHI2):** clock output at **1 kHz** by default (~1 ms per cycle). Configurable via
Expand Down
2 changes: 1 addition & 1 deletion docs/hardware/wiring.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The connection tables below list every net precisely.
| 65C02 pin 34 (RWB) | RAM pin 27 (WE#) | Write enable to RAM |
| Pico pin 32 (GP27) | 65C02 pin 40 (RESB) | Reset control |
| Pico pin 34 (GP28) | 65C02 pin 37 (PHI2) | Clock at 1 kHz (default) |
| Pico GP23 | 65C02 pin 34 (RWB) | Read/write sense for the bus monitor |
| *(optional)* Pico GP23 | 65C02 pin 34 (RWB) | Not used for `rw` on Pico 2; firmware infers from A15 |
| RAM pin 22 (OE#) | +3.3 V | Outputs disabled, writes only, avoids contention |

## Pull-up resistors (6 × 10 kΩ, all top to +3.3 V)
Expand Down
3 changes: 2 additions & 1 deletion src/hardware_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ void hardware_api_handle_enq(void) {
}

void hardware_api_on_bus_cycle(uint16_t addr, uint8_t data, bool rwb_pin) {
/* Protocol / docs: RWB high → read → rw=0; RWB low → write → rw=1. */
/* rwb_pin is read-high sense (not necessarily GPIO GP23). On this board
* main.c passes A15: ROM reads (high) → rw=0; RAM writes (low) → rw=1. */
uint8_t rw_report = rwb_pin ? 0u : 1u;

last_addr = addr;
Expand Down
3 changes: 2 additions & 1 deletion src/hardware_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ void hardware_api_init(const hw_context_t *ctx);
void hardware_api_handle_enq(void);

/* Call from rom_task on each PHI2 rising-edge sample.
* `rwb_pin` is the raw RWB GPIO level (1 = high). */
* `rwb_pin` is read-high sense (1 = read, 0 = write). On this build the
* caller derives it from A15, not from CPU RWB / GP23. */
void hardware_api_on_bus_cycle(uint16_t addr, uint8_t data, bool rwb_pin);

/* Send any pending capture frames (call from main after rom_task). */
Expand Down
93 changes: 69 additions & 24 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -166,23 +166,40 @@ static void rom_image_init(void) {

// ─── ROM emulation (polling) ───────────────────────────────────────────

/* Emit one captured cycle to the Hardware API and optional ASCII monitor. */
static void emit_bus_cycle(uint16_t addr, uint8_t data, bool a15_read) {
/* Infer RWB from A15: ROM (A15=1) = read, RAM (A15=0) = write.
* GP23 is not a usable header pin for CPU RWB on Pico 2. */
hardware_api_on_bus_cycle(addr, data, a15_read);

if (hardware_api_monitor_enabled() && !hardware_api_is_reading()) {
/* Match protocol: read → 0, write → 1 */
printf("| %02d | %02X | %04X | %d | %5.1f |\n",
seq_counter, data, addr, a15_read ? 0 : 1, current_hz);
seq_counter++;
if (seq_counter > 99) {
seq_counter = 1;
}
}
}

static void rom_task(void) {
/* Idle-hook can nest during USB waits — never re-enter edge sampling. */
static bool busy;
if (busy) {
return;
}
busy = true;

uint32_t pins = gpio_get_all();
bool phi2 = (pins >> PIN_PHI2) & 1u;
bool a15 = (pins >> PIN_A15) & 1u;

if (rom_active) {
if (a15) {
uint16_t addr = (pins >> PIN_A_FIRST) & 0x7FFFu;
uint8_t byte = rom_image[addr];
gpio_set_dir_out_masked(DATA_MASK);
gpio_put_masked(DATA_MASK, (uint32_t)byte << PIN_D_FIRST);
} else {
gpio_set_dir_in_masked(DATA_MASK);
}
}

if (phi2 && !phi2_last_state) {
/* Rising edge — handle capture before driving the next ROM byte so a
* deferred write sample cannot pick up Pico-driven opcode data. */
pins = gpio_get_all();
bool a15 = (pins >> PIN_A15) & 1u;

bool reset_state = gpio_get(PIN_RESET);
if (reset_state && !reset_last_state) {
seq_counter = 1;
Expand All @@ -195,21 +212,49 @@ static void rom_task(void) {
reset_last_state = reset_state;

uint16_t addr = (pins >> PIN_A_FIRST) & 0x7FFFu;
if (a15) addr |= 0x8000u;
uint8_t data = (uint8_t)((pins >> PIN_D_FIRST) & 0xFFu);
bool rwb = (pins >> PIN_RWB) & 1u;

hardware_api_on_bus_cycle(addr, data, rwb);

if (hardware_api_monitor_enabled() && !hardware_api_is_reading()) {
/* Match protocol: RWB high → read → 0 */
printf("| %02d | %02X | %04X | %d | %5.1f |\n",
seq_counter, data, addr, rwb ? 0 : 1, current_hz);
seq_counter++;
if (seq_counter > 99) seq_counter = 1;
if (a15) {
addr |= 0x8000u;
if (rom_active) {
uint8_t byte = rom_image[addr & 0x7FFFu];
gpio_set_dir_out_masked(DATA_MASK);
gpio_put_masked(DATA_MASK, (uint32_t)byte << PIN_D_FIRST);
}
pins = gpio_get_all();
uint8_t data = (uint8_t)((pins >> PIN_D_FIRST) & 0xFFu);
emit_bus_cycle(addr, data, true);
} else {
/* Write: Hi-Z and keep sampling while PHI2 is high. A fixed settle
* is too early on Pico W (CYW43/idle timing) → data=00, and a
* falling-edge sample can be too late → next opcode. The last
* sample before PHI2 falls matches the CPU write byte on both. */
gpio_set_dir_in_masked(DATA_MASK);
uint8_t data = (uint8_t)((gpio_get_all() >> PIN_D_FIRST) & 0xFFu);
uint32_t guard_us = phi2_half_us + (phi2_half_us / 2u);
if (guard_us < 10u) {
guard_us = 10u;
}
absolute_time_t deadline = make_timeout_time_us(guard_us);
while (gpio_get(PIN_PHI2) && !time_reached(deadline)) {
data = (uint8_t)((gpio_get_all() >> PIN_D_FIRST) & 0xFFu);
tight_loop_contents();
}
emit_bus_cycle(addr, data, false);
}
} else if (rom_active) {
/* Hold ROM drive / Hi-Z for the rest of the cycle. */
bool a15 = (pins >> PIN_A15) & 1u;
if (a15) {
uint16_t addr = (pins >> PIN_A_FIRST) & 0x7FFFu;
uint8_t byte = rom_image[addr];
gpio_set_dir_out_masked(DATA_MASK);
gpio_put_masked(DATA_MASK, (uint32_t)byte << PIN_D_FIRST);
} else {
gpio_set_dir_in_masked(DATA_MASK);
}
}

phi2_last_state = phi2;
busy = false;
}

// ─── Main loop ──────────────────────────────────────────────────────────
Expand Down
Loading