spectrusty_peripherals/zxprinter.rs
1/*
2 Copyright (C) 2020-2022 Rafal Michalski
3
4 This file is part of SPECTRUSTY, a Rust library for building emulators.
5
6 For the full copyright notice, see the lib.rs file.
7*/
8//! An emulator of the family of printers: **ZX Printer** / **Alphacom 32** / **Timex TS2040**.
9//!
10//! **ZX Printer** is an extremely compact, 32 column printer which uses an aluminum-coated paper.
11//! The printed image is being burned onto the surface of the paper by two metal pins that travel
12//! across the paper. A voltage is passed through these pins which cause a spark to be produced,
13//! leaving a black-ish (or blue-ish) dot.
14//!
15//! The Alphacom 32 is compatible with software for the ZX Printer but uses thermal paper instead
16//! of the ZX Printer's metalized paper.
17//!
18//! ZX Printer used special 4" (100 mm) wide black paper which was supplied coated with a thin layer of aluminum.
19//!
20//! Alphacom 32 printer's thermographic paper has 4.33" (110 mm) x 1.9" (48 mm) diameter.
21//!
22//! TS2040 was a branded version of Alphacom 32 distributed by Timex to the US.
23//!
24//! See [Printers](https://www.worldofspectrum.org/faq/reference/peripherals.htm#Printers).
25use core::fmt::Debug;
26use core::ops::{Deref, DerefMut};
27
28#[cfg(feature = "snapshot")]
29use serde::{Serialize, Deserialize};
30
31use spectrusty_core::clock::{FTs, TimestampOps};
32
33/// A number of printed dots in each line.
34pub const DOTS_PER_LINE: u32 = 256;
35/// A maximum number of bytes in each line.
36pub const BYTES_PER_LINE: u32 = 32;
37
38/// An interface to the **ZX Printer** spooler.
39///
40/// An implementation of this trait must be provided in order to complete the emulation of [ZxPrinterDevice].
41///
42/// Both [Debug] and [Default] traits also need to be implemented.
43pub trait Spooler: Debug + Default {
44 /// Can be implemented to get the notification that the printing will begin shortly.
45 fn motor_on(&mut self) {}
46 /// Can be implemented to get the notification that the printing has ended.
47 fn motor_off(&mut self) {}
48 /// After each printed line this method is being called with a reference to a slice containing
49 /// information about up to 256 printed dots stored in up to 32 bytes.
50 ///
51 /// 8 dots are encoded from the most significant to the least significant bit in each byte.
52 /// So the leftmost dot is encoded in the bit 7 of the first byte in a given slice and the
53 /// rightmost dot is encoded in the bit 0 of the last byte.
54 ///
55 /// A value of 1 in each bit signifies the presence of a dot, 0 means there is no dot.
56 ///
57 /// A received slice will never be larger than 32 bytes. If a user presses a `BREAK` key during
58 /// `COPY`, the slice may be smaller than 32 bytes but it will never be empty.
59 fn push_line(&mut self, line: &[u8]);
60}
61
62/// A simple **ZX Printer** spooler that outputs each line to the stdout as hexadecimal digits.
63#[derive(Clone, Copy, Default, Debug)]
64#[cfg_attr(feature = "snapshot", derive(Serialize, Deserialize))]
65pub struct DebugSpooler;
66
67/// This type implements a communication protocol of the **ZX Printer**.
68///
69/// A data port is being used with the following bits for reading:
70///
71/// * bit 6 is 0 if the printer is there, 1 if it is not, and is used solely to check if the printer
72/// is connected.
73/// * bit 0 is 1 if the printer is ready for the next bit or 0 if it's busy printing.
74/// * bit 7 is 1 if the printer is ready for the new line or 0 if it's not there yet.
75///
76/// and for writing:
77///
78/// * bit 7 - a value of 1 activates a stylus, a dot will be printed in this instance.
79/// When a 0 is written, the stylus is being deactivated.
80/// * bit 2 - a value of 1 stops the printer motor and when a 0 is written it starts
81/// the printer motor.
82/// * bit 1 - a value of 1 slows the printer motor, a value of 0 sets a normal motor speed.
83///
84/// An implementation of a [Spooler] trait is required as a generic parameter `S` to complete
85/// this type.
86///
87/// There's also a dedicated [bus device][crate::bus::zxprinter::ZxPrinterBusDevice]
88/// implementation to be used solely with the `ZxPrinterDevice`.
89#[derive(Clone, Debug)]
90#[cfg_attr(feature = "snapshot", derive(Serialize, Deserialize))]
91#[cfg_attr(feature = "snapshot", serde(rename_all="camelCase"))]
92pub struct ZxPrinterDevice<T, S> {
93 /// An instance of the [Spooler] trait implementation type.
94 #[cfg_attr(feature = "snapshot", serde(skip))]
95 pub spooler: S,
96 /// Can be changed to adjust speed. The default is 855 T-states (~16 dot lines / sec.).
97 pub bit_delay: u16,
98 motor: bool,
99 ready: bool,
100 cursor: u8,
101 ready_ts: T,
102 line: [u8;32]
103}
104
105const STATUS_OFF: u8 = 0b0011_1110;
106const STATUS_NEW_LINE: u8 = 0b1011_1111;
107const STATUS_BIT_READY: u8 = 0b0011_1111;
108const STATUS_NOT_READY: u8 = 0b0011_1110;
109
110const STYLUS_MASK: u8 = 0b1000_0000;
111const MOTOR_MASK: u8 = 0b0000_0100;
112const SLOW_MASK: u8 = 0b0000_0010;
113
114// the approximate speed of Alphacom 32
115const BIT_DELAY: u16 = 855;
116
117impl<T: Default, S: Default> Default for ZxPrinterDevice<T, S> {
118 fn default() -> Self {
119 ZxPrinterDevice {
120 spooler: S::default(),
121 bit_delay: BIT_DELAY,
122 motor: false,
123 ready: false,
124 cursor: 0,
125 ready_ts: T::default(),
126 line: [0u8;32]
127 }
128 }
129}
130
131impl<T, S> Deref for ZxPrinterDevice<T, S> {
132 type Target = S;
133 fn deref(&self) -> &Self::Target {
134 &self.spooler
135 }
136}
137
138impl<T, S> DerefMut for ZxPrinterDevice<T, S> {
139 fn deref_mut(&mut self) -> &mut Self::Target {
140 &mut self.spooler
141 }
142}
143
144// 2 lines / sec
145impl<T: TimestampOps, S: Spooler> ZxPrinterDevice<T, S> {
146 /// This method should be called after each emulated frame.
147 pub fn next_frame(&mut self, eof_timestamp: T) {
148 self.ready_ts = self.ready_ts.saturating_sub(eof_timestamp);
149 }
150 /// This method should be called when the device is being reset.
151 pub fn reset(&mut self) {
152 self.motor = false;
153 self.ready = false;
154 for p in self.line.iter_mut() {
155 *p = 0;
156 }
157 self.cursor = 0;
158 }
159 /// This method should be called when a `CPU` writes to the **ZX Printer** port.
160 pub fn write_control(&mut self, data: u8, timestamp: T) {
161 if data & MOTOR_MASK == 0 { // start
162 self.start(data, timestamp);
163 }
164 else {
165 self.stop();
166 }
167 }
168 /// This method should be called when a `CPU` reads from the **ZX Printer** port.
169 pub fn read_status(&mut self, timestamp: T) -> u8 {
170 if self.motor {
171 if self.ready || self.update_ready(timestamp) {
172 if self.cursor == 0 {
173 STATUS_NEW_LINE
174 }
175 else {
176 STATUS_BIT_READY
177 }
178 }
179 else {
180 STATUS_NOT_READY
181 }
182 }
183 else {
184 STATUS_OFF
185 }
186 }
187
188 fn update_ready(&mut self, timestamp: T) -> bool {
189 if timestamp > self.ready_ts {
190 self.ready = true;
191 return true;
192 }
193 false
194 }
195
196 #[inline]
197 fn flush(&mut self) {
198 if self.cursor != 0 {
199 let cursor = self.cursor;
200 self.cursor = 0;
201 let mask = 0x80 >> (cursor.wrapping_sub(1) & 7);
202 self.line[usize::from(cursor) >> 3] &= mask;
203 let index = (usize::from(cursor) + 7) >> 3;
204 self.spooler.push_line(&self.line[..index]);
205 }
206 }
207
208 fn stop(&mut self) {
209 if self.motor {
210 self.motor = false;
211 self.ready = false;
212 self.flush();
213 self.spooler.motor_off();
214 }
215 }
216
217 #[inline]
218 fn set_delay(&mut self, timestamp: T, data: u8) {
219 let mut delay: FTs = self.bit_delay.into();
220 if data & SLOW_MASK == SLOW_MASK {
221 delay *= 2;
222 }
223 self.ready_ts = timestamp + delay;
224 }
225
226 fn start(&mut self, data: u8, timestamp: T) {
227 if self.motor {
228 if self.ready {
229 self.write_bit(data & STYLUS_MASK == STYLUS_MASK);
230 self.set_delay(timestamp, data);
231 self.ready = false;
232 }
233 }
234 else {
235 self.motor = true;
236 self.ready = false;
237 self.cursor = 0;
238 self.set_delay(timestamp, data);
239 self.spooler.motor_on();
240 }
241 }
242
243 fn write_bit(&mut self, stylus: bool) {
244 let cursor = self.cursor;
245 let index = usize::from(cursor) >> 3;
246 let mask = 0x80 >> (cursor & 7);
247 if stylus {
248 self.line[index] |= mask;
249 }
250 else {
251 self.line[index] &= !mask;
252 }
253 let (cursor, over) = cursor.overflowing_add(1);
254 self.cursor = cursor;
255 if over {
256 self.spooler.push_line(&self.line);
257 }
258 }
259}
260
261impl Spooler for DebugSpooler {
262 fn push_line(&mut self, line: &[u8]) {
263 for b in line {
264 print!("{:02x}", b);
265 }
266 println!();
267 }
268}