//------------------------------------------------------------- // Super Big Hourglass Sand Simulation (32x64) // 2020.06.01 RSP (SAMD21) // 2025.10.10 RSP (ESP32-C3) // 2026.04.01 RSP (RP2040-Zero) // Target hardware: // RP2040-Zero // Custom dual 32x32 R/G LED panels with HT16K33 drivers // // User Interface: // Power On: displays test pattern; press any button to start. // BLU Button 1 - changes mode // 1 standard hourglass (joined interior corners), automatic gravity reverse // 2 wrap hourglass (joined exterior corners), runs forever // ORG Button 2 - changes color // R Dial 1 - changes speed // L Dial 2 - changes brightness // Mode and color settings are saved automatically. // LED Onboard RP2040-Zero // Green is good // Flashing red indicates error number (see E_xxx codes) //------------------------------------------------------------- //#define SERIAL_DEBUG // enable debug output & serial console #ifdef SERIAL_DEBUG #define TRACE(...) { Serial.printf(__VA_ARGS__); } #else #define TRACE(...) {;} #endif #define MAX_GRAINS 700 // Number of grains of sand in normal hourglass (mostly full) #define HALF_FULL 511 // amount of sand in continuous hourglass (half full) #define HEIGHT 64 // Display height in pixels #define WIDTH 32 // Display width in pixels //#define MAX_FPS 120 // '328 Arduino //#define MAX_FPS 200 // SAMD21 ADAFRUIT_TRINKET_M0 //#define MAX_FPS 250 // ESP32-C3 #define MAX_FPS 250 // RP2040-Zero #define DIAL1_PIN A0 // speed dial // brightness knob #define DIAL2_PIN A1 // brightness dial #define MAX_BRIGHTNESS 5 // 0 is min brightness, 15 max #define BRITE_SPAN (1023/(MAX_BRIGHTNESS+1)) #define BRITE_HYST (BRITE_SPAN/20) // 10% hysteresis #define Q1 1 // double hourglass quadrants #define Q2 2 #define _RED 0b01 // grain colors #define _GRN 0b10 #define _ORG 0b11 // The 'sand' grains exist in an integer coordinate space that's 256X // the scale of the pixel grid, allowing them to move and interact at // less than whole-pixel increments. #define MAX_X (WIDTH * 256 - 1) // Maximum X coordinate in grain space #define MAX_Y (HEIGHT * 256 - 1) // Maximum Y coordinate struct Grain { int x, y; // Position int vx, vy; // Velocity } grain[ MAX_GRAINS ]; uint grains; //------------------------------------------------------ // coroutine macros #define coBegin { static int _state_ = 0; static uint32_t _tm_; switch(_state_) { case 0:; #define coEnd _state_ = 0; }} #define coDelay(msec) { _state_ = __LINE__; _tm_=millis(); return; case __LINE__: if (millis()-_tm_ < msec) return; } //------------------------------------------------------------- // RP2040-Zero onboard LED is a neopixel on pin 16 #include Adafruit_NeoPixel status_pixel(1, 16, NEO_RGB + NEO_KHZ800); #define E_NONE 0 // normal #define E_PIO 1 // PIO failed to initialize #define E_DMA 2 // DMA failed to initialize #define E_OVERRUN 3 // DMA overrun (program logic problem) #define E_CORE1 4 // Core1 failed to stop #define E_TEST 5 // fault test uint current_error = E_NONE; String E_DESC[6] = { "NONE", "CAN'T ADD PIO PROGRAM", "DMA CHANNEL NOT AVAILABLE", "DMA OVERRUN", "CORE1 FAULT", "TEST" }; void fault( uint e_code ) { if (e_code > 5) e_code = 5; current_error = e_code; TRACE("%s\n", E_DESC[e_code]); } void faultTask() { static int i; coBegin // LED = GRN status_pixel.setPixelColor( 0, 0x020000 ); // GRN status_pixel.show(); do { coDelay(10) } while (current_error == 0); // blink error code do { for (i=current_error; i > 0; i--) { status_pixel.setPixelColor( 0, 0x000200 ); // RED status_pixel.show(); coDelay(100) status_pixel.setPixelColor( 0, 0 ); status_pixel.show(); coDelay(250) } coDelay(900) } while (current_error != 0); coEnd } //------------------------------------------------------------- // buttons #define MODE_PIN 14 // button wired to gnd #define COLOR_PIN 15 #define BUTTON_DEBOUNCE_MS 50UL struct { uint pin; boolean pressed; // press event flag boolean state; // debounced button state uint32_t tm; } button[2] = { {MODE_PIN,false,false,0}, {COLOR_PIN,false,false,0} }; #define MODE_BTN_INDEX 0 #define COLOR_BTN_INDEX 1 void monitorButtons() { for (uint i=0; i < 2; i++) { if (!digitalRead(button[i].pin)) // pressed ? { if (!button[i].state) // not already pressed ? { button[i].state = true; button[i].pressed = true; } button[i].tm = millis(); // keep debounce alive } else // not pressed if (button[i].state) if (millis() - button[i].tm > BUTTON_DEBOUNCE_MS) button[i].state = false; } } //------------------------------------------------------------- // settings in EEPROM // emulated by RP2040 #include #define SETTINGS_UPDATE_MSEC 5000UL // automatically update settings after 5 seconds #define NUMBER_COLORS 8 struct { uint c_border; uint c_sand; uint c_spot; } color_table[ NUMBER_COLORS ] = {//Border Sand Spot { _RED, _GRN, _ORG }, // with spot { _GRN, _RED, _ORG }, { _ORG, _GRN, _RED }, { _ORG, _RED, _GRN }, { _RED, _GRN, _GRN }, // no spot { _GRN, _RED, _RED }, { _ORG, _GRN, _GRN }, { _ORG, _RED, _RED } }; #define NUMBER_MODES 2 #define MODE_HOURGLASS 0 #define MODE_WRAP 1 struct SETTINGS_STRUCT { uint8_t mode; // MODE setting uint8_t color; // color scheme }; SETTINGS_STRUCT settings; SETTINGS_STRUCT original_settings; boolean settings_dirty; uint32_t settings_tm; boolean getSettings() { TRACE("getSettings\n"); EEPROM.get(0,settings); if (settings.mode >= NUMBER_MODES) settings.mode = 0; if (settings.color >= NUMBER_COLORS) settings.color = 0; original_settings = settings; TRACE("Settings: mode %i, color %i\n", settings.mode, settings.color ); settings_dirty = false; return true; // success } boolean putSettings() { TRACE("putSettings\n"); EEPROM.put(0,settings); EEPROM.commit(); original_settings = settings; settings_dirty = false; return true; // success } //------------------------------------------------------------- // HT16K33 LED driver // using RP2040 PIO #define LED_I2C_BASE_ADDR 0x70 // 0x70..0x77 #define NUM_MODULES_IN_BANK 8 #define NUM_BANKS 4 // a bank is 8 HT16K33's uint32_t r_pixels[HEIGHT]; // display state uint32_t g_pixels[HEIGHT]; // - - - - - - - - - - - - - - - - - - // PIO stuff #include "pico/stdlib.h" #include "i2c_blind_write.pio.h" #define I2C_CLOCK_RATE 400000 // max 400000 PIO pio = pio1; // uses all of one PIO (4 sm's) int dma_chan[NUM_BANKS]; // plus one DMA channel per sm too #define USE_DMA // write 32 bits to PIO tx fifo static inline void pio_i2c_put(uint sm, uint32_t data) { while (pio_sm_is_tx_fifo_full(pio, sm)) ; // DMA will stall here // some versions of GCC dislike this #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstrict-aliasing" #endif *(io_rw_32 *)&pio->txf[sm] = data; #ifdef __GNUC__ #pragma GCC diagnostic pop #endif } // DMA output buffer to SM // HT16K33 = 16 bytes * 32 = 512 bytes (both panels) // 256 bytes/panel // 4 I2C blocks total * 8 chips = 32 chips total const uint BLOCK_XFER_SIZE = 8 // 8 chips * (1 // I2C START + chip_addr + register, packed in 1 dword +8 // 16 bytes of data, packed 2 bytes per dword +1); // I2C STOP const uint DMA_BUF_SIZE = ( NUM_BANKS * BLOCK_XFER_SIZE); uint32_t dma_buff[ DMA_BUF_SIZE ]; // PIO data stream encoding #define START (1U << 24) // must include some data #define STOP (1U << 25) #define LEN_1 (0U << 26) // only dataA #define LEN_2 (1U << 26) // dataA, dataB #define LEN_3 (2U << 26) // dataA, dataB, dataC // write a command to HT16K33 void pio_i2c_write_command(uint sm, byte chip_addr, byte command) { // start, chip address, and command are all packed in one 32 bit word pio_i2c_put( sm, START | LEN_2 | (chip_addr << 17) | (command << 8) ); pio_i2c_put( sm, STOP ); } // write data to all HT16K33's // 4 banks of 8 displays void pio_i2c_write_leds() { uint bp_r = 0; uint bp_g = 0; uint32_t *op = dma_buff; // each sm talks to an 8 chip block for (uint sm=0; sm < NUM_BANKS; sm++) { { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+0) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+7] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+6] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+5] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+4] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+3] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+2] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+1] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+0] >> 16) & 0x0000FF00); // I2C stop *(op++) = STOP; } { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+1) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7]) & 0x00FF0000) | ((r_pixels[bp_r+7] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6]) & 0x00FF0000) | ((r_pixels[bp_r+6] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5]) & 0x00FF0000) | ((r_pixels[bp_r+5] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4]) & 0x00FF0000) | ((r_pixels[bp_r+4] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3]) & 0x00FF0000) | ((r_pixels[bp_r+3] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2]) & 0x00FF0000) | ((r_pixels[bp_r+2] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1]) & 0x00FF0000) | ((r_pixels[bp_r+1] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0]) & 0x00FF0000) | ((r_pixels[bp_r+0] >> 8) & 0x0000FF00); // I2C stop *(op++) = STOP; } { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+2) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7] << 8) & 0x00FF0000) | ((r_pixels[bp_r+7]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6] << 8) & 0x00FF0000) | ((r_pixels[bp_r+6]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5] << 8) & 0x00FF0000) | ((r_pixels[bp_r+5]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4] << 8) & 0x00FF0000) | ((r_pixels[bp_r+4]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3] << 8) & 0x00FF0000) | ((r_pixels[bp_r+3]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2] << 8) & 0x00FF0000) | ((r_pixels[bp_r+2]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1] << 8) & 0x00FF0000) | ((r_pixels[bp_r+1]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0] << 8) & 0x00FF0000) | ((r_pixels[bp_r+0]) & 0x0000FF00); // I2C stop *(op++) = STOP; } { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+3) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7] << 16) & 0x00FF0000) | ((r_pixels[bp_r+7] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6] << 16) & 0x00FF0000) | ((r_pixels[bp_r+6] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5] << 16) & 0x00FF0000) | ((r_pixels[bp_r+5] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4] << 16) & 0x00FF0000) | ((r_pixels[bp_r+4] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3] << 16) & 0x00FF0000) | ((r_pixels[bp_r+3] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2] << 16) & 0x00FF0000) | ((r_pixels[bp_r+2] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1] << 16) & 0x00FF0000) | ((r_pixels[bp_r+1] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0] << 16) & 0x00FF0000) | ((r_pixels[bp_r+0] << 8) & 0x0000FF00); // I2C stop *(op++) = STOP; } bp_g += 8; bp_r += 8; { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+4) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+7] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+6] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+5] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+4] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+3] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+2] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+1] >> 16) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0] >> 8) & 0x00FF0000) | ((r_pixels[bp_r+0] >> 16) & 0x0000FF00); // I2C stop *(op++) = STOP; } { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+5) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7]) & 0x00FF0000) | ((r_pixels[bp_r+7] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6]) & 0x00FF0000) | ((r_pixels[bp_r+6] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5]) & 0x00FF0000) | ((r_pixels[bp_r+5] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4]) & 0x00FF0000) | ((r_pixels[bp_r+4] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3]) & 0x00FF0000) | ((r_pixels[bp_r+3] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2]) & 0x00FF0000) | ((r_pixels[bp_r+2] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1]) & 0x00FF0000) | ((r_pixels[bp_r+1] >> 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0]) & 0x00FF0000) | ((r_pixels[bp_r+0] >> 8) & 0x0000FF00); // I2C stop *(op++) = STOP; } { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+6) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7] << 8) & 0x00FF0000) | ((r_pixels[bp_r+7]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6] << 8) & 0x00FF0000) | ((r_pixels[bp_r+6]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5] << 8) & 0x00FF0000) | ((r_pixels[bp_r+5]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4] << 8) & 0x00FF0000) | ((r_pixels[bp_r+4]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3] << 8) & 0x00FF0000) | ((r_pixels[bp_r+3]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2] << 8) & 0x00FF0000) | ((r_pixels[bp_r+2]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1] << 8) & 0x00FF0000) | ((r_pixels[bp_r+1]) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0] << 8) & 0x00FF0000) | ((r_pixels[bp_r+0]) & 0x0000FF00); // I2C stop *(op++) = STOP; } { // I2C start, chip address, and register 0 *(op++) = START | LEN_2 | ((LED_I2C_BASE_ADDR+7) << 17); // data *(op++) = LEN_2 | ((g_pixels[bp_g+7] << 16) & 0x00FF0000) | ((r_pixels[bp_r+7] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+6] << 16) & 0x00FF0000) | ((r_pixels[bp_r+6] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+5] << 16) & 0x00FF0000) | ((r_pixels[bp_r+5] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+4] << 16) & 0x00FF0000) | ((r_pixels[bp_r+4] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+3] << 16) & 0x00FF0000) | ((r_pixels[bp_r+3] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+2] << 16) & 0x00FF0000) | ((r_pixels[bp_r+2] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+1] << 16) & 0x00FF0000) | ((r_pixels[bp_r+1] << 8) & 0x0000FF00); *(op++) = LEN_2 | ((g_pixels[bp_g+0] << 16) & 0x00FF0000) | ((r_pixels[bp_r+0] << 8) & 0x0000FF00); // I2C stop *(op++) = STOP; } bp_g += 8; bp_r += 8; } // send all blocks to PIO TxFIFO's op = dma_buff; for (uint sm=0; sm < NUM_BANKS; sm++, op += BLOCK_XFER_SIZE) { // start all 4 DMA channels at the same time #ifdef USE_DMA if (!dma_channel_is_busy(sm)) // check for DMA overrun { dma_channel_set_read_addr (dma_chan[sm], op, false); dma_channel_set_trans_count(dma_chan[sm], BLOCK_XFER_SIZE, true); // start transfer } else fault(E_OVERRUN); #else // use blocking IO for development for (uint n = 0; n < BLOCK_XFER_SIZE; n++) pio_i2c_put( sm, *(op+n) ); #endif } } // - - - - - - - - - - - - - - - - - - // HT16K33 stuff #define HT16K33_BLINK_CMD 0x80 #define HT16K33_BLINK_DISPLAYON 0x01 #define HT16K33_BLINK_OFF 0 #define HT16K33_BLINK_2HZ 1 #define HT16K33_BLINK_1HZ 2 #define HT16K33_BLINK_HALFHZ 3 #define HT16K33_CMD_BRIGHTNESS 0xE0 void start_osc() { for (uint bank=0; bank < NUM_BANKS; bank++) for (uint i=0; i < NUM_MODULES_IN_BANK; i++) { delay(5); pio_i2c_write_command(bank, LED_I2C_BASE_ADDR+i, 0x21 ); // turn on oscillator } } void set_brightness( uint8_t b ) // 0..15 { for (uint bank=0; bank < NUM_BANKS; bank++) for (uint i=0; i < NUM_MODULES_IN_BANK; i++) { delay(5); pio_i2c_write_command(bank, LED_I2C_BASE_ADDR+i, HT16K33_CMD_BRIGHTNESS + b ); } } void set_display( boolean isOn ) { for (uint bank=0; bank < NUM_BANKS; bank++) for (uint i=0; i < NUM_MODULES_IN_BANK; i++) { delay(5); pio_i2c_write_command(bank, LED_I2C_BASE_ADDR+i, HT16K33_BLINK_CMD + (isOn ? HT16K33_BLINK_DISPLAYON : 0) ); } } // test pattern to help identify the location of HT16K33 modules const uint32_t test_pattern[] = { //12345678901234567890123456789012 //1 2 3 4 0b10000000110000001110000011110000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b11111000111111001111111011111111, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b10000000110000001110000011110000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b11111000111111001111111011111111, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b10000000110000001110000011110000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b11111000111111001111111011111111, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b10000000110000001110000011110000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, //1 2 3 4 0b11111000111111001111111011111111, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000, 0b00000000000000000000000000000000 }; // combine ht16k33 number (red) test pattern with bank number (green) void generateTestPattern() { for (int i=0; i < HEIGHT; i++) r_pixels[i] = g_pixels[i] = 0; // ht16k33 number in red for (uint i=0; i < HEIGHT; i++) r_pixels[i] = test_pattern[i]; // block number in green uint b = 0x80; for (uint i=0; i < 4; i++, b = (b >> 1) | 0x80) { g_pixels[i*16+1] = b | (b << 8) | (b << 16) | (b << 24); g_pixels[i*16+9] = b | (b << 8) | (b << 16) | (b << 24); } } // regular hourglass const uint32_t hourglass[] = { //12345678901234567890123456789012 0b11111111111111111111111111111111, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b01111111111111111111111111111111, 0b11111111111111111111111111111110, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b11111111111111111111111111111111}; // hourglass with a hole in the bottom that wraps to the top const uint32_t hourglass_wrap[] = { 0b11111111111111111111111111111110, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b01111111111111111111111111111111, 0b11111111111111111111111111111110, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b10000000000000000000000000000001, 0b01111111111111111111111111111111}; void paint_LEDs( ) // update matrix LEDs { pio_i2c_write_leds(); } // drop random pixels into a quadrant void randomGrain( uint quadrant, uint color, uint quantity ) { uint y0,y1; if (quadrant == Q1) { y0=0; y1=(HEIGHT/2); } else { y0=(HEIGHT/2); y1= HEIGHT; } for (; quantity; quantity--) { uint x,y; do // find an unused cell { x = random(1,WIDTH-1); // keep inside borders y = random(y0+1,y1-1); if ( (r_pixels[y] & (1 << x)) || (g_pixels[y] & (1 << x)) ) continue; else break; } while(1); // allocate pixel grain[grains].vx = grain[grains].vy = 0; // velocity is zero grain[grains].x = x*256; grain[grains].y = y*256; // render pixel if (color & _RED) r_pixels[y] |= (1 << x); if (color & _GRN) g_pixels[y] |= (1 << x); grains++; } } // initialize backgnd & grains per mode void initMode( uint8_t m ) { for (uint i=0; i < HEIGHT; i++) r_pixels[i] = g_pixels[i] = 0; // border const uint32_t *pattern = (m == MODE_HOURGLASS) ? hourglass : hourglass_wrap; if (color_table[ settings.color ].c_border & _RED) for (uint i=0; i < HEIGHT; i++) r_pixels[i] = pattern[i]; if (color_table[ settings.color ].c_border & _GRN) for (uint i=0; i < HEIGHT; i++) g_pixels[i] = pattern[i]; // sand grains = 0; // nothing in grain[] array randomGrain( Q1, color_table[ settings.color ].c_sand, (m == MODE_HOURGLASS) ? MAX_GRAINS-1 : HALF_FULL ); randomGrain( Q1, color_table[ settings.color ].c_spot, 1 ); } //------------------------------------------------------------- // SETUP - RUNS ONCE AT PROGRAM START ------------------------- // inter-core variables uint frame_ms = 25; // frame rate limiter int core1_control = 0; // core1 is stopped, -1 = request to stop, 1 = running void waitForCore1() // stop core1 so we can update the pixels without interference { core1_control = -1; // ask core1 to stop for (uint i=0; i < 1000; i++) if (core1_control == 0) return; else delay(1); fault(E_CORE1); // THAT TOOK TOO LONG } // I2C pins #define PIN_SCL1 2 #define PIN_SDA1 3 #define PIN_SCL2 4 #define PIN_SDA2 5 #define PIN_SCL3 6 #define PIN_SDA3 7 #define PIN_SCL4 8 #define PIN_SDA4 9 void setup(void) { #ifdef SERIAL_DEBUG Serial.begin(115200); for (int i=10; i > 0; i--) { TRACE("%i\n",i); delay(1000); } #endif // buttons pinMode(MODE_PIN, INPUT_PULLUP); pinMode(COLOR_PIN, INPUT_PULLUP); // make ADC 10 bit analogReadResolution(10); // the default for Arduino // initialize I2C for display // load I2C blind write PIO program int pio_program_offset = pio_add_program(pio, &i2c_blind_write_program); TRACE("PIO program offset = %i\n",pio_program_offset); if (pio_program_offset < 0) fault(E_PIO); else // start (4) copies of Tx handler, { // this uses all of the PIO for (uint sm=0; sm < 4; sm++) pio_sm_claim( pio, sm ); i2c_program_init(pio, 0, pio_program_offset, I2C_CLOCK_RATE, PIN_SDA1, PIN_SCL1 ); i2c_program_init(pio, 1, pio_program_offset, I2C_CLOCK_RATE, PIN_SDA2, PIN_SCL2 ); i2c_program_init(pio, 2, pio_program_offset, I2C_CLOCK_RATE, PIN_SDA3, PIN_SCL3 ); i2c_program_init(pio, 3, pio_program_offset, I2C_CLOCK_RATE, PIN_SDA4, PIN_SCL4 ); } // initialize onboard LED // this also uses a PIO & sm status_pixel.begin(); // get settings from EEPROM EEPROM.begin(512); getSettings(); #ifdef USE_DMA TRACE("DMA setup started\n"); // get (4) DMA channels connected to the (4) PIO SM's for (uint sm=0; sm < NUM_BANKS; sm++) // each sm gets a DMA channel { // claim and configure a DMA channel dma_chan[sm] = dma_claim_unused_channel(true); if (dma_chan[sm] < 0) fault(E_DMA); else { dma_channel_config cfg = dma_channel_get_default_config(dma_chan[sm]); channel_config_set_transfer_data_size(&cfg, DMA_SIZE_32); channel_config_set_read_increment(&cfg, true); channel_config_set_write_increment(&cfg, false); channel_config_set_dreq(&cfg, pio_get_dreq(pio, sm, true)); dma_channel_configure( dma_chan[sm], &cfg, &pio->txf[sm], NULL, // no transfer address for now 0, // zero count for now false // don't start yet ); } } // end of DMA -> PIO setup, she's armed now #endif TRACE("starting display\n"); // initialize matrix display start_osc(); set_brightness( 0 ); for (int i=0; i < HEIGHT; i++) r_pixels[i] = g_pixels[i] = 0; delay(100); paint_LEDs(); // blank the screen delay(100); set_display( true ); delay(100); generateTestPattern(); paint_LEDs(); delay(100); // wait for any button to be pressed while (digitalRead(MODE_PIN) && digitalRead(COLOR_PIN)) delay(1); for (int i=0; i < HEIGHT; i++) r_pixels[i] = g_pixels[i] = 0; paint_LEDs(); // blank the screen for (int n=50; n > 0; n--) if (digitalRead(MODE_PIN)==0 || digitalRead(COLOR_PIN)==0) n=50; else delay(1); // set initial display state initMode( settings.mode ); paint_LEDs(); delay(100); // after this point the I2C is owned by the second core core1_control = 1; // run // onboard LED = GRN status_pixel.setPixelColor( 0, 0x040000 ); // GRN status_pixel.show(); } //------------------------------------------------------------- // First core // user interface & slow stuff void loop() { static uint32_t poll_tm = millis(); static uint brightness = 0; //-- maintain the status LED faultTask(); // blinks the led if a fault has occurred //-- monitor buttons monitorButtons(); // debounce & generate press events // mode change if (button[MODE_BTN_INDEX].pressed) { // select next mode waitForCore1(); // stop core1 button[MODE_BTN_INDEX].pressed = false; initMode( settings.mode = (settings.mode+1) % NUMBER_MODES ); settings_dirty = true; settings_tm = millis(); TRACE("MODE %i\n",settings.mode); } // color change if (button[COLOR_BTN_INDEX].pressed) { // change color waitForCore1(); // stop core1 button[COLOR_BTN_INDEX].pressed = false; // color shift settings.color = (settings.color+1) % NUMBER_COLORS; TRACE("COLOR %i\n",settings.color); initMode( settings.mode ); settings_dirty = true; settings_tm = millis(); } //-- periodically monitor knobs & update settings if (millis() - poll_tm > 100) { poll_tm = millis(); { // brightness knob int x = analogRead(DIAL2_PIN); // x = ADC counts //TRACE("brite adc %i\n",x); int lp = brightness*BRITE_SPAN - BRITE_HYST; int hp = brightness*BRITE_SPAN+BRITE_SPAN + BRITE_HYST; if (lp < 0) lp = 0; if (hp > 1023) hp = 1023; if (x < lp || x > hp) { x = map( x, 0,1023, 0,MAX_BRIGHTNESS ); if (x != (int)brightness) { waitForCore1(); // stop core1 set_brightness( brightness = x ); TRACE("brite %i\n",brightness); } } } { // speed knob uint x = analogRead(DIAL1_PIN); // x = ADC counts x = map( x, 0,1023, 1,MAX_FPS ); // x = FPS x = 1000/x; // msec x = constrain( x, 4, 50 ); frame_ms = x; core1_control = 1; // reenable core1 after mode/color/brightness change //TRACE("frame %i\n",frame_ms); } // settings update if (settings_dirty) if (millis() - settings_tm > SETTINGS_UPDATE_MSEC) { if (settings.mode != original_settings.mode || settings.color != original_settings.color) putSettings(); } } //-- serial monitor #ifdef SERIAL_DEBUG { int c = Serial.read(); switch (c) { case 'C': for (int i=0; i < HEIGHT; i++) r_pixels[i] = g_pixels[i] = 0; paint_LEDs(); break; case 'c': fault(E_NONE); break; // clear error case 'e': fault(E_TEST); break; // error test case 'p': generateTestPattern(); paint_LEDs(); break; case 'g': status_pixel.setPixelColor( 0, 0x020000 ); status_pixel.show(); break; case 'r': status_pixel.setPixelColor( 0, 0x000200 ); status_pixel.show(); break; case 'b': status_pixel.setPixelColor( 0, 0x000002 ); status_pixel.show(); break; case '0': set_brightness( 0 ); break; case '1': set_brightness( 1 ); break; case '2': set_brightness( 2 ); break; case '3': set_brightness( 3 ); break; case '4': set_brightness( 4 ); break; case '5': set_brightness( 5 ); break; case '6': set_brightness( 6 ); break; case '7': set_brightness( 7 ); break; case '8': set_brightness( 8 ); break; case '9': set_brightness( 15); break; } } #endif } // - - - - - - - - - - - - - - - - - - // Second core // runs without interrupts void setup1() { // each core has it's own random() generator while (frame_ms == 0) random(0x7FFFFFFF);; // stall for core1 } void loop1() { static uint32_t poll_tm = millis(); // timers static uint32_t frame_tm = millis(); static int ax = 0; // gravity vector static int ay = 0; static boolean pixel_motion = true; // done detection static uint32_t deadman_tm = millis(); // gravity... // normal, steady // idle, counting down (5 secs), steady // shifting (3 secs -256..256 at 10/sec => 17/step) static int gravity_control = 0; static int gravity = 256; // -256..256 = -1..+1 #ifdef SERIAL_DEBUG // diagnostic - show actual FPS static uint fps_count = 0; static uint32_t fps_tm = millis(); if (millis() - fps_tm >= 5000) { fps_tm = millis(); Serial.print("FPS: "); Serial.println(fps_count/5); fps_count = 0; } else fps_count++; #endif // hold core1 if (core1_control < 0) core1_control = 0; // stop request response if (core1_control == 0) return; // stopped //-- frame rate // Limit the animation frame rate while (millis() - frame_tm < frame_ms) ; // STALL for frame sync frame_tm = millis(); // Display frame rendered on prior pass. // It's done immediately after the frame rate sync for consistent animation timing. paint_LEDs(); // apply 2D accel vector to grain velocities... for (uint i=0; i < grains; i++) { grain[i].vx += ax; // + az; //+ random(18); // A little randomness makes grain[i].vy += ay; // + az; //+ random(18); // tall stacks topple better! // Terminal velocity (in any direction) is 256 units -- equal to // 1 pixel -- which keeps moving grains from passing through each other // and other such mayhem. Though it takes some extra math, velocity is // clipped as a 2D vector (not separately-limited X & Y) so that // diagonal movement isn't faster uint32_t v2 = (int32_t)grain[i].vx * grain[i].vx + (int32_t)grain[i].vy * grain[i].vy; if(v2 > 65536) // If v^2 > 65536, then v > 256 { float v = sqrt((float)v2); // Velocity vector magnitude grain[i].vx = (int)(256.0*(float)grain[i].vx/v); // Maintain heading grain[i].vy = (int)(256.0*(float)grain[i].vy/v); // Limit magnitude } } // Update position of each grain for (uint i=0; i < grains; i++) { int newx, newy; uint dx, dy, oldpx, oldpy, newpx, newpy; boolean quadrant_leak = false; boolean wrapped = false; newx = grain[i].x + grain[i].vx; // New position in grain space newy = grain[i].y + grain[i].vy; // boundary collision check // double hourglass // 0,0+----+ +----+ // | Q1 | | Q1 | // | | | | // +----+ +----+ // 0,32+----+ | Q2 | // | Q2 | | | // | | +----+ // +----+ 32,64 newx = grain[i].x + grain[i].vx; // New position in grain space newy = grain[i].y + grain[i].vy; if (grain[i].y < (HEIGHT/2)*256) { // Q1: top square if ((newx < 0 && newy < 0) ) { newx =(WIDTH-1)*256; newy = (HEIGHT-1)*256; quadrant_leak = wrapped = true; } else if ((newx >= WIDTH*256 && newy >=WIDTH*256)) { newx = 0; newy = (HEIGHT/2)*256; quadrant_leak = true; } else { if (newx >= WIDTH*256) { newx = WIDTH*256-1; grain[i].vx /= -2; } else if (newx < 0) { newx = 0; grain[i].vx /= -2; } if (newy >= (HEIGHT/2)*256) { newy = (HEIGHT/2)*256-1; grain[i].vy /= -2; } else if (newy < 0) { newy = 0; grain[i].vy /= -2; } } } else { // Q2: bottom square if ((newx < 0 && newy < (HEIGHT/2)*256)) { newx =(WIDTH-1)*256; newy =(HEIGHT/2-1)*256; quadrant_leak = true; } else if ((newx >= WIDTH*256 && newy >=HEIGHT*256)) { newx = 0; newy = 0; quadrant_leak = wrapped = true; } else { if (newx >= WIDTH*256) { newx = WIDTH*256-1; grain[i].vx /= -2; } else if (newx < 0) { newx = 0; grain[i].vx /= -2; } if (newy >= MAX_Y) { newy = MAX_Y; grain[i].vy /= -2; } else if (newy < (HEIGHT/2)*256) { newy = (HEIGHT/2)*256; grain[i].vy /= -2; } } } oldpx = grain[i].x/256; oldpy = grain[i].y/256; newpx = newx/256; newpy = newy/256; if( ((newpy != oldpy) || (newpx != oldpx)) && ( (r_pixels[newpy] & (1 << newpx)) || (g_pixels[newpy] & (1 << newpx)) ) ) { // blocked if (newpx > oldpx) dx = newpx - oldpx; else dx = oldpx - newpx; if (newpy > oldpy) dy = newpy - oldpy; else dy = oldpy - newpy; if (dx == 1 && dy == 0) { newx = grain[i].x; // Cancel X motion grain[i].vx /= -2; // and bounce X velocity (Y is OK) //newpy = oldpy; newpx = oldpx; } else if (dx == 0 && dy == 1) { newy = grain[i].y; // Cancel Y motion grain[i].vy /= -2; // and bounce Y velocity (X is OK) newpy = oldpy; //newpx = oldpx; } else if (!quadrant_leak) // no sliding from quadrant to quadrant, diagonal only // Diagonal intersection is more tricky... { // Try skidding along just one axis of motion if possible (start w/ // faster axis). Because we've already established that diagonal // (both-axis) motion is occurring, moving on either axis alone WILL // change the pixel index, no need to check that again. if(abs(grain[i].vx) > abs(grain[i].vy)) // X axis is faster { newpy = grain[i].y / 256; newpx = newx / 256; if(!(r_pixels[newpy] & (1 << newpx)) && !(g_pixels[newpy] & (1 << newpx))) { newy = grain[i].y; // Cancel Y motion grain[i].vy /= -2; // and bounce Y velocity } else // X pixel is taken, so try Y... { newpy = newy / 256; newpx = grain[i].x / 256; if(!(r_pixels[newpy] & (1 << newpx)) && !(g_pixels[newpy] & (1 << newpx))) { newx = grain[i].x; // Cancel X motion grain[i].vx /= -2; // and bounce X velocity } else // Both spots are occupied { newx = grain[i].x; // Cancel X & Y motion newy = grain[i].y; grain[i].vx /= -2; // Bounce X & Y velocity grain[i].vy /= -2; newpy = oldpy; newpx = oldpx; } } } else { // Y axis is faster newpy = newy / 256; newpx = grain[i].x / 256; if(!(r_pixels[newpy] & (1 << newpx)) && !(g_pixels[newpy] & (1 << newpx))) { newx = grain[i].x; // Cancel X motion grain[i].vy /= -2; // and bounce Y velocity } else // Y pixel is taken, so try X... { newpy = grain[i].y / 256; newpx = newx / 256; if(!(r_pixels[newpy] & (1 << newpx)) && !(g_pixels[newpy] & (1 << newpx))) { newy = grain[i].y; // Cancel Y motion grain[i].vy /= -2; // and bounce Y velocity } else // Both spots are occupied { newx = grain[i].x; // Cancel X & Y motion newy = grain[i].y; grain[i].vx /= -2; // Bounce X & Y velocity grain[i].vy /= -2; newpy = oldpy; newpx = oldpx; } } } } else // blocked quadrant leak continue; } // Update grain position grain[i].x = newx; grain[i].y = newy; if ((newpy != oldpy) || (newpx != oldpx)) { pixel_motion = true; if (r_pixels[oldpy] & (1 << oldpx)) { r_pixels[oldpy] &= ~(1 << oldpx); r_pixels[newpy] |= 1 << newpx; } if (g_pixels[oldpy] & (1 << oldpx)) { g_pixels[oldpy] &= ~(1 << oldpx); g_pixels[newpy] |= 1 << newpx; } } } //-- 100 msec tasks if (millis() - poll_tm > 100) { poll_tm = millis(); switch (gravity_control) { case 0: // normal // reverse gravity after long period of inactivity if (pixel_motion) { pixel_motion = false; deadman_tm = millis(); } else // no motion if (millis() - deadman_tm > 2000UL) { if (gravity < 0) { gravity = -50; gravity_control = 5; } else { gravity = 50; gravity_control = -5; } TRACE("gravity reverse\n"); deadman_tm = millis(); } break; default: // gravity shift in progress gravity += gravity_control; if (gravity >= 256) { gravity = 256; gravity_control = 0; } if (gravity <= -256) { gravity = -256; gravity_control = 0; } } // random gravity variations ax = random(10,15); ay = random(10,15); ay = ay * gravity; // gravity = -256..256 ax = ax * gravity; // gravity = -256..256 ay /= 256; ax /= 256; } }