cpu/io/
dev_petr.rs

1//! Photoelectric Paper tape reader, unit 52
2//!
3//! The paper tape reader has 7 channels.
4//!
5//! Tape movement can occur in either of two directions, in the bin
6//! direction and in the reel direction.  Tape reads occur in the
7//! "reel" direction.  The modes of operation of the motor in the
8//! "bin" direction both stop and reverse direction when the END MARK
9//! is detected.  In other words, this is analogous to rewinding to the
10//! beginning-of-file in preparation for reading the file.
11//!
12//! The reader can read both strip tape and reel tape, and does so at
13//! different speeds (because of the different levels of rubustness of
14//! the two types of tape), but we operate as if all tapes are read
15//! at the same speed.
16//!
17//! We do not currently simulate acceleration/deceleration of the
18//! tape.  On start, reading begins immediately at full speed, and on
19//! stop, the tape movement stops immediately.  This will likely
20//! change in the future.
21//!
22//! Lincoln Lab Group Report 51-8 "The Lincoln Writer" states (page
23//! 13) that the "END MARK" punched by the LW is the 6-bit code octal
24//! 72 without a 7th hole punched on the tape.
25use std::fmt::Write;
26use std::fmt::{self, Debug, Display, Formatter};
27use std::time::Duration;
28
29use base::prelude::*;
30use std::cmp;
31
32use conv::*;
33use tracing::{Level, event};
34
35use crate::diagnostics::CurrentInstructionDiagnostics;
36
37use super::*;
38use super::{TransferFailed, Unit, UnitStatus};
39
40/// Is the tape motor running?
41#[derive(Debug, Clone, Copy, Eq, PartialEq)]
42enum Activity {
43    Stopped,
44    Started,
45}
46
47impl Display for Activity {
48    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
49        f.write_str(match self {
50            Activity::Stopped => "stopped",
51            Activity::Started => "running",
52        })
53    }
54}
55
56#[derive(Debug, Clone, Copy, Eq, PartialEq)]
57enum Direction {
58    // Capstan drive is running in the bin direction (as in
59    // immediately after IOS 30104, before END MARK has been read).
60    Bin,
61
62    // Capstan drive is running in the reel direction (as in after IOS
63    // 30104, after END MARK has been read).
64    Reel,
65}
66
67impl Display for Direction {
68    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
69        f.write_str(match self {
70            Direction::Bin => "bin",
71            Direction::Reel => "reel",
72        })
73    }
74}
75
76fn next_line_time(direction: Direction, system_time: &Duration) -> Duration {
77    // The reader reads at between 400 and 2500 lines per second.
78    //
79    // We don't want to avoidably generate missing data alarms, so
80    // for now, we simulate data reading at the slowest speed and
81    // non-reading tape movements at the highest speed.
82    *system_time
83        + match direction {
84            Direction::Bin => {
85                // At 2500 lines per second, the interval between
86                // lines is 1s / 2500 = 400 microseconds.
87                Duration::from_micros(400)
88            }
89            Direction::Reel => {
90                // At 400 lines per second, the interval between lines
91                // is 1s / 400 = 2500 microseconds.
92                Duration::from_micros(2500)
93            }
94        }
95}
96
97pub(crate) struct Petr {
98    // Activity and direction cannot just be left encoded in mode,
99    // because we need to be able to start/stop the motor and change
100    // direction without changing the mode value (since the programmer
101    // controls that).
102    activity: Activity,
103    direction: Direction,
104    tape_data: Vec<u8>,
105    tape_pos: usize,
106    data: Option<u8>,
107    already_warned_eof: bool,
108    read_failed: bool,
109    overrun: bool,
110    time_of_next_read: Option<Duration>,
111    connected_at_system_time: Option<Duration>,
112    connected_at_elapsed_time: Option<Duration>,
113    rewind_line_counter: usize,
114    mode: Unsigned12Bit,
115}
116
117impl Debug for Petr {
118    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
119        f.debug_struct("Petr")
120            .field("activity", &self.activity)
121            .field("direction", &self.direction)
122            .field("tape_data", &self.tape_data)
123            .field("tape_pos", &self.tape_pos)
124            .field("data", &self.data)
125            .field("already_warned_eof", &self.already_warned_eof)
126            .field("read_failed", &self.read_failed)
127            .field("overrun", &self.overrun)
128            .field("time_of_next_read", &self.time_of_next_read)
129            .field("connected_at_system_time", &self.connected_at_system_time)
130            .field("connected_at_elapsed_time", &self.connected_at_elapsed_time)
131            .field("rewind_line_counter", &self.rewind_line_counter)
132            .field("mode", &self.mode)
133            .finish_non_exhaustive()
134    }
135}
136
137impl Petr {
138    pub(crate) fn new() -> Petr {
139        Petr {
140            activity: Activity::Stopped,
141            direction: Direction::Bin,
142            tape_data: Vec::new(),
143            tape_pos: 0,
144            data: None,
145            already_warned_eof: false,
146            read_failed: false,
147            overrun: false,
148            time_of_next_read: None,
149            connected_at_system_time: None,
150            connected_at_elapsed_time: None,
151            rewind_line_counter: 0,
152            mode: Unsigned12Bit::ZERO,
153        }
154    }
155
156    fn next_poll_time(&mut self, system_time: &Duration) -> Duration {
157        match self.time_of_next_read {
158            Some(t) => {
159                let result = cmp::max(t, *system_time);
160                if let Some(interval) = result.checked_sub(*system_time) {
161                    event!(
162                        Level::DEBUG,
163                        "next_poll_time: please poll after {interval:?} at system time {t:?}"
164                    );
165                }
166                result
167            }
168            None => {
169                let interval = Duration::from_secs(1);
170                let next = *system_time + interval;
171                event!(
172                    Level::DEBUG,
173                    "next_poll_time: time_of_next_read is None, using default {system_time:?}+{interval:?}={:?}; self={:?}",
174                    next,
175                    &self
176                );
177                next
178            }
179        }
180    }
181
182    fn do_rewind(&mut self) {
183        match self.rewind_line_counter.checked_sub(1) {
184            None | Some(0) => {
185                // We reached - or were already at - the END MARK,
186                // reverse direction.
187                event!(Level::INFO, "reached the end mark, reversing direction");
188                assert!(self.direction == Direction::Bin);
189                self.direction = Direction::Reel;
190            }
191            Some(reduced_by_1) => {
192                self.rewind_line_counter = reduced_by_1;
193                event!(
194                    Level::DEBUG,
195                    "rewound over a line: {} more to go",
196                    self.rewind_line_counter
197                );
198            }
199        }
200    }
201
202    fn do_read(&mut self) {
203        // A line of the simulated tape should have appeared under the
204        // read head.
205        match self.tape_data.get(self.tape_pos) {
206            None => {
207                // At EOF.  We don't stop the motor, but if the
208                // real PETR device can detect when the whole tape
209                // has already passed through, perhaps we should.
210                if self.already_warned_eof {
211                    event!(
212                        Level::DEBUG,
213                        "reading again at end-of-file at position {}",
214                        self.tape_pos
215                    );
216                } else {
217                    self.already_warned_eof = true;
218                    event!(
219                        Level::WARN,
220                        "end-of-file on tape input file at position {}",
221                        self.tape_pos
222                    );
223                }
224                self.time_of_next_read = None;
225            }
226            Some(byte) => {
227                event!(
228                    Level::DEBUG,
229                    "read a byte at file position {}: {:?}",
230                    self.tape_pos,
231                    byte
232                );
233                self.already_warned_eof = false;
234                if !self.overrun {
235                    self.overrun = self.data.is_some();
236                    if self.overrun {
237                        event!(Level::WARN, "PETR input overrun");
238                    }
239                }
240                self.data = Some(*byte);
241                self.tape_pos += 1;
242            }
243        }
244    }
245
246    fn maybe_simulate_event(&mut self, system_time: &Duration) {
247        event!(
248            Level::TRACE,
249            "maybe_simulate_event: activity={:?}",
250            &self.activity
251        );
252        match self.activity {
253            Activity::Started => {
254                if let Some(t) = self.time_of_next_read
255                    && t > *system_time
256                {
257                    // The next line has not yet appeared under the read
258                    // head.
259                    let to_wait = || t - *system_time;
260                    event!(
261                        Level::TRACE,
262                        "motor running ({}) but next line will not be read for {:?} yet",
263                        self.direction,
264                        to_wait()
265                    );
266                    return;
267                }
268                match self.direction {
269                    Direction::Bin => {
270                        event!(Level::TRACE, "motor running, doing rewind action");
271                        self.do_rewind();
272                    }
273                    Direction::Reel => {
274                        event!(Level::TRACE, "motor running, doing read action");
275                        self.do_read();
276                    }
277                }
278                // do_read may have stopped the motor, so take account
279                // of that.
280                self.time_of_next_read = match self.activity {
281                    Activity::Started => Some(next_line_time(self.direction, system_time)),
282                    Activity::Stopped => None,
283                }
284            }
285            Activity::Stopped => {
286                // The motor is not running.  So no events (of lines
287                // passing under the photodetector) will happen.
288                event!(Level::TRACE, "motor stopped, nothing more to simulate");
289                self.time_of_next_read = None;
290            }
291        }
292    }
293
294    fn transfer_mode(&self) -> TransferMode {
295        if self.mode & 0o02 != 0 {
296            TransferMode::Assembly
297        } else {
298            TransferMode::Exchange
299        }
300    }
301}
302
303struct Throughput {
304    lines_per_second: f64,
305    total_seconds: f64,
306}
307
308fn compute_throughput(
309    pos: usize,
310    connect_time: Option<Duration>,
311    now: Duration,
312) -> Option<Throughput> {
313    if let Some(connected_at) = connect_time
314        && let Some(elapsed) = now.checked_sub(connected_at)
315    {
316        let elapsed = elapsed.as_secs_f64();
317
318        // It seems that in a WASM build, f64::value_from(usize)
319        // is irrefutable.  But clippy doesn't warn about it in
320        // non-WASM builds because it's not always irrefutable.
321        #[allow(irrefutable_let_patterns)]
322        if let Ok(n) = f64::value_from(pos) {
323            let throughput = n / elapsed;
324            return Some(Throughput {
325                lines_per_second: throughput,
326                total_seconds: elapsed,
327            });
328        }
329    }
330    None
331}
332
333impl Unit for Petr {
334    fn poll(&mut self, ctx: &Context) -> UnitStatus {
335        let system_time = &ctx.simulated_time;
336        event!(Level::TRACE, "poll called at system time  {system_time:?}");
337
338        self.maybe_simulate_event(system_time);
339        let data_ready: bool = self.data.is_some();
340        let poll_after = self.next_poll_time(system_time);
341        event!(Level::TRACE, "PETR poll: poll_after={poll_after:?}");
342        UnitStatus {
343            special: Unsigned12Bit::ZERO,
344            change_flag: if data_ready {
345                Some(FlagChange::Raise("data is ready"))
346            } else {
347                None
348            },
349            buffer_available_to_cpu: data_ready,
350            inability: self.read_failed,
351            missed_data: self.overrun,
352            mode: self.mode,
353            poll_after,
354            is_input_unit: true,
355        }
356    }
357
358    fn connect(&mut self, ctx: &Context, mode: Unsigned12Bit) {
359        let system_time = &ctx.simulated_time;
360        self.connected_at_elapsed_time = Some(ctx.real_elapsed_time);
361        self.connected_at_system_time = Some(ctx.simulated_time);
362        self.direction = if mode & 0o04 != 0 {
363            Direction::Bin
364        } else {
365            Direction::Reel
366        };
367        self.activity = if mode & 0o100 != 0 {
368            self.time_of_next_read = Some(next_line_time(self.direction, system_time));
369            Activity::Started
370        } else {
371            // While the motor is not running, no data will arrive.
372            self.time_of_next_read = None;
373            Activity::Stopped
374        };
375        self.tape_pos = 0;
376        self.mode = mode;
377        let transfer_mode_name = match self.transfer_mode() {
378            TransferMode::Assembly => "assembly",
379            TransferMode::Exchange => "exchange",
380        };
381        event!(
382            Level::INFO,
383            "PETR connected; motor {}, direction {}; {} mode {:o} (time_of_next_read={:?})",
384            self.activity,
385            self.direction,
386            transfer_mode_name,
387            self.mode,
388            self.time_of_next_read,
389        );
390    }
391
392    fn transfer_mode(&self) -> TransferMode {
393        self.transfer_mode()
394    }
395
396    fn read(
397        &mut self,
398        ctx: &Context,
399        _diagnostics: &CurrentInstructionDiagnostics,
400    ) -> Result<MaskedWord, TransferFailed> {
401        let system_time = &ctx.simulated_time;
402        match self.data.take() {
403            None => {
404                event!(Level::DEBUG, "no data is ready yet");
405                Err(TransferFailed::BufferNotFree)
406            }
407            Some(byte) => {
408                // This calculation of time_of_next_read is not right,
409                // as the interval is between physical paper-tape
410                // lines, not between I/O instrucitons.
411                self.time_of_next_read = Some(next_line_time(self.direction, system_time));
412                event!(Level::DEBUG, "read value {:03o}", byte & 0o77);
413                Ok(MaskedWord {
414                    bits: Unsigned36Bit::from(byte & 0o77),
415                    mask: u36!(0o77),
416                })
417            }
418        }
419    }
420
421    fn write(
422        &mut self,
423        _ctx: &Context,
424        _source: Unsigned36Bit,
425        diagnostics: &CurrentInstructionDiagnostics,
426    ) -> Result<Option<OutputEvent>, TransferFailed> {
427        unreachable!("attempted to write to the paper tape reader ({diagnostics})")
428    }
429
430    fn name(&self) -> String {
431        "PETR photoelectric paper tape reader".to_string()
432    }
433
434    fn disconnect(&mut self, _ctx: &Context) {
435        event!(Level::INFO, "PETR disconnecting");
436        self.activity = Activity::Stopped;
437    }
438
439    fn on_input_event(
440        &mut self,
441        _ctx: &Context,
442        event: InputEvent,
443    ) -> Result<InputFlagRaised, InputEventError> {
444        if let InputEvent::PetrMountPaperTape { data } = event {
445            event!(Level::DEBUG, "Mounting a tape ({} bytes)", data.len());
446            self.tape_data = data;
447            // The input flag is raised only when input is actually
448            // available.  That is, when a line passes under the
449            // detector.
450            Ok(InputFlagRaised::No)
451        } else {
452            Err(InputEventError::InputEventNotValidForDevice)
453        }
454    }
455
456    fn text_info(&self, ctx: &Context) -> String {
457        let build = || -> Result<String, std::fmt::Error> {
458            let mut result = String::new();
459            write!(result, "Motor {}", self.activity)?;
460            match self.activity {
461                Activity::Started => {
462                    write!(result, " (direction {})", self.direction)?;
463                }
464                Activity::Stopped => (),
465            }
466            write!(result, ". ")?;
467            if self.tape_data.is_empty() {
468                write!(result, "No tape (or blank tape) loaded. ")?;
469            } else {
470                write!(
471                    result,
472                    "Loaded tape has {} lines, we have read {}. ",
473                    self.tape_data.len(),
474                    self.tape_pos
475                )?;
476                if let Some(throughput) = compute_throughput(
477                    self.tape_pos,
478                    self.connected_at_system_time,
479                    ctx.simulated_time,
480                ) {
481                    write!(
482                        result,
483                        "Emulated duration {:.1} seconds, so emulated throughput is {:.1} lines/sec. ",
484                        throughput.total_seconds, throughput.lines_per_second,
485                    )?;
486                }
487                if let Some(throughput) = compute_throughput(
488                    self.tape_pos,
489                    self.connected_at_elapsed_time,
490                    ctx.real_elapsed_time,
491                ) {
492                    write!(
493                        result,
494                        "Real duration {:.1} seconds, so real throughput is {:.1} lines/sec.",
495                        throughput.total_seconds, throughput.lines_per_second,
496                    )?;
497                }
498            }
499            Ok(result)
500        };
501        build().expect("write! calls on a String should always succeed")
502    }
503}