/ Hallowing_Spirit_Board / Hallowing_Spirit_Board.ino
Hallowing_Spirit_Board.ino
1 // SPDX-FileCopyrightText: 2018 Phillip Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 // "Spirit Board" plaything for Adafruit Hallowing. Uses DMA and related 6 // shenanigans to smoothly scroll around a large image. Use the capacitive 7 // touch pads to get a random "spirit reading." Ooooo...spooky! 8 9 #include <Adafruit_LIS3DH.h> 10 #include <Adafruit_FreeTouch.h> 11 #include <Adafruit_GFX.h> 12 #include <Adafruit_ST7735.h> 13 #include <Adafruit_ZeroDMA.h> 14 #include "graphics.h" 15 #include "messages.h" // List of "spirit reading" messages is here! 16 17 #define TFT_CS 39 // Hallowing display control pins: chip select 18 #define TFT_RST 37 // Display reset 19 #define TFT_DC 38 // Display data/command select 20 #define TFT_BACKLIGHT 7 // Display backlight pin 21 22 // A small finite-state machine toggles the software through various actions: 23 #define STATE_SCROLL 0 // Fidgeting around with accelerometer 24 #define STATE_CHAR_DIRECT 1 // Straight line to next character in message 25 #define STATE_CHAR_CIRCLE 2 // Repeating character; moves in a small circle 26 #define STATE_CHAR_PAUSE 3 // Pause between words 27 28 uint8_t state = STATE_SCROLL; // Initial state = scrolling 29 30 // Declarations for various Hallowing hardware -- display, accelerometer and 31 // capacitive touch pads. 32 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); 33 Adafruit_LIS3DH accel = Adafruit_LIS3DH(); 34 Adafruit_FreeTouch pads[] = { 35 Adafruit_FreeTouch(A2, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE), 36 Adafruit_FreeTouch(A3, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE), 37 Adafruit_FreeTouch(A4, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE), 38 Adafruit_FreeTouch(A5, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE) 39 }; 40 41 // Declarations related to DMA (direct memory access), which lets us walk 42 // and chew gum at the same time. This is VERY specific to SAMD chips and 43 // means this is not trivially ported to other devices. 44 Adafruit_ZeroDMA dma; 45 DmacDescriptor *descriptor; 46 uint16_t dmaBuf[2][128]; 47 uint8_t dmaIdx = 0; // Active DMA buffer # (alternate fill/send) 48 49 // DMA transfer-in-progress indicator and callback 50 static volatile bool dma_busy = false; 51 static void dma_callback(Adafruit_ZeroDMA *dma) { 52 dma_busy = false; 53 } 54 55 // Sundry other global declarations 56 57 // (x,y) is the coordinate of the top-left pixel of the display relative to 58 // the larger board image that we'll be scrolling around. Units are 16X the 59 // pixel grid, to allow for smoother subpixel scrolling off cardinal axes. 60 int x = (BOARD_WIDTH / 2 - 64) * 16, 61 y = (BOARD_HEIGHT / 2 - 64) * 16, 62 vX = 0, vY = 0; // X & Y velocity when in manual SCROLL mode 63 uint8_t messageNum, // Index of message being "read" 64 messageCharNum, // Index of character within current message 65 lastNonSpaceChar, // Value of last character that's not a space 66 backlight_prev = 160; // For candle flicker backlight effect 67 uint32_t startTime; // Start time of character-to-character motion 68 int16_t startX, startY, // Start & end positions when moving character- 69 endX, endY, // to-character (same coord sys as x & y). 70 circleX, circleY; // Center of motion when repeating character 71 float angle, radius; // Initial angle, distance for repeat circle 72 SPISettings settings(12000000, MSBFIRST, SPI_MODE0); 73 74 // SETUP FUNCTION -- runs once at startup ---------------------------------- 75 76 void setup(void) { 77 randomSeed(analogRead(A4)); 78 79 // Hardware init -- display, backlight, accelerometer, capacitive touch pads 80 tft.initR(INITR_144GREENTAB); 81 tft.setRotation(2); // Display is rotated 180° on Hallowing 82 tft.fillScreen(0); 83 84 pinMode(TFT_BACKLIGHT, OUTPUT); 85 analogWriteResolution(8); 86 87 if(accel.begin(0x18) || accel.begin(0x19)) { 88 accel.setRange(LIS3DH_RANGE_2_G); 89 } 90 91 for(uint8_t i=0; i<4; i++) pads[i].begin(); 92 93 // Set up SPI DMA. While the Hallowing has a known SPI peripheral and this 94 // could be much simpler, the extra code here will help if adapting this 95 // sketch to other SAMD boards (Feather M0, M4, etc.) 96 int dmac_id; 97 volatile uint32_t *data_reg; 98 dma.allocate(); 99 if(&PERIPH_SPI == &sercom0) { 100 dma.setTrigger(SERCOM0_DMAC_ID_TX); 101 data_reg = &SERCOM0->SPI.DATA.reg; 102 #if defined SERCOM1 103 } else if(&PERIPH_SPI == &sercom1) { 104 dma.setTrigger(SERCOM1_DMAC_ID_TX); 105 data_reg = &SERCOM1->SPI.DATA.reg; 106 #endif 107 #if defined SERCOM2 108 } else if(&PERIPH_SPI == &sercom2) { 109 dma.setTrigger(SERCOM2_DMAC_ID_TX); 110 data_reg = &SERCOM2->SPI.DATA.reg; 111 #endif 112 #if defined SERCOM3 113 } else if(&PERIPH_SPI == &sercom3) { 114 dma.setTrigger(SERCOM3_DMAC_ID_TX); 115 data_reg = &SERCOM3->SPI.DATA.reg; 116 #endif 117 #if defined SERCOM4 118 } else if(&PERIPH_SPI == &sercom4) { 119 dma.setTrigger(SERCOM4_DMAC_ID_TX); 120 data_reg = &SERCOM4->SPI.DATA.reg; 121 #endif 122 #if defined SERCOM5 123 } else if(&PERIPH_SPI == &sercom5) { 124 dma.setTrigger(SERCOM5_DMAC_ID_TX); 125 data_reg = &SERCOM5->SPI.DATA.reg; 126 #endif 127 } 128 dma.setAction(DMA_TRIGGER_ACTON_BEAT); 129 descriptor = dma.addDescriptor( 130 NULL, // move data 131 (void *)data_reg, // to here 132 sizeof dmaBuf[0], // this many... 133 DMA_BEAT_SIZE_BYTE, // bytes/hword/words 134 true, // increment source addr? 135 false); // increment dest addr? 136 dma.setCallback(dma_callback); 137 } 138 139 // LOOP FUNCTION -- repeats indefinitely ----------------------------------- 140 141 void loop(void) { 142 // This just picks a random backlight intensity then starts a fractal 143 // subdivision (in the split() function) to make a candle flicker effect. 144 // split(), in turn, calls further functions that handle input and update 145 // the display... 146 uint8_t backlight_next = random(128, 192); 147 split(backlight_prev, backlight_next, 32); 148 backlight_prev = backlight_next; 149 } 150 151 void split(uint8_t v1, uint8_t v2, uint8_t offset) { 152 if(offset > 2) { // Split further into sub-segments w/midpoint at ±offset 153 uint8_t mid = (v1 + v2 + 1) / 2 + random(-offset, offset); 154 split(v1 , mid, offset / 2); // First segment (offset is halved) 155 split(mid, v2 , offset / 2); // Second segment (ditto) 156 } else { // No further subdivision; v1 determines LED brightness 157 // But first, some gamma correction... 158 v1 = (uint8_t)(pow((float)v1 / 255.0, 2.2) * 255.0 + 0.5); 159 analogWrite(TFT_BACKLIGHT, v1); 160 // We'll reach this point in the code at equal-ish intervals along the 161 // fractalization process, so it's as good a time as any to process input 162 // and render a new frame... 163 processFrame(); 164 } 165 } 166 167 // Handle one iteration of the finite state machine 168 void processFrame(void) { 169 170 if(state == STATE_SCROLL) { // Manual scrolling mode? 171 172 accel.read(); // Read accelerometer 173 vX += accel.y / 512; // Horizontal scroll from accel. Y 174 if(abs(accel.x) < abs(accel.z)) { // If device is sitting flat(ish), 175 vY -= accel.x / 512; // Use accel X for vertical scroll 176 } else { // Else held upright(ish), 177 vY += accel.z / 256; // Use accel Z for vertical scroll 178 } 179 if(vX > 128) vX = 128; // Limit scrolling velocity 180 if(vX < -128) vX = -128; // (units are 1/16 pixel, so 128 181 if(vY > 128) vY = 128; // equals 8 pixels max). 182 if(vY < -128) vY = -128; 183 x += vX; // Add velocity to position 184 y += vY; 185 // Constrain position so we don't scroll off the edges of the board... 186 if(x >= (BOARD_WIDTH - PLANCHETTE_WIDTH) * 16) { // Right edge 187 x = (BOARD_WIDTH - PLANCHETTE_WIDTH) * 16; 188 vX = 0; 189 } else if(x < 0) { // Left edge 190 x = 0; 191 vX = 0; 192 } 193 if(y >= (BOARD_HEIGHT - PLANCHETTE_HEIGHT) * 16) { // Bottom edge 194 y = (BOARD_HEIGHT - PLANCHETTE_HEIGHT) * 16; 195 vY = 0; 196 } else if(y < 0) { // Top edge 197 y = 0; 198 vY = 0; 199 } 200 201 // If ANY of capacitive pads are touched while in scrolling mode... 202 if(anyTouch()) { 203 state = STATE_CHAR_DIRECT; // Now in go-to-character mode 204 messageNum = random(NUM_MESSAGES); // Pick a random message 205 messageCharNum = 0; // And start at the 1st char 206 lastNonSpaceChar = 0; 207 startTime = micros(); // Note the starting time 208 startX = x; // and position for 209 startY = y; // go-to-character motion 210 setupMotionEnd(); // Initializes endX, endY 211 } 212 213 } else { // NOT in scrolling mode, one of the go-to-character states 214 215 uint32_t currentTime = micros(), // Get time since startTime 216 elapsed = currentTime - startTime; 217 218 if(elapsed >= 1250000) { // If over 1.25 seconds... 219 x = startX = endX; // Advance to next character... 220 y = startY = endY; 221 startTime = currentTime; 222 messageCharNum++; 223 uint8_t c = messages[messageNum][messageCharNum]; // New char 224 if(c == 0) { // If end of string, 225 state = STATE_SCROLL; // go back to manual scroll mode 226 } else if(c == ' ') { // If space 227 state = STATE_CHAR_PAUSE; // Hold steady for a moment, 228 startTime -= 300000; // but not a full char interval 229 } else if(c == lastNonSpaceChar) { // If repeating the same character... 230 // The cursor is moved in a small circular motion to return to the 231 // same character, to emphasize that it's being repeated. 232 state = STATE_CHAR_CIRCLE; 233 // In order to avoid scrolling off the board, the circular motion 234 // is always toward the board center. Save that direction: 235 angle = atan2(BOARD_HEIGHT * 8 - endY, BOARD_WIDTH * 8 - endX); 236 radius = random(150, 350); // Semi-random size 237 circleX = endX + cos(angle) * radius + 0.5; // Center of motion 238 circleY = endY + sin(angle) * radius + 0.5; 239 } else { // NOT space or repeating char...new destination... 240 state = STATE_CHAR_DIRECT; 241 lastNonSpaceChar = c; 242 setupMotionEnd(); // Sets up endX, endY for linear motion 243 } 244 } else { // Still within 1.25 sec motion period 245 if(state == STATE_CHAR_PAUSE) { 246 // If in pause state, just do nothing! 247 } else { 248 // Last 1/4 second is a pause at end position. So we really only 249 // do work during the initial 1 second (1M microseconds), else 250 // hold at the end position. 251 if(elapsed > 1000000) elapsed = 1000000; 252 float t = (float)elapsed / 1000000.0; // Linear motion 0.0-1.0 253 t = t * t * 3.0 - t * t * t * 2.0; // Apply ease in/out curve 254 if(state == STATE_CHAR_CIRCLE) { // Same-char circular motion 255 t *= M_PI * 2.0; // 0.0-1.0 -> 0-360 degrees 256 x = (int)(circleX - cos(angle + t) * radius + 0.5); 257 y = (int)(circleY - sin(angle + t) * radius + 0.5); 258 } else { // New char straight-line motion 259 x = (int)(startX + (endX - startX) * t + 0.5); 260 y = (int)(startY + (endY - startY) * t + 0.5); 261 } 262 } 263 } 264 } 265 266 drawFrame(x / 16, y / 16); // Redraw screen at new (x, y) position 267 } 268 269 // Any cap sense pads touched? Returns true if ANY, doesn't distinguish. 270 boolean anyTouch(void) { 271 for(uint8_t i=0; i<4; i++) { 272 if(pads[i].measure() > 700) return true; 273 } 274 return false; 275 } 276 277 // Initialize endX and endY based on the current messageCharNum. This is 278 // done in a couple places in processFrame(), so is functionalized here... 279 // most of the inputs and outputs are existing global vars. 280 void setupMotionEnd(void) { 281 int8_t n = getCoordIndex(messages[messageNum][messageCharNum]); 282 if(n < 0) return; // Unknown character, do nothing! 283 endX = (coord[n].x - 64) * 16; // Upper-left corner of screen 284 endY = (coord[n].y - 64) * 16; // relative to character's center coord 285 if(endX >= (BOARD_WIDTH - PLANCHETTE_WIDTH) * 16) { // Stay in bounds! 286 endX = (BOARD_WIDTH - PLANCHETTE_WIDTH) * 16; 287 } else if(endX < 0) { 288 endX = 0; 289 } 290 if(endY >= (BOARD_HEIGHT - PLANCHETTE_HEIGHT) * 16) { 291 endY = (BOARD_HEIGHT - PLANCHETTE_HEIGHT) * 16; 292 } else if(endY < 0) { 293 endY = 0; 294 } 295 } 296 297 // Given an ASCII character, return the corresponding index in the coord[] 298 // array, or -1 if no matching character. Only A-Z and 0-9 are supported, 299 // there's no punctuation on the spirit board! 300 int8_t getCoordIndex(uint8_t c) { 301 c = toupper(c); 302 if((c >= 'A') && (c <= 'Z')) return c - 'A'; 303 if((c >= '0') && (c <= '9')) return (c - '0') + 26; 304 if((c >= 1 ) && (c <= 6 )) return c + 35; // Yes, no, etc. 305 return -1; // Not in table 306 } 307 308 // Draw a single full frame of animation. (x,y) represent the planchette's 309 // top-left pixel coordinate over the larger board image. NO clipping or 310 // bounds-checking is performed...the given position must be in a valid range. 311 // Aside from DMA, it's just brute force...every pixel of every frame is 312 // computed. 313 void drawFrame(int x, int y) { 314 uint32_t // Graphics data, each 32-bit value holds 16 pixels (2 bits/pixel): 315 *planchettePtr, // Pointer into planchette graphics array 316 *boardPtr, // Pointer into board graphics array 317 planchetteWord, // Current 16 pixel block of planchette gfx data 318 boardWord; // Current 16 pixel block of board graphics data 319 uint16_t *dmaPtr; // Pointer into DMA output buffer (16 bits/pixel) 320 uint8_t row, // Current row along screen (top to bottom) 321 col, // Current column along screen (left to right) 322 c16, // Column # (0 to 15) within 16-pixel block 323 bc, // c16 value when boardWord value gets reloaded 324 idx; // Color palette index (2 bits/pixel = 0 to 3) 325 326 SPI.beginTransaction(settings); // SPI init 327 digitalWrite(TFT_CS, LOW); // Chip select 328 tft.setAddrWindow(0, 0, 128, 128); // Set address window to full screen 329 digitalWrite(TFT_CS, LOW); // Re-select after addr function 330 digitalWrite(TFT_DC, HIGH); // Data mode... 331 332 bc = 15 - (x & 15); 333 for(row = 0; row < PLANCHETTE_HEIGHT; row++) { // For each row... 334 // Set up source and destination pointers: 335 planchettePtr = (uint32_t *)&planchetteData[ 336 row * ((PLANCHETTE_WIDTH + 15) / 16)]; 337 boardPtr = (uint32_t *)&boardData[ 338 (y + row) * ((BOARD_WIDTH + 15) / 16) + (x / 16)]; 339 dmaPtr = &dmaBuf[dmaIdx][0]; 340 // Initial boardWord value depends on starting column: 341 boardWord = *boardPtr++ >> ((15 - bc) * 2); 342 for(col = 0; col < PLANCHETTE_WIDTH; col++) { // For each column... 343 c16 = col & 15; // Column # (0-15) within 16-pixel block 344 // On first pixel of block, reload planchetteWord, increment pointer: 345 if(c16 == 0) planchetteWord = *planchettePtr++; 346 if((idx = (planchetteWord & 3))) { // Color indices 1-3 are opaque, 347 *dmaPtr++ = planchettePalette[idx]; // use planchettePalette color 348 } else { // Color index 0 is transparent, 349 *dmaPtr++ = boardPalette[boardWord & 3]; // use boardPalette color 350 } 351 planchetteWord >>= 2; // Shift down 2 bits/pixel 352 if(c16 != bc) boardWord >>= 2; // Same with board graphics, 353 else boardWord = *boardPtr++; // except periodic reload 354 } 355 while(dma_busy); // Wait for prior DMA transfer to finish 356 // Set up DMA transfer from the newly-filled scan line buffer: 357 descriptor->SRCADDR.reg = (uint32_t)&dmaBuf[dmaIdx] + sizeof dmaBuf[0]; 358 dma_busy = true; // Mark as busy (DMA callback clears this) 359 dma.startJob(); // Start new DMA transfer 360 dmaIdx = 1 - dmaIdx; // Swap DMA buffers 361 } 362 363 while(dma_busy); // Wait for last DMA transfer to complete 364 digitalWrite(TFT_CS, HIGH); // Deselect 365 SPI.endTransaction(); // SPI done 366 }