cpu/control/op_io.rs
1use std::ops::BitAnd;
2
3use base::bitselect::{BitPos, BitSelector, bit_select};
4use base::prelude::*;
5
6use tracing::{Level, event};
7
8use super::super::event::OutputEvent;
9use super::super::*;
10use super::alarm::{Alarm, AlarmDetails, Alarmer, BadMemOp, BugActivity};
11use super::alarmunit::AlarmUnit;
12use super::context::Context;
13use super::control::{
14 ControlRegisters, ControlUnit, DeviceManager, OpcodeResult, ProgramCounterChange, TrapCircuit,
15};
16use super::exchanger::exchanged_value_for_load;
17use super::io::{TransferFailed, Unit};
18use super::memory::{MemoryMapped, MemoryOpFailure, MemoryUnit, MetaBitChange};
19
20#[derive(Debug, PartialEq, Eq)]
21enum TransferOutcome {
22 /// Indicates a successful transfer operation.
23 Success {
24 /// Indicates that the memory location's meta bit was set.
25 /// This allows the trap circuit to be triggered if necessary.
26 metabit_was_set: bool,
27
28 /// Describes the output (if any) generated by this
29 /// instruction.
30 output: Option<OutputEvent>,
31 },
32
33 /// When the outcome of the TSD is dismiss and wait, we don't
34 /// trigger the trap circuit (not because we think the TX-2 behaved
35 /// this way, but because it keeps the code simpler and we don't
36 /// know if the oposite behaviour is needed).
37 DismissAndWait,
38}
39
40type OpConversion = fn(Address) -> BadMemOp;
41
42fn bad_write(addr: Address) -> BadMemOp {
43 BadMemOp::Write(addr.into())
44}
45
46impl ControlUnit {
47 /// OPR: Implements the IOS opcode and the AOP opcode.
48 ///
49 /// Bits 2.7 and 2.8 of the N register (i.e. the instruction word)
50 /// distinguish between IOS and AOP (which together are known as
51 /// OPR). This decoding is described in section 7-11
52 /// ("MISCELLANEOUS OPERATION CODES") in Volume 1 of the Technical
53 /// Manual. Neither of these insructions are indexable or
54 /// configurable (probably because they don't perform a memory
55 /// access).
56 pub(crate) fn op_opr(
57 &mut self,
58 ctx: &Context,
59 mem: &mut MemoryUnit,
60 devices: &mut DeviceManager,
61 ) -> Result<OpcodeResult, Alarm> {
62 // Perform the decoding described in section 7-11
63 // ("MISCELLANEOUS OPERATION CODES") of Volume 1 of the TX-2
64 // Technical Manual.
65 //
66 // The decoding chart there is
67 //
68 // N2.8 | N2.7 | OPR
69 // 0 | 0 | IOS
70 // 0 | 1 | AOP
71 // 1 | 0 | undefined
72 // 1 | 1 | undefined
73 //
74 // Section 10-2.5.3 ("OPERATION CODE ALARM") of the TX-2
75 // Technical Manual (March 1961) states that if bits 2.6..2.1
76 // of an AOP instruction specify an invalid opcode, OCSAL is
77 // raised. But that text doesn't cover the case where
78 // N2.8==1, since that isn't the AOP case.
79 let b7: bool = bit_select(
80 self.regs.n.bits(),
81 BitSelector {
82 quarter: Quarter::Q2,
83 bitpos: BitPos::B7,
84 },
85 );
86 if bit_select(
87 self.regs.n.bits(),
88 BitSelector {
89 quarter: Quarter::Q2,
90 bitpos: BitPos::B8,
91 },
92 ) {
93 // This is the "undefined" case from the table above. The
94 // documentation doesn't explicitly state that OCSAL
95 // should be raised for this case, but it is a reasonably
96 // safe interpretation. If we find that some piece of
97 // software expects a different result, we can look at
98 // this again.
99 Err(self.invalid_opr_subcode(b7))
100 } else {
101 // This opcode is either IOS (00) or AOP (01).
102 if b7 {
103 // It's AOP. AOP isn't implemented because we have
104 // not found any information about what its opcodes
105 // are, or what they do.
106 let trailing_digit: char = if b7 { '1' } else { '0' };
107 Err(self.alarm_unit.always_fire(Alarm {
108 sequence: self.regs.k,
109 details: AlarmDetails::ROUNDTUITAL {
110 explanation: format!(
111 "The AOP instruction (opcode 04 with subcode 0{trailing_digit}) is not yet implemented.",
112 ),
113 bug_report_url: "https://github.com/TX-2/TX-2-simulator/issues/148",
114 },
115 }, &self.regs.diagnostic_only))
116 } else {
117 self.op_ios(ctx, mem, devices)
118 }
119 }
120 }
121
122 fn op_ios(
123 &mut self,
124 ctx: &Context,
125 mem: &mut MemoryUnit,
126 devices: &mut DeviceManager,
127 ) -> Result<OpcodeResult, Alarm> {
128 let j = self.regs.n.index_address();
129 let cf = self.regs.n.configuration();
130
131 if cf & 1 != 0 {
132 // Setting the report bit in the configuration value
133 // causes the device's status word from before any mode
134 // change to be copied into the E register (as stated in
135 // section 4-3.6 of the User Handbook).
136 //
137 // Note that this means that the current sequence and the
138 // sequence for which we are generating a report word are
139 // different (in general).
140 let flag_raised: bool = self.regs.flags.current_flag_state(&j);
141 mem.set_e_register(devices.report(
142 ctx,
143 self.regs.k,
144 j,
145 flag_raised,
146 &mut self.alarm_unit,
147 &self.regs.diagnostic_only,
148 )?);
149 }
150 let mut dismiss_reason: Option<&str> = if cf & 0o20 != 0 {
151 Some("dismiss bit set in config")
152 } else {
153 None
154 };
155
156 let operand = self.regs.n.operand_address_and_defer_bit();
157 let result = match u32::from(operand) {
158 0o20_000 => {
159 devices.disconnect(ctx, &j, &mut self.alarm_unit, &self.regs.diagnostic_only)
160 }
161 0o30_000..=0o37_777 => {
162 let mode: Unsigned12Bit = Unsigned12Bit::try_from(operand & 0o07_777).unwrap();
163 ControlUnit::connect_unit(
164 ctx,
165 devices,
166 &mut self.regs,
167 &mut self.trap,
168 j,
169 mode,
170 &mut self.alarm_unit,
171 )
172 }
173 0o40_000 => {
174 self.regs.flags.lower(&j);
175 if self.regs.k == Some(j) {
176 // J is the current sequence, so the flag is
177 // lowered but don't perform a drop-out (see User
178 // Handbook page 4-7).
179 self.regs.current_sequence_is_runnable = true;
180 }
181 Ok(())
182 }
183 0o50_000 => {
184 self.regs.flags.raise(&j);
185 if Some(j) == self.regs.k {
186 dismiss_reason = None;
187 }
188 Ok(())
189 }
190 0o60_000..=0o60777 => {
191 // Select unit XXX
192 Err(self.alarm_unit.always_fire(Alarm {
193 sequence: self.regs.k,
194 details: AlarmDetails::ROUNDTUITAL {
195 explanation: format!(
196 "IOS operand {operand:o}: Select Unit command is not yet implemented.",
197 ),
198 bug_report_url: "https://github.com/TX-2/TX-2-simulator/issues/139",
199 },
200 }, &self.regs.diagnostic_only))
201 }
202 _ => {
203 let command: u8 = (u32::from(operand) >> 12) as u8;
204 self.alarm_unit.fire_if_not_masked(Alarm {
205 sequence: self.regs.k,
206 details: AlarmDetails::IOSAL {
207 unit: j,
208 operand: Some(operand),
209 message: format!(
210 "IOS operand {operand:o} has unrecognised leading command digit {command:o}",
211 ),
212 },
213 }, &self.regs.diagnostic_only)?;
214 // IOSAL is masked. Just do nothing.
215 Ok(())
216 }
217 };
218 if let Some(reason) = dismiss_reason {
219 self.dismiss_unless_held(reason);
220 }
221 result.map(|()| OpcodeResult {
222 program_counter_change: None,
223 // poll_order_change doesn't always need to be set, but
224 // false positives cost us only compute efficiency.
225 poll_order_change: Some(j),
226 output: None,
227 })
228 }
229
230 fn connect_unit(
231 ctx: &Context,
232 devices: &mut DeviceManager,
233 regs: &mut ControlRegisters,
234 trap: &mut TrapCircuit,
235 unit: Unsigned6Bit,
236 mode: Unsigned12Bit,
237 alarm_unit: &mut AlarmUnit,
238 ) -> Result<(), Alarm> {
239 let maybe_flag_change: Option<FlagChange> = match u8::from(unit) {
240 0o42 => {
241 trap.connect(ctx, mode);
242 None
243 }
244 _ => devices.connect(ctx, regs.k, &unit, mode, alarm_unit, ®s.diagnostic_only)?,
245 };
246 match maybe_flag_change {
247 Some(FlagChange::Raise(reason)) => {
248 event!(Level::DEBUG, "unit {unit:o} raised its flag: {reason}");
249 regs.flags.raise(&unit);
250 }
251 None => (),
252 }
253 Ok(())
254 }
255
256 pub(crate) fn op_tsd(
257 &mut self,
258 ctx: &Context,
259 devices: &mut DeviceManager,
260 execution_address: Address,
261 mem: &mut MemoryUnit,
262 ) -> Result<OpcodeResult, Alarm> {
263 fn make_tsd_qsal(seq: Option<SequenceNumber>, inst: Instruction, op: BadMemOp) -> Alarm {
264 Alarm {
265 sequence: seq,
266 details: AlarmDetails::QSAL(inst, op, "TSD address is not mapped".to_string()),
267 }
268 }
269
270 let result: Result<TransferOutcome, Alarm> = if let Some(unit) = self.regs.k {
271 let target: Address = self.operand_address_with_optional_defer_and_index(ctx, mem)?;
272 let not_mapped = |op_conv: OpConversion| -> Alarm {
273 let op: BadMemOp = op_conv(target);
274 make_tsd_qsal(self.regs.k, self.regs.n, op)
275 };
276
277 let meta_op: MetaBitChange = if self.trap.set_metabits_of_operands() {
278 MetaBitChange::Set
279 } else {
280 MetaBitChange::None
281 };
282 // There are no sequence numbers below 0o40, besides 0.
283 if matches!(u8::from(unit), 0 | 0o75 | 0o76) {
284 // Non-INOUT sequences just cycle the target location;
285 // see section 4-1 (page 4-3) of the Users Handbook;
286 // also pages 4-2 and 4-9).
287 match mem.cycle_full_word_for_tsd(ctx, &target) {
288 Ok(extra_bits) => Ok(TransferOutcome::Success {
289 metabit_was_set: extra_bits.meta,
290 output: None,
291 }),
292 Err(MemoryOpFailure::ReadOnly(_address, extra_bits)) => {
293 // The read-only case is not an error, it's
294 // normal. The TSD instruction simply has no
295 // effect when the target address is
296 // read-only.
297 // TODO: should there be an effect on the E register?
298 Ok(TransferOutcome::Success {
299 metabit_was_set: extra_bits.meta,
300 output: None, // not an INOUT unit anyway
301 })
302 }
303 Err(MemoryOpFailure::NotMapped(_)) => {
304 self.alarm_unit.fire_if_not_masked(
305 not_mapped(bad_write),
306 &self.regs.diagnostic_only,
307 )?;
308 // QSAL is masked, carry on.
309 Ok(TransferOutcome::Success {
310 metabit_was_set: false, // act as if metabit unset
311 output: None,
312 })
313 }
314 }
315 } else {
316 devices.mark_device_changed(unit);
317 match devices.get_mut(&unit) {
318 None => {
319 event!(Level::WARN, "TSD on unknown unit {:o}", unit);
320 Ok(TransferOutcome::DismissAndWait)
321 }
322 Some(device) => {
323 let is_input_unit = device.is_input_unit;
324 if !device.connected {
325 // If the unit is not connected, perform
326 // dismiss and wait. This requirement is
327 // described in section 4-3.7 of the Users
328 // Handbook.
329 Ok(TransferOutcome::DismissAndWait)
330 } else if device.in_maintenance {
331 event!(
332 Level::WARN,
333 "TSD on unit {:o}, but it is in maintenance",
334 unit
335 );
336 Ok(TransferOutcome::DismissAndWait)
337 } else {
338 // We're actually going to do the (input or output) transfer.
339 // First load into the M register the existing contents of
340 // memory.
341 let transfer_mode = device
342 .transfer_mode(&mut self.alarm_unit, &self.regs.diagnostic_only)?;
343 let (m_register, extra_bits) = self
344 .fetch_operand_from_address_without_exchange(
345 ctx,
346 mem,
347 &target,
348 &UpdateE::No,
349 )?;
350 if is_input_unit {
351 // In read operations, data is transferred
352 // from the I/O device's buffer over the
353 // IOBM bus, into the E register. See
354 // figure 15-18 in Volume 2 of the TX-2
355 // Techical Manual.
356 match device.read(
357 ctx,
358 &mut self.alarm_unit,
359 &self.regs.diagnostic_only,
360 ) {
361 Ok(masked_word) => {
362 const UPDATE_E_YES: UpdateE = UpdateE::Yes;
363 let newval: Unsigned36Bit =
364 masked_word.apply(Unsigned36Bit::ZERO);
365 match transfer_mode {
366 TransferMode::Assembly => {
367 let bits: Unsigned6Bit =
368 newval.bitand(Unsigned6Bit::MAX);
369 self.memory_store_without_exchange(
370 ctx,
371 mem,
372 &target,
373 &cycle_and_splay(m_register, bits),
374 &UPDATE_E_YES,
375 &meta_op,
376 )?;
377 }
378 TransferMode::Exchange => {
379 self.memory_store_with_exchange(
380 ctx,
381 mem,
382 &target,
383 &newval,
384 &m_register,
385 &UPDATE_E_YES,
386 &meta_op,
387 )?;
388 }
389 }
390 Ok(TransferOutcome::Success {
391 metabit_was_set: extra_bits.meta,
392 output: None, // because this is a read unit.
393 })
394 }
395 Err(TransferFailed::BufferNotFree) => {
396 Ok(TransferOutcome::DismissAndWait)
397 }
398 Err(TransferFailed::Alarm(alarm)) => {
399 return Err(alarm);
400 }
401 }
402 } else {
403 // In write operations, data is
404 // transferred from the E register to
405 // the I/O device over the E bus. See
406 // figure 15-17 in Volume 2 of the
407 // TX-2 Techical Manual.
408 mem.set_e_register(exchanged_value_for_load(
409 &self.get_config(),
410 &m_register,
411 &mem.get_e_register(),
412 ));
413 match transfer_mode {
414 TransferMode::Exchange => {
415 match device.write(ctx, mem.get_e_register(), &self.regs.diagnostic_only) {
416 Err(TransferFailed::BufferNotFree) => {
417 Ok(TransferOutcome::DismissAndWait)
418 }
419 Err(TransferFailed::Alarm(alarm)) => Err(alarm),
420 Ok(maybe_output) => Ok(TransferOutcome::Success {
421 metabit_was_set: extra_bits.meta,
422 output: maybe_output,
423 }),
424 }
425 }
426 TransferMode::Assembly => Err(self
427 .alarm_unit
428 .always_fire(Alarm {
429 sequence: self.regs.k,
430 details: AlarmDetails::ROUNDTUITAL{
431 explanation:
432 "TSD output in assembly mode is not yet implemented."
433 .to_string(),
434 bug_report_url: "https://github.com/TX-2/TX-2-simulator/issues/140",
435 },
436 }, &self.regs.diagnostic_only)),
437 }
438 }
439 }
440 }
441 }
442 }
443 } else {
444 Err(self.alarm_unit.always_fire(Alarm {
445 sequence: self.regs.k,
446 details: AlarmDetails::BUGAL {
447 activity: BugActivity::Opcode,
448 diagnostics: self.regs.diagnostic_only.clone(),
449 message: "Executed TSD instruction while the K register is None (i.e. there is no current sequence)".to_string(),
450 }}, &self.regs.diagnostic_only))
451 };
452 match result {
453 Ok(TransferOutcome::Success {
454 metabit_was_set,
455 output,
456 }) => {
457 if metabit_was_set && self.trap.trap_on_operand() {
458 self.raise_trap();
459 }
460 Ok(OpcodeResult {
461 program_counter_change: None,
462 poll_order_change: self.regs.k,
463 output,
464 })
465 }
466 Ok(TransferOutcome::DismissAndWait) => {
467 // In the dismiss and wait case, the
468 // sequence is dismissed even if the hold
469 // bit is set (Users Handbook, section
470 // 4-3.2). The hold bit only governs what
471 // happens following the completion of an
472 // instruction.
473 self.dismiss("TSD while data was not ready caused dismiss-and-wait");
474 Ok(OpcodeResult {
475 program_counter_change: Some(ProgramCounterChange::DismissAndWait(
476 execution_address,
477 )),
478 poll_order_change: self.regs.k,
479 output: None,
480 })
481 }
482 Err(e) => Err(e),
483 }
484 }
485}