Combining FastLED and Other Functionality Simulataneously; Animatronics

siredward

Member
Teensy 3.5. Custom PCB (not audio shield). I am an EE Tech I make my own shields.

OK! The source code below works! My animatronic skull model turns on when I trigger the sensor and the jaw servo maps to the audio output!

So now I need to integrate some FastLED into the action. How is this done without multi-threading? I am still teaching myself about timing loops and Arduino here so bear with me (or better yet fill me in).

Thank you!!

Source Code:
Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <PWMServo.h>

// Configuration
// Connections
#define SERVO_PIN 10 // Jaw servo
#define PING_PIN 5 // ping / echo for sonar distance sensor
#define ECHO_PIN 6
#define MOSFETPin 9 // mostfet for servo power control

// How long until the ping returns to consider it a "trigger"
#define  SONAR_THRESHOLD 900 // microseconds.
// Speed of sound in air is about 343 m/s, so this is about 15 cm (~6 in), or a 30cm round trip.
// Quick calculators:
// const SONAR_THRESHOLD (148 * 6) // ~inches
// const SONAR_THRESHOLD (58 * 15) // ~cm

// Servo range configuration
#define JAW_OPEN 70
#define JAW_CLOSED 0

// Length of time for `loop()` to run.  1000/25 Hz (40ms) should be pretty fluid
#define  SAMPLE_TIME 200  // ms

// Sound file to play when sonar triggers
#define  SAMPLE_FILE "01.wav"

// Delay playback to compensate for the jaw servo's lag.
#define SERVO_DELAY 50 // ms

// To play with this, import the below block into https://www.pjrc.com/teensy/gui/

// playSdWav1 will use the built-in sd card slot, assumed to have a fat32 filesystem.
// If you're using some other way to attach SD, or you'd like to use in-memory or
// whatever, there are other options.

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioPlaySdWav           playSdWav1;     //xy=329,371
AudioMixer4              mixer1;         //xy=548,386
AudioEffectDelay         delay1;         //xy=749,424
AudioOutputAnalog        dac1;           //xy=951,421
AudioAnalyzePeak         peak1;          //xy=973,360
AudioConnection          patchCord1(playSdWav1, 0, mixer1, 0);
AudioConnection          patchCord2(playSdWav1, 1, mixer1, 1);
AudioConnection          patchCord3(mixer1, delay1);
AudioConnection          patchCord4(delay1, 0, peak1, 0);
AudioConnection          patchCord5(delay1, 1, dac1, 0);
// GUItool: end automatically generated code

#define SDCARD_CS_PIN    BUILTIN_SDCARD
#define SDCARD_MOSI_PIN  11  // not actually used
#define SDCARD_SCK_PIN   13

PWMServo jaw;

// Entirely optional.  For stable sample management.  `elapsedMillis` is a teensy type that
// automatically counts up, so it's useful for making sure if you want 25Hz, you _get_ 25Hz.
elapsedMillis timing;

int ping() {
  digitalWrite(PING_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(PING_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(PING_PIN, LOW);
  if (pulseIn(ECHO_PIN, HIGH, SONAR_THRESHOLD) != 0) {
    return HIGH;
  }
  return LOW;
}


uint8_t mosfet_state = LOW;
void set_mosfet(uint8_t state) {
  if (state != mosfet_state) {
    Serial.print("Setting MOSFET to ");
    Serial.println(state == LOW ? "LOW" : "HIGH");
    mosfet_state = state;
    digitalWrite(MOSFETPin, mosfet_state);
  }
}


void setup() {
  Serial.begin(9600);
  Serial.println("initializing audio memory");
 
  // Set up audio memory.  You need at least 1 block for each 2.9 ms of delay, plus
  // a minimum of 8 blocks for the player. 
  AudioMemory((int) (ceil(SERVO_DELAY / 2.9) + 8));
  Serial.println("initializing pins");
  jaw.attach(SERVO_PIN);
  pinMode(PING_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(MOSFETPin, OUTPUT);
  delay1.delay(0, 0);    
  delay1.delay(1, SERVO_DELAY);
  delay1.disable(2);
  delay1.disable(3);
  delay1.disable(4);
  delay1.disable(5);
  delay1.disable(6);
  delay1.disable(7);
  
  mixer1.gain(0, 1.0);
  mixer1.gain(1, 1.0);
  mixer1.gain(2, 0);
  mixer1.gain(3, 0);
  
  Serial.println("initializing SD access");
  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    while (1) {
      Serial.println(SD.begin(SDCARD_CS_PIN));
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }
}

void loop_state_waiting() {
  if (mosfet_state == HIGH) {
    Serial.println("playback ended; returning to sleep.");
  }
  set_mosfet(LOW);
  if (ping() == HIGH) {
    Serial.println("sonar ping!  Starting sound/animation.");
    set_mosfet(HIGH);
    playSdWav1.play(SAMPLE_FILE);
    delay(5);
  }
}

void loop_state_playing() {
  if (peak1.available()) {
    float level = peak1.readPeakToPeak();
    int new_servo_pos = (int) (level * (JAW_OPEN - JAW_CLOSED) / 2.0 + JAW_CLOSED);
    Serial.print("Level: ");
    Serial.print(level);
    Serial.print("; servo: ");
    Serial.println(new_servo_pos);
    jaw.write(new_servo_pos);
  }
}

void loop() {
  // resets the counter
  timing = 0;
  if (!playSdWav1.isPlaying()) {
    loop_state_waiting();
  } else {  
    loop_state_playing();
  }
  // Only delay for the remaining time in the sample.
  delay(SAMPLE_TIME - timing);
}


Want to include:
Code:
//Skull Eyes

#define FRAMES_PER_SECOND 8
#include <FastLED.h>
bool gReverseDirection = false;

#define NUM_LEDS_PER_STRIP 10
CRGB leds[NUM_LEDS_PER_STRIP];

// For mirroring strips, all the "special" stuff happens just in setup.  We
// just addLeds multiple times, once for each strip
void setup() {
  // tell FastLED there's 60 NEOPIXEL leds on pin 4
  FastLED.addLeds<NEOPIXEL, 2>(leds, NUM_LEDS_PER_STRIP);

  // tell FastLED there's 60 NEOPIXEL leds on pin 5
  FastLED.addLeds<NEOPIXEL, 3>(leds, NUM_LEDS_PER_STRIP);

 
}

void loop()
{
  // Add entropy to random number generator; we use a lot of it.
  // random16_add_entropy( random());

  Fire2012(); // run simulation frame
  
  FastLED.show(); // display this frame
  FastLED.delay(1000 / FRAMES_PER_SECOND);
}


#define COOLING  55

// SPARKING: What chance (out of 255) is there that a new spark will be lit?
// Higher chance = more roaring fire.  Lower chance = more flickery fire.
// Default 120, suggested range 50-200.
#define SPARKING 120


void Fire2012()
{
// Array of temperature readings at each simulation cell
  static byte heat[NUM_LEDS_PER_STRIP];

  // Step 1.  Cool down every cell a little
    for( int i = 0; i < NUM_LEDS_PER_STRIP; i++) {
      heat[i] = qsub8( heat[i],  random8(0, ((COOLING * 10) / NUM_LEDS_PER_STRIP) + 2));
    }
  
    // Step 2.  Heat from each cell drifts 'up' and diffuses a little
    for( int k= NUM_LEDS_PER_STRIP - 1; k >= 2; k--) {
      heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
    }
    
    // Step 3.  Randomly ignite new 'sparks' of heat near the bottom
    if( random8() < SPARKING ) {
      int y = random8(7);
      heat[y] = qadd8( heat[y], random8(160,255) );
    }

    // Step 4.  Map from heat cells to LED colors
    for( int j = 0; j < NUM_LEDS_PER_STRIP; j++) {
      CRGB color = HeatColor( heat[j]);
      int pixelnumber;
      if( gReverseDirection ) {
        pixelnumber = (NUM_LEDS_PER_STRIP-1) - j;
      } else {
        pixelnumber = j;
      }
      leds[pixelnumber] = color;
    }
}
 
Last edited:
For FASTled, you will need to consult with the documentation, or possibly the FASTled web site. I vaguely recall that FASTled has its own method for DMA usage, but it can also uses the WS218Serial library or the Octws2811 adapter. Other than knowing it exists, I have not used it.

Outside of FASTled there are two approaches that uses the Teesny's DMA engine to do the LEDs in the background. Which you use depends on how many LEDs you have to light up.

For light use (i.e. a couple hundred LEDs at most), look into the WS2812Serial library:

For this library, you use one or more of the pins connected to the Teensy UARTs as the data pin for the LED string. From the web page, the pins are;
  • Serial1: Pins 1, 5, or 26.
  • Serial2: Pin 10.
  • Serial3: Pin 8.
  • Serial4; Pin 32.
  • Serial5: Pin 33.
  • Serial6: Pad 48 underneath the Teensy.

You should be able to do up to 6 LED strips in parallel (but I have only done a single strand).

Obviously you will need the appropriate voltage level shifting, thick wires to power the LEDs, etc.

For larger LED displays, you want to use the Octows2811 library which does DMA double buffering for 8 independent streams. It is meant to be used with the Octows2811 adapter. I haven't used this personally.

Even if you ultimately decide to make your own board instead of the Octows2811, you might want to take a gander at the various suggestions on use in the adapter page.

Limits:

Outside of WS2812B/SK6812 LEDs, another approach is the Smart Matrix approach. http://docs.pixelmatix.com/SmartMatrix/
 
Last edited:
Hi Michael, thank you for your thoughtful reply. As I am just running 2 strings of about a dozen LEDs ea, so Paul's 2812 serial lib should work for this project. However before I go cutting traces and jumping wires on my PCB, I just want to try to understand what is going on below the hood.

I come from an EE background. To me the Teensy is a black box that receives and produces. Most of what I know about it comes from the basic documentation and testing one piece of hardware at a time pulling and tinkering with source codes from the examples libs. So this is my first time running multiple functions simultaneously.

Now that I am getting into coding I would like to understand how it processes information and so that I can design my PCBs with specific compatibility in mind. Do you know where I can look to start learning about this?

Thanks
 
Well the source code for the libraries is available. And the author (Paul Stoffregen) has posted about the design of the library. But since I haven't worked on the library, that's as far as I know.


In general though, it isn't running at the 'same time' since the current Teensies are single processor chips. Instead things are queued up to be run with the internal DMA (Direct Memory Access) and when an interrupt occurs. At interrupt level, the library does stuff and either issues a new request or indicates it is done, and then tells the Teensy to restart at the instruction it was working on before the interrupt. Generally, when you run stuff at interrupt level, you want to do as little as possible at that level, such as setting a flag, etc. Later you have code that checks things set in interrupt level, and does the higher level control.

Expect to do a lot of deep diving in the DMA section of the 1062 datasheet/manual. In particular, chapters 3-5 (pages 43 - 172).
 
Last edited:
Back
Top