The pre-ADB Macintoshes use a simple quadrature-encoded mouse input, no formal serial protocol.
Quadrature encoding is a simple, physical process, that lends itself to a convenient cheat if you're synthesising input. Picture a cog, with an optical sensor pointing through the grooves. If you observed the digital output of that sensor, you'd be able to tell how fast the cog is turning, which is half the problem solved.
(Disclaimer: it's not really a cog, but that's an easy image)
What a quadrature encoder does is it uses two sensors, half a groove apart. Then if the cog is turning one way you'd expect to see the time-ordered input:
sensor 1 on, sensor 2 on, sensor 1 off, sensor 2 off, sensor 1 on, etc
But if it's turning the other way, you'd instead see:
sensor 2 on, sensor 1 on, sensor 2 off, sensor 1 off, sensor 2 on, etc
Which the Macintosh (and most other similar computers) collapse into a simple process:
- every time sensor 1 changes state, observe the the mouse has moved by one pixel;
- if sensor 2 has the same value as sensor 1 when sensor 1 has just changed, that's one pixel left; otherwise it's one pixel right.
(for appropriate values of 'left' and 'right', of course)
This is specifically embodied in the Macintosh by having inputs 'X1' and 'Y1' wired up to its serial controller chip, the SCC, to generate interrupts anytime they transition, and having inputs 'X2' and 'Y2' wired up as inputs to the 6522 VIA, which the processor manually polls upon receiving the X1 or Y1 interrupt.
So the shortcut I currently take in my emulator is very straightforward:
- keep a couple of values representing "x offset not yet communicated" and "y offset not yet communicated", into which modern-style instantaneous mouse delta reports are accumulated;
- when motion is outstanding, toggle X1 and Y1 at a fixed frequency that's close to the maximum rate the Macintosh can process the relevant interrupts at; and
- upon each toggling, set X2 and Y2 as either the same value as X1 or Y1, if motion is leftward and downward, or the opposite value, if motion is rightward or upward.
That's a cheat coupled to the known software implementation of mouse input reading — really X2 and Y2 should mutate in between changes to X1 and Y1, rather than in lockstep with them. I will banish it from my emulator at some point, but it definitely works.
I unscientifically arrived at a figure of toggling X1/Y1 every 1,250 cycles relative to the internal 7,833,600 Hz clock. Obviously you don't have access to that clock, but that implies that ~6266 Hz is not too fast for handling mouse input. My thinking here was that since I'm receiving mouse pointer movements of multiple pixels atomically, feeding them to the emulated Macintosh "as close to atomically as possible" is technically as accurate as the available information, even if it's nothing like how someone would move a real mouse.
On the pinout, per here:
- pin 5 is X1, the one that indicates velocity of x movement;
- pin 4 is X2, the one that — through comparison to X1 — indicates direction of x movement;
- pin 9 is Y1, for velocity of y movement;
- pin 8 is Y2, for direction of y movement exactly as per x.
Otherwise it looks line pins 1 and 3 are grounds, for the chassis and electronics respectively, pin 2 is +5v, pin 7 is for the button, and 6 isn't connected to anything.
EDIT:
So, in case it's any help, this is all there is to the virtual mouse electronics in the current version of my emulator. It accepts multiple buttons because I'm sure I'll reuse it for other machines. The actual stuff of feeding the SCC and 6522 isn't in that file, naturally, it's Macintosh-specific wiring outside of the mouse itself.
EXAMPLE:
One-dimensional pseudocode:
int x_delta;
func add_x_motion(int number_of_pixels) {
x_delta += number_of_pixels;
}
// update() is called by a timer roughly 6,000 times/second;
// the timer needn't be very precise.
func update() {
// Do nothing if no pixels are left to communicate.
if(!x_delta) return;
// Toggle x1 (/pin 5) to indicate a single pixel's movement.
toggle_x1_output();
// Set x2 (/ pin 4) to the same level as x1 to indicate motion
// to the left. Set it to the opposite level to indicate motion
// to the right. And then reduce `x_delta` appropriately.
if(x_delta < 0) {
set_x2_output(get_x1_output());
++ x_delta;
} else {
set_x2_output(!get_x1_output());
-- x_delta;
}
}