/ M4_Eyes / M4_Eyes.ino
M4_Eyes.ino
  1  // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  // Animated eyes for Adafruit MONSTER M4SK and HALLOWING M4 dev boards.
  6  // This code is pretty tightly coupled to the resources of these boards
  7  // (one or two ST7789 240x240 pixel TFTs on separate SPI buses, and a
  8  // SAMD51 microcontroller), and not as generally portable as the prior
  9  // "Uncanny Eyes" project (better for SAMD21 chips or Teensy 3.X and
 10  // 128x128 TFT or OLED screens, single SPI bus).
 11  
 12  // IMPORTANT: in rare situations, a board may get "bricked" when running
 13  // this code while simultaneously connected to USB. A quick-flashing status
 14  // LED indicates the filesystem has gone corrupt. If this happens, install
 15  // CircuitPython to reinitialize the filesystem, copy over your eye files
 16  // (keep backups!), then re-upload this code. It seems to happen more often
 17  // at high optimization settings (above -O3), but there's not 1:1 causality.
 18  // The exact cause has not yet been found...possibly insufficient yield()
 19  // calls, or some rare alignment in the Arcada library or USB-handling code.
 20  
 21  // LET'S HAVE A WORD ABOUT COORDINATE SYSTEMS before continuing. From an
 22  // outside observer's point of view, looking at the display(s) on these
 23  // boards, the eyes are rendered COLUMN AT A TIME, working LEFT TO RIGHT,
 24  // rather than the horizontal scanline order of Uncanny Eyes and most other
 25  // graphics-heavy code. It was found much easier to animate the eyelids when
 26  // working along this axis. A "column major" display is easily achieved by
 27  // setting the screen(s) to ROTATION 3, which is a 90 degree
 28  // counterclockwise rotation relative to the default. This places (0,0) at
 29  // the BOTTOM-LEFT of the display, with +X being UP and +Y being RIGHT --
 30  // so, conceptually, just swapping axes you have a traditional Cartesian
 31  // coordinate system and trigonometric functions work As Intended, and the
 32  // code tends to "think" that way in most places. Since the rotation is done
 33  // in hardware though...from the display driver's point of view, one might
 34  // think of these as "horizontal" "scanlines," and that the eye is being
 35  // drawn sideways, with a left and right eyelid rather than bottom and top.
 36  // Just mentioning it here because there may still be lingering comments
 37  // and/or variables in the code where I refer to "scanlines" even though
 38  // visually/spatially these are columns. Will do my best to comment local
 39  // coordinate systems in different spots. (Any raster images loaded by
 40  // Adafruit_ImageReader are referenced in typical +Y = DOWN order.)
 41  
 42  // Oh also, "left eye" and "right eye" refer to the MONSTER'S left and
 43  // right. From an observer's point of view, looking AT the monster, the
 44  // "right eye" is on the left.
 45  
 46  #if !defined(USE_TINYUSB)
 47    #error "Please select Tools->USB Stack->TinyUSB before compiling"
 48  #endif
 49  
 50  #define GLOBAL_VAR
 51  #include "globals.h"
 52  
 53  // Global eye state that applies to all eyes (not per-eye):
 54  bool     eyeInMotion = false;
 55  float    eyeOldX, eyeOldY, eyeNewX, eyeNewY;
 56  uint32_t eyeMoveStartTime = 0L;
 57  int32_t  eyeMoveDuration  = 0L;
 58  uint32_t lastSaccadeStop  = 0L;
 59  int32_t  saccadeInterval  = 0L;
 60  
 61  // Some sloppy eye state stuff, some carried over from old eye code...
 62  // kinda messy and badly named and will get cleaned up/moved/etc.
 63  uint32_t timeOfLastBlink         = 0L,
 64           timeToNextBlink         = 0L;
 65  int      xPositionOverMap        = 0;
 66  int      yPositionOverMap        = 0;
 67  uint8_t  eyeNum                  = 0;
 68  uint32_t frames                  = 0;
 69  uint32_t lastFrameRateReportTime = 0;
 70  uint32_t lastLightReadTime       = 0;
 71  float    lastLightValue          = 0.5;
 72  double   irisValue               = 0.5;
 73  int      iPupilFactor            = 42;
 74  uint32_t boopSum                 = 0,
 75           boopSumFiltered         = 0;
 76  bool     booped                  = false;
 77  int      fixate                  = 7;
 78  uint8_t  lightSensorFailCount    = 0;
 79  
 80  // For autonomous iris scaling
 81  #define  IRIS_LEVELS 7
 82  float    iris_prev[IRIS_LEVELS] = { 0 };
 83  float    iris_next[IRIS_LEVELS] = { 0 };
 84  uint16_t iris_frame = 0;
 85  
 86  // Callback invoked after each SPI DMA transfer - sets a flag indicating
 87  // the next line of graphics can be issued as soon as its ready.
 88  static void dma_callback(Adafruit_ZeroDMA *dma) {
 89    // It's possible to assign each DMA channel its own callback function
 90    // (freeing up a few cycles vs. this channel-to-eye lookup), but it's
 91    // written this way to scale to as many eyes as needed (up to one per
 92    // SERCOM if this is ported to something like Grand Central).
 93    for(uint8_t e=0; e<NUM_EYES; e++) {
 94      if(dma == &eye[e].dma) {
 95        eye[e].dma_busy = false;
 96        return;
 97      }
 98    }
 99  }
100  
101  // >50MHz SPI was fun but just too glitchy to rely on
102  //#if F_CPU < 200000000
103  // #define DISPLAY_FREQ   (F_CPU / 2)
104  // #define DISPLAY_CLKSRC SERCOM_CLOCK_SOURCE_FCPU
105  //#else
106   #define DISPLAY_FREQ   50000000
107   #define DISPLAY_CLKSRC SERCOM_CLOCK_SOURCE_100M
108  //#endif
109  
110  SPISettings settings(DISPLAY_FREQ, MSBFIRST, SPI_MODE0);
111  
112  // The time required to issue one scanline (DISPLAY_SIZE pixels x 16 bits) over
113  // SPI is a known(ish) quantity. The DMA scheduler isn't always perfectly
114  // deterministic though...especially on startup, as things make their way
115  // into caches. Very occasionally, something (not known yet) is causing
116  // SPI DMA to seize up. This condition is pretty easy to check for...
117  // periodically the code needs to wait on a DMA transfer to finish
118  // anyway, and we can use the micros() function to determine if it's taken
119  // considerably longer than expected (a factor of 4 is used - the "4000"
120  // below, to allow for caching/scheduling fudge). If so, that's our signal
121  // that something is likely amiss and we take evasive maneuvers, resetting
122  // the affected DMA channel (DMAbuddy::fix()).
123  #define DMA_TIMEOUT (uint32_t)((DISPLAY_SIZE * 16 * 4000) / (DISPLAY_FREQ / 1000))
124  
125  static inline uint16_t readBoop(void) {
126    uint16_t counter = 0;
127    pinMode(boopPin, OUTPUT);
128    digitalWrite(boopPin, HIGH);
129    pinMode(boopPin, INPUT);
130    while(digitalRead(boopPin) && (++counter < 1000));
131    return counter;
132  }
133  
134  // Crude error handler. Prints message to Serial Monitor, blinks LED.
135  void fatal(const char *message, uint16_t blinkDelay) {
136    Serial.begin(9600);
137    Serial.println(message);
138    for(bool ledState = HIGH;; ledState = !ledState) {
139      digitalWrite(LED_BUILTIN, ledState);
140      delay(blinkDelay);
141    }
142  }
143  
144  #include <unistd.h> // sbrk() function
145  
146  uint32_t availableRAM(void) {
147    char top;                      // Local variable pushed on stack
148    return &top - (char *)sbrk(0); // Top of stack minus end of heap
149  }
150  
151  // SETUP FUNCTION - CALLED ONCE AT PROGRAM START ---------------------------
152  
153  void setup() {
154    if(!arcada.arcadaBegin())     fatal("Arcada init fail!", 100);
155  #if defined(USE_TINYUSB)
156    if(!arcada.filesysBeginMSD()) fatal("No filesystem found!", 250);
157  #else
158    if(!arcada.filesysBegin())    fatal("No filesystem found!", 250);
159  #endif
160  
161    user_setup();
162  
163    arcada.displayBegin();
164  
165    // Backlight(s) off ASAP, they'll switch on after screen(s) init & clear
166    arcada.setBacklight(0);
167  
168    DISPLAY_SIZE = min(ARCADA_TFT_WIDTH, ARCADA_TFT_HEIGHT);
169  
170    Serial.begin(115200);
171    //while(!Serial) yield();
172  
173    Serial.printf("Available RAM at start: %d\n", availableRAM());
174    Serial.printf("Available flash at start: %d\n", arcada.availableFlash());
175    yield(); // Periodic yield() makes sure mass storage filesystem stays alive
176  
177    // No file selector yet. In the meantime, you can override the default
178    // config file by holding one of the 3 edge buttons at startup (loads
179    // config1.eye, config2.eye or config3.eye instead). Keep fingers clear
180    // of the nose booper when doing this...it self-calibrates on startup.
181    // DO THIS BEFORE THE SPLASH SO IT DOESN'T REQUIRE A LENGTHY HOLD.
182    char *filename = (char *)"config.eye";
183   
184    uint32_t buttonState = arcada.readButtons();
185    if((buttonState & ARCADA_BUTTONMASK_UP) && arcada.exists("config1.eye")) {
186      filename = (char *)"config1.eye";
187    } else if((buttonState & ARCADA_BUTTONMASK_A) && arcada.exists("config2.eye")) {
188      filename = (char *)"config2.eye";
189    } else if((buttonState & ARCADA_BUTTONMASK_DOWN) && arcada.exists("config3.eye")) {
190      filename = (char *)"config3.eye";
191    }
192  
193    yield();
194    // Initialize display(s)
195  #if (NUM_EYES > 1)
196    eye[0].display = arcada._display;
197    eye[1].display = arcada.display2;  
198  #else
199    eye[0].display = arcada.display;
200  #endif
201  
202    // Initialize DMAs
203    yield();
204    uint8_t e;
205    for(e=0; e<NUM_EYES; e++) {
206  #if (ARCADA_TFT_WIDTH != 160) && (ARCADA_TFT_HEIGHT != 128) // 160x128 is ST7735 which isn't able to deal
207      eye[e].spi->setClockSource(DISPLAY_CLKSRC); // Accelerate SPI!
208  #endif
209      eye[e].display->fillScreen(0);
210      eye[e].dma.allocate();
211      eye[e].dma.setTrigger(eye[e].spi->getDMAC_ID_TX());
212      eye[e].dma.setAction(DMA_TRIGGER_ACTON_BEAT);
213      eye[e].dptr = eye[e].dma.addDescriptor(NULL, NULL, 42, DMA_BEAT_SIZE_BYTE, false, false);
214      eye[e].dma.setCallback(dma_callback);
215      eye[e].dma.setPriority(DMA_PRIORITY_0);
216      uint32_t spi_data_reg = (uint32_t)eye[e].spi->getDataRegister();
217      for(int i=0; i<2; i++) {   // For each of 2 scanlines...
218        for(int j=0; j<NUM_DESCRIPTORS; j++) { // For each descriptor on scanline...
219          eye[e].column[i].descriptor[j].BTCTRL.bit.VALID    = true;
220          eye[e].column[i].descriptor[j].BTCTRL.bit.EVOSEL   = DMA_EVENT_OUTPUT_DISABLE;
221          eye[e].column[i].descriptor[j].BTCTRL.bit.BLOCKACT = DMA_BLOCK_ACTION_NOACT;
222          eye[e].column[i].descriptor[j].BTCTRL.bit.BEATSIZE = DMA_BEAT_SIZE_BYTE;
223          eye[e].column[i].descriptor[j].BTCTRL.bit.DSTINC   = 0;
224          eye[e].column[i].descriptor[j].BTCTRL.bit.STEPSEL  = DMA_STEPSEL_SRC;
225          eye[e].column[i].descriptor[j].BTCTRL.bit.STEPSIZE = DMA_ADDRESS_INCREMENT_STEP_SIZE_1;
226          eye[e].column[i].descriptor[j].DSTADDR.reg         = spi_data_reg;
227        }
228      }
229      eye[e].colNum       = DISPLAY_SIZE; // Force initial wraparound to first column
230      eye[e].colIdx       = 0;
231      eye[e].dma_busy     = false;
232      eye[e].column_ready = false;
233      eye[e].dmaStartTime = 0;
234  
235      // Default settings that can be overridden in config file
236      eye[e].pupilColor        = 0x0000;
237      eye[e].backColor         = 0xFFFF;
238      eye[e].iris.color        = 0xFF01;
239      eye[e].iris.data         = NULL;
240      eye[e].iris.filename     = NULL;
241      eye[e].iris.startAngle   = (e & 1) ? 512 : 0; // Rotate alternate eyes 180 degrees
242      eye[e].iris.angle        = eye[e].iris.startAngle;
243      eye[e].iris.mirror       = 0;
244      eye[e].iris.spin         = 0.0;
245      eye[e].iris.iSpin        = 0;
246      eye[e].sclera.color      = 0xFFFF;
247      eye[e].sclera.data       = NULL;
248      eye[e].sclera.filename   = NULL;
249      eye[e].sclera.startAngle = (e & 1) ? 512 : 0; // Rotate alternate eyes 180 degrees
250      eye[e].sclera.angle      = eye[e].sclera.startAngle;
251      eye[e].sclera.mirror     = 0;
252      eye[e].sclera.spin       = 0.0;
253      eye[e].sclera.iSpin      = 0;
254      eye[e].rotation          = 3;
255  
256      // Uncanny eyes carryover stuff for now, all messy:
257      eye[e].blink.state = NOBLINK;
258      eye[e].blinkFactor = 0.0;
259    }
260  
261    // SPLASH SCREEN (IF FILE PRESENT) ---------------------------------------
262  
263    yield();
264    uint32_t startTime, elapsed;
265    if (showSplashScreen) {
266      showSplashScreen = ((arcada.drawBMP((char *)"/splash.bmp",
267                           0, 0, eye[0].display)) == IMAGE_SUCCESS);
268      if (showSplashScreen) { // Loaded OK?
269        Serial.println("Splashing");
270        if (NUM_EYES > 1) {   // Load on other eye too, ignore status
271          yield();
272          arcada.drawBMP((char *)"/splash.bmp", 0, 0, eye[1].display);
273        }
274        // Ramp up backlight over 1/2 sec duration
275        startTime = millis();
276        while ((elapsed = (millis() - startTime)) <= 500) {
277          yield();
278          arcada.setBacklight(255 * elapsed / 500);
279        }
280        arcada.setBacklight(255); // To the max
281        startTime = millis();     // Note current time for backlight hold later
282      }
283    }
284  
285    // If no splash, or load failed, turn backlight on early so user gets a
286    // little feedback, that the board is not locked up, just thinking.
287    if (!showSplashScreen) arcada.setBacklight(255);
288  
289    // LOAD CONFIGURATION FILE -----------------------------------------------
290  
291    loadConfig(filename);
292  
293    // LOAD EYELIDS AND TEXTURE MAPS -----------------------------------------
294  
295    // Experiencing a problem with MEMORY FRAGMENTATION when loading texture
296    // maps. These images only occupy RAM temporarily -- they're copied to
297    // internal flash memory and then freed. However, something is preventing
298    // the freed memory from restoring to a contiguous block. For example,
299    // if a texture image equal to about 50% of RAM is loaded/copied/freed,
300    // following this with a larger texture (or trying to allocate a larger
301    // polar lookup array) fails because RAM is fragmented into two segments.
302    // I've been through this code, Adafruit_ImageReader and Adafruit_GFX
303    // pretty carefully and they appear to be freeing RAM in the reverse order
304    // that they allocate (which should avoid fragmentation), but I'm likely
305    // overlooking something there or additional allocations are occurring
306    // in other libraries -- perhaps the filesystem and/or mass storage code.
307    // SO, here is the DIRTY WORKAROUND...
308    // Adafruit_ImageReader provides a bmpDimensions() function to determine
309    // the pixel size of an image without actually loading it. We can use this
310    // to estimate the RAM requirements for loading the image, then allocate
311    // a "booster seat" which makes the subsequent image load occur in higher
312    // memory, and the fragmenting part a bit beyond that. When the image and
313    // booster are both freed, that should restore a large contiguous chunk,
314    // leaving the fragments in high memory. Not TOO high though, we need to
315    // leave some RAM for the stack to operate over the lifetime of this
316    // program and to handle small heap allocations.
317  
318    uint32_t maxRam = availableRAM() - stackReserve;
319  
320    // Load texture maps for eyes
321    uint8_t e2;
322    for(e=0; e<NUM_EYES; e++) { // For each eye...
323      yield();
324      for(e2=0; e2<e; e2++) {    // Compare against each prior eye...
325        // If both eyes have the same iris filename...
326        if((eye[e].iris.filename && eye[e2].iris.filename) &&
327           (!strcmp(eye[e].iris.filename, eye[e2].iris.filename))) {
328          // Then eye 'e' can share the iris graphics from 'e2'
329          // rotate & mirror are kept distinct, just share image
330          eye[e].iris.data   = eye[e2].iris.data;
331          eye[e].iris.width  = eye[e2].iris.width;
332          eye[e].iris.height = eye[e2].iris.height;
333          break;
334        }
335      }
336      if((!e) || (e2 >= e)) { // If first eye, or no match found...
337        // If no iris filename was specified, or if file fails to load...
338        if((eye[e].iris.filename == NULL) || (loadTexture(eye[e].iris.filename,
339          &eye[e].iris.data, &eye[e].iris.width, &eye[e].iris.height,
340          maxRam) != IMAGE_SUCCESS)) {
341          // Point iris data at the color variable and set image size to 1px
342          eye[e].iris.data  = &eye[e].iris.color;
343          eye[e].iris.width = eye[e].iris.height = 1;
344        }
345        // Huh. The booster seat idea STILL doesn't always work right,
346        // something leaking in upper memory. Keep shrinking down the
347        // booster seat size a bit each time we load a texture. Feh.
348        maxRam -= 20;
349      }
350      // Repeat for sclera...
351      for(e2=0; e2<e; e2++) {    // Compare against each prior eye...
352        // If both eyes have the same sclera filename...
353        if((eye[e].sclera.filename && eye[e2].sclera.filename) &&
354           (!strcmp(eye[e].sclera.filename, eye[e2].sclera.filename))) {
355          // Then eye 'e' can share the sclera graphics from 'e2'
356          // rotate & mirror are kept distinct, just share image
357          eye[e].sclera.data   = eye[e2].sclera.data;
358          eye[e].sclera.width  = eye[e2].sclera.width;
359          eye[e].sclera.height = eye[e2].sclera.height;
360          break;
361        }
362      }
363      if((!e) || (e2 >= e)) { // If first eye, or no match found...
364        // If no sclera filename was specified, or if file fails to load...
365        if((eye[e].sclera.filename == NULL) || (loadTexture(eye[e].sclera.filename,
366          &eye[e].sclera.data, &eye[e].sclera.width, &eye[e].sclera.height,
367          maxRam) != IMAGE_SUCCESS)) {
368          // Point sclera data at the color variable and set image size to 1px
369          eye[e].sclera.data  = &eye[e].sclera.color;
370          eye[e].sclera.width = eye[e].sclera.height = 1;
371        }
372        maxRam -= 20; // See note above
373      }
374    }
375  
376    // Load eyelid graphics.
377    yield();
378    ImageReturnCode status;
379  
380    status = loadEyelid(upperEyelidFilename ?
381      upperEyelidFilename : (char *)"upper.bmp",
382      upperClosed, upperOpen, DISPLAY_SIZE-1, maxRam);
383  
384    status = loadEyelid(lowerEyelidFilename ?
385      lowerEyelidFilename : (char *)"lower.bmp",
386      lowerOpen, lowerClosed, 0, maxRam);
387  
388    // Filenames are no longer needed...
389    for(e=0; e<NUM_EYES; e++) {
390      if(eye[e].sclera.filename) free(eye[e].sclera.filename);
391      if(eye[e].iris.filename)   free(eye[e].iris.filename);
392    }
393    if(lowerEyelidFilename) free(lowerEyelidFilename);
394    if(upperEyelidFilename) free(upperEyelidFilename);
395  
396    // Note that calls to availableRAM() at this point will return something
397    // close to reserveSpace, suggesting very little RAM...but that function
398    // really just returns the space between the heap and stack, and we've
399    // established above that the top of the heap is something of a mirage.
400    // Large allocations CAN still take place in the lower heap!
401  
402    calcMap();
403    calcDisplacement();
404    Serial.printf("Free RAM: %d\n", availableRAM());
405  
406    randomSeed(SysTick->VAL + analogRead(A2));
407    eyeOldX = eyeNewX = eyeOldY = eyeNewY = mapRadius; // Start in center
408    for(e=0; e<NUM_EYES; e++) { // For each eye...
409      eye[e].display->setRotation(eye[e].rotation);
410      eye[e].eyeX = eyeOldX; // Set up initial position
411      eye[e].eyeY = eyeOldY;
412    }
413  
414    if (showSplashScreen) { // Image(s) loaded above?
415      // Hold backlight on for up to 2 seconds (minus other initialization time)
416      if ((elapsed = (millis() - startTime)) < 2000) {
417        delay(2000 - elapsed);
418      }
419      // Ramp down backlight over 1/2 sec duration
420      startTime = millis();
421      while ((elapsed = (millis() - startTime)) <= 500) {
422        yield();
423        arcada.setBacklight(255 - (255 * elapsed / 500));
424      }
425      arcada.setBacklight(0);
426      for(e=0; e<NUM_EYES; e++) {
427        eye[e].display->fillScreen(0);
428      }
429    }
430  
431  #if defined(ADAFRUIT_MONSTER_M4SK_EXPRESS)
432    if(voiceOn) {
433      if(!voiceSetup((waveform > 0))) {
434        Serial.println("Voice init fail, continuing without");
435        voiceOn = false;
436      } else {
437        voiceGain(gain);
438        currentPitch = voicePitch(currentPitch);
439        if(waveform) voiceMod(modulate, waveform);
440        arcada.enableSpeaker(true);
441      }
442    }
443  #endif
444  
445    arcada.setBacklight(255); // Back on, impending graphics
446  
447    yield();
448    if(boopPin >= 0) {
449      boopThreshold = 0;
450      for(int i=0; i<DISPLAY_SIZE; i++) {
451        boopThreshold += readBoop();
452      }
453      boopThreshold = boopThreshold * 110 / 100; // 10% overhead
454    }
455  
456    lastLightReadTime = micros() + 2000000; // Delay initial light reading
457  }
458  
459  
460  // LOOP FUNCTION - CALLED REPEATEDLY UNTIL POWER-OFF -----------------------
461  
462  /*
463  The loop() function in this code is a weird animal, operating a bit
464  differently from the earlier "Uncanny Eyes" eye project. Whereas in the
465  prior project we did this:
466  
467    for(each eye) {
468      * do position calculations, etc. for one frame of animation *
469      for(each scanline) {
470        * draw a row of pixels *
471      }
472    }
473  
474  This new code works "inside out," more like this:
475  
476    for(each column) {
477      if(first column of display) {
478        * do position calculations, etc. for one frame of animation *
479      }
480      * draw a column of pixels *
481    }
482  
483  The reasons for this are that A) we have an INORDINATE number of pixels to
484  draw compared to the old project (nearly 4X as much), and B) each screen is
485  now on its own SPI bus...data can be issued concurrently...so, rather than
486  stalling in a while() loop waiting for each scanline transfer to complete
487  (just wasting cycles), the code looks for opportunities to work on other
488  eyes (the eye updates aren't necessarily synchronized; each can function at
489  an independent frame rate depending on particular complexity at the moment).
490  */
491  
492  // loop() function processes ONE COLUMN of ONE EYE...
493  
494  void loop() {
495    if(++eyeNum >= NUM_EYES) eyeNum = 0; // Cycle through eyes...
496  
497    uint8_t  x = eye[eyeNum].colNum;
498    uint32_t t = micros();
499  
500    // If next column for this eye is not yet rendered...
501    if(!eye[eyeNum].column_ready) {
502      if(!x) { // If it's the first column...
503  
504        // ONCE-PER-FRAME EYE ANIMATION LOGIC HAPPENS HERE -------------------
505  
506        // Eye movement
507        float eyeX, eyeY;
508        if(moveEyesRandomly) {
509          int32_t dt = t - eyeMoveStartTime;      // uS elapsed since last eye event
510          if(eyeInMotion) {                       // Eye currently moving?
511            if(dt >= eyeMoveDuration) {           // Time up?  Destination reached.
512              eyeInMotion = false;                // Stop moving
513              // The "move" duration temporarily becomes a hold duration...
514              // Normally this is 35 ms to 1 sec, but don't exceed gazeMax setting
515              uint32_t limit = min(1000000, gazeMax);
516              eyeMoveDuration = random(35000, limit); // Time between microsaccades
517              if(!saccadeInterval) {              // Cleared when "big" saccade finishes
518                lastSaccadeStop = t;              // Time when saccade stopped
519                saccadeInterval = random(eyeMoveDuration, gazeMax); // Next in 30ms to 3sec
520              }
521              // Similarly, the "move" start time becomes the "stop" starting time...
522              eyeMoveStartTime = t;               // Save time of event
523              eyeX = eyeOldX = eyeNewX;           // Save position
524              eyeY = eyeOldY = eyeNewY;
525            } else { // Move time's not yet fully elapsed -- interpolate position
526              float e  = (float)dt / float(eyeMoveDuration); // 0.0 to 1.0 during move
527              e = 3 * e * e - 2 * e * e * e; // Easing function: 3*e^2-2*e^3 0.0 to 1.0
528              eyeX = eyeOldX + (eyeNewX - eyeOldX) * e; // Interp X
529              eyeY = eyeOldY + (eyeNewY - eyeOldY) * e; // and Y
530            }
531          } else {                       // Eye is currently stopped
532            eyeX = eyeOldX;
533            eyeY = eyeOldY;
534            if(dt > eyeMoveDuration) {   // Time up?  Begin new move.
535              if((t - lastSaccadeStop) > saccadeInterval) { // Time for a "big" saccade
536                // r is the radius in X and Y that the eye can go, from (0,0) in the center.
537                float r = ((float)mapDiameter - (float)DISPLAY_SIZE * M_PI_2) * 0.75;
538                eyeNewX = random(-r, r);
539                float h = sqrt(r * r - eyeNewX * eyeNewX);
540                eyeNewY = random(-h, h);
541                // Set the duration for this move, and start it going.
542                eyeMoveDuration = random(83000, 166000); // ~1/12 - ~1/6 sec
543                saccadeInterval = 0; // Calc next interval when this one stops
544              } else { // Microsaccade
545                // r is possible radius of motion, ~1/10 size of full saccade.
546                // We don't bother with clipping because if it strays just a little,
547                // that's okay, it'll get put in-bounds on next full saccade.
548                float r = (float)mapDiameter - (float)DISPLAY_SIZE * M_PI_2;
549                r *= 0.07;
550                float dx = random(-r, r);
551                eyeNewX = eyeX - mapRadius + dx;
552                float h = sqrt(r * r - dx * dx);
553                eyeNewY = eyeY - mapRadius + random(-h, h);
554                eyeMoveDuration = random(7000, 25000); // 7-25 ms microsaccade
555              }
556              eyeNewX += mapRadius;    // Translate new point into map space
557              eyeNewY += mapRadius;
558              eyeMoveStartTime = t;    // Save initial time of move
559              eyeInMotion      = true; // Start move on next frame
560            }
561          }
562        } else {
563          // Allow user code to control eye position (e.g. IR sensor, joystick, etc.)
564          float r = ((float)mapDiameter - (float)DISPLAY_SIZE * M_PI_2) * 0.9;
565          eyeX = mapRadius + eyeTargetX * r;
566          eyeY = mapRadius + eyeTargetY * r;
567        }
568  
569        // Eyes fixate (are slightly crossed) -- amount is filtered for boops
570        int nufix = booped ? 90 : 7;
571        fixate = ((fixate * 15) + nufix) / 16;
572        // save eye position to this eye's struct so it's same throughout render
573        if(eyeNum & 1) eyeX += fixate; // Eyes converge slightly toward center
574        else           eyeX -= fixate;
575        eye[eyeNum].eyeX = eyeX;
576        eye[eyeNum].eyeY = eyeY;
577  
578        // pupilFactor? irisValue? TO DO: pick a name and stick with it
579        eye[eyeNum].pupilFactor = irisValue;
580        // Also note - irisValue is calculated at the END of this function
581        // for the next frame (because the sensor must be read when there's
582        // no SPI traffic to the left eye)
583  
584        // Similar to the autonomous eye movement above -- blink start times
585        // and durations are random (within ranges).
586        if((t - timeOfLastBlink) >= timeToNextBlink) { // Start new blink?
587          timeOfLastBlink = t;
588          uint32_t blinkDuration = random(36000, 72000); // ~1/28 - ~1/14 sec
589          // Set up durations for both eyes (if not already winking)
590          for(uint8_t e=0; e<NUM_EYES; e++) {
591            if(eye[e].blink.state == NOBLINK) {
592              eye[e].blink.state     = ENBLINK;
593              eye[e].blink.startTime = t;
594              eye[e].blink.duration  = blinkDuration;
595            }
596          }
597          timeToNextBlink = blinkDuration * 3 + random(4000000);
598        }
599  
600        float uq, lq; // So many sloppy temp vars in here for now, sorry
601        if(tracking) {
602          // Eyelids naturally "track" the pupils (move up or down automatically)
603          int ix = (int)map2screen(mapRadius - eye[eyeNum].eyeX) + (DISPLAY_SIZE/2), // Pupil position
604              iy = (int)map2screen(mapRadius - eye[eyeNum].eyeY) + (DISPLAY_SIZE/2); // on screen
605          iy += irisRadius * trackFactor;
606          if(eyeNum & 1) ix = DISPLAY_SIZE - 1 - ix; // Flip for right eye
607          if(iy > upperOpen[ix]) {
608            uq = 1.0;
609          } else if(iy < upperClosed[ix]) {
610            uq = 0.0;
611          } else {
612            uq = (float)(iy - upperClosed[ix]) / (float)(upperOpen[ix] - upperClosed[ix]);
613          }
614          if(booped) {
615            uq = 0.9;
616            lq = 0.7;
617          } else {
618            lq = 1.0 - uq;
619          }
620        } else {
621          // If no tracking, eye is FULLY OPEN when not blinking
622          uq = 1.0;
623          lq = 1.0;
624        }
625        // Dampen eyelid movements slightly
626        // SAVE upper & lower lid factors per eye,
627        // they need to stay consistent across frame
628        eye[eyeNum].upperLidFactor = (eye[eyeNum].upperLidFactor * 0.6) + (uq * 0.4);
629        eye[eyeNum].lowerLidFactor = (eye[eyeNum].lowerLidFactor * 0.6) + (lq * 0.4);
630  
631        // Process blinks
632        if(eye[eyeNum].blink.state) { // Eye currently blinking?
633          // Check if current blink state time has elapsed
634          if((t - eye[eyeNum].blink.startTime) >= eye[eyeNum].blink.duration) {
635            if(++eye[eyeNum].blink.state > DEBLINK) { // Deblinking finished?
636              eye[eyeNum].blink.state = NOBLINK;      // No longer blinking
637              eye[eyeNum].blinkFactor = 0.0;
638            } else { // Advancing from ENBLINK to DEBLINK mode
639              eye[eyeNum].blink.duration *= 2; // DEBLINK is 1/2 ENBLINK speed
640              eye[eyeNum].blink.startTime = t;
641              eye[eyeNum].blinkFactor = 1.0;
642            }
643          } else {
644            eye[eyeNum].blinkFactor = (float)(t - eye[eyeNum].blink.startTime) / (float)eye[eyeNum].blink.duration;
645            if(eye[eyeNum].blink.state == DEBLINK) eye[eyeNum].blinkFactor = 1.0 - eye[eyeNum].blinkFactor;
646          }
647        }
648  
649        // Periodically report frame rate. Really this is "total number of
650        // eyeballs drawn." If there are two eyes, the overall refresh rate
651        // of both screens is about 1/2 this.
652        frames++;
653        if(((t - lastFrameRateReportTime) >= 1000000) && t) { // Once per sec.
654          Serial.println((frames * 1000) / (t / 1000));
655          lastFrameRateReportTime = t;
656        }
657  
658        // Once per frame (of eye #0), reset boopSum...
659        if((eyeNum == 0) && (boopPin >= 0)) {
660          boopSumFiltered = ((boopSumFiltered * 3) + boopSum) / 4;
661          if(boopSumFiltered > boopThreshold) {
662            if(!booped) {
663              Serial.println("BOOP!");
664            }
665            booped = true;
666          } else {
667            booped = false;
668          }
669          boopSum = 0;
670        }
671  
672        float mins = (float)millis() / 60000.0;
673        if(eye[eyeNum].iris.iSpin) {
674          // Spin works in fixed amount per frame (eyes may lose sync, but "wagon wheel" tricks work)
675          eye[eyeNum].iris.angle   += eye[eyeNum].iris.iSpin;
676        } else {
677          // Keep consistent timing in spin animation (eyes stay in sync, no "wagon wheel" effects)
678          eye[eyeNum].iris.angle    = (int)((float)eye[eyeNum].iris.startAngle   + eye[eyeNum].iris.spin   * mins + 0.5);
679        }
680        if(eye[eyeNum].sclera.iSpin) {
681          eye[eyeNum].sclera.angle += eye[eyeNum].sclera.iSpin;
682        } else {
683          eye[eyeNum].sclera.angle  = (int)((float)eye[eyeNum].sclera.startAngle + eye[eyeNum].sclera.spin * mins + 0.5);
684        }
685  
686        // END ONCE-PER-FRAME EYE ANIMATION ----------------------------------
687  
688      } // end first-scanline check
689  
690      // PER-COLUMN RENDERING ------------------------------------------------
691  
692      // Should be possible for these to be local vars,
693      // but the animation becomes super chunky then, what gives?
694      xPositionOverMap = (int)(eye[eyeNum].eyeX - (DISPLAY_SIZE/2.0));
695      yPositionOverMap = (int)(eye[eyeNum].eyeY - (DISPLAY_SIZE/2.0));
696  
697      // These are constant across frame and could be stored in eye struct
698      float upperLidFactor = (1.0 - eye[eyeNum].blinkFactor) * eye[eyeNum].upperLidFactor,
699            lowerLidFactor = (1.0 - eye[eyeNum].blinkFactor) * eye[eyeNum].lowerLidFactor;
700      iPupilFactor = (int)((float)eye[eyeNum].iris.height * 256 * (1.0 / eye[eyeNum].pupilFactor));
701  
702      int y1, y2;
703      int lidColumn = (eyeNum & 1) ? (DISPLAY_SIZE - 1 - x) : x; // Reverse eyelid columns for left eye
704  
705      DmacDescriptor *d = &eye[eyeNum].column[eye[eyeNum].colIdx].descriptor[0];
706  
707      if(upperOpen[lidColumn] == 255) {
708        // No eyelid data for this line; eyelid image is smaller than screen.
709        // Great! Make a full scanline of nothing, no rendering needed:
710        d->BTCTRL.bit.SRCINC = 0;
711        d->BTCNT.reg         = DISPLAY_SIZE * 2;
712        d->SRCADDR.reg       = (uint32_t)&eyelidIndex;
713        d->DESCADDR.reg      = 0; // No linked descriptor
714      } else {
715        y1 = lowerClosed[lidColumn] + (int)(0.5 + lowerLidFactor *
716          (float)((int)lowerOpen[lidColumn] - (int)lowerClosed[lidColumn]));
717        y2 = upperClosed[lidColumn] + (int)(0.5 + upperLidFactor *
718          (float)((int)upperOpen[lidColumn] - (int)upperClosed[lidColumn]));
719        if(y1 > DISPLAY_SIZE-1)    y1 = DISPLAY_SIZE-1; // Clip results in case lidfactor
720        else if(y1 < 0) y1 = 0;   // is beyond the usual 0.0 to 1.0 range
721        if(y2 > DISPLAY_SIZE-1)    y2 = DISPLAY_SIZE-1;
722        else if(y2 < 0) y2 = 0;
723        if(y1 >= y2) {
724          // Eyelid is fully or partially closed, enough that there are no
725          // pixels to be rendered for this line. Make "nothing," as above.
726          d->BTCTRL.bit.SRCINC = 0;
727          d->BTCNT.reg         = DISPLAY_SIZE * 2;
728          d->SRCADDR.reg       = (uint32_t)&eyelidIndex;
729          d->DESCADDR.reg      = 0; // No linked descriptors
730        } else {
731          // If single eye, dynamically build descriptor list as needed,
732          // else use a single descriptor & fully buffer each line.
733  #if NUM_DESCRIPTORS > 1
734          DmacDescriptor *next;
735          int             renderlen;
736          if(y1 > 0) { // Do upper eyelid unless at top of image
737            d->BTCTRL.bit.SRCINC = 0;
738            d->BTCNT.reg         = y1 * 2;
739            d->SRCADDR.reg       = (uint32_t)&eyelidIndex;
740            next                 = &eye[eyeNum].column[eye[eyeNum].colIdx].descriptor[1];
741            d->DESCADDR.reg      = (uint32_t)next; // Link to next descriptor
742            d                    = next;           // Advance to next descriptor
743          }
744          // Partial column will be rendered
745          renderlen            = y2 - y1 + 1;
746          d->BTCTRL.bit.SRCINC = 1;
747          d->BTCNT.reg         = renderlen * 2;
748          d->SRCADDR.reg       = (uint32_t)eye[eyeNum].column[eye[eyeNum].colIdx].renderBuf + renderlen * 2; // Point to END of data!
749  #else
750          // Full column will be rendered; DISPLAY_SIZE pixels, point source to end of
751          // renderBuf and enable source increment.
752          d->BTCTRL.bit.SRCINC = 1;
753          d->BTCNT.reg         = DISPLAY_SIZE * 2;
754          d->SRCADDR.reg       = (uint32_t)eye[eyeNum].column[eye[eyeNum].colIdx].renderBuf + DISPLAY_SIZE * 2;
755          d->DESCADDR.reg      = 0; // No linked descriptors
756  #endif
757          // Render column 'x' into eye's next available renderBuf
758          uint16_t *ptr = eye[eyeNum].column[eye[eyeNum].colIdx].renderBuf;
759          int xx = xPositionOverMap + x;
760          int y;
761  
762  #if NUM_DESCRIPTORS == 1
763          // Render lower eyelid if needed
764          for(y=0; y<y1; y++) *ptr++ = eyelidColor;
765  #else
766          y = y1;
767  #endif
768  
769          // tablegen.cpp explains a bit of the displacement mapping trick.
770          uint8_t *displaceX, *displaceY;
771          int8_t   xmul; // Sign of X displacement: +1 or -1
772          int      doff; // Offset into displacement arrays
773          if(x < (DISPLAY_SIZE/2)) {  // Left half of screen (quadrants 2, 3)
774            displaceX = &displace[ (DISPLAY_SIZE/2 - 1) - x       ];
775            displaceY = &displace[((DISPLAY_SIZE/2 - 1) - x) * (DISPLAY_SIZE/2)];
776            xmul      = -1; // X displacement is always negative
777          } else {       // Right half of screen( quadrants 1, 4)
778            displaceX = &displace[ x - (DISPLAY_SIZE/2)       ];
779            displaceY = &displace[(x - (DISPLAY_SIZE/2)) * (DISPLAY_SIZE/2)];
780            xmul      =  1; // X displacement is always positive
781          }
782  
783          for(; y<=y2; y++) { // For each pixel of open eye in this column...
784            int yy = yPositionOverMap + y;
785            int dx, dy;
786  
787            if(y < (DISPLAY_SIZE/2)) { // Lower half of screen (quadrants 3, 4)
788              doff = (DISPLAY_SIZE/2 - 1) - y;
789              dy   = -displaceY[doff];
790            } else {      // Upper half of screen (quadrants 1, 2)
791              doff = y - (DISPLAY_SIZE/2);
792              dy   =  displaceY[doff];
793            }
794            dx = displaceX[doff * (DISPLAY_SIZE/2)];
795            if(dx < 255) {      // Inside eyeball area
796              dx *= xmul;       // Flip sign of x offset if in quadrants 2 or 3
797              int mx = xx + dx; // Polar angle/dist map coords
798              int my = yy + dy;
799              if((mx >= 0) && (mx < mapDiameter) && (my >= 0) && (my < mapDiameter)) {
800                // Inside polar angle/dist map
801                int angle, dist, moff;
802                if(my >= mapRadius) {
803                  if(mx >= mapRadius) { // Quadrant 1
804                    // Use angle & dist directly
805                    mx   -= mapRadius;
806                    my   -= mapRadius;
807                    moff  = my * mapRadius + mx; // Offset into map arrays
808                    angle = polarAngle[moff];
809                    dist  = polarDist[moff];
810                  } else {                // Quadrant 2
811                    // ROTATE angle by 90 degrees (270 degrees clockwise; 768)
812                    // MIRROR dist on X axis
813                    mx    = mapRadius - 1 - mx;
814                    my   -= mapRadius;
815                    angle = polarAngle[mx * mapRadius + my] + 768;
816                    dist  = polarDist[ my * mapRadius + mx];
817                  }
818                } else {
819                  if(mx < mapRadius) {  // Quadrant 3
820                    // ROTATE angle by 180 degrees
821                    // MIRROR dist on X & Y axes
822                    mx    = mapRadius - 1 - mx;
823                    my    = mapRadius - 1 - my;
824                    moff  = my * mapRadius + mx;
825                    angle = polarAngle[moff] + 512;
826                    dist  = polarDist[ moff];
827                  } else {                // Quadrant 4
828                    // ROTATE angle by 270 degrees (90 degrees clockwise; 256)
829                    // MIRROR dist on Y axis
830                    mx   -= mapRadius;
831                    my    = mapRadius - 1 - my;
832                    angle = polarAngle[mx * mapRadius + my] + 256;
833                    dist  = polarDist[ my * mapRadius + mx];
834                  }
835                }
836                // Convert angle/dist to texture map coords
837                if(dist >= 0) { // Sclera
838                  angle = ((angle + eye[eyeNum].sclera.angle) & 1023) ^ eye[eyeNum].sclera.mirror;
839                  int tx = angle * eye[eyeNum].sclera.width  / 1024; // Texture map x/y
840                  int ty = dist  * eye[eyeNum].sclera.height / 128;
841                  *ptr++ = eye[eyeNum].sclera.data[ty * eye[eyeNum].sclera.width + tx];
842                } else if(dist > -128) { // Iris or pupil
843                  int ty = dist * iPupilFactor / -32768;
844                  if(ty >= eye[eyeNum].iris.height) { // Pupil
845                    *ptr++ = eye[eyeNum].pupilColor;
846                  } else { // Iris
847                    angle = ((angle + eye[eyeNum].iris.angle) & 1023) ^ eye[eyeNum].iris.mirror;
848                    int tx = angle * eye[eyeNum].iris.width / 1024;
849                    *ptr++ = eye[eyeNum].iris.data[ty * eye[eyeNum].iris.width + tx];
850                  }
851                } else {
852                  *ptr++ = eye[eyeNum].backColor; // Back of eye
853                }
854              } else {
855                *ptr++ = eye[eyeNum].backColor; // Off map, use back-of-eye color
856              }
857            } else { // Outside eyeball area
858              *ptr++ = eyelidColor;
859            }
860          }
861  
862  #if NUM_DESCRIPTORS == 1
863          // Render upper eyelid if needed
864          for(; y<DISPLAY_SIZE; y++) *ptr++ = eyelidColor;
865  #else
866          if(y2 >= (DISPLAY_SIZE-1)) {
867            // No third descriptor; close it off
868            d->DESCADDR.reg      = 0;
869          } else {
870            next                 = &eye[eyeNum].column[eye[eyeNum].colIdx].descriptor[(y1 > 0) ? 2 : 1];
871            d->DESCADDR.reg      = (uint32_t)next; // link to next descriptor
872            d                    = next; // Increment descriptor
873            d->BTCTRL.bit.SRCINC = 0;
874            d->BTCNT.reg         = ((DISPLAY_SIZE-1) - y2) * 2;
875            d->SRCADDR.reg       = (uint32_t)&eyelidIndex;
876            d->DESCADDR.reg      = 0; // end of descriptor list
877          }
878  #endif
879        }
880      }
881      eye[eyeNum].column_ready = true; // Line is rendered!
882    }
883  
884    // If DMA for this eye is currently busy, don't block, try next eye...
885    if(eye[eyeNum].dma_busy) {
886      if((micros() - eye[eyeNum].dmaStartTime) < DMA_TIMEOUT) return;
887      // If we reach this point in the code, an SPI DMA transfer has taken
888      // noticably longer than expected and is probably stalled (see comments
889      // in the DMAbuddy.h file and above the DMA_TIMEOUT declaration earlier
890      // in this code). Take action!
891      // digitalWrite(13, HIGH);
892      Serial.printf("Eye #%d stalled, resetting DMA channel...\n", eyeNum);
893      eye[eyeNum].dma.fix();
894      // If this somehow proves to be inadequate, we still have the Nuclear
895      // Option of just completely restarting the sketch from the beginning,
896      // though this stalls animation for several seconds during startup.
897      // DO NOT enable this line unless the fix() function isn't fixing!
898      //NVIC_SystemReset();
899    }
900  
901    // At this point, above checks confirm that column is ready and DMA is free
902    if(!x) { // If it's the first column...
903      // End prior SPI transaction...
904      digitalWrite(eye[eyeNum].cs, HIGH); // Deselect
905      eye[eyeNum].spi->endTransaction();
906      // Initialize new SPI transaction & address window...
907      eye[eyeNum].spi->beginTransaction(settings);
908      digitalWrite(eye[eyeNum].cs, LOW);  // Chip select
909      eye[eyeNum].display->setAddrWindow((eye[eyeNum].display->width() - DISPLAY_SIZE) / 2, (eye[eyeNum].display->height() - DISPLAY_SIZE) / 2, DISPLAY_SIZE, DISPLAY_SIZE);
910      delayMicroseconds(1);
911      digitalWrite(eye[eyeNum].dc, HIGH); // Data mode
912      if(eyeNum == (NUM_EYES-1)) {
913        // Handle pupil scaling
914        if(lightSensorPin >= 0) {
915          // Read light sensor, but not too often (Seesaw hates that)
916          #define LIGHT_INTERVAL (1000000 / 10) // 10 Hz, don't poll Seesaw too often
917          if((t - lastLightReadTime) >= LIGHT_INTERVAL) {
918            // Fun fact: eyes have a "consensual response" to light -- both
919            // pupils will react even if the opposite eye is stimulated.
920            // Meaning we can get away with using a single light sensor for
921            // both eyes. This comment has nothing to do with the code.
922            uint16_t rawReading = arcada.readLightSensor();
923            if(rawReading <= 1023) {
924              if(rawReading < lightSensorMin)      rawReading = lightSensorMin; // Clamp light sensor range
925              else if(rawReading > lightSensorMax) rawReading = lightSensorMax; // to within usable range
926              float v = (float)(rawReading - lightSensorMin) / (float)(lightSensorMax - lightSensorMin); // 0.0 to 1.0
927              v = pow(v, lightSensorCurve);
928              lastLightValue    = irisMin + v * irisRange;
929              lastLightReadTime = t;
930              lightSensorFailCount = 0;
931            } else { // I2C error
932              if(++lightSensorFailCount >= 25) { // If repeated errors in succession...
933                lightSensorPin = -1; // Stop trying to use the light sensor
934              } else {
935                lastLightReadTime = t - LIGHT_INTERVAL + 30000; // Try again in 30 ms
936              } }
937          }
938          irisValue = (irisValue * 0.97) + (lastLightValue * 0.03); // Filter response for smooth reaction
939        } else {
940          // Not light responsive. Use autonomous iris w/fractal subdivision
941          float n, sum = 0.5;
942          for(uint16_t i=0; i<IRIS_LEVELS; i++) { // 0,1,2,3,...
943            uint16_t iexp  = 1 << (i+1);          // 2,4,8,16,...
944            uint16_t imask = (iexp - 1);          // 2^i-1 (1,3,7,15,...)
945            uint16_t ibits = iris_frame & imask;  // 0 to mask
946            if(ibits) {
947              float weight = (float)ibits / (float)iexp; // 0.0 to <1.0
948              n            = iris_prev[i] * (1.0 - weight) + iris_next[i] * weight;
949            } else {
950              n            = iris_next[i];
951              iris_prev[i] = iris_next[i];
952              iris_next[i] = -0.5 + ((float)random(1000) / 999.0); // -0.5 to +0.5
953            }
954            iexp = 1 << (IRIS_LEVELS - i); // ...8,4,2,1
955            sum += n / (float)iexp;
956          }
957          irisValue = irisMin + (sum * irisRange); // 0.0-1.0 -> iris min/max
958          if((++iris_frame) >= (1 << IRIS_LEVELS)) iris_frame = 0;
959        }
960  #if defined(ADAFRUIT_MONSTER_M4SK_EXPRESS)
961        if(voiceOn) {
962          // Read buttons, change pitch
963          arcada.readButtons();
964          uint32_t buttonState = arcada.justPressedButtons();
965          if(       buttonState & ARCADA_BUTTONMASK_UP) {
966            currentPitch *= 1.05;
967          } else if(buttonState & ARCADA_BUTTONMASK_A) {
968            currentPitch = defaultPitch;
969          } else if(buttonState & ARCADA_BUTTONMASK_DOWN) {
970            currentPitch *= 0.95;
971          }
972          if(buttonState & (ARCADA_BUTTONMASK_UP | ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_DOWN)) {
973            currentPitch = voicePitch(currentPitch);
974            if(waveform) voiceMod(modulate, waveform);
975            Serial.print("Voice pitch: ");
976            Serial.println(currentPitch);
977          }
978        }
979  #endif
980        user_loop();
981      }
982    } // end first-column check
983  
984    // MUST read the booper when there’s no SPI traffic across the nose!
985    if((eyeNum == (NUM_EYES-1)) && (boopPin >= 0)) {
986      boopSum += readBoop();
987    }
988  
989    memcpy(eye[eyeNum].dptr, &eye[eyeNum].column[eye[eyeNum].colIdx].descriptor[0], sizeof(DmacDescriptor));
990    eye[eyeNum].dma_busy       = true;
991    eye[eyeNum].dma.startJob();
992    eye[eyeNum].dmaStartTime   = micros();
993    if(++eye[eyeNum].colNum >= DISPLAY_SIZE) { // If last line sent...
994      eye[eyeNum].colNum      = 0;    // Wrap to beginning
995    }
996    eye[eyeNum].colIdx       ^= 1;    // Alternate 0/1 line structs
997    eye[eyeNum].column_ready = false; // OK to render next line
998  }