2017-02-20 18:55:16 +00:00
|
|
|
#include <ctype.h> // isgraph
|
2020-07-06 20:46:14 +00:00
|
|
|
#include <DMAChannel.h>
|
|
|
|
|
2017-02-20 18:55:16 +00:00
|
|
|
#include "teensy-display.h"
|
|
|
|
|
|
|
|
#include "bios-font.h"
|
2018-01-07 19:43:17 +00:00
|
|
|
#include "appleui.h"
|
2020-07-06 20:46:14 +00:00
|
|
|
#include <SPI.h>
|
2017-02-20 18:55:16 +00:00
|
|
|
|
2020-07-08 20:36:54 +00:00
|
|
|
#define _clock 75000000
|
2020-07-06 11:04:22 +00:00
|
|
|
|
|
|
|
#define PIN_RST 8
|
|
|
|
#define PIN_DC 9
|
|
|
|
#define PIN_CS 10
|
|
|
|
#define PIN_MOSI 11
|
|
|
|
#define PIN_MISO 12
|
|
|
|
#define PIN_SCK 13
|
2017-02-20 18:55:16 +00:00
|
|
|
|
2020-07-06 20:46:14 +00:00
|
|
|
// Inside the 320x240 display, the Apple display is 280x192.
|
|
|
|
// (That's half the "correct" width, b/c of double-hi-res.)
|
|
|
|
#define apple_display_w 280
|
|
|
|
#define apple_display_h 192
|
|
|
|
|
|
|
|
// Inset inside the apple2 "frame" where we draw the display
|
|
|
|
// remember these are "starts at pixel number" values, where 0 is the first.
|
|
|
|
#define HOFFSET 18
|
|
|
|
#define VOFFSET 13
|
2017-02-20 18:55:16 +00:00
|
|
|
|
|
|
|
#include "globals.h"
|
|
|
|
#include "applevm.h"
|
|
|
|
|
2020-07-08 21:44:25 +00:00
|
|
|
volatile DMAMEM uint16_t dmaBuffer[240][320]; // 240 rows, 320 columns
|
2020-07-06 20:46:14 +00:00
|
|
|
|
2020-07-08 21:44:25 +00:00
|
|
|
#define RGBto565(r,g,b) ((((r) & 0xF8) << 8) | (((g) & 0xFC) << 3) | ((b) >> 3))
|
2020-07-09 06:38:03 +00:00
|
|
|
#define _565toR(c) ( ((c) & 0xF800) >> 8 )
|
|
|
|
#define _565toG(c) ( ((c) & 0x07E0) >> 5 )
|
|
|
|
#define _565toB(c) ( ((c) & 0x001F) )
|
2020-07-06 20:46:14 +00:00
|
|
|
|
2020-07-06 11:04:22 +00:00
|
|
|
ILI9341_t3 tft = ILI9341_t3(PIN_CS, PIN_DC, PIN_RST, PIN_MOSI, PIN_SCK, PIN_MISO);
|
|
|
|
|
2020-07-06 20:46:14 +00:00
|
|
|
DMAChannel dmatx;
|
|
|
|
DMASetting dmaSetting;
|
|
|
|
|
2017-02-20 18:55:16 +00:00
|
|
|
// RGB map of each of the lowres colors
|
2020-07-06 11:04:22 +00:00
|
|
|
const uint16_t loresPixelColors[16] = { 0x0000, // 0 black
|
|
|
|
0xC006, // 1 magenta
|
|
|
|
0x0010, // 2 dark blue
|
|
|
|
0xA1B5, // 3 purple
|
|
|
|
0x0480, // 4 dark green
|
|
|
|
0x6B4D, // 5 dark grey
|
|
|
|
0x1B9F, // 6 med blue
|
|
|
|
0x0DFD, // 7 light blue
|
|
|
|
0x92A5, // 8 brown
|
|
|
|
0xF8C5, // 9 orange
|
|
|
|
0x9555, // 10 light gray
|
|
|
|
0xFCF2, // 11 pink
|
|
|
|
0x07E0, // 12 green
|
|
|
|
0xFFE0, // 13 yellow
|
|
|
|
0x87F0, // 14 aqua
|
|
|
|
0xFFFF // 15 white
|
2018-02-18 01:44:04 +00:00
|
|
|
};
|
|
|
|
|
2020-07-06 11:04:22 +00:00
|
|
|
const uint16_t loresPixelColorsGreen[16] = { 0x0000,
|
|
|
|
0x0140,
|
|
|
|
0x0040,
|
|
|
|
0x0280,
|
|
|
|
0x0300,
|
|
|
|
0x0340,
|
|
|
|
0x0300,
|
|
|
|
0x0480,
|
|
|
|
0x02C0,
|
|
|
|
0x0240,
|
|
|
|
0x0500,
|
|
|
|
0x0540,
|
|
|
|
0x0580,
|
|
|
|
0x0700,
|
|
|
|
0x0680,
|
|
|
|
0x07C0
|
2018-02-18 01:44:04 +00:00
|
|
|
};
|
|
|
|
|
2020-07-06 11:04:22 +00:00
|
|
|
const uint16_t loresPixelColorsWhite[16] = { 0x0000,
|
|
|
|
0x2945,
|
|
|
|
0x0841,
|
|
|
|
0x528A,
|
|
|
|
0x630C,
|
|
|
|
0x6B4D,
|
|
|
|
0x630C,
|
|
|
|
0x9492,
|
|
|
|
0x5ACB,
|
|
|
|
0x4A49,
|
|
|
|
0xA514,
|
|
|
|
0xAD55,
|
|
|
|
0xB596,
|
|
|
|
0xE71C,
|
|
|
|
0xD69A,
|
|
|
|
0xFFDF
|
2017-02-20 18:55:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
TeensyDisplay::TeensyDisplay()
|
|
|
|
{
|
2020-07-06 20:46:14 +00:00
|
|
|
memset(dmaBuffer, 0x80, sizeof(dmaBuffer));
|
2018-02-18 01:44:04 +00:00
|
|
|
|
2020-07-06 11:04:22 +00:00
|
|
|
tft.begin();
|
|
|
|
tft.setRotation(3);
|
|
|
|
tft.setClock(_clock);
|
2017-02-20 18:55:16 +00:00
|
|
|
|
2020-07-06 20:46:14 +00:00
|
|
|
// Set up automatic DMA transfers. cf.
|
2020-07-06 11:04:22 +00:00
|
|
|
// https://forum.pjrc.com/threads/25778-Could-there-be-something-like-an-ISR-template-function/page4
|
2020-07-06 20:46:14 +00:00
|
|
|
#if 0
|
|
|
|
dmaSetting.TCD->CSR = 0;
|
|
|
|
dmaSetting.TCD->SADDR = dmaBuffer;
|
|
|
|
dmaSetting.TCD->SOFF = 2; // 2 bytes per pixel
|
|
|
|
dmaSetting.TCD->ATTR_SRC = 1;
|
|
|
|
dmaSetting.TCD->NBYTES = 2;
|
|
|
|
dmaSetting.TCD->SLAST = -320*240*2;
|
|
|
|
dmaSetting.TCD->BITER = 320*240;
|
|
|
|
dmaSetting.TCD->CITER = 320*240;
|
|
|
|
|
|
|
|
dmaSetting.TCD->DADDR = &LPSPI4_TDR; // FIXME is this correct?
|
|
|
|
dmaSetting.TCD->DOFF = 0;
|
|
|
|
dmaSetting.TCD->ATTR_DST = 1;
|
|
|
|
dmaSetting.TCD->DLASTSGA = 0;
|
|
|
|
|
|
|
|
// Make it loop on itself
|
|
|
|
dmaSetting.replaceSettingsOnCompletion(dmaSetting);
|
|
|
|
|
|
|
|
dmatx.begin(false);
|
|
|
|
dmatx.triggerAtHardwareEvent(DMAMUX_SOURCE_LPSPI4_TX); // FIXME what's the right source ID
|
|
|
|
dmatx = &dmaSetting;
|
|
|
|
#endif
|
2020-07-06 11:04:22 +00:00
|
|
|
|
2017-02-20 18:55:16 +00:00
|
|
|
// LCD initialization complete
|
|
|
|
|
2020-07-06 20:46:14 +00:00
|
|
|
tft.fillScreen(ILI9341_BLACK);
|
2017-02-20 18:55:16 +00:00
|
|
|
|
2017-02-27 01:25:47 +00:00
|
|
|
driveIndicator[0] = driveIndicator[1] = false;
|
|
|
|
driveIndicatorDirty = true;
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TeensyDisplay::~TeensyDisplay()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void TeensyDisplay::redraw()
|
|
|
|
{
|
2018-01-07 19:43:17 +00:00
|
|
|
g_ui->drawStaticUIElement(UIeOverlay);
|
2017-02-20 18:55:16 +00:00
|
|
|
|
|
|
|
if (g_vm) {
|
2018-01-07 19:43:17 +00:00
|
|
|
g_ui->drawOnOffUIElement(UIeDisk1_state, ((AppleVM *)g_vm)->DiskName(0)[0] == '\0');
|
|
|
|
g_ui->drawOnOffUIElement(UIeDisk2_state, ((AppleVM *)g_vm)->DiskName(1)[0] == '\0');
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TeensyDisplay::clrScr()
|
|
|
|
{
|
2020-07-06 20:46:14 +00:00
|
|
|
memset(dmaBuffer, 0x00, sizeof(dmaBuffer));
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
|
2018-02-18 01:44:04 +00:00
|
|
|
void TeensyDisplay::drawUIPixel(uint16_t x, uint16_t y, uint16_t color)
|
|
|
|
{
|
2020-07-08 21:44:25 +00:00
|
|
|
// These pixels are just cached in the buffer; they're not drawn directly.
|
|
|
|
dmaBuffer[y][x] = color;
|
2018-02-18 01:44:04 +00:00
|
|
|
}
|
|
|
|
|
2017-02-20 18:55:16 +00:00
|
|
|
void TeensyDisplay::drawPixel(uint16_t x, uint16_t y, uint16_t color)
|
|
|
|
{
|
2020-07-06 11:04:22 +00:00
|
|
|
tft.drawPixel(x,y,color);
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TeensyDisplay::drawPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b)
|
|
|
|
{
|
|
|
|
uint16_t color16 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
|
|
|
|
|
2020-07-06 11:04:22 +00:00
|
|
|
drawPixel(x,y,color16);
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
|
2020-07-06 20:46:14 +00:00
|
|
|
void TeensyDisplay::flush()
|
2017-02-20 18:55:16 +00:00
|
|
|
{
|
2020-07-06 20:46:14 +00:00
|
|
|
blit({0,0,191,279});
|
|
|
|
}
|
2018-02-18 01:44:04 +00:00
|
|
|
|
2020-07-07 01:31:37 +00:00
|
|
|
void TeensyDisplay::blit()
|
2020-07-06 20:46:14 +00:00
|
|
|
{
|
|
|
|
// The goal here is for blitting to happen automatically in DMA transfers.
|
2018-02-18 01:44:04 +00:00
|
|
|
|
2020-07-06 20:46:14 +00:00
|
|
|
// Since that isn't the case yet, here's a manual blit of the whole
|
|
|
|
// screen (b/c the rect is kinda meaningless in the final "draw
|
|
|
|
// everything always" DMA mode)
|
|
|
|
tft.writeRect(0,0,320,240,(const uint16_t *)dmaBuffer);
|
|
|
|
|
|
|
|
// draw overlay, if any, occasionally
|
|
|
|
{
|
|
|
|
static uint32_t nextMessageTime = 0;
|
|
|
|
if (millis() >= nextMessageTime) {
|
|
|
|
if (overlayMessage[0]) {
|
|
|
|
drawString(M_SELECTDISABLED, 1, 240 - 16 - 12, overlayMessage);
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
2020-07-06 20:46:14 +00:00
|
|
|
nextMessageTime = millis() + 1000;
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-07 01:31:37 +00:00
|
|
|
void TeensyDisplay::blit(AiieRect r)
|
|
|
|
{
|
|
|
|
// It's probably faster to just blit the whole thing, rather than a piece,
|
|
|
|
// because of how it streams data easily when the buffer aligns properly.
|
|
|
|
tft.writeRect(0,0,320,240,(const uint16_t *)dmaBuffer);
|
|
|
|
|
|
|
|
// ... but if we wanted to blit just part, we'd have to create a new
|
|
|
|
// subset of teh dmaBuffer that has the right row length to match
|
|
|
|
// the rect width we're blitting, and then do something like this:
|
|
|
|
//
|
|
|
|
// tft.writeRect(r.left+HOFFSET,,r.top+VOFFSET,r.right-r.left+HOFFSET,r.bottom-r.top+VOFFSET,(const uint16_t *)some_subset_of_dmaBuffer);
|
|
|
|
}
|
|
|
|
|
2017-02-20 18:55:16 +00:00
|
|
|
void TeensyDisplay::drawCharacter(uint8_t mode, uint16_t x, uint8_t y, char c)
|
|
|
|
{
|
|
|
|
int8_t xsize = 8,
|
|
|
|
ysize = 0x0C,
|
|
|
|
offset = 0x20;
|
|
|
|
uint16_t temp;
|
|
|
|
|
|
|
|
c -= offset;// font starts with a space
|
|
|
|
|
|
|
|
uint16_t offPixel, onPixel;
|
|
|
|
switch (mode) {
|
|
|
|
case M_NORMAL:
|
|
|
|
onPixel = 0xFFFF;
|
|
|
|
offPixel = 0x0010;
|
|
|
|
break;
|
|
|
|
case M_SELECTED:
|
|
|
|
onPixel = 0x0000;
|
|
|
|
offPixel = 0xFFFF;
|
|
|
|
break;
|
|
|
|
case M_DISABLED:
|
|
|
|
default:
|
|
|
|
onPixel = 0x7BEF;
|
|
|
|
offPixel = 0x0000;
|
|
|
|
break;
|
|
|
|
case M_SELECTDISABLED:
|
|
|
|
onPixel = 0x7BEF;
|
|
|
|
offPixel = 0xFFE0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
temp=(c*ysize);
|
2018-02-07 15:20:26 +00:00
|
|
|
|
2017-02-20 18:55:16 +00:00
|
|
|
for (int8_t y_off = 0; y_off <= ysize; y_off++) {
|
|
|
|
uint8_t ch = pgm_read_byte(&BiosFont[temp]);
|
|
|
|
for (int8_t x_off = 0; x_off <= xsize; x_off++) {
|
|
|
|
if (ch & (1 << (7-x_off))) {
|
2020-07-06 20:46:14 +00:00
|
|
|
dmaBuffer[y+y_off][x+x_off] = onPixel;
|
2017-02-20 18:55:16 +00:00
|
|
|
} else {
|
2020-07-06 20:46:14 +00:00
|
|
|
dmaBuffer[y+y_off][x+x_off] = offPixel;
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TeensyDisplay::drawString(uint8_t mode, uint16_t x, uint8_t y, const char *str)
|
|
|
|
{
|
|
|
|
int8_t xsize = 8; // width of a char in this font
|
|
|
|
|
|
|
|
for (int8_t i=0; i<strlen(str); i++) {
|
|
|
|
drawCharacter(mode, x, y, str[i]);
|
|
|
|
x += xsize; // fixme: any inter-char spacing?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-07 19:43:17 +00:00
|
|
|
void TeensyDisplay::drawImageOfSizeAt(const uint8_t *img,
|
|
|
|
uint16_t sizex, uint8_t sizey,
|
|
|
|
uint16_t wherex, uint8_t wherey)
|
2017-02-20 18:55:16 +00:00
|
|
|
{
|
2018-01-07 19:43:17 +00:00
|
|
|
uint8_t r, g, b;
|
2017-02-20 18:55:16 +00:00
|
|
|
|
2018-01-07 19:43:17 +00:00
|
|
|
for (uint8_t y=0; y<sizey; y++) {
|
|
|
|
for (uint16_t x=0; x<sizex; x++) {
|
|
|
|
r = pgm_read_byte(&img[(y*sizex + x)*3 + 0]);
|
|
|
|
g = pgm_read_byte(&img[(y*sizex + x)*3 + 1]);
|
|
|
|
b = pgm_read_byte(&img[(y*sizex + x)*3 + 2]);
|
2020-07-06 20:46:14 +00:00
|
|
|
dmaBuffer[y+wherey][x+wherex] = RGBto565(r,g,b);
|
2017-02-20 18:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-18 01:44:04 +00:00
|
|
|
|
|
|
|
// "DoubleWide" means "please double the X because I'm in low-res
|
|
|
|
// width mode". But we only have half the horizontal width required on
|
2020-07-06 20:46:14 +00:00
|
|
|
// the Teensy, so it's divided in half.
|
2018-02-18 01:44:04 +00:00
|
|
|
void TeensyDisplay::cacheDoubleWidePixel(uint16_t x, uint16_t y, uint8_t color)
|
|
|
|
{
|
2020-07-06 20:46:14 +00:00
|
|
|
uint16_t color16;
|
|
|
|
color16 = loresPixelColors[(( color & 0x0F ) )];
|
|
|
|
dmaBuffer[y+VOFFSET][x+HOFFSET] = color16;
|
2018-02-18 01:44:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// This exists for 4bpp optimization. We could totally call
|
|
|
|
// cacheDoubleWidePixel twice, but the (x&1) pfutzing is messy if
|
|
|
|
// we're just storing both halves anyway...
|
|
|
|
void TeensyDisplay::cache2DoubleWidePixels(uint16_t x, uint16_t y,
|
|
|
|
uint8_t colorA, uint8_t colorB)
|
|
|
|
{
|
2020-07-06 20:46:14 +00:00
|
|
|
// FIXME: Convert 4-bit colors to 16-bit colors?
|
|
|
|
dmaBuffer[y+VOFFSET][x+ HOFFSET] = loresPixelColors[colorB&0xF];
|
|
|
|
dmaBuffer[y+VOFFSET][x+1+HOFFSET] = loresPixelColors[colorA&0xF];
|
2018-02-18 01:44:04 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 06:38:03 +00:00
|
|
|
inline double logfn(double x)
|
|
|
|
{
|
|
|
|
// At a value of x=255, log(base 1.022)(x) is 254.636.
|
|
|
|
return log(x)/log(1.022);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inline uint16_t blendColors(uint16_t a, uint16_t b)
|
|
|
|
{
|
|
|
|
// Straight linear average doesn't work well for inverted text, because the
|
|
|
|
// whites overwhelm the blacks.
|
|
|
|
//return ((uint32_t)a + (uint32_t)b)/2;
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
// Testing a logarithmic color scale. My theory was that, since our
|
|
|
|
// colors here are mostly black or white, it would be reasonable to
|
|
|
|
// use a log scale of the average to bump up the brightness a
|
|
|
|
// little. In practice, it's not really legible.
|
|
|
|
return RGBto565( (uint8_t)(logfn((_565toR(a) + _565toR(b))/2)),
|
|
|
|
(uint8_t)(logfn((_565toG(a) + _565toG(b))/2)),
|
|
|
|
(uint8_t)(logfn((_565toB(a) + _565toB(b))/2)) );
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// Doing an R/G/B average works okay for legibility. It's not great for
|
|
|
|
// inverted text.
|
|
|
|
return RGBto565( (_565toR(a) + _565toR(b))/2,
|
|
|
|
(_565toG(a) + _565toG(b))/2,
|
|
|
|
(_565toB(a) + _565toB(b))/2 );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-02-18 01:44:04 +00:00
|
|
|
// This is the full 560-pixel-wide version -- and we only have 280
|
2020-07-06 20:46:14 +00:00
|
|
|
// pixels in our buffer b/c the display is only 320 pixels wide
|
|
|
|
// itself. So we'll divide x by 2. On odd-numbered X pixels, we also
|
2020-07-09 06:38:03 +00:00
|
|
|
// blend the colors of the two virtual pixels that share an onscreen
|
|
|
|
// pixel
|
2018-02-18 01:44:04 +00:00
|
|
|
void TeensyDisplay::cachePixel(uint16_t x, uint16_t y, uint8_t color)
|
|
|
|
{
|
2020-07-09 06:38:03 +00:00
|
|
|
#if 0
|
|
|
|
static uint8_t previousColor = 0;
|
|
|
|
#endif
|
|
|
|
if (x&1) {
|
|
|
|
// Blend the two pixels. This takes advantage of the fact that we
|
|
|
|
// always call this linearly for 80-column text drawing -- we never
|
|
|
|
// do partial screen blits, but always draw at least a whole character.
|
|
|
|
// So we can look at the pixel in the "shared" cell of RAM, and come up
|
|
|
|
// with a color between the two.
|
|
|
|
|
|
|
|
#if 1
|
|
|
|
// This is straight blending, R/G/B average
|
|
|
|
uint16_t origColor = dmaBuffer[y+VOFFSET][(x>>1)+HOFFSET];
|
|
|
|
uint16_t newColor = loresPixelColors[color];
|
|
|
|
cacheDoubleWidePixel(x>>1, y, blendColors(origColor, newColor));
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
// The model we use for the SDL display works better, strangely - it keeps
|
|
|
|
// the lores pixel index color (black, magenda, dark blue, purple, dark
|
|
|
|
// green, etc.) until render time; so when it does the blend here, it's
|
|
|
|
// actually blending in a very nonlinear way - e.g. "black + white / 2"
|
|
|
|
// is actually "black(0) + white(15) / 2 = 15/2 = 7 (light blue)". Weird,
|
|
|
|
// but definitely legible in a mini laptop SDL window with the same scale.
|
|
|
|
// Unfortunately, it doesn't translate well to a ILI9341 panel; the pixels
|
|
|
|
// are kind of muddy and indistinct, so the blue spills over and makes it
|
|
|
|
// very difficult to read.
|
|
|
|
uint8_t origColor = previousColor;
|
|
|
|
uint8_t newColor = (uint16_t)(origColor + color) / 2;
|
|
|
|
cacheDoubleWidePixel(x>>1, y, (uint16_t)color + (uint16_t)previousColor/2);
|
|
|
|
#endif
|
2018-02-18 01:44:04 +00:00
|
|
|
} else {
|
2020-07-09 06:38:03 +00:00
|
|
|
#if 0
|
|
|
|
previousColor = color; // used for blending
|
|
|
|
#endif
|
|
|
|
cacheDoubleWidePixel(x>>1, y, color);
|
2018-02-18 01:44:04 +00:00
|
|
|
}
|
|
|
|
}
|