mirror of https://github.com/MLXXXp/Arduboy2.git
545 lines
14 KiB
C++
545 lines
14 KiB
C++
/**
|
|
* @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()
|
|
{
|
|
// Port B INPUT_PULLUP or HIGH
|
|
PORTB |= _BV(B_BUTTON_BIT) | _BV(UP_BUTTON_BIT) | _BV(DOWN_BUTTON_BIT);
|
|
// Port B INPUT or LOW
|
|
PORTB &= ~(_BV(SPEAKER_BIT));
|
|
// Port B inputs
|
|
DDRB &= ~(_BV(B_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(SPEAKER_BIT);
|
|
|
|
// Port C INPUT_PULLUP or HIGH (none)
|
|
// Port C INPUT or LOW
|
|
PORTC &= ~(_BV(BATT_STAT_BIT) | _BV(LEDL_BIT));
|
|
// Port C inputs
|
|
DDRC &= ~(_BV(BATT_STAT_BIT));
|
|
// Port C outputs
|
|
DDRC |= _BV(LEDL_BIT);
|
|
|
|
// Port D INPUT_PULLUP or HIGH (none)
|
|
// Port D INPUT or LOW
|
|
PORTD &= ~(_BV(I2C_SCL_BIT) | _BV(I2C_SDA_BIT) | _BV(BLE_IRQ_BIT) |
|
|
_BV(RTC_INT_BIT) | _BV(BATT_EN_BIT) | _BV(BATT_LVL_BIT) |
|
|
_BV(LEDR_BIT));
|
|
// Port D inputs
|
|
DDRD &= ~(_BV(I2C_SCL_BIT) | _BV(I2C_SDA_BIT) | _BV(BLE_IRQ_BIT) |
|
|
_BV(RTC_INT_BIT) | _BV(BATT_LVL_BIT));
|
|
// Port D outputs
|
|
DDRD |= _BV(BATT_EN_BIT) | _BV(LEDR_BIT);
|
|
|
|
// Port E INPUT_PULLUP or HIGH (none)
|
|
// Port E INPUT or LOW
|
|
PORTE &= ~(_BV(MPU_INT_BIT));
|
|
// Port E inputs
|
|
DDRE &= ~(_BV(MPU_INT_BIT));
|
|
// Port E outputs (none)
|
|
|
|
// Port F INPUT_PULLUP or HIGH
|
|
PORTF |= _BV(CS_BIT) | _BV(BLE_CS_BIT) | _BV(BLE_RST_BIT);
|
|
// Port F INPUT or LOW
|
|
PORTF &= ~(_BV(RAND_SEED_IN_BIT) | _BV(RST_BIT));
|
|
// Port F inputs
|
|
DDRF &= ~(_BV(RAND_SEED_IN_BIT));
|
|
// Port F outputs
|
|
DDRF |= _BV(CS_BIT) | _BV(RST_BIT) | _BV(DC_BIT) |
|
|
_BV(BLE_CS_BIT) | _BV(BLE_RST_BIT);
|
|
}
|
|
|
|
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 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)
|
|
{
|
|
(void) blue; // parameter not used
|
|
|
|
// from http://r6500.blogspot.com/2014/12/fast-pwm-on-arduino-leonardo.html
|
|
TCCR4D = 0; // timer 4, Fast PWM
|
|
TCCR4B = _BV(CS41); // 187500Hz
|
|
PLLFRQ = (PLLFRQ & 0xCF) | _BV(PLLTM1) | _BV(PLLTM0); // 96MHz/2 = 48MHz
|
|
OCR4C = 255; // terminal count for timer 4 PWM
|
|
|
|
// timer 4A (LEDL / Arduino pin 13)
|
|
TCCR4A |= _BV(COM4A1) | _BV(PWM4A);
|
|
OCR4A = red;
|
|
|
|
// timer 4D (LEDR / Arduino pin 6)
|
|
TCCR4C |= _BV(COM4D1) | _BV(PWM4D);
|
|
OCR4D = green;
|
|
}
|
|
|
|
void Arduboy2Core::setRGBled(uint8_t color, uint8_t val)
|
|
{
|
|
if (color == RED_LED)
|
|
{
|
|
OCR4A = val; // (LEDL / Arduino pin 13)
|
|
}
|
|
else if (color == GREEN_LED)
|
|
{
|
|
OCR4D = val; // (LEDR / Arduino pin 6)
|
|
}
|
|
}
|
|
|
|
void Arduboy2Core::freeRGBled()
|
|
{
|
|
// clear the COM bits to return the pins to normal I/O mode
|
|
TCCR4A = 0;
|
|
TCCR4C = 0;
|
|
}
|
|
|
|
void Arduboy2Core::digitalWriteRGB(uint8_t red, uint8_t green, uint8_t blue)
|
|
{
|
|
(void) blue; // parameter not used
|
|
|
|
bitWrite(LEDL_PORT, LEDL_BIT, red);
|
|
bitWrite(LEDR_PORT, LEDR_BIT, green);
|
|
}
|
|
|
|
void Arduboy2Core::digitalWriteRGB(uint8_t color, uint8_t val)
|
|
{
|
|
if (color == RED_LED)
|
|
{
|
|
bitWrite(LEDL_PORT, LEDL_BIT, val);
|
|
}
|
|
else if (color == GREEN_LED)
|
|
{
|
|
bitWrite(LEDR_PORT, LEDR_BIT, val);
|
|
}
|
|
}
|
|
|
|
/* Buttons */
|
|
|
|
uint8_t Arduboy2Core::buttonsState()
|
|
{
|
|
uint8_t buttons;
|
|
|
|
// up, down, B
|
|
buttons = ((~PINB) & (_BV(UP_BUTTON_BIT) | _BV(B_BUTTON_BIT) |
|
|
_BV(DOWN_BUTTON_BIT)));
|
|
|
|
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;
|
|
}
|
|
|