general VM structure and a test OpenCV target

This commit is contained in:
Jorj Bauer 2017-02-19 18:55:54 -05:00
parent 680e57013f
commit 218b3b77a0
44 changed files with 34708 additions and 5 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
apple2e.rom
disk.rom
*~
*.o

View File

@ -1,16 +1,30 @@
LDFLAGS=-L/usr/local/lib
CXXFLAGS=-Wall -I .. -I . -O3
LDFLAGS=-L/usr/local/lib -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_features2d -lopencv_calib3d
CXXFLAGS=-Wall -I .. -I . -I apple -O3
TSRC=cpu.cpp util/testharness.cpp
all:
@echo There is no 'all' target. Yet.
OPENCVOBJS=cpu.o opencv/dummy-speaker.o opencv/opencv-display.o opencv/opencv-keyboard.o opencv/opencv-paddles.o opencv/opencv-filemanager.o apple/appledisplay.o apple/applekeyboard.o apple/applemmu.o apple/applevm.o apple/diskii.o apple/nibutil.o apple/slot.o RingBuffer.o globals.o opencv/aiie.o
ROMS=apple/applemmu-rom.h apple/diskii-rom.h
all: opencv
opencv: roms $(OPENCVOBJS)
g++ $(LDFLAGS) -o aiie-opencv $(OPENCVOBJS)
clean:
rm -f *.o *~ */*.o */*~ testharness.basic testharness.verbose testharness.extended
rm -f *.o *~ */*.o */*~ testharness.basic testharness.verbose testharness.extended aiie-opencv apple/diskii-rom.h apple/applemmu-rom.h
test: $(TSRC)
g++ $(CXXFLAGS) -DBASICTEST $(TSRC) -o testharness.basic
g++ $(CXXFLAGS) -DVERBOSETEST $(TSRC) -o testharness.verbose
g++ $(CXXFLAGS) -DEXTENDEDTEST $(TSRC) -o testharness.extended
roms: apple2e.rom disk.rom
./util/genrom.pl apple2e.rom disk.rom
apple/applemmu-rom.h: roms
apple/diskii-rom.h: roms

View File

@ -19,6 +19,54 @@ Build log:
https://hackaday.io/project/19925-aiie-an-embedded-apple-e-emulator
Building (on a Mac)
===================
While this isn't the purpose of the emulator, it is functional, and is
my first test target for most of the work. With MacOS 10.11.6 and
Homebrew, you can build and run it like this:
$ make opencv
$ ./aiie-opencv /path/to/disk.dsk
As the name implies, this requires that OpenCV is installed and in
/usr/local/lib. I've done that with Homebrew like this
$ brew install opencv
"Why OpenCV?" you might ask. Well, it's just because I had code from
another project lying around that directly manipulated OpenCV bitmap
data. It's functional, and the Mac build is only about functional
testing (for me).
VM
==
The virtual machine architecture is broken in half - the virtual and
physical pieces. There's the root VM object (vm.h), which ties
together the MMU, virtual keyboard, and virtual display.
Then there are the physical interfaces, which aren't as well
organized. They exist as globals in globals.cpp:
FileManager *g_filemanager = NULL;
PhysicalDisplay *g_display = NULL;
PhysicalKeyboard *g_keyboard = NULL;
PhysicalSpeaker *g_speaker = NULL;
PhysicalPaddles *g_paddles = NULL;
There are the two globals that point to the VM and the virtual CPU:
Cpu *g_cpu = NULL;
VM *g_vm = NULL;
And there are two global configuration values that probably belong in
some sort of Prefs class:
int16_t g_volume;
uint8_t g_displayType;
CPU
===

113
RingBuffer.cpp Normal file
View File

@ -0,0 +1,113 @@
#include "RingBuffer.h"
#include <stdlib.h>
RingBuffer::RingBuffer(int16_t length)
{
this->buffer = (uint8_t *)malloc(length);
this->max = length;
this->fill = 0;
this->ptr = 0;
this->cursor = 0;
}
RingBuffer::~RingBuffer()
{
free (this->buffer);
}
void RingBuffer::clear()
{
this->fill = 0;
}
bool RingBuffer::isFull()
{
return (this->max == this->fill);
}
bool RingBuffer::hasData()
{
return (this->fill != 0);
}
bool RingBuffer::addByte(uint8_t b)
{
if (this->max == this->fill)
return false;
int idx = (this->ptr + this->fill) % this->max;
this->buffer[idx] = b;
this->fill++;
return true;
}
bool RingBuffer::replaceByte(uint8_t b)
{
if (cursor < fill) {
buffer[cursor] = b;
cursor++;
if (cursor >= fill) {
cursor = 0;
}
return true;
}
return false;
}
bool RingBuffer::addBytes(uint8_t *b, int count)
{
for (int i=0; i<count; i++) {
if (!addByte(b[i]))
return false;
}
return true;
}
uint8_t RingBuffer::consumeByte()
{
if (this->fill == 0)
return 0;
uint8_t ret = this->buffer[this->ptr];
this->fill--;
this->ptr++;
this->ptr %= this->max;
return ret;
}
uint8_t RingBuffer::peek(int16_t idx)
{
uint16_t p = (this->ptr + idx) % this->max;
return this->buffer[p];
}
int16_t RingBuffer::count()
{
return this->fill;
}
uint16_t RingBuffer::getPeekCursor()
{
return this->cursor;
}
void RingBuffer::setPeekCursor(int16_t idx)
{
this->cursor = idx;
}
void RingBuffer::resetPeekCursor()
{
this->cursor = 0;
}
uint8_t RingBuffer::peekNext()
{
uint8_t ret = peek(cursor);
cursor++;
if (cursor >= fill) {
cursor = 0;
}
return ret;
}

34
RingBuffer.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef __RINGBUFFER_H
#define __RINGBUFFER_H
#include <stdint.h>
class RingBuffer {
public:
RingBuffer(int16_t length);
~RingBuffer();
void clear();
bool isFull();
bool hasData();
bool addByte(uint8_t b);
bool addBytes(uint8_t *b, int count);
bool replaceByte(uint8_t b);
uint8_t consumeByte();
uint8_t peek(int16_t idx);
uint16_t getPeekCursor();
void setPeekCursor(int16_t idx);
void resetPeekCursor();
uint8_t peekNext();
int16_t count();
private:
uint8_t *buffer;
int16_t max;
int16_t ptr;
int16_t fill;
int16_t cursor;
};
#endif

610
apple/appledisplay.cpp Normal file
View File

@ -0,0 +1,610 @@
#include <ctype.h> // isgraph
#include <stdlib.h> // calloc
#include <string.h> // strlen
#include "appledisplay.h"
#include "applemmu.h" // for switch constants
#include "font.h"
/* Fourpossible Hi-Res color-drawing modes..
MONOCHROME: show all the pixels, but only in green;
BLACKANDWHITE: monochrome, but use B&W instead of B&G;
NTSCLIKE: reduce the resolution to 140 pixels wide, similar to how an NTSC monitor would blend it
PERFECTCOLOR: as the Apple RGB monitor shows it, which means you can't have a solid color field
*/
#include "globals.h"
AppleDisplay::AppleDisplay(uint8_t *vb) : VMDisplay(vb)
{
this->switches = NULL;
this->dirty = true;
}
AppleDisplay::~AppleDisplay()
{
}
bool AppleDisplay::deinterlaceAddress(uint16_t address, uint8_t *row, uint8_t *col)
{
if (address >= 0x800 && address < 0xC00) {
address -= 0x400;
}
uint8_t block = (address >> 7) - 0x08;
uint8_t blockOffset = (address & 0x00FF) - ((block & 0x01) ? 0x80 : 0x00);
if (blockOffset < 0x28) {
*row = block;
*col = blockOffset;
} else if (blockOffset < 0x50) {
*row = block + 8;
*col = blockOffset - 0x28;
} else {
*row = block + 16;
*col = blockOffset - 0x50;
}
return true;
}
// calculate x/y pixel offsets from a memory address.
// Note that this is the first of 7 pixels that will be affected by this write;
// we'll need to update all 7 starting at this x.
bool AppleDisplay::deinterlaceHiresAddress(uint16_t address, uint8_t *row, uint16_t *col)
{
// each row is 40 bytes, for 7 pixels each, totalling 128
// pixels wide.
// They are grouped in to 3 "runs" of 40-byte blocks, where
// each group is 64 lines after the one before.
// Then repeat at +400, +800, +c00, +1000, +1400, +1800, +1c00 for
// the other 7 pixels tall.
// Repeat the whole shebang at +0x80, +0x100, +0x180, ... to +280
// for each 8-pixel tall group.
// There are 8 bytes at the end of each run that we ignore. Skip them.
if ((address & 0x07f) >= 0x78 &&
(address & 0x7f) <= 0x7f) {
*row = 255;
*col = 65535;
return false;
}
*row = ((address & 0x380) >> 4) +
((address & 0x1c00)>>10) +
64 * ((address & 0x7f) / 40);
*col = ((address & 0x7f) % 40) * 7;
return true;
}
void AppleDisplay::writeLores(uint16_t address, uint8_t v)
{
if (address >= 0x800 && !((*switches) & S_80COL)) {
if (!((*switches) & S_PAGE2)) {
// writing to page2 text/lores, but that's not displayed right now, so nothing to do
return;
}
}
uint8_t row, col;
deinterlaceAddress(address, &row, &col);
if (col <= 39) {
if ((*switches) & S_TEXT ||
(((*switches) & S_MIXED) && row >= 20)) {
if ((*switches) & S_80COL) {
Draw80CharacterAt(v, col, row, ((*switches) & S_PAGE2) ? 0 : 1);
} else {
DrawCharacterAt(v, col, row);
}
}
}
if (!((*switches) & S_TEXT) &&
!((*switches) & S_HIRES)) {
if (col <= 39) {
if (row < 20 ||
(! ((*switches) & S_MIXED))) {
// low-res graphics mode. Each character has two 4-bit
// values in it: first half is the "top" pixel, and second "bottom".
if (((*switches) & S_80COL) && ((*switches) & S_DHIRES)) {
Draw80LoresPixelAt(v, col, row, ((*switches) & S_PAGE2) ? 0 : 1);
} else {
DrawLoresPixelAt(v, col, row);
}
}
}
}
dirty = true;
}
void AppleDisplay::writeHires(uint16_t address, uint8_t v)
{
if ((*switches) & S_HIRES) {
if ((*switches) & S_DHIRES) {
// Double hires: make sure we're drawing to the page that's visible.
// If S_80STORE is on, then it's $2000 (and S_PAGE2 controls main/aux bank r/w).
// If S_80STORE is off, then it's $4000 (and RAMRD/RAMWT are used).
if (((*switches) & S_80STORE) && address >= 0x4000 && address <= 0x5FFF)
return;
if ( !((*switches) & S_80STORE) && address >= 0x2000 && address <= 0x3FFF)
return;
Draw14DoubleHiresPixelsAt(address);
dirty = true;
return;
}
// If it's a write to single hires but the 80store byte is on, then what do we do?
if ((*switches) & S_80STORE) {
// FIXME
return;
}
// Make sure we're writing to the page that's visible before we draw
if (address >= 0x2000 && address <= 0x3FFF && ((*switches) & S_PAGE2))
return;
if (address >= 0x4000 && address <= 0x5FFF && !((*switches) & S_PAGE2))
return;
Draw14HiresPixelsAt(address & 0xFFFE);
dirty = true;
}
}
// return a pointer to the right glyph, and set *invert appropriately
const unsigned char *AppleDisplay::xlateChar(uint8_t c, bool *invert)
{
if (c <= 0x3F) {
// 0-3f: inverted @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ !"#$%&'()*+,-./0123456789:;<=>?
// (same w/o mousetext, actually)
*invert = true;
return &ucase_glyphs[c * 8];
} else if (c <= 0x5F) {
// 40-5f: normal mousetext
// (these are flashing @ABCDEFG..[\]^_ when not in mousetext mode)
if ((*switches) & S_ALTCH) {
*invert = false;
return &mousetext_glyphs[(c - 0x40) * 8];
} else {
*invert = true;
return &ucase_glyphs[(c - 0x40) * 8];
}
} else if (c <= 0x7F) {
// 60-7f: inverted `abcdefghijklmnopqrstuvwxyz{|}~*
// (these are flashing (sp)!"#$%...<=>? when not in mousetext)
if ((*switches) & S_ALTCH) {
*invert = true;
return &lcase_glyphs[(c - 0x60) * 8];
} else {
*invert = true;
return &ucase_glyphs[((c-0x60) + 0x20) * 8];
}
} else if (c <= 0xBF) {
// 80-BF: normal @ABCD... <=>? in both character sets
*invert = false;
return &ucase_glyphs[(c - 0x80) * 8];
} else if (c <= 0xDF) {
// C0-DF: normal @ABCD...Z[\]^_ in both character sets
*invert = false;
return &ucase_glyphs[(c - 0xC0) * 8];
} else {
// E0- : normal `abcdef... in both character sets
*invert = false;
return &lcase_glyphs[(c - 0xE0) * 8];
}
/* NOTREACHED */
}
void AppleDisplay::Draw80CharacterAt(uint8_t c, uint8_t x, uint8_t y, uint8_t offset)
{
bool invert;
const uint8_t *cptr = xlateChar(c, &invert);
// Even characters are in bank 0 ram. Odd characters are in bank 1 ram.
// Technically, this would need 560 columns to work correctly - and I
// don't have that, so it's going to be a bit wonky.
//
// First pass: draw two pixels on top of each other, clearing only
// if both are black. This would be blocky but probably passable if
// it weren't for the fact that characters are 7 pixels wide, so we
// wind up sharing a half-pixel between two characters. So we'll
// render these as 3-pixel-wide characters and make sure they always
// even-align the drawing on the left side so we don't overwrite
// every other one on the left or right side.
uint16_t fgColor = g_displayType == m_monochrome?c_green:c_white;
for (uint8_t y2 = 0; y2<8; y2++) {
uint8_t d = *(cptr + y2);
for (uint8_t x2 = 0; x2 <= 7; x2+=2) {
uint16_t basex = ((x * 2 + offset) * 7) & 0xFFFE; // even aligned
bool pixelOn = ( (d & (1<<x2)) | (d & (1<<(x2+1))) );
if (pixelOn) {
uint8_t val = (invert ? c_black : fgColor);
drawPixel(val, (basex+x2)/2, y*8+y2);
} else {
uint8_t val = (invert ? fgColor : c_black);
drawPixel(val, (basex+x2)/2, y*8+y2);
}
}
}
}
void AppleDisplay::DrawCharacterAt(uint8_t c, uint8_t x, uint8_t y)
{
bool invert;
const uint8_t *cptr = xlateChar(c, &invert);
uint16_t fgColor = g_displayType == m_monochrome?c_green:c_white;
for (uint8_t y2 = 0; y2<8; y2++) {
uint8_t d = *(cptr + y2);
for (uint8_t x2 = 0; x2 < 7; x2++) {
if (d & (1 << x2)) {
uint8_t val = (invert ? c_black : fgColor);
drawPixel(val, x*7+x2, y*8+y2);
} else {
uint8_t val = (invert ? fgColor : c_black);
drawPixel(val, x*7+x2, y*8+y2);
}
}
}
}
void AppleDisplay::Draw14DoubleHiresPixelsAt(uint16_t addr)
{
// We will consult 4 bytes (2 in main, 2 in aux) for any single-byte
// write. Align to the first byte in that series based on what
// address we were given...
addr &= ~0x01;
// Figure out the position of that address on the "normal" hires screen
uint8_t row;
uint16_t col;
deinterlaceHiresAddress(addr, &row, &col);
if (row >= 160 &&
((*switches) & S_MIXED)) {
// displaying text, so don't have to draw this line
return;
}
// Make sure it's a valid graphics area, not a dead hole
if (col <= 280 && row <= 192) {
// Grab the 4 bytes we care about
uint8_t b1A = mmu->readDirect(addr, 0);
uint8_t b2A = mmu->readDirect(addr+1, 0);
uint8_t b1B = mmu->readDirect(addr, 1);
uint8_t b2B = mmu->readDirect(addr+1, 1);
// Construct the 28 bit wide bitstream, like we do for the simpler 14 Hires pixel draw
uint32_t bitTrain = b2A & 0x7F;
bitTrain <<= 7;
bitTrain |= (b2B & 0x7F);
bitTrain <<= 7;
bitTrain |= (b1A & 0x7F);
bitTrain <<= 7;
bitTrain |= (b1B & 0x7F);
// Now we pop groups of 4 bits off the bottom and draw our
// NTSC-style-only color. The display for this project only has
// 320 columns, so it's silly to try to do 560 columns of
// monochrome; and likewise, we can't do "perfect" representation
// of shifted color pixels. So NTSC it is, and we'll draw two screen
// pixels for every color.
for (int8_t xoff = 0; xoff < 14; xoff += 2) {
drawPixel(bitTrain & 0x0F, col+xoff, row);
drawPixel(bitTrain & 0x0F, col+xoff+1, row);
bitTrain >>= 4;
}
}
}
// Whenever we change a byte, it's possible that it will have an affect on the byte next to it -
// because between two bytes there is a shared bit.
// FIXME: what happens when the high bit of the left doesn't match the right? Which high bit does
// the overlap bit get?
void AppleDisplay::Draw14HiresPixelsAt(uint16_t addr)
{
uint8_t row;
uint16_t col;
deinterlaceHiresAddress(addr, &row, &col);
if (row >= 160 &&
((*switches) & S_MIXED)) {
return;
}
if (col <= 280 && row <= 192) {
/*
The high bit only selects the color palette.
There are only really two bits here, and they can be one of six colors.
color highbit even odd restriction
black x 0x80,0x00
green 0 0x2A 0x55 odd only
violet 0 0x55 0x2A even only
white x 0xFF,0x7F
orange 1 0xAA 0xD5 odd only
blue 1 0xD5 0xAA even only
in other words, we can look at the pixels in pairs and we get
00 black
01 green/orange
10 violet/blue
11 white
When the horizontal byte number is even, we ignore the last
bit. When the horizontal byte number is odd, we use that dropped
bit.
So each even byte turns in to 3 bits; and each odd byte turns in
to 4. Our effective output is therefore 140 pixels (half the
actual B&W resolution).
(Note that I swap 0x02 and 0x01 below, because we're running the
bit train backward, so the bits are reversed.)
*/
uint8_t b1 = mmu->read(addr);
uint8_t b2 = mmu->read(addr+1);
// Used for color modes...
bool highBitOne = (b1 & 0x80);
bool highBitTwo = (b2 & 0x80);
uint16_t bitTrain = (b1 & 0x7F) | ((b2 & 0x7F) << 7);
for (int8_t xoff = 0; xoff < 14; xoff += 2) {
if (g_displayType == m_monochrome) {
draw2Pixels(((bitTrain & 0x01 ? c_green : c_black) << 4) |
(bitTrain & 0x02 ? c_green : c_black),
col+xoff, row);
} else if (g_displayType == m_blackAndWhite) {
draw2Pixels(((bitTrain & 0x01 ? c_white : c_black) << 4) |
(bitTrain & 0x02 ? c_white : c_black),
col+xoff, row);
} else if (g_displayType == m_ntsclike) {
// Use the NTSC-like color mode, where we're only 140 pixels wide.
bool highBitSet = (xoff >= 7 ? highBitTwo : highBitOne);
uint8_t color;
switch (bitTrain & 0x03) {
case 0x00:
color = c_black;
break;
case 0x02:
color = (highBitSet ? c_orange : c_green);
break;
case 0x01:
color = (highBitSet ? c_medblue : c_purple);
break;
case 0x03:
color = c_white;
break;
}
draw2Pixels( (color << 4) | color, col+xoff, row );
} else {
// Use the "perfect" color mode, like the Apple RGB monitor showed.
bool highBitSet = (xoff >= 7 ? highBitTwo : highBitOne);
uint8_t color;
switch (bitTrain & 0x03) {
case 0x00:
color = c_black;
break;
case 0x02:
color = (highBitSet ? c_orange : c_green);
break;
case 0x01:
color = (highBitSet ? c_medblue : c_purple);
break;
case 0x03:
color = c_white;
break;
}
uint16_t twoColors;
if (color == c_black || color == c_white || bitTrain & 0x01) {
twoColors = color;
} else {
twoColors = c_black;
}
twoColors <<= 4;
if (color == c_black || color == c_white || bitTrain & 0x02) {
twoColors |= color;
} else {
twoColors |= c_black;
}
draw2Pixels(twoColors, col+xoff, row);
}
bitTrain >>= 2;
}
}
}
void AppleDisplay::modeChange()
{
if ((*switches) & S_TEXT) {
if ((*switches) & S_80COL) {
for (uint16_t addr = 0x400; addr <= 0x400 + 0x3FF; addr++) {
uint8_t row, col;
deinterlaceAddress(addr, &row, &col);
if (col <= 39 && row <= 23) {
Draw80CharacterAt(mmu->readDirect(addr, 0), col, row, 1);
Draw80CharacterAt(mmu->readDirect(addr, 1), col, row, 0);
}
}
} else {
uint16_t start = ((*switches) & S_PAGE2) ? 0x800 : 0x400;
for (uint16_t addr = start; addr <= start + 0x3FF; addr++) {
uint8_t row, col;
deinterlaceAddress(addr, &row, &col);
if (col <= 39 && row <= 23) {
DrawCharacterAt(mmu->read(addr), col, row);
}
}
}
dirty = true;
return;
}
// Not text mode - what mode are we in?
if ((*switches) & S_HIRES) {
// Hires
// FIXME: make this draw a row efficiently
// FIXME: can make more efficient by checking S_MIXED for lower bound
uint16_t start = ((*switches) & S_PAGE2) ? 0x4000 : 0x2000;
if ((*switches) & S_80STORE) {
// Apple IIe, technical nodes #3: 80STORE must be OFF to display Page 2
start = 0x2000;
}
for (uint16_t addr = start; addr <= start + 0x1FFF; addr+=2) {
if ((*switches) & S_DHIRES) {
Draw14DoubleHiresPixelsAt(addr);
} else {
Draw14HiresPixelsAt(addr);
}
}
} else {
// Lores
// FIXME: can make more efficient by checking S_MIXED for lower bound
if (((*switches) & S_80COL) && ((*switches) & S_DHIRES)) {
for (uint16_t addr = 0x400; addr <= 0x400 + 0x3ff; addr++) {
uint8_t row, col;
deinterlaceAddress(addr, &row, &col);
if (col <= 39 && row <= 23) {
Draw80LoresPixelAt(mmu->readDirect(addr, 0), col, row, 1);
Draw80LoresPixelAt(mmu->readDirect(addr, 1), col, row, 0);
}
}
} else {
uint16_t start = ((*switches) & S_PAGE2) ? 0x800 : 0x400;
for (uint16_t addr = start; addr <= start + 0x3FF; addr++) {
uint8_t row, col;
deinterlaceAddress(addr, &row, &col);
if (col <= 39 && row <= 23) {
DrawLoresPixelAt(mmu->read(addr), col, row);
}
}
}
}
if ((*switches) & S_MIXED) {
// Text at the bottom of the screen...
// FIXME: deal with 80-char properly
uint16_t start = ((*switches) & S_PAGE2) ? 0x800 : 0x400;
for (uint16_t addr = start; addr <= start + 0x3FF; addr++) {
uint8_t row, col;
deinterlaceAddress(addr, &row, &col);
if (col <= 39 && row >= 20 && row <= 23) {
if ((*switches) & S_80COL) {
Draw80CharacterAt(mmu->read(addr), col, row, ((*switches) & S_PAGE2) ? 0 : 1);
} else {
DrawCharacterAt(mmu->read(addr), col, row);
}
}
}
}
dirty = true;
}
void AppleDisplay::Draw80LoresPixelAt(uint8_t c, uint8_t x, uint8_t y, uint8_t offset)
{
// Just like 80-column text, this has a minor problem; we're talimg
// a 7-pixel-wide space and dividing it in half. Here I'm drawing
// every other column 1 pixel narrower (the ">= offset" in the for
// loop condition).
//
// Make those ">= 0" and change the "*7" to "*8" and you've got
// 320-pixel-wide slightly distorted but cleaner double-lores...
if (!offset) {
// The colors in every other column are swizzled. Un-swizzle.
c = ((c & 0x77) << 1) | ((c & 0x88) >> 3);
}
uint8_t pixel = c & 0x0F;
for (uint8_t y2 = 0; y2<4; y2++) {
for (int8_t x2 = 3; x2>=offset; x2--) {
drawPixel(pixel, x*7+x2+offset*3, y*8+y2);
}
}
pixel = (c >> 4);
for (uint8_t y2 = 4; y2<8; y2++) {
for (int8_t x2 = 3; x2>=offset; x2--) {
drawPixel(pixel, x*7+x2+offset*3, y*8+y2);
}
}
}
// col, row are still character-like positions, as in DrawCharacterAt.
// Each holds two pixels (one on top of the other).
void AppleDisplay::DrawLoresPixelAt(uint8_t c, uint8_t x, uint8_t y)
{
uint8_t pixel = c & 0x0F;
for (uint8_t y2 = 0; y2<4; y2++) {
for (int8_t x2 = 6; x2>=0; x2--) {
drawPixel(pixel, x*7+x2, y*8+y2);
}
}
pixel = (c >> 4);
for (uint8_t y2 = 4; y2<8; y2++) {
for (int8_t x2 = 6; x2>=0; x2--) {
drawPixel(pixel, x*7+x2, y*8+y2);
}
}
}
void AppleDisplay::draw2Pixels(uint16_t two4bitColors, uint16_t x, uint8_t y)
{
videoBuffer[(y * 320 + x) /2] = two4bitColors;
}
void AppleDisplay::drawPixel(uint8_t color4bit, uint16_t x, uint8_t y)
{
uint16_t idx = (y * 320 + x) / 2;
if (x & 1) {
videoBuffer[idx] = (videoBuffer[idx] & 0xF0) | color4bit;
} else {
videoBuffer[idx] = (videoBuffer[idx] & 0x0F) | (color4bit << 4);
}
}
void AppleDisplay::setSwitches(uint16_t *switches)
{
dirty = true;
this->switches = switches;
}
bool AppleDisplay::needsRedraw()
{
return dirty;
}
void AppleDisplay::didRedraw()
{
// FIXME: there's a drawing bug somewhere that's not getting the dirty flag set right.
// dirty = false;
}

75
apple/appledisplay.h Normal file
View File

@ -0,0 +1,75 @@
#ifndef __APPLEDISPLAY_H
#define __APPLEDISPLAY_H
#ifdef TEENSYDUINO
#include <Arduino.h>
#else
#include <stdlib.h>
#endif
#include "vmdisplay.h"
enum {
c_black = 0,
c_magenta = 1,
c_darkblue = 2,
c_purple = 3,
c_darkgreen = 4,
c_darkgrey = 5,
c_medblue = 6,
c_lightblue = 7,
c_brown = 8,
c_orange = 9,
c_lightgray = 10,
c_pink = 11,
c_green = 12,
c_yellow = 13,
c_aqua = 14,
c_white = 15
};
enum {
m_blackAndWhite = 0,
m_monochrome = 1,
m_ntsclike = 2,
m_perfectcolor = 3
};
class AppleMMU;
class AppleDisplay : public VMDisplay{
public:
AppleDisplay(uint8_t *vb);
virtual ~AppleDisplay();
virtual bool needsRedraw();
virtual void didRedraw();
void modeChange(); // FIXME: rename 'redraw'?
void setSwitches(uint16_t *switches);
void writeLores(uint16_t address, uint8_t v);
void writeHires(uint16_t address, uint8_t v);
private:
bool deinterlaceAddress(uint16_t address, uint8_t *row, uint8_t *col);
bool deinterlaceHiresAddress(uint16_t address, uint8_t *row, uint16_t *col);
void Draw80CharacterAt(uint8_t c, uint8_t x, uint8_t y, uint8_t offset);
void DrawCharacterAt(uint8_t c, uint8_t x, uint8_t y);
void Draw14DoubleHiresPixelsAt(uint16_t address);
void Draw14HiresPixelsAt(uint16_t addr);
void Draw80LoresPixelAt(uint8_t c, uint8_t x, uint8_t y, uint8_t offset);
void DrawLoresPixelAt(uint8_t c, uint8_t x, uint8_t y);
void draw2Pixels(uint16_t two4bitColors, uint16_t x, uint8_t y);
void drawPixel(uint8_t cidx, uint16_t x, uint8_t y);
const unsigned char *xlateChar(uint8_t c, bool *invert);
private:
volatile bool dirty;
uint16_t *switches; // pointer to the MMU's switches
};
#endif

196
apple/applekeyboard.cpp Normal file
View File

@ -0,0 +1,196 @@
#include "applekeyboard.h"
#include "physicalkeyboard.h" // for LA/RA constants
#include "applemmu.h"
#include "globals.h"
// How many CPU cycles before we begin repeating a key?
#define STARTREPEAT 700000
// How many CPU cycles between repeats of a key?
#define REPEATAGAIN 66667
AppleKeyboard::AppleKeyboard(AppleMMU *m)
{
this->mmu = m;
for (uint16_t i=0; i<sizeof(keysDown); i++) {
keysDown[i] = false;
}
anyKeyIsDown = false;
startRepeatTimer = 0;
repeatTimer = 0;
capsLockEnabled = true;
}
AppleKeyboard::~AppleKeyboard()
{
}
bool AppleKeyboard::isVirtualKey(uint8_t kc)
{
if (kc >= 0x81 && kc <= 0x86) {
return true;
}
return false;
}
// apply the apple keymap.
// FIXME: easier with an array, but is that better?
uint8_t AppleKeyboard::translateKeyWithModifiers(uint8_t k)
{
// tolower, so we know what we're working with...
if (k >= 'A' && k <= 'Z') {
k = k - 'A' + 'a';
}
if (keysDown[_CTRL]) {
if (k >= 'a' && k <= 'z') {
return k - 'a' + 1;
}
// FIXME: any other control keys honored on the //e keyboard?
}
if (capsLockEnabled && k >= 'a' && k <= 'z') {
return k - 'a' + 'A';
}
if (keysDown[LSHFT] || keysDown[RSHFT]) {
if (k >= 'a' && k <= 'z') {
return k - 'a' + 'A';
}
switch (k) {
case '1':
return '!';
case '2':
return '@';
case '3':
return '#';
case '4':
return '$';
case '5':
return '%';
case '6':
return '^';
case '7':
return '&';
case '8':
return '*';
case '9':
return '(';
case '0':
return ')';
case '-':
return '_';
case '=':
return '+';
case '[':
return '{';
case ']':
return '}';
case '\\':
return '|';
case '`':
return '~';
case ';':
return ':';
case '\'':
return '"';
case ',':
return '<';
case '.':
return '>';
case '/':
return '?';
}
// FIXME: what the heck is it? I guess we don't need to shift it?
}
// And if we fall through, then just return it as-is
return k;
}
void AppleKeyboard::keyDepressed(uint8_t k)
{
keysDown[k] = true;
// If it's not a virtual key, then set the anyKeyDown flag
// (the VM will see this as a keyboard key)
if (!isVirtualKey(k)) {
if (!anyKeyIsDown) {
mmu->setKeyDown(true);
anyKeyIsDown = true;
}
keyThatIsRepeating = translateKeyWithModifiers(k);
startRepeatTimer = g_cpu->cycles + STARTREPEAT;
mmu->keyboardInput(keyThatIsRepeating);
} else if (k == LA) {
// Special handling: apple keys
mmu->isOpenApplePressed = true;
return;
} else if (k == RA) {
// Special handling: apple keys
mmu->isClosedApplePressed = true;
return;
} else if (k == LOCK) {
// Special handling: caps lock
capsLockEnabled = !capsLockEnabled;
return;
}
}
void AppleKeyboard::keyReleased(uint8_t k)
{
keysDown[k] = false;
// Special handling: apple keys
if (k == LA) {
mmu->isOpenApplePressed = false;
return;
}
if (k == RA) {
mmu->isClosedApplePressed = false;
return;
}
if (k == LOCK) {
// Nothing to do when the caps lock key is released.
return;
}
if (anyKeyIsDown) {
anyKeyIsDown = false;
for (int i=0; i<sizeof(keysDown); i++) {
if (keysDown[i] && !isVirtualKey(i)) {
anyKeyIsDown = true;
break;
}
}
if (!anyKeyIsDown) {
mmu->setKeyDown(false);
}
}
}
void AppleKeyboard::maintainKeyboard(uint32_t cycleCount)
{
if (anyKeyIsDown) {
if (startRepeatTimer) {
if (cycleCount >= startRepeatTimer) {
// waiting to start repeating
startRepeatTimer = 0;
repeatTimer = 0;
// Will fall through...
} else {
// Don't fall through; not time to start repeating yet
return;
}
}
// already repeating; keep it up
if (cycleCount >= repeatTimer) {
mmu->keyboardInput(keyThatIsRepeating);
repeatTimer = cycleCount + REPEATAGAIN;
}
}
}

57
apple/applekeyboard.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef __APPLEKEYBOARD_H
#define __APPLEKEYBOARD_H
#include <stdint.h>
#include "vmkeyboard.h"
#include "applemmu.h"
class AppleKeyboard : public VMKeyboard {
public:
AppleKeyboard(AppleMMU *m);
virtual ~AppleKeyboard();
virtual void keyDepressed(uint8_t k);
virtual void keyReleased(uint8_t k);
virtual void maintainKeyboard(uint32_t cycleCount);
protected:
bool isVirtualKey(uint8_t kc);
uint8_t translateKeyWithModifiers(uint8_t k);
private:
AppleMMU *mmu;
bool capsLockEnabled;
// This is a trade-off. I'm choosing speed over RAM size. If we need
// to reclaim RAM, we can get some bytes here at the expense of speed.
// These are flags for whether or not each of the individual keys are
// down, so that we can repeat appropriately. We're tracking state
// of all of the keys because of special modifier key situations.
// It's lazily using 256 bytes instead of whatever 62 we'd actually need.
bool keysDown[256];
bool anyKeyIsDown;
// While one - and only one - key is down, we repeat keypresses
// after about "534 to 801 milliseconds" (UTA2E, p. 7-15); and then
// while repeating, we send that keypress (reset keystrobe) about 15
// times a second (every 66667-ish CPU cycles).
//
// startRepeatTimer is the time (in CPU clock cycles) when we will
// start repeating the key that's currently down (note: rollover
// happens every 4925 seconds because it's a 32-bit counter, which means
// that roughly once every 82 minutes it's possible that a key will begin
// repeating early).
//
// keyThatIsRepeating is set to the actual key pressed.
// repeatTimer is the cpu cycle count at which we would repeat again.
// (It also has the rollover problem once every 82 minutes.)
uint32_t startRepeatTimer;
uint8_t keyThatIsRepeating;
uint32_t repeatTimer;
};
#endif

853
apple/applemmu.cpp Normal file
View File

@ -0,0 +1,853 @@
#ifdef TEENSYDUINO
#include <Arduino.h>
#else
#include <stdio.h>
#include <unistd.h>
#endif
#include "applemmu.h"
#include "applemmu-rom.h"
#include "physicalspeaker.h"
#include "cpu.h"
#include "globals.h"
// apple //e memory map
/*
page 0x00: zero page (straight ram)
page 0x01: stack (straight ram)
page 0x02:
page 0x03:
text/lores page 1: 0x0400 - 0x7FF
text/lores page 2: 0x0800 - 0xBFF
pages 0x0C - 0x1F: straight ram
hires page 1: pages 0x20 - 0x3F
hires page 2: pages 0x40 - 0x5F
pages 0x60 - 0xBF: straight ram
page 0xc0: I/O switches
pages 0xc1 - 0xcf: slot ROMs
pages 0xd0 - 0xdf: Basic ROM
pages 0xe0 - 0xff: monitor ROM
*/
AppleMMU::AppleMMU(AppleDisplay *display)
{
anyKeyDown = false;
keyboardStrobe = 0x00;
isOpenApplePressed = false;
isClosedApplePressed = false;
for (int8_t i=0; i<=7; i++) {
slots[i] = NULL;
}
allocateMemory();
this->display = display;
this->display->setSwitches(&switches);
resetRAM(); // initialize RAM, load ROM
}
AppleMMU::~AppleMMU()
{
delete display;
// FIXME: clean up the memory we allocated
}
void AppleMMU::Reset()
{
resetRAM();
resetDisplay(); // sets the switches properly
}
uint8_t AppleMMU::read(uint16_t address)
{
if (address >= 0xC000 &&
address <= 0xC0FF) {
return readSwitches(address);
}
uint8_t res = readPages[(address & 0xFF00) >> 8][address & 0xFF];
return res;
}
// Bypass MMU and read directly from a given page - also bypasses switches
uint8_t AppleMMU::readDirect(uint16_t address, uint8_t fromPage)
{
return ramPages[(address & 0xFF00) >> 8][fromPage][address & 0xFF];
}
void AppleMMU::write(uint16_t address, uint8_t v)
{
if (address >= 0xC000 &&
address <= 0xC0FF) {
return writeSwitches(address, v);
}
// Don't allow writes to ROM
if (address >= 0xC100 && address <= 0xCFFF)
return;
if (address >= 0xD000 && address <= 0xFFFF && !writebsr) {
// memory-protected, so don't allow writes
return;
}
writePages[(address & 0xFF00) >> 8][address & 0xFF] = v;
if (address >= 0x400 &&
address <= 0x7FF) {
display->writeLores(address, v);
return;
}
if (address >= 0x2000 &&
address <= 0x5FFF) {
display->writeHires(address, v);
}
}
// FIXME: this is no longer "MMU", is it?
void AppleMMU::resetDisplay()
{
updateMemoryPages();
display->modeChange();
}
void AppleMMU::handleMemorySwitches(uint16_t address, uint16_t lastSwitch)
{
// many of these are spelled out here:
// http://apple2.org.za/gswv/a2zine/faqs/csa2pfaq.html
switch (address) {
// These are write-only and perform no action on read
case 0xC000: // CLR80STORE
switches &= ~S_80STORE;
updateMemoryPages();
break;
case 0xC001: // SET80STORE
switches |= S_80STORE;
updateMemoryPages();
break;
case 0xC002: // CLRAUXRD read from main 48k RAM
auxRamRead = false;
break;
case 0xC003: // SETAUXRD read from aux/alt 48k
auxRamRead = true;
break;
case 0xC004: // CLRAUXWR write to main 48k RAM
auxRamWrite = false;
break;
case 0xC005: // SETAUXWR write to aux/alt 48k
auxRamWrite = true;
break;
case 0xC006: // CLRCXROM use ROM on cards
intcxrom = false;
break;
case 0xC007: // SETCXROM use internal ROM
intcxrom = true;
break;
case 0xC008: // CLRAUXZP use main zero page, stack, LC
altzp = false;
break;
case 0xC009: // SETAUXZP use alt zero page, stack, LC
altzp = true;
break;
case 0xC00A: // CLRC3ROM use internal slot 3 ROM
slot3rom = false;
break;
case 0xC00B: // SETC3ROM use external slot 3 ROM
slot3rom = true;
break;
// these are probably read/write ?
case 0xC080: // READBSR2 and shadow copy
case 0xC084:
bank1 = false; // LC RAM bank2, Read and Write-prot RAM
readbsr = true; // read from bank2
writebsr = false; // write-protected
break;
case 0xC081: // ROMIN
case 0xC085:
bank1 = false; // LC RAM bank2, read ROM, write-enable RAM
readbsr = false; // if it's read twice in a row
writebsr = writebsr || ((lastSwitch & 0xF3) == (address & 0xF3));
break;
case 0xC082:
case 0xC086:
bank1 = false; // LC RAM bank2, Read ROM instead of RAM,
readbsr = false; // WR-protect RAM
writebsr = false;
break;
case 0xC083: // READWRBSR2
case 0xC087:
bank1 = false;
readbsr = true;
writebsr = writebsr || ((lastSwitch & 0xF3) == (address & 0xF3));
break;
case 0xC088: // READBSR1
case 0xC08C:
bank1 = true;
readbsr = true;
writebsr = false;
break;
case 0xC089: // WRITEBSR1
case 0xC08D:
bank1 = true;
readbsr = false;
writebsr = writebsr || ((lastSwitch & 0xF3) == (address & 0xF3));
break;
case 0xC08A: // OFFBSR1
case 0xC08E:
bank1 = true;
readbsr = false;
writebsr = false;
break;
case 0xC08B: // READWRBSR1
case 0xC08F:
bank1 = true;
readbsr = true;
writebsr = writebsr || ((lastSwitch & 0xF3) == (address & 0xF3));
break;
}
updateMemoryPages();
}
// many (most? all?) switches are documented here:
// http://apple2.org.za/gswv/a2zine/faqs/csa2pfaq.html
uint8_t AppleMMU::readSwitches(uint16_t address)
{
static uint16_t lastReadSwitch = 0x0000;
static uint16_t thisReadSwitch = 0x0000;
lastReadSwitch = thisReadSwitch;
thisReadSwitch = address;
// If this is a read for any of the slot switches, and we have
// hardware in that slot, then return its result.
if (address >= 0xC090 && address <= 0xC0FF) {
for (uint8_t i=1; i<=7; i++) {
if (address >= (0xC080 | (i << 4)) &&
address <= (0xC08F | (i << 4))) {
if (slots[i]) {
return slots[i]->readSwitches(address & ~(0xC080 | (i<<4)));
}
else
return FLOATING;
}
}
}
switch (address) {
case 0xC010:
// consume the keyboard strobe flag
keyboardStrobe &= 0x7F;
return (anyKeyDown ? 0x80 : 0x00);
#if 0
case 0xC000:
case 0xC001:
case 0xC002:
case 0xC003:
case 0xC004:
case 0xC005:
case 0xC006:
case 0xC007:
case 0xC008:
case 0xC009:
case 0xC00A:
case 0xC00B:
// nothing happens in read mode for these, so don't call this:
// handleMemorySwitches(address, lastReadSwitch);
case 0xC00C:
case 0xC00D:
case 0xC00E:
case 0xC00F:
// But according to UTA2E, these ALL return keyboard strobe data.
// We do that after the switch statement so we get the other affects
// from reads to these addresses within the switch statement.
break;
#endif
case 0xC080:
case 0xC081:
case 0xC082:
case 0xC083:
case 0xC084:
case 0xC085:
case 0xC086:
case 0xC087:
case 0xC088:
case 0xC089:
case 0xC08A:
case 0xC08B:
case 0xC08C:
case 0xC08D:
case 0xC08E:
case 0xC08F:
// but read does affect these, same as write
handleMemorySwitches(address, lastReadSwitch);
break;
case 0xC00C: // CLR80VID disable 80-col video mode
if (switches & S_80COL) {
switches &= ~S_80COL;
resetDisplay();
}
break;
case 0xC00D: // SET80VID enable 80-col video mode
if (!(switches & S_80COL)) {
switches |= S_80COL;
resetDisplay();
}
break;
case 0xC00E: // CLRALTCH use main char set - norm LC, flash UC
switches &= ~S_ALTCH;
break;
case 0xC00F: // SETALTCH use alt char set - norm inverse, LC; no flash
switches |= S_ALTCH;
break;
case 0xC011: // RDLCBNK2
return bank1 ? 0x00 : 0x80;
case 0xC012: // RDLCRAM
return readbsr ? 0x80 : 0x00;
case 0xC013: // RDRAMRD
return auxRamRead ? 0x80 : 0x00;
case 0xC014: // RDRAMWR
return auxRamWrite ? 0x80 : 0x00;
case 0xC015: // RDCXROM
return intcxrom ? 0x80 : 0x00;
case 0xC016: // RDAUXZP
return altzp ? 0x80 : 0x00;
case 0xC017: // RDC3ROM
return slot3rom ? 0x80 : 0x00;
case 0xC018: // RD80COL
return (switches & S_80STORE) ? 0x80 : 0x00;
// 0xC019: RDVBLBAR -- is the vert