Fix Intel 4004 CPU instruction set and improve MCS-4 timing accuracy
Overhaul the Intel 4004 instruction set to match the real opcode map, adding missing instructions (NOP, FIM, FIN, JIN, ISZ, KBP, DCL, WR0-3, RD0-3) and refining two-byte instruction handling with proper M1/M2 data phases. Rewrite the CPU fetch-execute cycle to correctly process two-byte opcodes across separate clock cycles. Improve Intel 4001 ROM timing with M1/M2 nibble phase tracking and an A3 wait counter so the ROM drives data only when the CPU expects it. Fix state-machine transition helpers in Intel 4002 and 4003 to capture the old state before processing events, preventing incorrect exit/entry callbacks. Add non-propagating pin driver methods to prevent deadlocks caused by bidirectional pin connections during signal propagation. Add Fibonacci integration test and opcode fix plan document. Update all README files to reflect the new instruction set.
This commit is contained in:
parent
54144d10e9
commit
6a335a15d4
14 changed files with 4125 additions and 1062 deletions
|
|
@ -38,6 +38,16 @@ pub struct Intel4001 {
|
|||
// Clock edge detection
|
||||
prev_phi1: PinValue, // Previous Φ1 clock state for edge detection
|
||||
prev_phi2: PinValue, // Previous Φ2 clock state for edge detection
|
||||
// M1/M2 data phase tracking
|
||||
// M1: ROM drives instruction high nibble (OPR, bits 7-4)
|
||||
// M2: ROM drives instruction low nibble (OPA, bits 3-0)
|
||||
data_nibble_phase: bool, // false = OPR (high nibble), true = OPA (low nibble)
|
||||
// A3 wait counter: after latching both address nibbles (A1+A2), the ROM
|
||||
// must wait one more full clock cycle for the CPU's A3 cycle before driving data.
|
||||
// This counter tracks how many PHI2 rising edges to wait in WaitLatency.
|
||||
a3_wait_cycles: u8,
|
||||
// Track whether data was actually driven during this PHI2 high period
|
||||
data_driven_this_cycle: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
|
|
@ -102,6 +112,10 @@ impl Intel400xResetHandling for Intel4001 {
|
|||
// This is critical for tests that reset the chip between operations
|
||||
self.prev_phi1 = PinValue::Low;
|
||||
self.prev_phi2 = PinValue::Low;
|
||||
|
||||
// Reset A3 wait counter and data nibble phase
|
||||
self.a3_wait_cycles = 0;
|
||||
self.data_nibble_phase = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,10 +194,19 @@ impl Intel400xComponentBehavior for Intel4001 {
|
|||
Intel400xUnifiedState::Idle => {
|
||||
// Entry actions for Idle state
|
||||
self.tri_state_data_bus();
|
||||
// Reset address state for next fetch
|
||||
self.state_machine.set_address_high_nibble(None);
|
||||
self.state_machine.set_address_low_nibble(None);
|
||||
// Reset data driving state for next fetch
|
||||
self.data_driven_this_cycle = false;
|
||||
self.data_nibble_phase = false; // Reset to OPR phase
|
||||
}
|
||||
Intel400xUnifiedState::AddressPhase => {
|
||||
// Entry actions for AddressPhase state
|
||||
// Address latching will happen in execute_state_logic
|
||||
// Reset address state to ensure clean latch
|
||||
self.state_machine.set_address_high_nibble(None);
|
||||
self.state_machine.set_address_low_nibble(None);
|
||||
}
|
||||
Intel400xUnifiedState::WaitLatency => {
|
||||
// Entry actions for WaitLatency state
|
||||
|
|
@ -327,6 +350,9 @@ impl Intel4001 {
|
|||
),
|
||||
prev_phi1: PinValue::Low,
|
||||
prev_phi2: PinValue::Low,
|
||||
data_nibble_phase: false, // Start with OPR (high nibble)
|
||||
a3_wait_cycles: 0, // No A3 wait initially
|
||||
data_driven_this_cycle: false, // No data driven yet
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,7 +427,7 @@ impl Intel4001 {
|
|||
/// Intel 4004 Address Timing (CPU drives address during PHI1):
|
||||
/// - A1 cycle: CPU drives PC high nibble on PHI1 rising
|
||||
/// - A2 cycle: CPU drives PC low nibble on PHI1 rising
|
||||
/// - ROM latches each nibble on PHI1 FALLING edge (after data is stable)
|
||||
/// - ROM latches each nibble on PHI1 RISING edge (CPU has just driven the address)
|
||||
fn handle_phi1_rising(&mut self) {
|
||||
// Handle system reset first (highest priority)
|
||||
self.handle_reset("RESET");
|
||||
|
|
@ -421,8 +447,21 @@ impl Intel4001 {
|
|||
if self.state_machine.get_state() == Intel400xUnifiedState::Idle {
|
||||
self.process_chip_select();
|
||||
}
|
||||
// NOTE: Address nibble latching moved to PHI1 falling edge
|
||||
// This gives the data bus time to stabilize after CPU drives it on PHI1 rising
|
||||
}
|
||||
|
||||
// Latch address nibble on PHI1 rising edge
|
||||
// The CPU drives the address on PHI1 rising, and since CPU update runs
|
||||
// before ROM update, the address is already on the bus at this point.
|
||||
if self.state_machine.get_state() == Intel400xUnifiedState::AddressPhase {
|
||||
let nibble = self.read_data_bus() & 0x0F;
|
||||
|
||||
if self.state_machine.handle_address_nibble(nibble) {
|
||||
// Address is complete (both nibbles latched), transition to latency wait
|
||||
let _addr = self.state_machine.get_full_address().unwrap_or(0xFFFF);
|
||||
self.process_address_complete();
|
||||
// Set A3 wait counter
|
||||
self.a3_wait_cycles = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle I/O operations (higher priority than memory operations)
|
||||
|
|
@ -433,39 +472,33 @@ impl Intel4001 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle Φ1 falling edge - Latch address nibbles
|
||||
/// Hardware: Φ1 low = CPU has finished driving address, data is stable
|
||||
/// ROM latches address nibbles on PHI1 falling edge to ensure data is stable
|
||||
/// Handle Φ1 falling edge - No specific action needed
|
||||
/// Address latching is now done on PHI1 rising edge
|
||||
fn handle_phi1_falling(&mut self) {
|
||||
// Only latch if we're in AddressPhase
|
||||
// Note: We do NOT check chip_select here because:
|
||||
// 1. Chip select was already verified when we entered AddressPhase
|
||||
// 2. CM pin may be lowered between A1 and A2 cycles (this is valid)
|
||||
// 3. Once address phase starts, we must complete both nibbles
|
||||
// 4. The io_select (CI) pin distinguishes I/O vs memory operations
|
||||
if self.state_machine.get_state() == Intel400xUnifiedState::AddressPhase {
|
||||
// Latch address nibble on PHI1 falling edge (data is now stable)
|
||||
let nibble = self.read_data_bus() & 0x0F;
|
||||
|
||||
if self.state_machine.handle_address_nibble(nibble) {
|
||||
// Address is complete (both nibbles latched), transition to latency wait
|
||||
self.process_address_complete();
|
||||
}
|
||||
}
|
||||
// Address latching moved to PHI1 rising edge
|
||||
}
|
||||
|
||||
/// Handle Φ2 rising edge - Data phase
|
||||
/// Hardware: Φ2 high = Peripherals drive bus with data
|
||||
/// Focus: Data driving operations
|
||||
fn handle_phi2_rising(&mut self) {
|
||||
// Check for latency elapsed and transition to DriveData if needed
|
||||
// Handle A3 wait cycle: after address complete, wait one full clock cycle
|
||||
// for the CPU's A3 cycle before transitioning to DriveData.
|
||||
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
|
||||
if self.state_machine.is_latency_elapsed() {
|
||||
if self.a3_wait_cycles > 0 {
|
||||
// Still waiting for A3 cycle to complete
|
||||
self.a3_wait_cycles -= 1;
|
||||
} else {
|
||||
// A3 wait complete, transition to DriveData.
|
||||
// IMPORTANT: Do NOT drive data on this same edge.
|
||||
// The first data drive happens on the NEXT PHI2 rising edge.
|
||||
// This ensures the ROM's OPR/OPA phases align with the CPU's M1/M2 cycles.
|
||||
self.process_latency_elapsed();
|
||||
return; // Skip data driving on this edge
|
||||
}
|
||||
}
|
||||
|
||||
// Drive data if in DriveData state
|
||||
// Drive data if in DriveData state (only on subsequent PHI2 rising edges)
|
||||
if self.state_machine.get_state() == Intel400xUnifiedState::DriveData {
|
||||
self.handle_data_driving();
|
||||
}
|
||||
|
|
@ -478,9 +511,25 @@ impl Intel4001 {
|
|||
fn handle_phi2_falling(&mut self) {
|
||||
match self.state_machine.get_state() {
|
||||
Intel400xUnifiedState::DriveData => {
|
||||
// End of data phase - tri-state bus and return to idle
|
||||
self.tri_state_data_bus();
|
||||
self.process_operation_complete();
|
||||
if self.data_driven_this_cycle {
|
||||
// Data was actually driven this cycle - toggle nibble phase
|
||||
if !self.data_nibble_phase {
|
||||
// Just finished OPR (M1), switch to OPA (M2)
|
||||
self.data_nibble_phase = true;
|
||||
self.tri_state_data_bus();
|
||||
// Stay in DriveData for M2 cycle
|
||||
} else {
|
||||
// Just finished OPA (M2) - full 8-bit instruction sent
|
||||
// Return to idle so the next fetch can begin
|
||||
self.data_nibble_phase = false;
|
||||
self.tri_state_data_bus();
|
||||
self.process_operation_complete();
|
||||
}
|
||||
} else {
|
||||
// No data driven this cycle (e.g., just entered DriveData)
|
||||
// Don't toggle - just tri-state
|
||||
self.tri_state_data_bus();
|
||||
}
|
||||
}
|
||||
|
||||
Intel400xUnifiedState::Idle
|
||||
|
|
@ -740,7 +789,12 @@ impl Intel4001 {
|
|||
/// Handle data driving during DriveData state
|
||||
/// Hardware: ROM drives data bus when all conditions are met
|
||||
/// Data stays on bus during Φ2 high period, tri-stated on Φ2 falling edge
|
||||
/// IMPORTANT: Do NOT return to idle here after OPA - the data must stay on the bus
|
||||
/// for the CPU to read. Return to idle happens in handle_phi2_falling().
|
||||
fn handle_data_driving(&mut self) {
|
||||
// Reset the data-driven flag before each drive attempt
|
||||
self.data_driven_this_cycle = false;
|
||||
|
||||
// Once we're in DriveData state, we should drive data regardless of current control signals
|
||||
// The control signals were already checked when we started memory operation
|
||||
if self.state_machine.is_full_address_ready() {
|
||||
|
|
@ -749,8 +803,18 @@ impl Intel4001 {
|
|||
let address = self.state_machine.get_full_address().unwrap_or(0) as u16;
|
||||
if (address as usize) < self.memory.len() {
|
||||
let data = self.memory[address as usize];
|
||||
self.write_data_bus(data);
|
||||
// Note: Don't call return_to_idle() here - wait for Φ2 falling edge
|
||||
// Intel 4004 data bus is 4 bits wide. Instructions are 8 bits.
|
||||
// M1 (data_nibble_phase=false): Drive high nibble (OPR, bits 7-4)
|
||||
// M2 (data_nibble_phase=true): Drive low nibble (OPA, bits 3-0)
|
||||
let nibble = if self.data_nibble_phase {
|
||||
data & 0x0F // OPA: low nibble
|
||||
} else {
|
||||
(data >> 4) & 0x0F // OPR: high nibble
|
||||
};
|
||||
self.write_data_bus(nibble);
|
||||
self.data_driven_this_cycle = true;
|
||||
// Do NOT return to idle here! The data must persist on the bus
|
||||
// for the CPU to read. Return to idle happens in handle_phi2_falling().
|
||||
} else {
|
||||
// Invalid address, tri-state
|
||||
self.tri_state_data_bus();
|
||||
|
|
@ -763,13 +827,13 @@ impl Intel4001 {
|
|||
|
||||
/// Helper function to process latency elapsed event
|
||||
fn process_latency_elapsed(&mut self) {
|
||||
let old_state = self.state_machine.get_state();
|
||||
let event = Intel400xEvent::LatencyElapsed;
|
||||
// Use the process_event_direct function to avoid borrowing conflicts
|
||||
let new_state = process_event_direct(&mut self.state_machine, event);
|
||||
|
||||
// Handle state entry/exit manually
|
||||
if new_state != self.state_machine.get_state() {
|
||||
self.on_state_exit(self.state_machine.get_state());
|
||||
if new_state != old_state {
|
||||
self.on_state_exit(old_state);
|
||||
self.state_machine.current_state = new_state;
|
||||
self.on_state_entry(new_state);
|
||||
}
|
||||
|
|
@ -777,42 +841,36 @@ impl Intel4001 {
|
|||
|
||||
/// Helper function to process chip select event
|
||||
fn process_chip_select(&mut self) {
|
||||
let old_state = self.state_machine.get_state();
|
||||
let event = Intel400xEvent::ChipSelect;
|
||||
// Use the process_event_direct function to avoid borrowing conflicts
|
||||
let new_state = process_event_direct(&mut self.state_machine, event);
|
||||
|
||||
// Handle state entry/exit manually
|
||||
if new_state != self.state_machine.get_state() {
|
||||
self.on_state_exit(self.state_machine.get_state());
|
||||
self.state_machine.current_state = new_state;
|
||||
if new_state != old_state {
|
||||
self.on_state_exit(old_state);
|
||||
self.on_state_entry(new_state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to process operation complete event
|
||||
fn process_operation_complete(&mut self) {
|
||||
let old_state = self.state_machine.get_state();
|
||||
let event = Intel400xEvent::OperationComplete;
|
||||
// Use the process_event_direct function to avoid borrowing conflicts
|
||||
let new_state = process_event_direct(&mut self.state_machine, event);
|
||||
|
||||
// Handle state entry/exit manually
|
||||
if new_state != self.state_machine.get_state() {
|
||||
self.on_state_exit(self.state_machine.get_state());
|
||||
self.state_machine.current_state = new_state;
|
||||
if new_state != old_state {
|
||||
self.on_state_exit(old_state);
|
||||
self.on_state_entry(new_state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to process address complete event
|
||||
fn process_address_complete(&mut self) {
|
||||
let old_state = self.state_machine.get_state();
|
||||
let event = Intel400xEvent::AddressComplete;
|
||||
// Use the process_event_direct function to avoid borrowing conflicts
|
||||
let new_state = process_event_direct(&mut self.state_machine, event);
|
||||
|
||||
// Handle state entry/exit manually
|
||||
if new_state != self.state_machine.get_state() {
|
||||
self.on_state_exit(self.state_machine.get_state());
|
||||
self.state_machine.current_state = new_state;
|
||||
if new_state != old_state {
|
||||
self.on_state_exit(old_state);
|
||||
self.on_state_entry(new_state);
|
||||
}
|
||||
}
|
||||
|
|
@ -867,21 +925,25 @@ impl Component for Intel4001 {
|
|||
}
|
||||
|
||||
// Process clock events through component-specific handlers
|
||||
// These handlers contain the actual logic for chip select, address latching, etc.
|
||||
if phi1_rising {
|
||||
self.handle_phi1_rising();
|
||||
// IMPORTANT ORDER: Handle falling edges BEFORE rising edges.
|
||||
// When PHI1 rises and PHI2 falls simultaneously (same half-cycle),
|
||||
// we must complete the previous operation (PHI2 falling) before
|
||||
// starting a new one (PHI1 rising). This ensures the ROM returns
|
||||
// to idle from OPA completion before starting a new address phase.
|
||||
if phi2_falling {
|
||||
self.handle_phi2_falling();
|
||||
}
|
||||
|
||||
if phi1_falling {
|
||||
self.handle_phi1_falling();
|
||||
}
|
||||
|
||||
if phi2_rising {
|
||||
self.handle_phi2_rising();
|
||||
if phi1_rising {
|
||||
self.handle_phi1_rising();
|
||||
}
|
||||
|
||||
if phi2_falling {
|
||||
self.handle_phi2_falling();
|
||||
if phi2_rising {
|
||||
self.handle_phi2_rising();
|
||||
}
|
||||
|
||||
// Note: execute_state_machine() is NOT called here because:
|
||||
|
|
@ -938,6 +1000,22 @@ impl RunnableComponent for Intel4001 {}
|
|||
|
||||
// Intel 4001 specific methods
|
||||
impl Intel4001 {
|
||||
/// Debug: get current state machine state name
|
||||
pub fn debug_state(&self) -> &'static str {
|
||||
match self.state_machine.get_state() {
|
||||
Intel400xUnifiedState::Idle => "Idle",
|
||||
Intel400xUnifiedState::AddressPhase => "AddrPhase",
|
||||
Intel400xUnifiedState::WaitLatency => "WaitLatency",
|
||||
Intel400xUnifiedState::DriveData => "DriveData",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug: get current data nibble phase
|
||||
pub fn debug_nibble_phase(&self) -> bool {
|
||||
self.data_nibble_phase
|
||||
}
|
||||
|
||||
/// Get ROM size in bytes
|
||||
/// Returns: Total number of bytes in ROM (256 for 4001)
|
||||
pub fn get_rom_size(&self) -> usize {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue