Skip to content

Instantly share code, notes, and snippets.

@uXeBoy
Last active May 9, 2018 06:48
Show Gist options
  • Save uXeBoy/5c9e99e0b1966aac1ef3284693e791d7 to your computer and use it in GitHub Desktop.
Save uXeBoy/5c9e99e0b1966aac1ef3284693e791d7 to your computer and use it in GitHub Desktop.
Arduboy2Core.cpp
/**
* @file Arduboy2Core.cpp
* \brief
* The Arduboy2Core class for Arduboy hardware initilization and control.
*/
#include "Arduboy2Core.h"
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
//
// 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
};
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) | _BV(GREEN_LED_BIT) | _BV(BLUE_LED_BIT) |
_BV(B_BUTTON_BIT);
// Port B INPUT or LOW (none)
// Port B inputs
DDRB &= ~(_BV(B_BUTTON_BIT) | _BV(SPI_MISO_BIT));
// Port B outputs
DDRB |= _BV(RED_LED_BIT) | _BV(GREEN_LED_BIT) | _BV(BLUE_LED_BIT) |
_BV(SPI_MOSI_BIT) | _BV(SPI_SCK_BIT) | _BV(SPI_SS_BIT);
// Port C
// Speaker: Not set here. Controlled by audio class
// 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 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 or LOW
PORTF &= ~(_BV(LEFT_BUTTON_BIT) | _BV(RIGHT_BUTTON_BIT) |
_BV(UP_BUTTON_BIT) | _BV(DOWN_BUTTON_BIT) |
_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) |
_BV(SPI_MISO_BIT));
// Port B outputs
DDRB |= _BV(SPI_MOSI_BIT) | _BV(SPI_SCK_BIT) | _BV(SPI_SS_BIT) |
_BV(BLUE_LED_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);
#ifndef ARDUBOY_CORE // for Arduboy core timer 0 should remain enabled
// prevent the bootloader magic number from being overwritten by timer 0
// when a timer variable overlaps the magic number location
power_timer0_disable();
#endif
while (true) { }
}
}
/* Power Management */
void Arduboy2Core::idle()
{
SMCR = _BV(SE); // select idle mode and enable sleeping
sleep_cpu();
SMCR = 0; // disable sleeping
}
void Arduboy2Core::bootPowerSaving()
{
// disable Two Wire Interface (I2C) and the ADC
// All other bits will be written with 0 so will be enabled
PRR0 = _BV(PRTWI);
// disable USART1
PRR1 |= _BV(PRUSART1);
}
// 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)
{
for (int i = 0; i < (HEIGHT*WIDTH)/8; i++)
{
SPItransfer(pgm_read_byte(image + i));
}
}
// paint from a memory buffer, this should be FAST as it's likely what
// will be used by any buffer based subclass
//
// The following assembly code runs "open loop". It relies on instruction
// execution times to allow time for each byte of data to be clocked out.
// It is specifically tuned for a 16MHz CPU clock and SPI clocking at 8MHz.
void Arduboy2Core::paintScreen(uint8_t image[], bool clear)
{
uint16_t count;
asm volatile (
" ldi %A[count], %[len_lsb] \n\t" //for (len = WIDTH * HEIGHT / 8)
" ldi %B[count], %[len_msb] \n\t"
"1: ld __tmp_reg__, %a[ptr] ;2 \n\t" //tmp = *(image)
" out %[spdr], __tmp_reg__ ;1 \n\t" //SPDR = tmp
" cpse %[clear], __zero_reg__ ;1/2 \n\t" //if (clear) tmp = 0;
" mov __tmp_reg__, __zero_reg__ ;1 \n\t"
"2: sbiw %A[count], 1 ;2 \n\t" //len --
" sbrc %A[count], 0 ;1/2 \n\t" //loop twice for cheap delay
" rjmp 2b ;2 \n\t"
" st %a[ptr]+, __tmp_reg__ ;2 \n\t" //*(image++) = tmp
" brne 1b ;1/2 :18 \n\t" //len > 0
" in __tmp_reg__, %[spsr] \n\t" //read SPSR to clear SPIF
: [ptr] "+&e" (image),
[count] "=&w" (count)
: [spdr] "I" (_SFR_IO_ADDR(SPDR)),
[spsr] "I" (_SFR_IO_ADDR(SPSR)),
[len_msb] "M" (WIDTH * (HEIGHT / 8 * 2) >> 8), // 8: pixels per byte
[len_lsb] "M" (WIDTH * (HEIGHT / 8 * 2) & 0xFF), // 2: for delay loop multiplier
[clear] "r" (clear)
);
}
#if 0
// For reference, this is the "closed loop" C++ version of paintScreen()
// used prior to the above version.
void Arduboy2Core::paintScreen(uint8_t image[], bool clear)
{
uint8_t c;
int i = 0;
if (clear)
{
SPDR = image[i]; // set the first SPI data byte to get things started
image[i++] = 0; // clear the first image byte
}
else
SPDR = image[i++];
// the code to iterate the loop and get the next byte from the buffer is
// executed while the previous byte is being sent out by the SPI controller
while (i < (HEIGHT * WIDTH) / 8)
{
// get the next byte. It's put in a local variable so it can be sent as
// as soon as possible after the sending of the previous byte has completed
if (clear)
{
c = image[i];
// clear the byte in the image buffer
image[i++] = 0;
}
else
c = image[i++];
while (!(SPSR & _BV(SPIF))) { } // wait for the previous byte to be sent
// put the next byte in the SPI data register. The SPI controller will
// clock it out while the loop continues and gets the next byte ready
SPDR = c;
}
while (!(SPSR & _BV(SPIF))) { } // wait for the last byte to be sent
}
#endif
void Arduboy2Core::blank()
{
for (int i = 0; i < (HEIGHT*WIDTH)/8; i++)
SPItransfer(0x00);
}
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
// timer 0: Fast PWM, OC0A clear on compare / set at top
// We must stay in Fast PWM mode because timer 0 is used for system timing.
// We can't use "inverted" mode because it won't allow full shut off.
TCCR0A = _BV(COM0A1) | _BV(WGM01) | _BV(WGM00);
OCR0A = 255 - green;
// timer 1: Phase correct PWM 8 bit
// OC1A and OC1B set on up-counting / clear on down-counting (inverted). This
// allows the value to be directly loaded into the OCR with common anode LED.
TCCR1A = _BV(COM1A1) | _BV(COM1A0) | _BV(COM1B1) | _BV(COM1B0) | _BV(WGM10);
OCR1AL = blue;
OCR1BL = red;
#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::setRGBled(uint8_t color, uint8_t val)
{
#ifdef ARDUBOY_10
if (color == RED_LED)
{
OCR1BL = val;
}
else if (color == GREEN_LED)
{
OCR0A = 255 - val;
}
else if (color == BLUE_LED)
{
OCR1AL = val;
}
#elif defined(AB_DEVKIT)
// only blue on DevKit, which is not PWM capable
if (color == BLUE_LED)
{
bitWrite(BLUE_LED_PORT, BLUE_LED_BIT, val ? RGB_ON : RGB_OFF);
}
#endif
}
void Arduboy2Core::freeRGBled()
{
#ifdef ARDUBOY_10
// clear the COM bits to return the pins to normal I/O mode
TCCR0A = _BV(WGM01) | _BV(WGM00);
TCCR1A = _BV(WGM10);
#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()
{
analogRead(0);
int value1 = analogRead(0);
analogRead(1);
int value2 = analogRead(1);
uint8_t buttons = 0;
// up, down, left, right
if (value1 < 480) buttons |= UP_BUTTON; // Up
if (value1 > 540) buttons |= DOWN_BUTTON; // Down
if (value2 < 480) buttons |= LEFT_BUTTON; // Left
if (value2 > 540) buttons |= RIGHT_BUTTON; // Right
// A
if (bitRead(A_BUTTON_PORTIN, A_BUTTON_BIT) == 0) buttons |= A_BUTTON;
// B
if (bitRead(B_BUTTON_PORTIN, B_BUTTON_BIT) == 0) buttons |= B_BUTTON;
return buttons;
}
// delay in ms with 16 bit duration
void Arduboy2Core::delayShort(uint16_t ms)
{
delay((unsigned long) ms);
}
void Arduboy2Core::exitToBootloader()
{
cli();
// set bootloader magic key
// storing two uint8_t instead of one uint16_t saves an instruction
// when high and low bytes of the magic key are the same
*(uint8_t *)MAGIC_KEY_POS = lowByte(MAGIC_KEY);
*(uint8_t *)(MAGIC_KEY_POS + 1) = highByte(MAGIC_KEY);
// enable watchdog timer reset, with 16ms timeout
wdt_reset();
WDTCSR = (_BV(WDCE) | _BV(WDE));
WDTCSR = _BV(WDE);
while (true) { }
}
// Replacement main() that eliminates the USB stack code.
// Used by the ARDUBOY_NO_USB macro. This should not be called
// directly from a sketch.
void Arduboy2Core::mainNoUSB()
{
// disable USB
UDCON = _BV(DETACH);
UDIEN = 0;
UDINT = 0;
USBCON = _BV(FRZCLK);
UHWCON = 0;
power_usb_disable();
init();
// This would normally be done in the USB code that uses the TX and RX LEDs
TX_RX_LED_INIT;
TXLED0;
RXLED0;
// Set the DOWN button pin for INPUT_PULLUP
bitSet(DOWN_BUTTON_PORT, DOWN_BUTTON_BIT);
bitClear(DOWN_BUTTON_DDR, DOWN_BUTTON_BIT);
// Delay to give time for the pin to be pulled high if it was floating
delayShort(10);
// if the DOWN button is pressed
if (bitRead(DOWN_BUTTON_PORTIN, DOWN_BUTTON_BIT) == 0) {
exitToBootloader();
}
// The remainder is a copy of the Arduino main() function with the
// USB code and other unneeded code commented out.
// init() was called above.
// The call to function initVariant() is commented out to fix compiler
// error: "multiple definition of 'main'".
// The return statement is removed since this function is type void.
// init();
// initVariant();
//#if defined(USBCON)
// USBDevice.attach();
//#endif
setup();
for (;;) {
loop();
// if (serialEventRun) serialEventRun();
}
// return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment