/** * @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; }