On my list for a good while has been mouse support - mostly because I'd like to add networking support, but most of the programs that have Uthernet card support (which is what I want to emulate) also require a mouse card. So, quite some time ago I started working on the mouse for Aiie, and was stymied with the lack of CPU power on the Teensy 3 (mostly architectural, based on how I'd designed it).
With the Teensy 4.1 running the show, things are looking pretty good - I'm running the CPU at 396 MHz, downclocked from the stock 600 Mhz and well lower than the 816 MHz it can run at without cooling... so I shouldn't have any problems there. Which means it's just a matter of time and understanding.
The time has presented itself, and here's the understanding. Let's start with how the mouse works on the //e.
The AppleMouse II was the same mouse used on the Mac 128/512/Plus. The card it used on the //e interfaced with the system bus via a 6521 PIA ("Peripheral Interface Adapter") chip. It was glued together with a fairly substantial ROM, which not only used the standard 256 bytes of peripheral card space but also page-swapped in another 2k of extended ROM on demand.
My first thought was to implement a soft 6521 and glue it in to the bus; then use the real ROM images to provide a driver. It seemed a tedious, but likely robust, way to build it out.
The 6521 code wasn't hard to write, but testing it is a different story; the only way I have of testing it is by booting something that has mouse support, and seeing what happens. Which means blindly interfacing the mouse card on top of the untested 6521, and figuring out which problems are bugs in the 6521 vs. which are problems in my interface to the program running on the Aiie.
I figured that GEOS would be a good way to test the mouse itself. I'd used GEOS back in the 80s, so I knew what to expect - particularly that it used quite a lot of what the Apple //e was capable of back in the day. Double-high-res graphics, all 128k of RAM, all on top of PRODOS. But when I booted it, the mouse didn't work, and I couldn't quite figure out how to debug it.
Which is approximately where I put it down in 2019, waiting for some stroke of inspiration. Or fortitude.
So when I picked up the mouse driver again, I wanted to do it another way. I've spent some time bringing the SDL build up to snuff, working from the same code base as the Teensy 4.1 so I can directly debug on my Mac. So to find out how the mouse was supposed to work, I started reading all the AppleMouse documentation I could find.
Which isn't much. There's the AppleMouse II User's Manual; the related Addendum; a smattering of old usenet (as far as I can tell) the exists in various forms around the Internet. A few other dribs and drabs but nothing substantial.
At that point, I figured I'd start disassembling the original ROM and building my own. But I had some substantial questions.
How is the mouse card identified?
I'd already decided I'd put the mouse in slot 4, so booting up the machine it's straightforward to look at the basic 256 bytes of ROM directly in the system monitor as disassembly or raw data. I went for disassembly.
] CALL -151 * C400L
But this was all built in the days before anyone had hardware you could interrogate - there's no handshake to ask the board what it is, so it's not the code that's important right now. The OS detected the hardware by reading bytes out of its ROM and guessing at what made it a mouse. Over years of hardware appearing, patterns emerged and it became standard practice to look for certain fingerprints of data. Eventually Apple released the 1988 specification "Pascal 1.1 Firmware Protocol ID Bytes". It says that the bytes at offsets $05, $07, and $0B must be $38, $18, and $01 respectively. And all of that is true in this firmware. It also says that byte at offset $0C is the hardware identifier - in this case, its value is $20. And the mouse user's guide says that it also should have the value $D6 at offset $FB.
So at application load time, the app scans all 7 slots looking for those bytes. If I wanted to write my own ROM I'd need to start with those specific bytes. Easy enough.
How is the ROM driver structured?
The mouse manual says there are 8 routines in the ROM; and to find each one's entry point, you read a single byte from that routine's dedicated place in ROM to find the offset. So, in slot 4 (which begins at memory $C400), to find the InitMouse function, we look at offset $19 (ergo, memory location $C419) and get back the byte $97, telling us that the function we want is at memory $C497.
This accounts for 8 more bytes of the ROM that are fixed (or, rather, that have fixed meanings) - bytes $12 through $19 are this lookup table for the functions we need. And one more is added in one of the pieces of trivia I found around the Internet, saying that if you look at offset $1C, you can call that function before InitMouse to change its frequency from 60Hz to 50Hz - which, looking at my ROM image, isn't actually doing anything and betrays something else about the structure of the ROM.
Because at $1C, in my ROM, is the byte $AE. And at $C4AE, I have
C4AE- A2 03 LDX #$03
C4B0- 38 SEC
C4B1- 60 RTS
That's not doing anything useful. It's setting X to the value 3 (why? I don't know); setting the status register carry, which is how the driver signals an error to the caller; and then returning. I guess my ROM isn't capable of doing that thing Apple was talking about. Maybe it was too early, or a different variant. It's somewhat unimportant to me, but is very interesting - because of the ROM around the lookup table. Which reads like this, with my annotations:
C40D- AE AE AE AE
C411- 00
C412- 6D ;(setmouse @ c46d)
C413- 75 ;(servemouse @ C475)
C414- 8E ;(readmouse @ C48E)
C415- 9F ;(clearmouse @ C49F)
C416- A4 ;(posmouse @ C4A4)
C417- 86 ;(clampmouse @ C486)
C418- A9 ;(homemouse @ C4A9)
C419- 97 ;(initmouse @ C497)
C41A- AE
C41B- AE
C41C- AE ;(semi-documented: sets mouse frequency handler to 60 or 50 hz)
C41D- AE
C41E- AE
C41F- AE
I think it's safe to say that all of those are lookup table addresses. $11 might be the entry point for PR# ("booting" from a peripheral or setting its output), and it looks like all the others with $AE are probably an "unimplemented function" placeholder. So now we know code shouldn't be placed between $0D and $1F.
So there's some initialization code at $C400; a few identification bytes; then a lookup table from $C40D through $C41F; and the "main" code block begins at $C420.
How does it work?
All of that is pretty straightforward if you're using an assembly or Pascal program to call those entry points. You can call InitMouse, then SetMouse to turn on the mouse peripheral, then ReadMouse in a loop to keep reading position data. The mouse returns X and Y positions in specific memory locations that the caller can retrieve, and sets the Carry flag if any errors occur. But none of that actually interfaces with a mouse for me, at least not yet. I'll need some physical interface that the user interacts with, which these functions can then read from. With that abstraction in mind, I'll need a way to get the position and button state... something like this, which is in physicalmouse.h:
virtual void getPosition(uint16_t *x, uint16_t *y) = 0;
virtual bool getButton() = 0;
then in the SDL build, there's an sdl-mouse.cpp; and in the Teensy build, there's a teensy-mouse.cpp. The SDL one reads the Mac's mouse movements and tracks the position within the window, while the Teensy one uses the joystick and uses the left shift key as a mouse button.
That just leaves tying together the two halves - how do we bridge between the mouse interface card ROM and this C++ code?
Enter the soft switch
Part of the allure of early computers - for me, at least - is the way the hardware and software so fluidly cross over each other. In the Apple II series, there are 16 memory addresses for each peripheral that look like RAM to the processor and, in hardware, could do any number of interesting things - because they're just electrical signals. These are the "soft switches" from $C090 through $C0FF. A card in slot 1 would get $C090 through $C09F. Since we're in slot 4, we get $C0C0 through $C0CF. When something writes to those addresses, some of them are tied to signal lines for the mouse card's 6521 PIA chip - and that's how the driver winds up talking to the mouse. Since I've removed the PIA and I'm writing the ROM driver from scratch, we can use any of those 16 to either read or write, giving us 32 ways to transfer data across this border.
For example, take the new InitMouse function in my ROM.
$C494 8D CC C0 STA $C0CC
$C497 18 CLC
$C498 60 RTS
Dead simple: write whatever's in the accumulator to $C0CC, which activates soft switch $C for the peripheral in Slot 4; then clear the carry (to tell the caller no error occurred) and return.
Then we can write out the interface in C++, in the mouse.cpp object:
void Mouse::writeSwitches(uint8_t s, uint8_t v)
{
switch (s) {
case SW_R_INITMOUSE:
// Set clamp to (0,0) - (1023,1023)
g_vm->getMMU()->write(0x578, 0); // high of lowclamp
g_vm->getMMU()->write(0x478, 0); // low of lowclamp
g_vm->getMMU()->write(0x5F8, 0x03); // high of highclamp
g_vm->getMMU()->write(0x4F8, 0xFF); // low of highclamp
g_mouse->setClamp(XCLAMP, 0, 1023);
g_mouse->setClamp(YCLAMP, 0, 1023);
break;
...
That is to say: when InitMouse is called, soft switch 0xC is activated for write, which calls writeSwitches(...) in my emulation. That sets the clamping window (the bounds in which the mouse operates) to [0,1023]; and we'll write those values back to the reserved memory locations for the clamping window bounds (per the docs in the user manual) before returning from the "write to memory" operation.
The rest of the code is similarly trivial. The only real complicated piece is ServiceMouse, which is half of an interrupt handler pattern.
Which is something I've not done on Aiie at all before, so this ought to be interesting.
Programmer Interruptus
I let this one convince me it was going to be a problem for waaaaay longer than I should have. Eventually my years of experience told me to *just do something* and figure it out better after I had some context.
The problem is twofold.
First, there are the interrupts themselves. There's an IRQ ("interrupt request") vector on the 65C02 that, when an interrupt is asserted, stops code flow and jumps to the address stored in that memory location. But the Apple //e didn't have any sources of interrupts natively. These interrupts don't generally happen. You had to have hardware that was generating interrupts... and nothing I've written so far has done that. So I figured there was a good chance that I'd wind up debugging basic interrupt functionality, instead of the code I was trying to write. (Spoiler: it did not.)
Second, there's the nature of the interrupts. There are 3 supported interrupts on the AppleMouse card - an interrupt when the button is pressed; an interrupt when the mouse is moved; and an interrupt in the video vertical blanking period.
If you don't know what that is - the "vertical blanking period" is (simplifying a bit) the period of time during which the electron beam from a Cathode Ray Tube display is moving from the bottom-right to the top-left corner. Since it's a single beam being deflected by magnetic fields, it takes some time for the field to shift to get the beam back where it needs to be. And on most //e models, that's at a rate of 60 Hz.
So I need something new that runs at a rate of 60 cycles per second.
Previously, I'd had two timing-sensitive parts of Aiie - the CPU (which runs as 1.023 MHz) and the audio (which runs at 44.1 KHz at the moment). There's a third section that does maintenance - for the physical keyboard as well as the USB keyboard, polling at an arbitrarily-set 10 Hz. So I stepped that up to 60 Hz, and added a poll in to the mouse:
g_mouse->maintainMouse(); g_keyboard->maintainKeyboard(); usb.maintain();
Then in the mouse.cpp object, it was a matter of adding code to trigger a CPU interrupt whenever the time arrived:
if ( (status & ST_MOUSEENABLE) &&
(status & ST_INTVBL) &&
(cycleCount >= nextInterruptTime) ) {
g_cpu->irq();
interruptsTriggered |= ST_INTVBL;
}
That says "If the mouse is enabled; and it was configured to send vertical blanking interrupts; and it's time for an interrupt, then trigger the IRQ in the CPU, and keep track of the reason why an interrupt has gone off".
Finally, in the ServeMouse call, we can tell the caller that we did indeed cause an interrupt, and it was caused because of the vertical blanking interrupt.
And with that, we've got mouse support.
So far I've tested it in GEOS, MultiScribe, Fantavision, Blazing Paddles, and Copy II+ 9.1.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.