From a0c58545e1dc0d1a5962a894151148224981f655 Mon Sep 17 00:00:00 2001 From: "Mr.Blinky" Date: Sun, 19 Nov 2017 19:41:30 +0100 Subject: [PATCH] Multiple OLED support added multiple oled display support added flexible width and height support optimized buttonsState with optional bootlkey support --- Arduboy2.cpp | 1322 ++++++++++++++++++++++++++++++++++++++++++++++ Arduboy2Core.cpp | 696 ++++++++++++++++++++++++ Arduboy2Core.h | 735 ++++++++++++++++++++++++++ Sprites.cpp | 362 +++++++++++++ Sprites.h | 235 ++++++++ 5 files changed, 3350 insertions(+) create mode 100644 Arduboy2.cpp create mode 100644 Arduboy2Core.cpp create mode 100644 Arduboy2Core.h create mode 100644 Sprites.cpp create mode 100644 Sprites.h diff --git a/Arduboy2.cpp b/Arduboy2.cpp new file mode 100644 index 0000000..e02f269 --- /dev/null +++ b/Arduboy2.cpp @@ -0,0 +1,1322 @@ +/** + * @file Arduboy2.cpp + * \brief + * The Arduboy2Base and Arduboy2 classes and support objects and definitions. + */ + +#include "Arduboy2.h" +#include "ab_logo.c" +#include "glcdfont.c" + +//======================================== +//========== class Arduboy2Base ========== +//======================================== + +uint8_t Arduboy2Base::sBuffer[]; + +Arduboy2Base::Arduboy2Base() +{ + currentButtonState = 0; + previousButtonState = 0; + // frame management + setFrameRate(60); + frameCount = -1; + nextFrameStart = 0; + justRendered = false; + // init not necessary, will be reset after first use + // lastFrameStart + // lastFrameDurationMs +} + +// functions called here should be public so users can create their +// own init functions if they need different behavior than `begin` +// provides by default +void Arduboy2Base::begin() +{ + boot(); // raw hardware + + blank(); // blank the display + + flashlight(); // light the RGB LED and screen if UP button is being held. + + // check for and handle buttons held during start up for system control + systemButtons(); + + audio.begin(); + + bootLogo(); + // alternative logo functions. Work the same a bootLogo() but may reduce + // memory size if the sketch uses the same bitmap drawing function +// bootLogoCompressed(); +// bootLogoSpritesSelfMasked(); +// bootLogoSpritesOverwrite(); + + // wait for all buttons to be released + do { + delayShort(50); + } while (buttonsState()); +} + +void Arduboy2Base::flashlight() +{ + if (!pressed(UP_BUTTON)) { + return; + } + + sendLCDCommand(OLED_ALL_PIXELS_ON); // smaller than allPixelsOn() + digitalWriteRGB(RGB_ON, RGB_ON, RGB_ON); + + // prevent the bootloader magic number from being overwritten by timer 0 + // when a timer variable overlaps the magic number location, for when + // flashlight mode is used for upload problem recovery + power_timer0_disable(); + + while (true) { + idle(); + } +} + +void Arduboy2Base::systemButtons() +{ + while (pressed(B_BUTTON)) { + digitalWriteRGB(BLUE_LED, RGB_ON); // turn on blue LED + sysCtrlSound(UP_BUTTON + B_BUTTON, GREEN_LED, 0xff); + sysCtrlSound(DOWN_BUTTON + B_BUTTON, RED_LED, 0); + delayShort(200); + } + + digitalWriteRGB(BLUE_LED, RGB_OFF); // turn off blue LED +} + +void Arduboy2Base::sysCtrlSound(uint8_t buttons, uint8_t led, uint8_t eeVal) +{ + if (pressed(buttons)) { + digitalWriteRGB(BLUE_LED, RGB_OFF); // turn off blue LED + delayShort(200); + digitalWriteRGB(led, RGB_ON); // turn on "acknowledge" LED + EEPROM.update(EEPROM_AUDIO_ON_OFF, eeVal); + delayShort(500); + digitalWriteRGB(led, RGB_OFF); // turn off "acknowledge" LED + + while (pressed(buttons)) { } // Wait for button release + } +} + +void Arduboy2Base::bootLogo() +{ + bootLogoShell(drawLogoBitmap); +} + +void Arduboy2Base::drawLogoBitmap(int16_t y) +{ + drawBitmap(20 - (64 - WIDTH / 2), y, arduboy_logo, 88, 16); +} + +void Arduboy2Base::bootLogoCompressed() +{ + bootLogoShell(drawLogoCompressed); +} + +void Arduboy2Base::drawLogoCompressed(int16_t y) +{ + drawCompressed(20 - (64 - WIDTH / 2), y, arduboy_logo_compressed); +} + +void Arduboy2Base::bootLogoSpritesSelfMasked() +{ + bootLogoShell(drawLogoSpritesSelfMasked); +} + +void Arduboy2Base::drawLogoSpritesSelfMasked(int16_t y) +{ + Sprites::drawSelfMasked(20 - (64 - WIDTH / 2), y, arduboy_logo_sprite, 0); +} + +void Arduboy2Base::bootLogoSpritesOverwrite() +{ + bootLogoShell(drawLogoSpritesOverwrite); +} + +void Arduboy2Base::drawLogoSpritesOverwrite(int16_t y) +{ + Sprites::drawOverwrite(20 - (64 - WIDTH / 2), y, arduboy_logo_sprite, 0); +} + +// bootLogoText() should be kept in sync with bootLogoShell() +// if changes are made to one, equivalent changes should be made to the other +void Arduboy2Base::bootLogoShell(void (*drawLogo)(int16_t)) +{ + digitalWriteRGB(RED_LED, RGB_ON); + + for (int16_t y = -18; y <= 24; y++) { + if (pressed(RIGHT_BUTTON)) { + digitalWriteRGB(RGB_OFF, RGB_OFF, RGB_OFF); // all LEDs off + return; + } + + if (y == -4) { + digitalWriteRGB(RED_LED, RGB_OFF); // red LED off + digitalWriteRGB(GREEN_LED, RGB_ON); // green LED on + } + else if (y == 24) { + digitalWriteRGB(GREEN_LED, RGB_OFF); // green LED off + digitalWriteRGB(BLUE_LED, RGB_ON); // blue LED on + } + + clear(); + (*drawLogo)(y); // call the function that actually draws the logo + display(); + delayShort(27); + // longer delay post boot, we put it inside the loop to + // save the flash calling clear/delay again outside the loop + if (y==-16) { + delayShort(250); + } + } + + delayShort(700); + digitalWriteRGB(BLUE_LED, RGB_OFF); + + bootLogoExtra(); +} + +// Virtual function overridden by derived class +void Arduboy2Base::bootLogoExtra() { } + +/* Frame management */ + +void Arduboy2Base::setFrameRate(uint8_t rate) +{ + eachFrameMillis = 1000 / rate; +} + +bool Arduboy2Base::everyXFrames(uint8_t frames) +{ + return frameCount % frames == 0; +} + +bool Arduboy2Base::nextFrame() +{ + unsigned long now = millis(); + bool tooSoonForNextFrame = now < nextFrameStart; + + if (justRendered) { + lastFrameDurationMs = now - lastFrameStart; + justRendered = false; + return false; + } + else if (tooSoonForNextFrame) { + // if we have MORE than 1ms to spare (hence our comparison with 2), + // lets sleep for power savings. We don't compare against 1 to avoid + // potential rounding errors - say we're actually 0.5 ms away, but a 1 + // is returned if we go to sleep we might sleep a full 1ms and then + // we'd be running the frame slighly late. So the last 1ms we stay + // awake for perfect timing. + + // This is likely trading power savings for absolute timing precision + // and the power savings might be the better goal. At 60 FPS trusting + // chance here might actually achieve a "truer" 60 FPS than the 16ms + // frame duration we get due to integer math. + + // We should be woken up by timer0 every 1ms, so it's ok to sleep. + if ((uint8_t)(nextFrameStart - now) >= 2) + idle(); + + return false; + } + + // pre-render + justRendered = true; + lastFrameStart = now; + nextFrameStart = now + eachFrameMillis; + frameCount++; + + return true; +} + +bool Arduboy2Base::nextFrameDEV() +{ + bool ret = nextFrame(); + + if (ret) { + if (lastFrameDurationMs > eachFrameMillis) + TXLED1; + else + TXLED0; + } + return ret; +} + +int Arduboy2Base::cpuLoad() +{ + return lastFrameDurationMs*100 / eachFrameMillis; +} + +void Arduboy2Base::initRandomSeed() +{ + power_adc_enable(); // ADC on + + // do an ADC read from an unconnected input pin + ADCSRA |= _BV(ADSC); // start conversion (ADMUX has been pre-set in boot()) + while (bit_is_set(ADCSRA, ADSC)) { } // wait for conversion complete + + randomSeed(((unsigned long)ADC << 16) + micros()); + + power_adc_disable(); // ADC off +} + +/* Graphics */ + +void Arduboy2Base::clear() +{ + fillScreen(BLACK); +} + + +// Used by drawPixel to help with left bitshifting since AVR has no +// multiple bit shift instruction. We can bit shift from a lookup table +// in flash faster than we can calculate the bit shifts on the CPU. +const uint8_t bitshift_left[] PROGMEM = { + _BV(0), _BV(1), _BV(2), _BV(3), _BV(4), _BV(5), _BV(6), _BV(7) +}; + +void Arduboy2Base::drawPixel(int16_t x, int16_t y, uint8_t color) +{ + #ifdef PIXEL_SAFE_MODE + if (x < 0 || x > (WIDTH-1) || y < 0 || y > (HEIGHT-1)) + { + return; + } + #endif + + uint16_t row_offset; + uint8_t bit; + + // uint8_t row = (uint8_t)y / 8; + // row_offset = (row*WIDTH) + (uint8_t)x; + // bit = _BV((uint8_t)y % 8); + + // the above math can also be rewritten more simply as; + // row_offset = (y * WIDTH/8) & ~0b01111111 + (uint8_t)x; + // which is what the below assembler does + + // local variable for the bitshift_left array pointer, + // which can be declared a read-write operand + const uint8_t* bsl = bitshift_left; + + asm volatile + ( + "mov r16,%A[y] \n" + "andi r16, 0xF8 \n" + "mul %[width_offset], r16 \n" + "movw %[row_offset], r0 \n" + "clr __zero_reg__ \n" + "add %A[row_offset], %[x] \n" + "adc %B[row_offset], __zero_reg__ \n" + // mask for only 0-7 + "andi %A[y], 0x07 \n" + // Z += y + "add r30, %A[y] \n" + "adc r31, __zero_reg__ \n" + // load correct bitshift from program RAM + "lpm %[bit], Z \n" + : [row_offset] "=&x" (row_offset), // upper register (ANDI) + [bit] "=r" (bit), + [y] "+d" (y), // upper register (ANDI), must be writable + "+z" (bsl) // is modified to point to the proper shift array element + : [width_offset] "r" ((uint8_t)(WIDTH/8)), + [x] "r" ((uint8_t)x) + : "r16" + ); + + if (color) { + sBuffer[row_offset] |= bit; + } else { + sBuffer[row_offset] &= ~ bit; + } +} + +uint8_t Arduboy2Base::getPixel(uint8_t x, uint8_t y) +{ + uint8_t row = y / 8; + uint8_t bit_position = y % 8; + return (sBuffer[(row*WIDTH) + x] & _BV(bit_position)) >> bit_position; +} + +void Arduboy2Base::drawCircle(int16_t x0, int16_t y0, uint8_t r, uint8_t color) +{ + int16_t f = 1 - r; + int16_t ddF_x = 1; + int16_t ddF_y = -2 * r; + int16_t x = 0; + int16_t y = r; + + drawPixel(x0, y0+r, color); + drawPixel(x0, y0-r, color); + drawPixel(x0+r, y0, color); + drawPixel(x0-r, y0, color); + + while (x= 0) + { + y--; + ddF_y += 2; + f += ddF_y; + } + + x++; + ddF_x += 2; + f += ddF_x; + + drawPixel(x0 + x, y0 + y, color); + drawPixel(x0 - x, y0 + y, color); + drawPixel(x0 + x, y0 - y, color); + drawPixel(x0 - x, y0 - y, color); + drawPixel(x0 + y, y0 + x, color); + drawPixel(x0 - y, y0 + x, color); + drawPixel(x0 + y, y0 - x, color); + drawPixel(x0 - y, y0 - x, color); + } +} + +void Arduboy2Base::drawCircleHelper +(int16_t x0, int16_t y0, uint8_t r, uint8_t corners, uint8_t color) +{ + int16_t f = 1 - r; + int16_t ddF_x = 1; + int16_t ddF_y = -2 * r; + int16_t x = 0; + int16_t y = r; + + while (x= 0) + { + y--; + ddF_y += 2; + f += ddF_y; + } + + x++; + ddF_x += 2; + f += ddF_x; + + if (corners & 0x4) // lower right + { + drawPixel(x0 + x, y0 + y, color); + drawPixel(x0 + y, y0 + x, color); + } + if (corners & 0x2) // upper right + { + drawPixel(x0 + x, y0 - y, color); + drawPixel(x0 + y, y0 - x, color); + } + if (corners & 0x8) // lower left + { + drawPixel(x0 - y, y0 + x, color); + drawPixel(x0 - x, y0 + y, color); + } + if (corners & 0x1) // upper left + { + drawPixel(x0 - y, y0 - x, color); + drawPixel(x0 - x, y0 - y, color); + } + } +} + +void Arduboy2Base::fillCircle(int16_t x0, int16_t y0, uint8_t r, uint8_t color) +{ + drawFastVLine(x0, y0-r, 2*r+1, color); + fillCircleHelper(x0, y0, r, 3, 0, color); +} + +void Arduboy2Base::fillCircleHelper +(int16_t x0, int16_t y0, uint8_t r, uint8_t sides, int16_t delta, + uint8_t color) +{ + int16_t f = 1 - r; + int16_t ddF_x = 1; + int16_t ddF_y = -2 * r; + int16_t x = 0; + int16_t y = r; + + while (x < y) + { + if (f >= 0) + { + y--; + ddF_y += 2; + f += ddF_y; + } + + x++; + ddF_x += 2; + f += ddF_x; + + if (sides & 0x1) // right side + { + drawFastVLine(x0+x, y0-y, 2*y+1+delta, color); + drawFastVLine(x0+y, y0-x, 2*x+1+delta, color); + } + + if (sides & 0x2) // left side + { + drawFastVLine(x0-x, y0-y, 2*y+1+delta, color); + drawFastVLine(x0-y, y0-x, 2*x+1+delta, color); + } + } +} + +void Arduboy2Base::drawLine +(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) +{ + // bresenham's algorithm - thx wikpedia + bool steep = abs(y1 - y0) > abs(x1 - x0); + if (steep) { + swap(x0, y0); + swap(x1, y1); + } + + if (x0 > x1) { + swap(x0, x1); + swap(y0, y1); + } + + int16_t dx, dy; + dx = x1 - x0; + dy = abs(y1 - y0); + + int16_t err = dx / 2; + int8_t ystep; + + if (y0 < y1) + { + ystep = 1; + } + else + { + ystep = -1; + } + + for (; x0 <= x1; x0++) + { + if (steep) + { + drawPixel(y0, x0, color); + } + else + { + drawPixel(x0, y0, color); + } + + err -= dy; + if (err < 0) + { + y0 += ystep; + err += dx; + } + } +} + +void Arduboy2Base::drawRect +(int16_t x, int16_t y, uint8_t w, uint8_t h, uint8_t color) +{ + drawFastHLine(x, y, w, color); + drawFastHLine(x, y+h-1, w, color); + drawFastVLine(x, y, h, color); + drawFastVLine(x+w-1, y, h, color); +} + +void Arduboy2Base::drawFastVLine +(int16_t x, int16_t y, uint8_t h, uint8_t color) +{ + int end = y+h; + for (int a = max(0,y); a < min(end,HEIGHT); a++) + { + drawPixel(x,a,color); + } +} + +void Arduboy2Base::drawFastHLine +(int16_t x, int16_t y, uint8_t w, uint8_t color) +{ + int16_t xEnd; // last x point + 1 + + // Do y bounds checks + if (y < 0 || y >= HEIGHT) + return; + + xEnd = x + w; + + // Check if the entire line is not on the display + if (xEnd <= 0 || x >= WIDTH) + return; + + // Don't start before the left edge + if (x < 0) + x = 0; + + // Don't end past the right edge + if (xEnd > WIDTH) + xEnd = WIDTH; + + // calculate actual width (even if unchanged) + w = xEnd - x; + + // buffer pointer plus row offset + x offset + register uint8_t *pBuf = sBuffer + ((y / 8) * WIDTH) + x; + + // pixel mask + register uint8_t mask = 1 << (y & 7); + + switch (color) + { + case WHITE: + while (w--) + { + *pBuf++ |= mask; + } + break; + + case BLACK: + mask = ~mask; + while (w--) + { + *pBuf++ &= mask; + } + break; + } +} + +void Arduboy2Base::fillRect +(int16_t x, int16_t y, uint8_t w, uint8_t h, uint8_t color) +{ + // stupidest version - update in subclasses if desired! + for (int16_t i=x; i= y1 >= y0) + if (y0 > y1) + { + swap(y0, y1); swap(x0, x1); + } + if (y1 > y2) + { + swap(y2, y1); swap(x2, x1); + } + if (y0 > y1) + { + swap(y0, y1); swap(x0, x1); + } + + if(y0 == y2) + { // Handle awkward all-on-same-line case as its own thing + a = b = x0; + if(x1 < a) + { + a = x1; + } + else if(x1 > b) + { + b = x1; + } + if(x2 < a) + { + a = x2; + } + else if(x2 > b) + { + b = x2; + } + drawFastHLine(a, y0, b-a+1, color); + return; + } + + int16_t dx01 = x1 - x0, + dy01 = y1 - y0, + dx02 = x2 - x0, + dy02 = y2 - y0, + dx12 = x2 - x1, + dy12 = y2 - y1, + sa = 0, + sb = 0; + + // For upper part of triangle, find scanline crossings for segments + // 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1 + // is included here (and second loop will be skipped, avoiding a /0 + // error there), otherwise scanline y1 is skipped here and handled + // in the second loop...which also avoids a /0 error here if y0=y1 + // (flat-topped triangle). + if (y1 == y2) + { + last = y1; // Include y1 scanline + } + else + { + last = y1-1; // Skip it + } + + + for(y = y0; y <= last; y++) + { + a = x0 + sa / dy01; + b = x0 + sb / dy02; + sa += dx01; + sb += dx02; + + if(a > b) + { + swap(a,b); + } + + drawFastHLine(a, y, b-a+1, color); + } + + // For lower part of triangle, find scanline crossings for segments + // 0-2 and 1-2. This loop is skipped if y1=y2. + sa = dx12 * (y - y1); + sb = dx02 * (y - y0); + + for(; y <= y2; y++) + { + a = x1 + sa / dy12; + b = x0 + sb / dy02; + sa += dx12; + sb += dx02; + + if(a > b) + { + swap(a,b); + } + + drawFastHLine(a, y, b-a+1, color); + } +} + +void Arduboy2Base::drawBitmap +(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t w, uint8_t h, + uint8_t color) +{ + // no need to draw at all if we're offscreen + if (x+w < 0 || x > WIDTH-1 || y+h < 0 || y > HEIGHT-1) + return; + + int yOffset = abs(y) % 8; + int sRow = y / 8; + if (y < 0) { + sRow--; + yOffset = 8 - yOffset; + } + int rows = h/8; + if (h%8!=0) rows++; + for (int a = 0; a < rows; a++) { + int bRow = sRow + a; + if (bRow > (HEIGHT/8)-1) break; + if (bRow > -2) { + for (int iCol = 0; iCol (WIDTH-1)) break; + if (iCol + x >= 0) { + if (bRow >= 0) { + if (color == WHITE) + sBuffer[(bRow*WIDTH) + x + iCol] |= pgm_read_byte(bitmap+(a*w)+iCol) << yOffset; + else if (color == BLACK) + sBuffer[(bRow*WIDTH) + x + iCol] &= ~(pgm_read_byte(bitmap+(a*w)+iCol) << yOffset); + else + sBuffer[(bRow*WIDTH) + x + iCol] ^= pgm_read_byte(bitmap+(a*w)+iCol) << yOffset; + } + if (yOffset && bRow<(HEIGHT/8)-1 && bRow > -2) { + if (color == WHITE) + sBuffer[((bRow+1)*WIDTH) + x + iCol] |= pgm_read_byte(bitmap+(a*w)+iCol) >> (8-yOffset); + else if (color == BLACK) + sBuffer[((bRow+1)*WIDTH) + x + iCol] &= ~(pgm_read_byte(bitmap+(a*w)+iCol) >> (8-yOffset)); + else + sBuffer[((bRow+1)*WIDTH) + x + iCol] ^= pgm_read_byte(bitmap+(a*w)+iCol) >> (8-yOffset); + } + } + } + } + } +} + + +void Arduboy2Base::drawSlowXYBitmap +(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t w, uint8_t h, uint8_t color) +{ + // no need to draw at all of we're offscreen + if (x+w < 0 || x > WIDTH-1 || y+h < 0 || y > HEIGHT-1) + return; + + int16_t xi, yi, byteWidth = (w + 7) / 8; + for(yi = 0; yi < h; yi++) { + for(xi = 0; xi < w; xi++ ) { + if(pgm_read_byte(bitmap + yi * byteWidth + xi / 8) & (128 >> (xi & 7))) { + drawPixel(x + xi, y + yi, color); + } + } + } +} + +typedef struct CSESSION { + int byte; + int bit; + const uint8_t *src; + int src_pos; +} CSESSION; +static CSESSION cs; + +static int getval(int bits) +{ + int val = 0; + int i; + for (i = 0; i < bits; i++) + { + if (cs.bit == 0x100) + { + cs.bit = 0x1; + cs.byte = pgm_read_byte(&cs.src[cs.src_pos]); + cs.src_pos ++; + } + if (cs.byte & cs.bit) + val += (1 << i); + cs.bit <<= 1; + } + return val; +} + +void Arduboy2Base::drawCompressed(int16_t sx, int16_t sy, const uint8_t *bitmap, uint8_t color) +{ + int bl, len; + int col; + int i; + int a, iCol; + int byte = 0; + int bit = 0; + int w, h; + + // set up decompress state + + cs.src = bitmap; + cs.bit = 0x100; + cs.byte = 0; + cs.src_pos = 0; + + // read header + + w = getval(8) + 1; + h = getval(8) + 1; + + col = getval(1); // starting colour + + // no need to draw at all if we're offscreen + if (sx + w < 0 || sx > WIDTH - 1 || sy + h < 0 || sy > HEIGHT - 1) + return; + + // sy = sy - (frame*h); + + int yOffset = abs(sy) % 8; + int sRow = sy / 8; + if (sy < 0) { + sRow--; + yOffset = 8 - yOffset; + } + int rows = h / 8; + if (h % 8 != 0) rows++; + + a = 0; // +(frame*rows); + iCol = 0; + + byte = 0; bit = 1; + while (a < rows) // + (frame*rows)) + { + bl = 1; + while (!getval(1)) + bl += 2; + + len = getval(bl) + 1; // span length + + // draw the span + + + for (i = 0; i < len; i++) + { + if (col) + byte |= bit; + bit <<= 1; + + if (bit == 0x100) // reached end of byte + { + // draw + + int bRow = sRow + a; + + //if (byte) // possible optimisation + if (bRow <= (HEIGHT / 8) - 1) + if (bRow > -2) + if (iCol + sx <= (WIDTH - 1)) + if (iCol + sx >= 0) { + + if (bRow >= 0) + { + if (color) + sBuffer[(bRow * WIDTH) + sx + iCol] |= byte << yOffset; + else + sBuffer[(bRow * WIDTH) + sx + iCol] &= ~(byte << yOffset); + } + if (yOffset && bRow < (HEIGHT / 8) - 1 && bRow > -2) + { + if (color) + sBuffer[((bRow + 1)*WIDTH) + sx + iCol] |= byte >> (8 - yOffset); + else + sBuffer[((bRow + 1)*WIDTH) + sx + iCol] &= ~(byte >> (8 - yOffset)); + } + } + + // iterate + iCol ++; + if (iCol >= w) + { + iCol = 0; + a ++; + } + + // reset byte + byte = 0; bit = 1; + } + } + + col = 1 - col; // toggle colour for next span + } +} + +void Arduboy2Base::display() +{ + paintScreen(sBuffer); +} + +void Arduboy2Base::display(bool clear) +{ + paintScreen(sBuffer, clear); +} + +uint8_t* Arduboy2Base::getBuffer() +{ + return sBuffer; +} + +bool Arduboy2Base::pressed(uint8_t buttons) +{ + return (buttonsState() & buttons) == buttons; +} + +bool Arduboy2Base::notPressed(uint8_t buttons) +{ + return (buttonsState() & buttons) == 0; +} + +void Arduboy2Base::pollButtons() +{ + previousButtonState = currentButtonState; + currentButtonState = buttonsState(); +} + +bool Arduboy2Base::justPressed(uint8_t button) +{ + return (!(previousButtonState & button) && (currentButtonState & button)); +} + +bool Arduboy2Base::justReleased(uint8_t button) +{ + return ((previousButtonState & button) && !(currentButtonState & button)); +} + +bool Arduboy2Base::collide(Point point, Rect rect) +{ + return ((point.x >= rect.x) && (point.x < rect.x + rect.width) && + (point.y >= rect.y) && (point.y < rect.y + rect.height)); +} + +bool Arduboy2Base::collide(Rect rect1, Rect rect2) +{ + return !(rect2.x >= rect1.x + rect1.width || + rect2.x + rect2.width <= rect1.x || + rect2.y >= rect1.y + rect1.height || + rect2.y + rect2.height <= rect1.y); +} + +uint16_t Arduboy2Base::readUnitID() +{ + return EEPROM.read(EEPROM_UNIT_ID) | + (((uint16_t)(EEPROM.read(EEPROM_UNIT_ID + 1))) << 8); +} + +void Arduboy2Base::writeUnitID(uint16_t id) +{ + EEPROM.update(EEPROM_UNIT_ID, (uint8_t)(id & 0xff)); + EEPROM.update(EEPROM_UNIT_ID + 1, (uint8_t)(id >> 8)); +} + +uint8_t Arduboy2Base::readUnitName(char* name) +{ + char val; + uint8_t dest; + uint8_t src = EEPROM_UNIT_NAME; + + for (dest = 0; dest < ARDUBOY_UNIT_NAME_LEN; dest++) + { + val = EEPROM.read(src); + name[dest] = val; + src++; + if (val == 0x00 || (byte)val == 0xFF) { + break; + } + } + + name[dest] = 0x00; + return dest; +} + +void Arduboy2Base::writeUnitName(char* name) +{ + bool done = false; + uint8_t dest = EEPROM_UNIT_NAME; + + for (uint8_t src = 0; src < ARDUBOY_UNIT_NAME_LEN; src++) + { + if (name[src] == 0x00) { + done = true; + } + // write character or 0 pad if finished + EEPROM.update(dest, done ? 0x00 : name[src]); + dest++; + } +} + +bool Arduboy2Base::readShowUnitNameFlag() +{ + return (EEPROM.read(EEPROM_SYS_FLAGS) & SYS_FLAG_UNAME_MASK); +} + +void Arduboy2Base::writeShowUnitNameFlag(bool val) +{ + uint8_t flags = EEPROM.read(EEPROM_SYS_FLAGS); + + bitWrite(flags, SYS_FLAG_UNAME, val); + EEPROM.update(EEPROM_SYS_FLAGS, flags); +} + +void Arduboy2Base::swap(int16_t& a, int16_t& b) +{ + int16_t temp = a; + a = b; + b = temp; +} + + +//==================================== +//========== class Arduboy2 ========== +//==================================== + +Arduboy2::Arduboy2() +{ + cursor_x = 0; + cursor_y = 0; + textColor = 1; + textBackground = 0; + textSize = 1; + textWrap = 0; +} + +// bootLogoText() should be kept in sync with bootLogoShell() +// if changes are made to one, equivalent changes should be made to the other +void Arduboy2::bootLogoText() +{ + digitalWriteRGB(RED_LED, RGB_ON); + + textSize = 2; + + for (int8_t y = -18; y <= 24; y++) { + if (pressed(RIGHT_BUTTON)) { + digitalWriteRGB(RGB_OFF, RGB_OFF, RGB_OFF); // all LEDs off + textSize = 1; + return; + } + + if (y == -4) { + digitalWriteRGB(RED_LED, RGB_OFF); // red LED off + digitalWriteRGB(GREEN_LED, RGB_ON); // green LED on + } + else if (y == 24) { + digitalWriteRGB(GREEN_LED, RGB_OFF); // green LED off + digitalWriteRGB(BLUE_LED, RGB_ON); // blue LED on + } + + clear(); + cursor_x = 23 - (64 - WIDTH / 2); + cursor_y = y; + print(F("ARDUBOY")); + display(); + delayShort(27); + // longer delay post boot, we put it inside the loop to + // save the flash calling clear/delay again outside the loop + if (y==-16) { + delayShort(250); + } + } + + delayShort(700); + digitalWriteRGB(BLUE_LED, RGB_OFF); + textSize = 1; + + bootLogoExtra(); +} + +void Arduboy2::bootLogoExtra() +{ + uint8_t c; + + if (!readShowUnitNameFlag()) + { + return; + } + + c = EEPROM.read(EEPROM_UNIT_NAME); + + if (c != 0xFF && c != 0x00) + { + uint8_t i = EEPROM_UNIT_NAME; + cursor_x = 50 - (64 - WIDTH / 2); + cursor_y = 56; + + do + { + write(c); + c = EEPROM.read(++i); + } + while (i < EEPROM_UNIT_NAME + ARDUBOY_UNIT_NAME_LEN); + + display(); + delayShort(1000); + } +} + +size_t Arduboy2::write(uint8_t c) +{ + if (c == '\n') + { + cursor_y += textSize * 8; + cursor_x = 0; + } + else if (c == '\r') + { + // skip em + } + else + { + drawChar(cursor_x, cursor_y, c, textColor, textBackground, textSize); + cursor_x += textSize * 6; + if (textWrap && (cursor_x > (WIDTH - textSize * 6))) + { + // calling ourselves recursively for 'newline' is + // 12 bytes smaller than doing the same math here + write('\n'); + } + } + return 1; +} + +void Arduboy2::drawChar + (int16_t x, int16_t y, unsigned char c, uint8_t color, uint8_t bg, uint8_t size) +{ + uint8_t line; + bool draw_background = bg != color; + const unsigned char* bitmap = font + c * 5; + + if ((x >= WIDTH) || // Clip right + (y >= HEIGHT) || // Clip bottom + ((x + 5 * size - 1) < 0) || // Clip left + ((y + 8 * size - 1) < 0) // Clip top + ) + { + return; + } + + for (uint8_t i = 0; i < 6; i++ ) + { + line = pgm_read_byte(bitmap++); + if (i == 5) { + line = 0x0; + } + + for (uint8_t j = 0; j < 8; j++) + { + uint8_t draw_color = (line & 0x1) ? color : bg; + + if (draw_color || draw_background) { + for (uint8_t a = 0; a < size; a++ ) { + for (uint8_t b = 0; b < size; b++ ) { + drawPixel(x + (i * size) + a, y + (j * size) + b, draw_color); + } + } + } + line >>= 1; + } + } +} + +void Arduboy2::setCursor(int16_t x, int16_t y) +{ + cursor_x = x; + cursor_y = y; +} + +int16_t Arduboy2::getCursorX() +{ + return cursor_x; +} + +int16_t Arduboy2::getCursorY() +{ + return cursor_y; +} + +void Arduboy2::setTextColor(uint8_t color) +{ + textColor = color; +} + +uint8_t Arduboy2::getTextColor() +{ + return textColor; +} + +void Arduboy2::setTextBackground(uint8_t bg) +{ + textBackground = bg; +} + +uint8_t Arduboy2::getTextBackground() +{ + return textBackground; +} + +void Arduboy2::setTextSize(uint8_t s) +{ + // size must always be 1 or higher + textSize = max(1, s); +} + +uint8_t Arduboy2::getTextSize() +{ + return textSize; +} + +void Arduboy2::setTextWrap(bool w) +{ + textWrap = w; +} + +bool Arduboy2::getTextWrap() +{ + return textWrap; +} + +void Arduboy2::clear() +{ + Arduboy2Base::clear(); + cursor_x = cursor_y = 0; +} + diff --git a/Arduboy2Core.cpp b/Arduboy2Core.cpp new file mode 100644 index 0000000..659efa8 --- /dev/null +++ b/Arduboy2Core.cpp @@ -0,0 +1,696 @@ +/** + * @file Arduboy2Core.cpp + * \brief + * The Arduboy2Core class for Arduboy hardware initilization and control. + */ + +#include "Arduboy2Core.h" +#include + +const uint8_t PROGMEM lcdBootProgram[] = { + // boot defaults are commented out but left here in case they + // might prove useful for reference + // Further reading: https://www.adafruit.com/datasheets/SSD1306.pdf + +#ifdef OLED_SH1106 + 0x8D, 0x14, // Charge Pump Setting v = enable (0x14) + 0xA1, // Set Segment Re-map + 0xC8, // Set COM Output Scan Direction + 0x81, 0xCF, // Set Contrast v = 0xCF + 0xD9, 0xF1, // Set Precharge = 0xF1 + OLED_SET_COLUMN_ADDRESS_LO, //Set column address for left most pixel + 0xAF // Display On +#elif defined(OLED_96X96) + 0x15, 0x10, 0x3f, //Set column start and end address + 0x75, 0x00, 0x5f, //Set row start and end address + 0xA0, 0x55, //set re-map: split odd-even COM signals|COM remap|vertical address increment|column address remap + 0xA1, 0x00, //set display start line + 0xA2, 0x60, //set display offset + //0xA4, //Normal display + 0xA8, 0x5F, //Set MUX ratio 96MUX + //0xB2, 0x23, + //0xB3, 0xF0, //set devider clock | oscillator frequency + //0x81, 0x0F, //Set contrast + //0xBC, 0x1F, //set precharge voltage + //0x82, 0xFE, //set second Precharge speed + 0xB1, 0x21, //reset and 1st precharge phase length phase 2:2 DCLKs, Phase 1: 1 DCLKs + //0xBB, 0x0F, //set 2nd precharge period: 15 DCLKs + //0xbe, 0x1F, //output level high voltage com signal + //0xB8, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, //set gray scale table + 0xAF //Display on +#else + // for SSD1306 and compatible displays + // + // Display Off + // 0xAE, + + // Set Display Clock Divisor v = 0xF0 + // default is 0x80 + 0xD5, 0xF0, + + // Set Multiplex Ratio v = 0x3F + // 0xA8, 0x3F, + + // Set Display Offset v = 0 + // 0xD3, 0x00, + + // Set Start Line (0) + // 0x40, + + // Charge Pump Setting v = enable (0x14) + // default is disabled + 0x8D, 0x14, + + // Set Segment Re-map (A0) | (b0001) + // default is (b0000) + 0xA1, + + // Set COM Output Scan Direction + 0xC8, + + // Set COM Pins v + // 0xDA, 0x12, + + // Set Contrast v = 0xCF + 0x81, 0xCF, + + // Set Precharge = 0xF1 + 0xD9, 0xF1, + + // Set VCom Detect + // 0xDB, 0x40, + + // Entire Display ON + // 0xA4, + + // Set normal/inverse display + // 0xA6, + + // Display On + 0xAF, + + // set display mode = horizontal addressing mode (0x00) + 0x20, 0x00, + + // set col address range + // 0x21, 0x00, COLUMN_ADDRESS_END, + + // set page address range + // 0x22, 0x00, PAGE_ADDRESS_END +#endif + +#if defined OLED_SSD1309 //required additionally for SSD1309 + 0x21, 0x00, COLUMN_ADDRESS_END +#endif +}; + +Arduboy2Core::Arduboy2Core() { } + +void Arduboy2Core::boot() +{ + #ifdef ARDUBOY_SET_CPU_8MHZ + // ARDUBOY_SET_CPU_8MHZ will be set by the IDE using boards.txt + setCPUSpeed8MHz(); + #endif + + // Select the ADC input here so a delay isn't required in initRandomSeed() + ADMUX = RAND_SEED_IN_ADMUX; + + bootPins(); + bootSPI(); + bootOLED(); + bootPowerSaving(); +} + +#ifdef ARDUBOY_SET_CPU_8MHZ +// If we're compiling for 8MHz we need to slow the CPU down because the +// hardware clock on the Arduboy is 16MHz. +// We also need to readjust the PLL prescaler because the Arduino USB code +// likely will have incorrectly set it for an 8MHz hardware clock. +void Arduboy2Core::setCPUSpeed8MHz() +{ + uint8_t oldSREG = SREG; + cli(); // suspend interrupts + PLLCSR = _BV(PINDIV); // dissable the PLL and set prescale for 16MHz) + CLKPR = _BV(CLKPCE); // allow reprogramming clock + CLKPR = 1; // set clock divisor to 2 (0b0001) + PLLCSR = _BV(PLLE) | _BV(PINDIV); // enable the PLL (with 16MHz prescale) + SREG = oldSREG; // restore interrupts +} +#endif + +// Pins are set to the proper modes and levels for the specific hardware. +// This routine must be modified if any pins are moved to a different port +void Arduboy2Core::bootPins() +{ +#ifdef ARDUBOY_10 + + // Port B INPUT_PULLUP or HIGH + PORTB |= _BV(RED_LED_BIT) | + #ifndef ARDUINO_AVR_PROMICRO + _BV(GREEN_LED_BIT) | + #endif + _BV(BLUE_LED_BIT) | _BV(B_BUTTON_BIT); + // Port B INPUT or LOW (none) + // Port B inputs + DDRB &= ~(_BV(B_BUTTON_BIT)); + // Port B outputs + DDRB |= _BV(RED_LED_BIT) | + #ifndef ARDUINO_AVR_PROMICRO + _BV(GREEN_LED_BIT) | + #endif + _BV(BLUE_LED_BIT) | _BV(SPI_MOSI_BIT) | _BV(SPI_SCK_BIT); + + // Port C + // Speaker: Not set here. Controlled by audio class + + // Port D INPUT_PULLUP or HIGH + #ifdef ARDUINO_AVR_PROMICRO + PORTD |= _BV(CS_BIT) | _BV(GREEN_LED_BIT); + #else + PORTD |= _BV(CS_BIT); + #endif + // Port D INPUT or LOW + PORTD &= ~(_BV(RST_BIT)); + // Port D inputs (none) + // Port D outputs + DDRD |= _BV(RST_BIT) | _BV(CS_BIT) | + #ifdef ARDUINO_AVR_PROMICRO + _BV(GREEN_LED_BIT) | + #endif + _BV(DC_BIT); + + // Port E INPUT_PULLUP or HIGH + PORTE |= _BV(A_BUTTON_BIT); + // Port E INPUT or LOW (none) + // Port E inputs + DDRE &= ~(_BV(A_BUTTON_BIT)); + // Port E outputs (none) + + // Port F INPUT_PULLUP or HIGH + PORTF |= _BV(LEFT_BUTTON_BIT) | _BV(RIGHT_BUTTON_BIT) | + _BV(UP_BUTTON_BIT) | _BV(DOWN_BUTTON_BIT); + // Port F INPUT or LOW + PORTF &= ~(_BV(RAND_SEED_IN_BIT)); + // Port F inputs + DDRF &= ~(_BV(LEFT_BUTTON_BIT) | _BV(RIGHT_BUTTON_BIT) | + _BV(UP_BUTTON_BIT) | _BV(DOWN_BUTTON_BIT) | + _BV(RAND_SEED_IN_BIT)); + // Port F outputs (none) + +#elif defined(AB_DEVKIT) + + // Port B INPUT_PULLUP or HIGH + PORTB |= _BV(LEFT_BUTTON_BIT) | _BV(UP_BUTTON_BIT) | _BV(DOWN_BUTTON_BIT) | + _BV(BLUE_LED_BIT); + // Port B INPUT or LOW (none) + // Port B inputs + DDRB &= ~(_BV(LEFT_BUTTON_BIT) | _BV(UP_BUTTON_BIT) | _BV(DOWN_BUTTON_BIT)); + // Port B outputs + DDRB |= _BV(BLUE_LED_BIT) | _BV(SPI_MOSI_BIT) | _BV(SPI_SCK_BIT); + + // Port C INPUT_PULLUP or HIGH + PORTC |= _BV(RIGHT_BUTTON_BIT); + // Port C INPUT or LOW (none) + // Port C inputs + DDRC &= ~(_BV(RIGHT_BUTTON_BIT)); + // Port C outputs (none) + + // Port D INPUT_PULLUP or HIGH + PORTD |= _BV(CS_BIT); + // Port D INPUT or LOW + PORTD &= ~(_BV(RST_BIT)); + // Port D inputs (none) + // Port D outputs + DDRD |= _BV(RST_BIT) | _BV(CS_BIT) | _BV(DC_BIT); + + // Port E (none) + + // Port F INPUT_PULLUP or HIGH + PORTF |= _BV(A_BUTTON_BIT) | _BV(B_BUTTON_BIT); + // Port F INPUT or LOW + PORTF &= ~(_BV(RAND_SEED_IN_BIT)); + // Port F inputs + DDRF &= ~(_BV(A_BUTTON_BIT) | _BV(B_BUTTON_BIT) | _BV(RAND_SEED_IN_BIT)); + // Port F outputs (none) + // Speaker: Not set here. Controlled by audio class + +#endif +} + +void Arduboy2Core::bootOLED() +{ + // reset the display + delayShort(5); // reset pin should be low here. let it stay low a while + bitSet(RST_PORT, RST_BIT); // set high to come out of reset + delayShort(5); // wait a while + + // select the display (permanently, since nothing else is using SPI) + bitClear(CS_PORT, CS_BIT); + + // run our customized boot-up command sequence against the + // OLED to initialize it properly for Arduboy + LCDCommandMode(); + for (uint8_t i = 0; i < sizeof(lcdBootProgram); i++) { + SPItransfer(pgm_read_byte(lcdBootProgram + i)); + } + LCDDataMode(); +} + +void Arduboy2Core::LCDDataMode() +{ + bitSet(DC_PORT, DC_BIT); +} + +void Arduboy2Core::LCDCommandMode() +{ + bitClear(DC_PORT, DC_BIT); +} + +// Initialize the SPI interface for the display +void Arduboy2Core::bootSPI() +{ +// master, mode 0, MSB first, CPU clock / 2 (8MHz) + SPCR = _BV(SPE) | _BV(MSTR); + SPSR = _BV(SPI2X); +} + +// Write to the SPI bus (MOSI pin) +void Arduboy2Core::SPItransfer(uint8_t data) +{ + SPDR = data; + /* + * The following NOP introduces a small delay that can prevent the wait + * loop form iterating when running at the maximum speed. This gives + * about 10% more speed, even if it seems counter-intuitive. At lower + * speeds it is unnoticed. + */ + asm volatile("nop"); + while (!(SPSR & _BV(SPIF))) { } // wait +} + +void Arduboy2Core::safeMode() +{ + if (buttonsState() == UP_BUTTON) + { + digitalWriteRGB(RED_LED, RGB_ON); + + // prevent the bootloader magic number from being overwritten by timer 0 + // when a timer variable overlaps the magic number location + power_timer0_disable(); + + while (true) { } + } +} + + +/* Power Management */ + +void Arduboy2Core::idle() +{ + set_sleep_mode(SLEEP_MODE_IDLE); + sleep_mode(); +} + +void Arduboy2Core::bootPowerSaving() +{ + // disable Two Wire Interface (I2C) and the ADC + PRR0 = _BV(PRTWI) | _BV(PRADC); + // disable USART1 + PRR1 = _BV(PRUSART1); + // All other bits will be written with 0 so will be enabled +} + +// Shut down the display +void Arduboy2Core::displayOff() +{ + LCDCommandMode(); + SPItransfer(0xAE); // display off + SPItransfer(0x8D); // charge pump: + SPItransfer(0x10); // disable + delayShort(250); + bitClear(RST_PORT, RST_BIT); // set display reset pin low (reset state) +} + +// Restart the display after a displayOff() +void Arduboy2Core::displayOn() +{ + bootOLED(); +} + +uint8_t Arduboy2Core::width() { return WIDTH; } + +uint8_t Arduboy2Core::height() { return HEIGHT; } + + +/* Drawing */ + +void Arduboy2Core::paint8Pixels(uint8_t pixels) +{ + SPItransfer(pixels); +} + +void Arduboy2Core::paintScreen(const uint8_t *image) +{ +#ifdef OLED_SH1106 + for (uint8_t i = 0; i < HEIGHT / 8; i++) + { + LCDCommandMode(); + SPDR = (OLED_SET_PAGE_ADDRESS + i); + while (!(SPSR & _BV(SPIF))); + SPDR = (OLED_SET_COLUMN_ADDRESS_HI); // only reset hi nibble to zero + while (!(SPSR & _BV(SPIF))); + LCDDataMode(); + for (uint8_t j = WIDTH; j > 0; j--) + { + SPDR = pgm_read_byte(image++); + while (!(SPSR & _BV(SPIF))); + } + } +#elif defined(OLED_96X96) + uint16_t i = 0; + for (uint8_t col = 0; col < WIDTH / 2; col++) + { + for (uint8_t row = 0; row < HEIGHT / 8; row++) + { + uint8_t b1 = pgm_read_byte(image + i); + uint8_t b2 = pgm_read_byte(image + i + 1); + for (uint8_t shift = 0; shift < 8; shift++) + { + uint8_t c = 0xFF; + if ((b1 & 1) == 0) c &= 0x0F; + if ((b2 & 1) == 0) c &= 0xF0; + SPDR = c; + b1 = b1 >> 1; + b2 = b2 >> 1; + while (!(SPSR & _BV(SPIF))); + } + i += WIDTH; + } + i -= HEIGHT / 8 * WIDTH - 2; + } + +#else + //OLED SSD1306 and compatibles + for (int i = 0; i < (HEIGHT*WIDTH)/8; i++) + { + SPItransfer(pgm_read_byte(image + i)); + } +#endif +} + +// paint from a memory buffer, this should be FAST as it's likely what +// will be used by any buffer based subclass +void Arduboy2Core::paintScreen(uint8_t image[], bool clear) +{ +#ifdef OLED_SH1106 + //Assembly optimized page mode display code with clear support. + //Each byte transfer takes 18 cycles + asm volatile ( + " ldi r25, %[page_cmd] \n\t" + ".l1: \n\t" + " ldi r24, %[width] ;1 \n\t" + " ldi r20,5 ;1 \n\t" + " cbi %[dc_port], %[dc_bit] ;2 cmd mode \n\t" + " out %[spdr], r25 ;1 \n\t" + ".l2: subi r20,1 ;r20*3-1 : 14 \n\t" + " brne .l2 \n\t" + " rjmp .+0 ;2 \n\t" + " ldi r20,%[col_cmd] ;1 \n\t" + " out %[spdr], r20 ;1 \n\t" + ".l3: rjmp .l7 ;2 \n\t" + ".l4: ld r20, Z ;2 \n\t" + " cp %[clear], __zero_reg__ ;1 \n\t" + " brne .l5 ;1/2 \n\t" + " nop ;1 \n\t" + " rjmp .l6 ;2 \n\t" + ".l5: st Z, __zero_reg__ ;2 : 7 \n\t" + ".l6: sbi %[dc_port], %[dc_bit] ;2 data mode \n\t" + " out %[spdr], r20 ;1 \n\t" + " adiw r30, 1 ;2 \n\t" + ".l7: rjmp .+0 ;2 \n\t" + " nop ;1 \n\t" + " subi r24, 1 ;1 \n\t" + " brne .l4 ;1/2 : 5/6 \n\t" + " rjmp .+0 ;2 \n\t" + " subi r25, -1 ;1 \n\t" + " cpi r25,%[page_end] ;1 \n\t" + " brne .l1 ;1/2 : 5/6 \n\t" + : + : [ptr] "z" (image), + [page_cmd] "M" (OLED_SET_PAGE_ADDRESS), + [page_end] "M" (OLED_SET_PAGE_ADDRESS + (HEIGHT / 8)), + [dc_port] "I" (_SFR_IO_ADDR(DC_PORT)), + [dc_bit] "I" (DC_BIT), + [spdr] "I" (_SFR_IO_ADDR(SPDR)), + [col_cmd] "M" (OLED_SET_COLUMN_ADDRESS_HI), + [width] "M" (WIDTH + 1), + [clear] "a" (clear) + : "r20", "r24", "r25" + ); +#elif defined(OLED_96X96) + // 1 bit to 4-bit display code with clear support. + // Each transfer takes 18 cycles with additional 4 cycles for a column change. + asm volatile( + " ldi r25, %[col] \n\t" + ".lcolumn: \n\t" + " ldi r24, %[row] ;1 \n\t" + ".lrow: \n\t" + " ldi r21, 7 ;1 \n\t" + " ld r22, z ;2 \n\t" + " ldd r23, z+1 ;2 \n\t" + ".lshiftstart: \n\t" + " ldi r20, 0xFF ;1 \n\t" + " sbrs r22, 0 ;1 \n\t" + " andi r20, 0x0f ;1 \n\t" + " sbrs r23, 0 ;1 \n\t" + " andi r20,0xf0 ;1 \n\t" + " out %[spdr], r20 ;1 \n\t" + " \n\t" + " cp %[clear], __zero_reg__ ;1 \n\t" + " brne .lclear1 ;1/2 \n\t" + ".lshiftothers: \n\t" + " movw r18, %A[ptr] ;1 \n\t" + " rjmp .+0 ;2 \n\t" + " rjmp .lshiftnext ;2 \n\t" + ".lclear1: \n\t" + " st z, __zero_reg__ ;2 \n\t" + " std z+1, __zero_reg__ ;2 \n\t" + ".lshiftnext: \n\t" + " \n\t" + " lsr r22 ;1 \n\t" + " lsr r23 ;1 \n\t" + " \n\t" + " ldi r20, 0xFF ;1 \n\t" + " sbrs r22, 0 ;1/2 \n\t" + " andi r20, 0x0f ;1 \n\t" + " sbrs r23, 0 ;1/2 \n\t" + " andi r20,0xf0 ;1 \n\t" + " \n\t" + " subi r18, %[top_lsb] ;1 \n\t" //image - (HEIGHT / 8) * ((WIDTH / 8) - 1) + 2 + " sbci r19, %[top_msb] ;1 \n\t" + " subi r21, 1 ;1 \n\t" + " out %[spdr], r20 ;1 \n\t" + " brne .lshiftothers ;1/2 \n\t" + " \n\t" + " nop ;1 \n\t" + " subi %A[ptr], %[width] ;1 \n\t" //image + width (negated addition) + " sbci %B[ptr], -1 ;1 \n\t" + " subi r24, 1 ;1 \n\t" + " brne .lrow ;1/2 \n\t" + " \n\t" + " movw %A[ptr], r18 ;1 \n\t" + " subi r25, 1 ;1 \n\t" + " brne .lcolumn ;1/2 \n\t" + : + : [ptr] "z" (image), + [spdr] "I" (_SFR_IO_ADDR(SPDR)), + [row] "M" (HEIGHT / 8), + [col] "M" (WIDTH / 2), + [width] "M" (256 - WIDTH), + [top_lsb] "M" ((WIDTH * ((HEIGHT / 8) - 1) - 2) & 0xFF), + [top_msb] "M" ((WIDTH * ((HEIGHT / 8) - 1) - 2) >> 8), + [clear] "a" (clear) + : "r18", "r19", "r20", "r21", "r22", "r23", "r24", "r25" + ); +#else + //OLED SSD1306 and compatibles + //data only transfer with clear support at 18 cycles per transfer + asm volatile ( + " ldi r24,%[len_lsb] \n\t" + " ldi r25,%[len_msb] \n\t" + ".l1: ld r20, Z ;2 \n\t" + " out %[spdr], r20 ;1 \n\t" + " cp %[clear], __zero_reg__ ;1 \n\t" //if (clear) *(image++) = 0 + " breq .l2 ;1/2 : 5/6 \n\t" + " st Z+, __zero_reg__ ;2 \n\t" + " rjmp .l3 ;2 \n\t" + ".l2: \n\t" + " adiw r30, 1 ;2 \n\t" // else *(image++) + " nop ;1 \n\t" + ".l3: \n\t" + " rjmp .+0 ;2 \n\t" + " rjmp .+0 ;2 \n\t" + " rjmp .+0 ;2 \n\t" + " sbiw r24, 1 ;1 \n\t" + " brne .l1 ;1/2 : 18 \n\t" + : + : [ptr] "z" (image), + [spdr] "I" (_SFR_IO_ADDR(SPDR)), + [len_msb] "M" (WIDTH * (HEIGHT / 8) >> 8), + [len_lsb] "M" (WIDTH * (HEIGHT / 8) & 0xFF), + [clear] "a" (clear) + : "r20", "r24", "r25" + ); +#endif + while (!(SPSR & _BV(SPIF))); // wait for the last transfer to finish +} + +void Arduboy2Core::blank() +{ +#ifdef OLED_SH1106 + for (int i = 0; i < (HEIGHT*132)/8; i++) + SPItransfer(0x00); +#elif defined(OLED_96X96) + for (int i = 0; i < (HEIGHT*WIDTH)/2; i++) + SPItransfer(0x00); +#else //OLED SSD1306 and compatibles + for (int i = 0; i < (HEIGHT*WIDTH)/8; i++) + SPItransfer(0x00); +#endif +} + +void Arduboy2Core::sendLCDCommand(uint8_t command) +{ + LCDCommandMode(); + SPItransfer(command); + LCDDataMode(); +} + +// invert the display or set to normal +// when inverted, a pixel set to 0 will be on +void Arduboy2Core::invert(bool inverse) +{ + sendLCDCommand(inverse ? OLED_PIXELS_INVERTED : OLED_PIXELS_NORMAL); +} + +// turn all display pixels on, ignoring buffer contents +// or set to normal buffer display +void Arduboy2Core::allPixelsOn(bool on) +{ + sendLCDCommand(on ? OLED_ALL_PIXELS_ON : OLED_PIXELS_FROM_RAM); +} + +// flip the display vertically or set to normal +void Arduboy2Core::flipVertical(bool flipped) +{ + sendLCDCommand(flipped ? OLED_VERTICAL_FLIPPED : OLED_VERTICAL_NORMAL); +} + +// flip the display horizontally or set to normal +void Arduboy2Core::flipHorizontal(bool flipped) +{ + sendLCDCommand(flipped ? OLED_HORIZ_FLIPPED : OLED_HORIZ_NORMAL); +} + +/* RGB LED */ + +void Arduboy2Core::setRGBled(uint8_t red, uint8_t green, uint8_t blue) +{ +#ifdef ARDUBOY_10 // RGB, all the pretty colors + // inversion is necessary because these are common annode LEDs + analogWrite(RED_LED, 255 - red); + analogWrite(GREEN_LED, 255 - green); + analogWrite(BLUE_LED, 255 - blue); +#elif defined(AB_DEVKIT) + // only blue on DevKit, which is not PWM capable + (void)red; // parameter unused + (void)green; // parameter unused + bitWrite(BLUE_LED_PORT, BLUE_LED_BIT, blue ? RGB_ON : RGB_OFF); +#endif +} + +void Arduboy2Core::digitalWriteRGB(uint8_t red, uint8_t green, uint8_t blue) +{ +#ifdef ARDUBOY_10 + bitWrite(RED_LED_PORT, RED_LED_BIT, red); + bitWrite(GREEN_LED_PORT, GREEN_LED_BIT, green); + bitWrite(BLUE_LED_PORT, BLUE_LED_BIT, blue); +#elif defined(AB_DEVKIT) + // only blue on DevKit + (void)red; // parameter unused + (void)green; // parameter unused + bitWrite(BLUE_LED_PORT, BLUE_LED_BIT, blue); +#endif +} + +void Arduboy2Core::digitalWriteRGB(uint8_t color, uint8_t val) +{ +#ifdef ARDUBOY_10 + if (color == RED_LED) + { + bitWrite(RED_LED_PORT, RED_LED_BIT, val); + } + else if (color == GREEN_LED) + { + bitWrite(GREEN_LED_PORT, GREEN_LED_BIT, val); + } + else if (color == BLUE_LED) + { + bitWrite(BLUE_LED_PORT, BLUE_LED_BIT, val); + } +#elif defined(AB_DEVKIT) + // only blue on DevKit + if (color == BLUE_LED) + { + bitWrite(BLUE_LED_PORT, BLUE_LED_BIT, val); + } +#endif +} + +/* Buttons */ + +uint8_t Arduboy2Core::buttonsState() +{ + uint8_t buttons; + + // using ports here is ~100 bytes smaller than digitalRead() +#ifdef AB_DEVKIT + // down, left, up + buttons = ((~PINB) & B01110000); + // right button + if ((PINC & _BV(6)) == 0) buttons |= RIGHT_BUTTON; //compiles to shorter and faster code + + // A and B + if ((PINF & _BV(7)) == 0) buttons |= A_BUTTON; + if ((PINF & _BV(6)) == 0) buttons |= B_BUTTON; +#elif defined(ARDUBOY_10) + // down, up, left right + buttons = ((~PINF) & B11110000); + // A (left) +if ((PINE & _BV(6)) == 0) {buttons |= A_BUTTON;} + // B (right) + if ((PINB & _BV(4)) == 0) {buttons |= B_BUTTON;} +#endif +#ifdef ENABLE_BOOTLOADER_KEYS + //bootloader button combo + if (buttons == (LEFT_BUTTON | UP_BUTTON | A_BUTTON | B_BUTTON)) + { cli(); + //set magic boot key + *(uint8_t *)0x0800 = 0x77;//using uint8_t saves an instruction + *(uint8_t *)0x0801 = 0x77; + //enable and trigger watchdog by timeout + wdt_enable(WDTO_15MS); + while (true); + } +#endif + + return buttons; +} + +// delay in ms with 16 bit duration +void Arduboy2Core::delayShort(uint16_t ms) +{ + delay((unsigned long) ms); +} + diff --git a/Arduboy2Core.h b/Arduboy2Core.h new file mode 100644 index 0000000..f7e03b2 --- /dev/null +++ b/Arduboy2Core.h @@ -0,0 +1,735 @@ +/** + * @file Arduboy2Core.h + * \brief + * The Arduboy2Core class for Arduboy hardware initilization and control. + */ + +#ifndef ARDUBOY2_CORE_H +#define ARDUBOY2_CORE_H + +#include +#include +#include +#include + + +// main hardware compile flags + +#if !defined(ARDUBOY_10) && !defined(AB_DEVKIT) +/* defaults to Arduboy Release 1.0 if not using a boards.txt file + * + * we default to Arduboy Release 1.0 if a compile flag has not been + * passed to us from a boards.txt file + * + * if you wish to compile for the devkit without using a boards.txt + * file simply comment out the ARDUBOY_10 define and uncomment + * the AB_DEVKIT define like this: + * + * // #define ARDUBOY_10 + * #define AB_DEVKIT + */ +#define ARDUBOY_10 //< compile for the production Arduboy v1.0 +// #define AB_DEVKIT //< compile for the official dev kit +#endif + +#define RGB_ON LOW /**< For digitially setting an RGB LED on using digitalWriteRGB() */ +#define RGB_OFF HIGH /**< For digitially setting an RGB LED off using digitalWriteRGB() */ + +// ----- Arduboy pins ----- +#ifdef ARDUBOY_10 + +#ifdef ARDUINO_AVR_PROMICRO + #define PIN_CS 3 // Pro Micro alternative display CS pin + #define CS_PORT PORTD + #define CS_BIT PORTD3 +#else + #define PIN_CS 12 // Display CS Arduino pin number + #define CS_PORT PORTD // Display CS port + #define CS_BIT PORTD6 // Display CS physical bit number +#endif + +#define PIN_DC 4 // Display D/C Arduino pin number +#define DC_PORT PORTD // Display D/C port +#define DC_BIT PORTD4 // Display D/C physical bit number + +#define PIN_RST 6 // Display reset Arduino pin number +#define RST_PORT PORTD // Display reset port +#define RST_BIT PORTD7 // Display reset physical bit number + +#define SPI_MOSI_PORT PORTB +#define SPI_MOSI_BIT PORTB2 + +#define SPI_SCK_PORT PORTB +#define SPI_SCK_BIT PORTB1 + +#define RED_LED 10 /**< The pin number for the red color in the RGB LED. */ +#ifdef ARDUINO_AVR_PROMICRO + #define GREEN_LED 3 // Pro Micro alternative green LED pin +#else + #define GREEN_LED 11 /**< The pin number for the green color in the RGB LED. */ +#endif +#define BLUE_LED 9 /**< The pin number for the blue color in the RGB LED. */ + +#define RED_LED_PORT PORTB +#define RED_LED_BIT PORTB6 + +#ifdef ARDUINO_AVR_PROMICRO + #define GREEN_LED_PORT PORTD // Pro Micro alternative green LED port + #define GREEN_LED_BIT PORTD0 +#else + #define GREEN_LED_PORT PORTB + #define GREEN_LED_BIT PORTB7 +#endif + +#define BLUE_LED_PORT PORTB +#define BLUE_LED_BIT PORTB5 + +// bit values for button states +// these are determined by the buttonsState() function +#define LEFT_BUTTON _BV(5) /**< The Left button value for functions requiring a bitmask */ +#define RIGHT_BUTTON _BV(6) /**< The Right button value for functions requiring a bitmask */ +#define UP_BUTTON _BV(7) /**< The Up button value for functions requiring a bitmask */ +#define DOWN_BUTTON _BV(4) /**< The Down button value for functions requiring a bitmask */ +#define A_BUTTON _BV(3) /**< The A button value for functions requiring a bitmask */ +#define B_BUTTON _BV(2) /**< The B button value for functions requiring a bitmask */ + +#define PIN_LEFT_BUTTON A2 +#define LEFT_BUTTON_PORT PORTF +#define LEFT_BUTTON_BIT PORTF5 + +#define PIN_RIGHT_BUTTON A1 +#define RIGHT_BUTTON_PORT PORTF +#define RIGHT_BUTTON_BIT PORTF6 + +#define PIN_UP_BUTTON A0 +#define UP_BUTTON_PORT PORTF +#define UP_BUTTON_BIT PORTF7 + +#define PIN_DOWN_BUTTON A3 +#define DOWN_BUTTON_PORT PORTF +#define DOWN_BUTTON_BIT PORTF4 + +#define PIN_A_BUTTON 7 +#define A_BUTTON_PORT PORTE +#define A_BUTTON_BIT PORTE6 + +#define PIN_B_BUTTON 8 +#define B_BUTTON_PORT PORTB +#define B_BUTTON_BIT PORTB4 + +#define PIN_SPEAKER_1 5 /**< The pin number of the first lead of the speaker */ + +#define SPEAKER_1_PORT PORTC +#define SPEAKER_1_DDR DDRC +#define SPEAKER_1_BIT PORTC6 + +#ifdef ARDUINO_AVR_PROMICRO + #define PIN_SPEAKER_2 2 //Pro Micro alternative for 2nd speaker pin + #define SPEAKER_2_PORT PORTD + #define SPEAKER_2_DDR DDRD + #define SPEAKER_2_BIT PORTD1 +#else + #define PIN_SPEAKER_2 13 /**< The pin number of the second lead of the speaker */ + #define SPEAKER_2_PORT PORTC + #define SPEAKER_2_DDR DDRC + #define SPEAKER_2_BIT PORTC7 +#endif + +#define RAND_SEED_IN A4 // Open analog input used for noise by initRandomSeed() +#define RAND_SEED_IN_PORTF +#define RAND_SEED_IN_BIT PORTF1 +// Value for ADMUX to read the random seed pin: 2.56V reference, ADC1 +#define RAND_SEED_IN_ADMUX (_BV(REFS0) | _BV(REFS1) | _BV(MUX0)) +// ----------------------- + +// ----- DevKit pins ----- +#elif defined(AB_DEVKIT) + +#define PIN_CS 6 // Display CS Arduino pin number +#define CS_PORT PORTD // Display CS port +#define CS_BIT PORTD7 // Display CS physical bit number + +#define PIN_DC 4 // Display D/C Arduino pin number +#define DC_PORT PORTD // Display D/C port +#define DC_BIT PORTD4 // Display D/C physical bit number + +#define PIN_RST 12 // Display reset Arduino pin number +#define RST_PORT PORTD // Display reset port +#define RST_BIT PORTD6 // Display reset physical bit number + +#define SPI_MOSI_PORT PORTB +#define SPI_MOSI_BIT PORTB2 + +#define SPI_SCK_PORT PORTB +#define SPI_SCK_BIT PORTB1 + +// map all LEDs to the single TX LED on DEVKIT +#define RED_LED 17 +#define GREEN_LED 17 +#define BLUE_LED 17 + +#define BLUE_LED_PORT PORTB +#define BLUE_LED_BIT PORTB0 + +// bit values for button states +// these are determined by the buttonsState() function +#define LEFT_BUTTON _BV(5) +#define RIGHT_BUTTON _BV(2) +#define UP_BUTTON _BV(4) +#define DOWN_BUTTON _BV(6) +#define A_BUTTON _BV(1) +#define B_BUTTON _BV(0) + +// pin values for buttons, probably shouldn't use these +#define PIN_LEFT_BUTTON 9 +#define LEFT_BUTTON_PORT PORTB +#define LEFT_BUTTON_BIT PORTB5 + +#define PIN_RIGHT_BUTTON 5 +#define RIGHT_BUTTON_PORT PORTC +#define RIGHT_BUTTON_BIT PORTC6 + +#define PIN_UP_BUTTON 8 +#define UP_BUTTON_PORT PORTB +#define UP_BUTTON_BIT PORTB4 + +#define PIN_DOWN_BUTTON 10 +#define DOWN_BUTTON_PORT PORTB +#define DOWN_BUTTON_BIT PORTB6 + +#define PIN_A_BUTTON A0 +#define A_BUTTON_PORT PORTF +#define A_BUTTON_BIT PORTF7 + +#define PIN_B_BUTTON A1 +#define B_BUTTON_PORT PORTF +#define B_BUTTON_BIT PORTF6 + +#define PIN_SPEAKER_1 A2 +#define SPEAKER_1_PORT PORTF +#define SPEAKER_1_DDR DDRF +#define SPEAKER_1_BIT PORTF5 +// SPEAKER_2 is purposely not defined for DEVKIT as it could potentially +// be dangerous and fry your hardware (because of the devkit wiring). +// +// Reference: https://github.com/Arduboy/Arduboy/issues/108 + +#define RAND_SEED_IN A4 // Open analog input used for noise by initRandomSeed() +#define RAND_SEED_IN_PORTF +#define RAND_SEED_IN_BIT PORTF1 +// Value for ADMUX to read the random seed pin: 2.56V reference, ADC1 +#define RAND_SEED_IN_ADMUX (_BV(REFS0) | _BV(REFS1) | _BV(MUX0)) + +#endif +// -------------------- + +// OLED hardware (SSD1306,SSD1309,SH1106,OLED_64X64) + +#define OLED_PIXELS_INVERTED 0xA7 // All pixels inverted +#define OLED_PIXELS_NORMAL 0xA6 // All pixels normal + +#define OLED_ALL_PIXELS_ON 0xA5 // all pixels on +#define OLED_PIXELS_FROM_RAM 0xA4 // pixels mapped to display RAM contents + +#define OLED_VERTICAL_FLIPPED 0xC0 // reversed COM scan direction +#define OLED_VERTICAL_NORMAL 0xC8 // normal COM scan direction + +#define OLED_HORIZ_FLIPPED 0xA0 // reversed segment re-map +#define OLED_HORIZ_NORMAL 0xA1 // normal segment re-map + +#define OLED_SET_PAGE_ADDRESS 0xB0 +#ifdef OLED_SH1106 + #define OLED_SET_COLUMN_ADDRESS_LO 0x02 //SH1106 only: 1st pixel starts on column 2 +#else + #define OLED_SET_COLUMN_ADDRESS_LO 0x00 +#endif +#define OLED_SET_COLUMN_ADDRESS_HI 0x10 +// ----- +#ifdef OLED_96X96 + #define WIDTH 96 + #define HEIGHT 96 +#elif defined(OLED_128X64) + #define WIDTH 128 + #define HEIGHT 96 +#else + #define WIDTH 128 /**< The width of the display in pixels */ + #define HEIGHT 64 /**< The height of the display in pixels */ +#endif + +#define COLUMN_ADDRESS_END (WIDTH - 1) & 127 // 128 pixels wide +#define PAGE_ADDRESS_END ((HEIGHT/8)-1) & 7 // 8 pages high + +/** \brief + * Lower level functions generally dealing directly with the hardware. + * + * \details + * This class is inherited by Arduboy2Base and thus also Arduboy2, so wouldn't + * normally be used directly by a sketch. + * + * \note + * A friend class named _Arduboy2Ex_ is declared by this class. The intention + * is to allow a sketch to create an _Arduboy2Ex_ class which would have access + * to the private and protected members of the Arduboy2Core class. It is hoped + * that this may eliminate the need to create an entire local copy of the + * library, in order to extend the functionality, in most circumstances. + */ +class Arduboy2Core +{ + friend class Arduboy2Ex; + + public: + Arduboy2Core(); + + /** \brief + * Idle the CPU to save power. + * + * \details + * This puts the CPU in _idle_ sleep mode. You should call this as often + * as you can for the best power savings. The timer 0 overflow interrupt + * will wake up the chip every 1ms, so even at 60 FPS a well written + * app should be able to sleep maybe half the time in between rendering + * it's own frames. + */ + void static idle(); + + /** \brief + * Put the display into data mode. + * + * \details + * When placed in data mode, data that is sent to the display will be + * considered as data to be displayed. + * + * \note + * This is a low level function that is not intended for general use in a + * sketch. It has been made public and documented for use by derived + * classes. + * + * \see LCDCommandMode() SPItransfer() + */ + inline void static LCDDataMode() __attribute__((always_inline)); + /** \brief + * Put the display into command mode. + * + * \details + * When placed in command mode, data that is sent to the display will be + * treated as commands. + * + * See the SSD1306 controller and OLED display documents for available + * commands and command sequences. + * + * Links: + * + * - https://www.adafruit.com/datasheets/SSD1306.pdf + * - http://www.buydisplay.com/download/manual/ER-OLED013-1_Series_Datasheet.pdf + * + * \note + * This is a low level function that is not intended for general use in a + * sketch. It has been made public and documented for use by derived + * classes. + * + * \see LCDDataMode() sendLCDCommand() SPItransfer() + */ + inline void static LCDCommandMode() __attribute__((always_inline)); + /** \brief + * Transfer a byte to the display. + * + * \param data The byte to be sent to the display. + * + * \details + * Transfer one byte to the display over the SPI port and wait for the + * transfer to complete. The byte will either be interpreted as a command + * or as data to be placed on the screen, depending on the command/data + * mode. + * + * \see LCDDataMode() LCDCommandMode() sendLCDCommand() + */ + void static SPItransfer(uint8_t data); + + /** \brief + * Turn the display off. + * + * \details + * The display will clear and be put into a low power mode. This can be + * used to extend battery life when a game is paused or when a sketch + * doesn't require anything to be displayed for a relatively long period + * of time. + * + * \see displayOn() + */ + void static displayOff(); + + /** \brief + * Turn the display on. + * + * \details + * Used to power up and reinitialize the display after calling + * `displayOff()`. + * + * \note + * The previous call to `displayOff()` will have caused the display's + * buffer contents to be lost. The display will have to be re-painted, + * which is usually done by calling `display()`. + * + * \see displayOff() + */ + void static displayOn(); + + /** \brief + * Get the width of the display in pixels. + * + * \return The width of the display in pixels. + * + * \note + * In most cases, the defined value `WIDTH` would be better to use instead + * of this function. + */ + uint8_t static width(); + + /** \brief + * Get the height of the display in pixels. + * + * \return The height of the display in pixels. + * + * \note + * In most cases, the defined value `HEIGHT` would be better to use instead + * of this function. + */ + uint8_t static height(); + + /** \brief + * Get the current state of all buttons as a bitmask. + * + * \return A bitmask of the state of all the buttons. + * + * \details + * The returned mask contains a bit for each button. For any pressed button, + * its bit will be 1. For released buttons their associated bits will be 0. + * + * The following defined mask values should be used for the buttons: + * + * LEFT_BUTTON, RIGHT_BUTTON, UP_BUTTON, DOWN_BUTTON, A_BUTTON, B_BUTTON + */ + uint8_t static buttonsState(); + + /** \brief + * Paint 8 pixels vertically to the display. + * + * \param pixels A byte whose bits specify a vertical column of 8 pixels. + * + * \details + * A byte representing a vertical column of 8 pixels is written to the + * display at the current page and column address. The address is then + * incremented. The page/column address will wrap to the start of the + * display (the top left) when it increments past the end (lower right). + * + * The least significant bit represents the top pixel in the column. + * A bit set to 1 is lit, 0 is unlit. + * + * Example: + * + * X = lit pixels, . = unlit pixels + * + * blank() paint8Pixels() 0xFF, 0, 0xF0, 0, 0x0F + * v TOP LEFT corner (8x9) v TOP LEFT corner + * . . . . . . . . (page 1) X . . . X . . . (page 1) + * . . . . . . . . X . . . X . . . + * . . . . . . . . X . . . X . . . + * . . . . . . . . X . . . X . . . + * . . . . . . . . X . X . . . . . + * . . . . . . . . X . X . . . . . + * . . . . . . . . X . X . . . . . + * . . . . . . . . (end of page 1) X . X . . . . . (end of page 1) + * . . . . . . . . (page 2) . . . . . . . . (page 2) + */ + void static paint8Pixels(uint8_t pixels); + + /** \brief + * Paints an entire image directly to the display from program memory. + * + * \param image A byte array in program memory representing the entire + * contents of the display. + * + * \details + * The contents of the specified array in program memory is written to the + * display. Each byte in the array represents a vertical column of 8 pixels + * with the least significant bit at the top. The bytes are written starting + * at the top left, progressing horizontally and wrapping at the end of each + * row, to the bottom right. The size of the array must exactly match the + * number of pixels in the entire display. + * + * \see paint8Pixels() + */ + void static paintScreen(const uint8_t *image); + + /** \brief + * Paints an entire image directly to the display from an array in RAM. + * + * \param image A byte array in RAM representing the entire contents of + * the display. + * \param clear If `true` the array in RAM will be cleared to zeros upon + * return from this function. If `false` the RAM buffer will remain + * unchanged. (optional; defaults to `false`) + * + * \details + * The contents of the specified array in RAM is written to the display. + * Each byte in the array represents a vertical column of 8 pixels with + * the least significant bit at the top. The bytes are written starting + * at the top left, progressing horizontally and wrapping at the end of + * each row, to the bottom right. The size of the array must exactly + * match the number of pixels in the entire display. + * + * If parameter `clear` is set to `true` the RAM array will be cleared to + * zeros after its contents are written to the display. + * + * \see paint8Pixels() + */ + void static paintScreen(uint8_t image[], bool clear = false); + + /** \brief + * Blank the display screen by setting all pixels off. + * + * \details + * All pixels on the screen will be written with a value of 0 to turn + * them off. + */ + void static blank(); + + /** \brief + * Invert the entire display or set it back to normal. + * + * \param inverse `true` will invert the display. `false` will set the + * display to no-inverted. + * + * \details + * Calling this function with a value of `true` will set the display to + * inverted mode. A pixel with a value of 0 will be on and a pixel set to 1 + * will be off. + * + * Once in inverted mode, the display will remain this way + * until it is set back to non-inverted mode by calling this function with + * `false`. + */ + void static invert(bool inverse); + + /** \brief + * Turn all display pixels on or display the buffer contents. + * + * \param on `true` turns all pixels on. `false` displays the contents + * of the hardware display buffer. + * + * \details + * Calling this function with a value of `true` will override the contents + * of the hardware display buffer and turn all pixels on. The contents of + * the hardware buffer will remain unchanged. + * + * Calling this function with a value of `false` will set the normal state + * of displaying the contents of the hardware display buffer. + * + * \note + * All pixels will be lit even if the display is in inverted mode. + * + * \see invert() + */ + void static allPixelsOn(bool on); + + /** \brief + * Flip the display vertically or set it back to normal. + * + * \param flipped `true` will set vertical flip mode. `false` will set + * normal vertical orientation. + * + * \details + * Calling this function with a value of `true` will cause the Y coordinate + * to start at the bottom edge of the display instead of the top, + * effectively flipping the display vertically. + * + * Once in vertical flip mode, it will remain this way until normal + * vertical mode is set by calling this function with a value of `false`. + * + * \see flipHorizontal() + */ + void static flipVertical(bool flipped); + + /** \brief + * Flip the display horizontally or set it back to normal. + * + * \param flipped `true` will set horizontal flip mode. `false` will set + * normal horizontal orientation. + * + * \details + * Calling this function with a value of `true` will cause the X coordinate + * to start at the left edge of the display instead of the right, + * effectively flipping the display horizontally. + * + * Once in horizontal flip mode, it will remain this way until normal + * horizontal mode is set by calling this function with a value of `false`. + * + * \see flipVertical() + */ + void static flipHorizontal(bool flipped); + + /** \brief + * Send a single command byte to the display. + * + * \param command The command byte to send to the display. + * + * \details + * The display will be set to command mode then the specified command + * byte will be sent. The display will then be set to data mode. + * Multi-byte commands can be sent by calling this function multiple times. + * + * \note + * Sending improper commands to the display can place it into invalid or + * unexpected states, possibly even causing physical damage. + */ + void static sendLCDCommand(uint8_t command); + + /** \brief + * Set the light output of the RGB LED. + * + * \param red,green,blue The brightness value for each LED. + * + * \details + * The RGB LED is actually individual red, green and blue LEDs placed + * very close together in a single package. By setting the brightness of + * each LED, the RGB LED can show various colors and intensities. + * The brightness of each LED can be set to a value from 0 (fully off) + * to 255 (fully on). + * + * \note + * \parblock + * Certain libraries that take control of the hardware timers may interfere + * with the ability of this function to properly control the RGB LED. + *_ArduboyPlaytune_ is one such library known to do this. + * The digitalWriteRGB() function will still work properly in this case. + * \endparblock + * + * \note + * \parblock + * Many of the Kickstarter Arduboys were accidentally shipped with the + * RGB LED installed incorrectly. For these units, the green LED cannot be + * lit. As long as the green led is set to off, setting the red LED will + * actually control the blue LED and setting the blue LED will actually + * control the red LED. If the green LED is turned fully on, none of the + * LEDs will light. + * \endparblock + * + * \see digitalWriteRGB() + */ + void static setRGBled(uint8_t red, uint8_t green, uint8_t blue); + + /** \brief + * Set the RGB LEDs digitally, to either fully on or fully off. + * + * \param red,green,blue Use value RGB_ON or RGB_OFF to set each LED. + * + * \details + * The RGB LED is actually individual red, green and blue LEDs placed + * very close together in a single package. This 3 parameter version of the + * function will set each LED either on or off, to set the RGB LED to + * 7 different colors at their highest brightness or turn it off. + * + * The colors are as follows: + * + * RED LED GREEN_LED BLUE_LED COLOR + * ------- --------- -------- ----- + * RGB_OFF RGB_OFF RGB_OFF OFF + * RGB_OFF RGB_OFF RGB_ON Blue + * RGB_OFF RGB_ON RGB_OFF Green + * RGB_OFF RGB_ON RGB_ON Cyan + * RGB_ON RGB_OFF RGB_OFF Red + * RGB_ON RGB_OFF RGB_ON Magenta + * RGB_ON RGB_ON RGB_OFF Yellow + * RGB_ON RGB_ON RGB_ON White + * + * \note + * Many of the Kickstarter Arduboys were accidentally shipped with the + * RGB LED installed incorrectly. For these units, the green LED cannot be + * lit. As long as the green led is set to off, turning on the red LED will + * actually light the blue LED and turning on the blue LED will actually + * light the red LED. If the green LED is turned on, none of the LEDs + * will light. + * + * \see digitalWriteRGB(uint8_t, uint8_t) setRGBled() + */ + void static digitalWriteRGB(uint8_t red, uint8_t green, uint8_t blue); + + /** \brief + * Set one of the RGB LEDs digitally, to either fully on or fully off. + * + * \param color The name of the LED to set. The value given should be one + * of RED_LED, GREEN_LED or BLUE_LED. + * + * \param val Indicates whether to turn the specified LED on or off. + * The value given should be RGB_ON or RGB_OFF. + * + * \details + * This 2 parameter version of the function will set a single LED within + * the RGB LED either fully on or fully off. See the description of the + * 3 parameter version of this function for more details on the RGB LED. + * + * \see digitalWriteRGB(uint8_t, uint8_t, uint8_t) setRGBled() + */ + void static digitalWriteRGB(uint8_t color, uint8_t val); + + /** \brief + * Initialize the Arduboy's hardware. + * + * \details + * This function initializes the display, buttons, etc. + * + * This function is called by begin() so isn't normally called within a + * sketch. However, in order to free up some code space, by eliminating + * some of the start up features, it can be called in place of begin(). + * The functions that begin() would call after boot() can then be called + * to add back in some of the start up features, if desired. + * See the README file or documentation on the main page for more details. + * + * \see Arduboy2Base::begin() + */ + void static boot(); + + /** \brief + * Allow upload when the bootloader "magic number" could be corrupted. + * + * \details + * If the UP button is held when this function is entered, the RGB LED + * will be lit and timer 0 will be disabled, then the sketch will remain + * in a tight loop. This is to address a problem with uploading a new + * sketch, for sketches that interfere with the bootloader "magic number". + * The problem occurs with certain sketches that use large amounts of RAM. + * + * This function should be called after `boot()` in sketches that + * potentially could cause the problem. + * + * It is intended to replace the `flashlight()` function when more + * program space is required. If possible, it is more desirable to use + * `flashlight()`, so that the actual flashlight feature isn't lost. + * + * \see Arduboy2Base::flashlight() boot() + */ + void static safeMode(); + + /** \brief + * Delay for the number of milliseconds, specified as a 16 bit value. + * + * \param ms The delay in milliseconds. + * + * \details + * This function works the same as the Arduino `delay()` function except + * the provided value is 16 bits long, so the maximum delay allowed is + * 65535 milliseconds (about 65.5 seconds). Using this function instead + * of Arduino `delay()` will save a few bytes of code. + */ + void static delayShort(uint16_t ms) __attribute__ ((noinline)); + + protected: + // internals + void static setCPUSpeed8MHz(); + void static bootSPI(); + void static bootOLED(); + void static bootPins(); + void static bootPowerSaving(); +}; + +#endif diff --git a/Sprites.cpp b/Sprites.cpp new file mode 100644 index 0000000..7c3d8b0 --- /dev/null +++ b/Sprites.cpp @@ -0,0 +1,362 @@ +/** + * @file Sprites.cpp + * \brief + * A class for drawing animated sprites from image and mask bitmaps. + */ + +#include "Sprites.h" + +void Sprites::drawExternalMask(int16_t x, int16_t y, const uint8_t *bitmap, + const uint8_t *mask, uint8_t frame, uint8_t mask_frame) +{ + draw(x, y, bitmap, frame, mask, mask_frame, SPRITE_MASKED); +} + +void Sprites::drawOverwrite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_OVERWRITE); +} + +void Sprites::drawErase(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_IS_MASK_ERASE); +} + +void Sprites::drawSelfMasked(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_IS_MASK); +} + +void Sprites::drawPlusMask(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame) +{ + draw(x, y, bitmap, frame, NULL, 0, SPRITE_PLUS_MASK); +} + + +//common functions +void Sprites::draw(int16_t x, int16_t y, + const uint8_t *bitmap, uint8_t frame, + const uint8_t *mask, uint8_t sprite_frame, + uint8_t drawMode) +{ + unsigned int frame_offset; + + if (bitmap == NULL) + return; + + uint8_t width = pgm_read_byte(bitmap); + uint8_t height = pgm_read_byte(++bitmap); + bitmap++; + if (frame > 0 || sprite_frame > 0) { + frame_offset = (width * ( height / 8 + ( height % 8 == 0 ? 0 : 1))); + // sprite plus mask uses twice as much space for each frame + if (drawMode == SPRITE_PLUS_MASK) { + frame_offset *= 2; + } else if (mask != NULL) { + mask += sprite_frame * frame_offset; + } + bitmap += frame * frame_offset; + } + + // if we're detecting the draw mode then base it on whether a mask + // was passed as a separate object + if (drawMode == SPRITE_AUTO_MODE) { + drawMode = mask == NULL ? SPRITE_UNMASKED : SPRITE_MASKED; + } + + drawBitmap(x, y, bitmap, mask, width, height, drawMode); +} + +void Sprites::drawBitmap(int16_t x, int16_t y, + const uint8_t *bitmap, const uint8_t *mask, + uint8_t w, uint8_t h, uint8_t draw_mode) +{ + // no need to draw at all of we're offscreen + if (x + w <= 0 || x > WIDTH - 1 || y + h <= 0 || y > HEIGHT - 1) + return; + + if (bitmap == NULL) + return; + + // xOffset technically doesn't need to be 16 bit but the math operations + // are measurably faster if it is + uint16_t xOffset, ofs; + int8_t yOffset = abs(y) % 8; + int8_t sRow = y / 8; + uint8_t loop_h, start_h, rendered_width; + + if (y < 0 && yOffset > 0) { + sRow--; + yOffset = 8 - yOffset; + } + + // if the left side of the render is offscreen skip those loops + if (x < 0) { + xOffset = abs(x); + } else { + xOffset = 0; + } + + // if the right side of the render is offscreen skip those loops + if (x + w > WIDTH - 1) { + rendered_width = ((WIDTH - x) - xOffset); + } else { + rendered_width = (w - xOffset); + } + + // if the top side of the render is offscreen skip those loops + if (sRow < -1) { + start_h = abs(sRow) - 1; + } else { + start_h = 0; + } + + loop_h = h / 8 + (h % 8 > 0 ? 1 : 0); // divide, then round up + + // if (sRow + loop_h - 1 > (HEIGHT/8)-1) + if (sRow + loop_h > (HEIGHT / 8)) { + loop_h = (HEIGHT / 8) - sRow; + } + + // prepare variables for loops later so we can compare with 0 + // instead of comparing two variables + loop_h -= start_h; + + sRow += start_h; + ofs = (sRow * WIDTH) + x + xOffset; + uint8_t *bofs = (uint8_t *)bitmap + (start_h * w) + xOffset; + uint8_t data; + + uint8_t mul_amt = 1 << yOffset; + uint16_t mask_data; + uint16_t bitmap_data; + + switch (draw_mode) { + case SPRITE_UNMASKED: + // we only want to mask the 8 bits of our own sprite, so we can + // calculate the mask before the start of the loop + mask_data = ~(0xFF * mul_amt); + // really if yOffset = 0 you have a faster case here that could be + // optimized + for (uint8_t a = 0; a < loop_h; a++) { + for (uint8_t iCol = 0; iCol < rendered_width; iCol++) { + bitmap_data = pgm_read_byte(bofs) * mul_amt; + + if (sRow >= 0) { + data = Arduboy2Base::sBuffer[ofs]; + data &= (uint8_t)(mask_data); + data |= (uint8_t)(bitmap_data); + Arduboy2Base::sBuffer[ofs] = data; + } + if (yOffset != 0 && sRow < ((HEIGHT / 8) - 1)) { + data = Arduboy2Base::sBuffer[ofs + WIDTH]; + data &= (*((unsigned char *) (&mask_data) + 1)); + data |= (*((unsigned char *) (&bitmap_data) + 1)); + Arduboy2Base::sBuffer[ofs + WIDTH] = data; + } + ofs++; + bofs++; + } + sRow++; + bofs += w - rendered_width; + ofs += WIDTH - rendered_width; + } + break; + + case SPRITE_IS_MASK: + for (uint8_t a = 0; a < loop_h; a++) { + for (uint8_t iCol = 0; iCol < rendered_width; iCol++) { + bitmap_data = pgm_read_byte(bofs) * mul_amt; + if (sRow >= 0) { + Arduboy2Base::sBuffer[ofs] |= (uint8_t)(bitmap_data); + } + if (yOffset != 0 && sRow < ((HEIGHT / 8) - 1)) { + Arduboy2Base::sBuffer[ofs + WIDTH] |= (*((unsigned char *) (&bitmap_data) + 1)); + } + ofs++; + bofs++; + } + sRow++; + bofs += w - rendered_width; + ofs += WIDTH - rendered_width; + } + break; + + case SPRITE_IS_MASK_ERASE: + for (uint8_t a = 0; a < loop_h; a++) { + for (uint8_t iCol = 0; iCol < rendered_width; iCol++) { + bitmap_data = pgm_read_byte(bofs) * mul_amt; + if (sRow >= 0) { + Arduboy2Base::sBuffer[ofs] &= ~(uint8_t)(bitmap_data); + } + if (yOffset != 0 && sRow < ((HEIGHT / 8) - 1)) { + Arduboy2Base::sBuffer[ofs + WIDTH] &= ~(*((unsigned char *) (&bitmap_data) + 1)); + } + ofs++; + bofs++; + } + sRow++; + bofs += w - rendered_width; + ofs += WIDTH - rendered_width; + } + break; + + case SPRITE_MASKED: + uint8_t *mask_ofs; + mask_ofs = (uint8_t *)mask + (start_h * w) + xOffset; + for (uint8_t a = 0; a < loop_h; a++) { + for (uint8_t iCol = 0; iCol < rendered_width; iCol++) { + // NOTE: you might think in the yOffset==0 case that this results + // in more effort, but in all my testing the compiler was forcing + // 16-bit math to happen here anyways, so this isn't actually + // compiling to more code than it otherwise would. If the offset + // is 0 the high part of the word will just never be used. + + // load data and bit shift + // mask needs to be bit flipped + mask_data = ~(pgm_read_byte(mask_ofs) * mul_amt); + bitmap_data = pgm_read_byte(bofs) * mul_amt; + + if (sRow >= 0) { + data = Arduboy2Base::sBuffer[ofs]; + data &= (uint8_t)(mask_data); + data |= (uint8_t)(bitmap_data); + Arduboy2Base::sBuffer[ofs] = data; + } + if (yOffset != 0 && sRow < ((HEIGHT / 8) - 1)) { + data = Arduboy2Base::sBuffer[ofs + WIDTH]; + data &= (*((unsigned char *) (&mask_data) + 1)); + data |= (*((unsigned char *) (&bitmap_data) + 1)); + Arduboy2Base::sBuffer[ofs + WIDTH] = data; + } + ofs++; + mask_ofs++; + bofs++; + } + sRow++; + bofs += w - rendered_width; + mask_ofs += w - rendered_width; + ofs += WIDTH - rendered_width; + } + break; + + + case SPRITE_PLUS_MASK: + // *2 because we use double the bits (mask + bitmap) + bofs = (uint8_t *)(bitmap + ((start_h * w) + xOffset) * 2); + + uint8_t xi = rendered_width; // counter for x loop below + + asm volatile( + "push r28\n" // save Y + "push r29\n" + "movw r28, %[buffer_ofs]\n" // Y = buffer_ofs_2 + "subi r28, %[neg_width]\n" // buffer_ofs_2 = buffer_ofs + WIDTH + "sbci r29, -1\n" + "loop_y:\n" + "loop_x:\n" + // load bitmap and mask data + "lpm %A[bitmap_data], Z+\n" + "lpm %A[mask_data], Z+\n" + + // shift mask and buffer data + "tst %[yOffset]\n" + "breq skip_shifting\n" + "mul %A[bitmap_data], %[mul_amt]\n" + "movw %[bitmap_data], r0\n" + "mul %A[mask_data], %[mul_amt]\n" + "movw %[mask_data], r0\n" + + // SECOND PAGE + // if yOffset != 0 && sRow < ((HEIGHT / 8) - 1) + "cpi %[sRow], %[row_height]\n" + "brge end_second_page\n" + // then + "ld %[data], Y\n" + "com %B[mask_data]\n" // invert high byte of mask + "and %[data], %B[mask_data]\n" + "or %[data], %B[bitmap_data]\n" + // update buffer, increment + "st Y+, %[data]\n" + + "end_second_page:\n" + "skip_shifting:\n" + + // FIRST PAGE + // if sRow >= 0 + "tst %[sRow]\n" + "brmi skip_first_page\n" + "ld %[data], %a[buffer_ofs]\n" + // then + "com %A[mask_data]\n" + "and %[data], %A[mask_data]\n" + "or %[data], %A[bitmap_data]\n" + // update buffer, increment + "st %a[buffer_ofs]+, %[data]\n" + "jmp end_first_page\n" + + "skip_first_page:\n" + // since no ST Z+ when skipped we need to do this manually + "adiw %[buffer_ofs], 1\n" + + "end_first_page:\n" + + // "x_loop_next:\n" + "dec %[xi]\n" + "brne loop_x\n" + + // increment y + "next_loop_y:\n" + "dec %[yi]\n" + "breq finished\n" + "mov %[xi], %[x_count]\n" // reset x counter + // sRow++; + "inc %[sRow]\n" + "clr __zero_reg__\n" + // sprite_ofs += (w - rendered_width) * 2; + "add %A[sprite_ofs], %A[sprite_ofs_jump]\n" + "adc %B[sprite_ofs], __zero_reg__\n" + // buffer_ofs += WIDTH - rendered_width; + "add %A[buffer_ofs], %A[buffer_ofs_jump]\n" + "adc %B[buffer_ofs], __zero_reg__\n" + // buffer_ofs_page_2 += WIDTH - rendered_width; + "add r28, %A[buffer_ofs_jump]\n" + "adc r29, __zero_reg__\n" + + "rjmp loop_y\n" + "finished:\n" + // put the Y register back in place + "pop r29\n" + "pop r28\n" + "clr __zero_reg__\n" // just in case + : [xi] "+&a" (xi), + [yi] "+&a" (loop_h), + [sRow] "+&a" (sRow), // CPI requires an upper register (r16-r23) + [data] "=&l" (data), + [mask_data] "=&l" (mask_data), + [bitmap_data] "=&l" (bitmap_data) + : + [screen_width] "M" (WIDTH), + [x_count] "l" (rendered_width), // lower register + [sprite_ofs] "z" (bofs), + [buffer_ofs] "x" (Arduboy2Base::sBuffer+ofs), + [buffer_ofs_jump] "a" (WIDTH-rendered_width), // upper reg (r16-r23) + [sprite_ofs_jump] "a" ((w-rendered_width)*2), // upper reg (r16-r23) + + // [sprite_ofs_jump] "r" (0), + [yOffset] "l" (yOffset), // lower register + [mul_amt] "l" (mul_amt), // lower register + [neg_width] "M" (256 - WIDTH), + [row_height] "M" ((HEIGHT / 8) - 1) + // NOTE: We also clobber r28 and r29 (y) but sometimes the compiler + // won't allow us, so in order to make this work we don't tell it + // that we clobber them. Instead, we push/pop to preserve them. + // Then we need to guarantee that the the compiler doesn't put one of + // our own variables into r28/r29. + // We do that by specifying all the inputs and outputs use either + // lower registers (l) or simple (r16-r23) upper registers (a). + : // pushes/clobbers/pops r28 and r29 (y) + ); + break; + } +} diff --git a/Sprites.h b/Sprites.h new file mode 100644 index 0000000..d3a95d8 --- /dev/null +++ b/Sprites.h @@ -0,0 +1,235 @@ +/** + * @file Sprites.h + * \brief + * A class for drawing animated sprites from image and mask bitmaps. + */ + +#ifndef Sprites_h +#define Sprites_h + +#include "Arduboy2.h" + +#define SPRITE_MASKED 1 +#define SPRITE_UNMASKED 2 +#define SPRITE_OVERWRITE 2 +#define SPRITE_PLUS_MASK 3 +#define SPRITE_IS_MASK 250 +#define SPRITE_IS_MASK_ERASE 251 +#define SPRITE_AUTO_MODE 255 + +/** \brief + * A class for drawing animated sprites from image and mask bitmaps. + * + * \details + * The functions in this class will draw to the screen buffer an image + * contained in an array located in program memory. A mask can also be + * specified or implied, which dictates how existing pixels in the buffer, + * within the image boundaries, will be affected. + * + * A sprite or mask array contains one or more "frames". Each frame is intended + * to show whatever the sprite represents in a different position, such as the + * various poses for a running or jumping character. By specifying a different + * frame each time the sprite is drawn, it can be animated. + * + * Each image array begins with values for the width and height of the sprite, + * in pixels. The width can be any value. The height must be a multiple of + * 8 pixels, but with proper masking, a sprite of any height can be created. + * + * For a separate mask array, as is used with `drawExternalMask()`, the width + * and height are not included but must contain data of the same dimensions + * as the corresponding image array. + * + * Following the width and height values for an image array, or the from the + * beginning of a separate mask array, the array contains the image and/or + * mask data for each frame. Each byte represents a vertical column of 8 pixels + * with the least significant bit (bit 0) at the top. The bytes are drawn as + * 8 pixel high rows from left to right, top to bottom. When the end of a row + * is reached, as specified by the width value, the next byte in the array will + * be the start of the next row. + * + * Data for each frame after the first one immediately follows the previous + * frame. Frame numbers start at 0. + */ +class Sprites +{ + public: + /** \brief + * Draw a sprite using a separate image and mask array. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param mask A pointer to the array containing the mask frames. + * \param frame The frame number of the image to draw. + * \param mask_frame The frame number for the mask to use (can be different + * from the image frame number). + * + * \details + * An array containing the image frames, and another array containing + * corresponding mask frames, are used to draw a sprite. + * + * Bits set to 1 in the mask indicate that the pixel will be set to the + * value of the corresponding image bit. Bits set to 0 in the mask will be + * left unchanged. + * + * image mask before after + * + * ..... .OOO. ..... ..... + * ..O.. OOOOO ..... ..O.. + * OO.OO OO.OO ..... OO.OO + * ..O.. OOOOO ..... ..O.. + * ..... .OOO. ..... ..... + * + * image mask before after + * + * ..... .OOO. OOOOO O...O + * ..O.. OOOOO OOOOO ..O.. + * OO.OO OOOOO OOOOO OO.OO + * ..O.. OOOOO OOOOO ..O.. + * ..... .OOO. OOOOO O...O + */ + static void drawExternalMask(int16_t x, int16_t y, const uint8_t *bitmap, + const uint8_t *mask, uint8_t frame, uint8_t mask_frame); + + /** \brief + * Draw a sprite using an array containing both image and mask values. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image/mask frames. + * \param frame The frame number of the image to draw. + * + * \details + * An array containing combined image and mask data is used to draw a + * sprite. Bytes are given in pairs with the first byte representing the + * image pixels and the second byte specifying the corresponding mask. + * The width given in the array still specifies the image width, so each + * row of image and mask bytes will be twice the width value. + * + * Bits set to 1 in the mask indicate that the pixel will be set to the + * value of the corresponding image bit. Bits set to 0 in the mask will be + * left unchanged. + * + * image mask before after + * + * ..... .OOO. ..... ..... + * ..O.. OOOOO ..... ..O.. + * OO.OO OO.OO ..... OO.OO + * ..O.. OOOOO ..... ..O.. + * ..... .OOO. ..... ..... + * + * image mask before after + * + * ..... .OOO. OOOOO O...O + * ..O.. OOOOO OOOOO ..O.. + * OO.OO OOOOO OOOOO OO.OO + * ..O.. OOOOO OOOOO ..O.. + * ..... .OOO. OOOOO O...O + */ + static void drawPlusMask(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + /** \brief + * Draw a sprite by replacing the existing content completely. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param frame The frame number of the image to draw. + * + * \details + * A sprite is drawn by overwriting the pixels in the buffer with the data + * from the specified frame in the array. No masking is done. A bit set + * to 1 in the frame will set the pixel to 1 in the buffer, and a 0 in the + * array will set a 0 in the buffer. + * + * image before after + * + * ..... ..... ..... + * ..O.. ..... ..O.. + * OO.OO ..... OO.OO + * ..O.. ..... ..O.. + * ..... ..... ..... + * + * image before after + * + * ..... OOOOO ..... + * ..O.. OOOOO ..O.. + * OO.OO OOOOO OO.OO + * ..O.. OOOOO ..O.. + * ..... OOOOO ..... + */ + static void drawOverwrite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + /** \brief + * "Erase" a sprite. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param frame The frame number of the image to erase. + * + * \details + * The data from the specified frame in the array is used to erase a + * sprite. To "erase" a sprite, bits set to 1 in the frame will set the + * corresponding pixel in the buffer to 0. Frame bits set to 0 will remain + * unchanged in the buffer. + * + * image before after + * + * ..... ..... ..... + * ..O.. ..... ..... + * OO.OO ..... ..... + * ..O.. ..... ..... + * ..... ..... ..... + * + * image before after + * + * ..... OOOOO OOOOO + * ..O.. OOOOO OO.OO + * OO.OO OOOOO ..O.. + * ..O.. OOOOO OO.OO + * ..... OOOOO OOOOO + */ + static void drawErase(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + /** \brief + * Draw a sprite using only the bits set to 1. + * + * \param x,y The coordinates of the top left pixel location. + * \param bitmap A pointer to the array containing the image frames. + * \param frame The frame number of the image to draw. + * + * \details + * Bits set to 1 in the frame will be used to draw the sprite by setting + * the corresponding pixel in the buffer to 1. Bits set to 0 in the frame + * will remain unchanged in the buffer. + * + * image before after + * + * ..... ..... ..... + * ..O.. ..... ..O.. + * OO.OO ..... OO.OO + * ..O.. ..... ..O.. + * ..... ..... ..... + * + * image before after + * + * ..... OOOOO OOOOO (no change because all pixels were + * ..O.. OOOOO OOOOO already white) + * OO.OO OOOOO OOOOO + * ..O.. OOOOO OOOOO + * ..... OOOOO OOOOO + */ + static void drawSelfMasked(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame); + + // Master function. Needs to be abstracted into separate function for + // every render type. + // (Not officially part of the API) + static void draw(int16_t x, int16_t y, + const uint8_t *bitmap, uint8_t frame, + const uint8_t *mask, uint8_t sprite_frame, + uint8_t drawMode); + + // (Not officially part of the API) + static void drawBitmap(int16_t x, int16_t y, + const uint8_t *bitmap, const uint8_t *mask, + uint8_t w, uint8_t h, uint8_t draw_mode); +}; + +#endif