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 }