/ 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  }