Sync multiple Teensys

Hi. Background: I was sort of involuntarily tasked with the LED lighting for an art project I'm doing with some friends. I don't have hardly any coding experience, thus I'm trying to use some AI assistants (have tried ChatGPT and Claude) for help, and very disappointed with the results, to say the least. I can read the code a bit and sometimes have an idea of what's going on but it is taking a really, really, really long time for very little progress. I'm sure many would judge this as an overly ambitious project for a first-timer but I am who I am and the project is the project. Can't change either, really. Anyway.

I'm doing a Hueshift on one Teensy and I'm trying to send a GPIO High signal to the other Teensys when the Hueshift loops back to '0', so that the other Teensys keep their Hue shift in sync. You might ask why I am not using Serial and that's because I couldn't get it to work for some reason. GPIO is working great, however.

This is the latest of what ChatGPT is trying to do (paraphrasing the entire code for just the HueShift bits):

int hueShift = 0; // Start hue rotation at Blue (0 degrees in the HSV color space)
int previousHueShift = 255; // Track the previous value of hueShift to detect reset
bool isHueReset = false; // Flag to track if hueShift reset from 255 to 0

// Send the Hue sync signal only when hueShift goes from 255 to 0
if (hueShift == 0 && previousHueShift == 255 && !isHueReset) {
digitalWrite(21, HIGH); // Send trigger to Teensy 2
delay(50); // Ensure the signal is received
digitalWrite(21, LOW); // Reset GPIO pin for Teensy 2
isHueReset = true; // Mark that the reset has been triggered
}

// Reset isHueReset flag when hueShift reaches 255 (so we can trigger it again)
if (hueShift == 255) {
isHueReset = false; // Ready to send signal again when hue resets to 0
}

// Increment hue for color rotation
hueShift++; // Slowly increment the hue value for color transition (rotate through hues)
if (hueShift > 255) {
hueShift = 0; // Reset hue to blue when it exceeds the max value
}

previousHueShift = hueShift; // Track previous hueShift value
}

Every variation of this code that is tried is the same result: The Teensy is sending the High signal for every incremental change of HueShift, not just when it resets back to '0'.

So what's wrong? Spent all day trying to solve this, I'm ready to shortcut this, ha. Thanks for any help and apologies for the remedial user here, ha.
 
So what's wrong? Spent all day trying to solve this, I'm ready to shortcut this, ha. Thanks for any help and apologies for the remedial user here, ha.

Impossible to say without seeing the code that calls what you've showed us. The best way to get help here is to post a small, complete program that illustrates the problem you're trying to solve. It doesn't have to be your entire project, just a program that shows the part that is giving you trouble. Also, when you post code, first click on the </> button and paste your code into the dialog box that opens up. That will keep the formatting of your code and make it easier to read.
 
Code:
#include <FastLED.h>

// Define Data and Clock pins for all LED strips
#define DATA_PIN_1_FRONT 3
#define CLOCK_PIN_1_FRONT 4
#define DATA_PIN_1_REAR 5
#define CLOCK_PIN_1_REAR 6
#define DATA_PIN_2_FRONT 7
#define CLOCK_PIN_2_FRONT 8
#define DATA_PIN_2_REAR 9
#define CLOCK_PIN_2_REAR 10
#define DATA_PIN_3_FRONT 11
#define CLOCK_PIN_3_FRONT 12
#define DATA_PIN_3_REAR 13
#define CLOCK_PIN_3_REAR 14
#define DATA_PIN_4_FRONT 15
#define CLOCK_PIN_4_FRONT 16
#define DATA_PIN_4_REAR 17
#define CLOCK_PIN_4_REAR 18
#define DATA_PIN_BODY 19
#define CLOCK_PIN_BODY 20

// Number of LEDs for each strip
#define NUM_LEDS_1_FRONT 121
#define NUM_LEDS_1_REAR 65
#define NUM_LEDS_2_FRONT 121
#define NUM_LEDS_2_REAR 65
#define NUM_LEDS_3_FRONT 121
#define NUM_LEDS_3_REAR 65
#define NUM_LEDS_4_FRONT 121
#define NUM_LEDS_4_REAR 65
#define NUM_LEDS_BODY 450

// Set up the FastLED array
CRGB leds_1_front[NUM_LEDS_1_FRONT];
CRGB leds_1_rear[NUM_LEDS_1_REAR];
CRGB leds_2_front[NUM_LEDS_2_FRONT];
CRGB leds_2_rear[NUM_LEDS_2_REAR];
CRGB leds_3_front[NUM_LEDS_3_FRONT];
CRGB leds_3_rear[NUM_LEDS_3_REAR];
CRGB leds_4_front[NUM_LEDS_4_FRONT];
CRGB leds_4_rear[NUM_LEDS_4_REAR];
CRGB leds_body[NUM_LEDS_BODY];

int hueShift = 0;  // Start hue rotation at Blue (0 degrees in the HSV color space)
int previousHueShift = 255;  // Track the previous value of hueShift to detect reset
bool isHueReset = false;  // Flag to track if hueShift reset from 255 to 0

int wavePosition = 0; // Variable to track the position of the wave
int maxLitLEDs = 12;  // Limit the lit LEDs to 10% (approx 10% of NUM_LEDS_1_FRONT)



void setup() {
  // Initialize the FastLED library with BRG color order
  FastLED.addLeds<SK9822, DATA_PIN_1_FRONT, CLOCK_PIN_1_FRONT, BRG>(leds_1_front, NUM_LEDS_1_FRONT);
  FastLED.addLeds<SK9822, DATA_PIN_1_REAR, CLOCK_PIN_1_REAR, BRG>(leds_1_rear, NUM_LEDS_1_REAR);
  FastLED.addLeds<SK9822, DATA_PIN_2_FRONT, CLOCK_PIN_2_FRONT, BRG>(leds_2_front, NUM_LEDS_2_FRONT);
  FastLED.addLeds<SK9822, DATA_PIN_2_REAR, CLOCK_PIN_2_REAR, BRG>(leds_2_rear, NUM_LEDS_2_REAR);
  FastLED.addLeds<SK9822, DATA_PIN_3_FRONT, CLOCK_PIN_3_FRONT, BRG>(leds_3_front, NUM_LEDS_3_FRONT);
  FastLED.addLeds<SK9822, DATA_PIN_3_REAR, CLOCK_PIN_3_REAR, BRG>(leds_3_rear, NUM_LEDS_3_REAR);
  FastLED.addLeds<SK9822, DATA_PIN_4_FRONT, CLOCK_PIN_4_FRONT, BRG>(leds_4_front, NUM_LEDS_4_FRONT);
  FastLED.addLeds<SK9822, DATA_PIN_4_REAR, CLOCK_PIN_4_REAR, BRG>(leds_4_rear, NUM_LEDS_4_REAR);
  FastLED.addLeds<SK9822, DATA_PIN_BODY, CLOCK_PIN_BODY, BRG>(leds_body, NUM_LEDS_BODY);

  FastLED.setBrightness(128);  // Set brightness to 50%

  // Set all LEDs to blue initially
  fill_solid(leds_1_front, NUM_LEDS_1_FRONT, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_1_rear, NUM_LEDS_1_REAR, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_2_front, NUM_LEDS_2_FRONT, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_2_rear, NUM_LEDS_2_REAR, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_3_front, NUM_LEDS_3_FRONT, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_3_rear, NUM_LEDS_3_REAR, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_4_front, NUM_LEDS_4_FRONT, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_4_rear, NUM_LEDS_4_REAR, CHSV(hueShift, 255, 255)); 
  fill_solid(leds_body, NUM_LEDS_BODY, CHSV(hueShift, 255, 255)); 

  // Show initial state
  FastLED.show();

  // Initialize GPIO pins for hue sync
  pinMode(21, OUTPUT);  // GPIO pin to send hue sync to Teensy 2
}

void loop() {
  // Clear all LEDs to ensure no previous state remains
  fill_solid(leds_1_front, NUM_LEDS_1_FRONT, CRGB::Black);  // Black to reset state
  fill_solid(leds_1_rear, NUM_LEDS_1_REAR, CRGB::Black);
  fill_solid(leds_2_front, NUM_LEDS_2_FRONT, CRGB::Black);
  fill_solid(leds_2_rear, NUM_LEDS_2_REAR, CRGB::Black);
  fill_solid(leds_3_front, NUM_LEDS_3_FRONT, CRGB::Black);
  fill_solid(leds_3_rear, NUM_LEDS_3_REAR, CRGB::Black);
  fill_solid(leds_4_front, NUM_LEDS_4_FRONT, CRGB::Black);
  fill_solid(leds_4_rear, NUM_LEDS_4_REAR, CRGB::Black);
  fill_solid(leds_body, NUM_LEDS_BODY, CRGB::Black);  // Reset all LEDs to black

  // Debugging the sync signal and hueShift reset
  Serial.print("hueShift: ");
  Serial.println(hueShift);

  // Send the Hue sync signal only when hueShift goes from 255 to 0
  if (hueShift == 0 && previousHueShift == 255 && !isHueReset) {
    digitalWrite(21, HIGH);  // Send trigger to Teensy 2
    delay(50);  // Ensure the signal is received
    digitalWrite(21, LOW);  // Reset GPIO pin for Teensy 2
    isHueReset = true;  // Mark that the reset has been triggered
  }

 // Reset isHueReset flag when hueShift reaches 255 (so we can trigger it again)
  if (hueShift == 255) {
    isHueReset = false;  // Ready to send signal again when hue resets to 0
  }

  // Apply the wave effect on all strips (Color is rotating with hue)
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_1_FRONT;
    leds_1_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_1_REAR;
    leds_1_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_2_FRONT;
    leds_2_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_2_REAR;
    leds_2_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_3_FRONT;
    leds_3_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_3_REAR;
    leds_3_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_4_FRONT;
    leds_4_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_4_REAR;
    leds_4_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < maxLitLEDs; i++) {
    int ledIndex = (wavePosition + i) % NUM_LEDS_BODY;
    leds_body[ledIndex] = CHSV(hueShift, 255, 255);
  }

  // Apply sparkle effect on LEDs (same as before, applying sparkle for each LED strip)
  for (int i = 0; i < NUM_LEDS_1_FRONT / 10; i++) {
    int ledIndex = random(NUM_LEDS_1_FRONT);
    leds_1_front[ledIndex] = CHSV(hueShift, 255, 255);  // Use same hue for sparkle
  }
  for (int i = 0; i < NUM_LEDS_1_REAR / 10; i++) {
    int ledIndex = random(NUM_LEDS_1_REAR);
    leds_1_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < NUM_LEDS_2_FRONT / 10; i++) {
    int ledIndex = random(NUM_LEDS_2_FRONT);
    leds_2_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < NUM_LEDS_2_REAR / 10; i++) {
    int ledIndex = random(NUM_LEDS_2_REAR);
    leds_2_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
for (int i = 0; i < NUM_LEDS_3_FRONT / 10; i++) {
    int ledIndex = random(NUM_LEDS_3_FRONT);
    leds_3_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < NUM_LEDS_3_REAR / 10; i++) {
    int ledIndex = random(NUM_LEDS_3_REAR);
    leds_3_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < NUM_LEDS_4_FRONT / 10; i++) {
    int ledIndex = random(NUM_LEDS_4_FRONT);
    leds_4_front[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < NUM_LEDS_4_REAR / 10; i++) {
    int ledIndex = random(NUM_LEDS_4_REAR);
    leds_4_rear[ledIndex] = CHSV(hueShift, 255, 255);
  }
  for (int i = 0; i < NUM_LEDS_BODY / 10; i++) {
    int ledIndex = random(NUM_LEDS_BODY);
    leds_body[ledIndex] = CHSV(hueShift, 255, 255);
  }


  FastLED.show(); // Show the updated LEDs

  // Control the speed of the wave
  delay(100);  // Delay between each wave step
  wavePosition++;  // Move the wave forward

  // Increment hue for color rotation
  hueShift++;  // Slowly increment the hue value for color transition (rotate through hues)
  if (hueShift > 255) {
    hueShift = 0;  // Reset hue to blue when it exceeds the max value
  }

  previousHueShift = hueShift;  // Track previous hueShift value
}

Sorry, I had posted the code but not in the format with the </> button. Thanks for the tip. And yes this is the entire code, so far
 
Code:
  if (hueShift == 0 && previousHueShift == 255 && !isHueReset) {
I suspect this will never be true since you set previousHueShift to hueShift in the last line of your loop
 
Yes, thank you for pointing that out. I took that out, cleared some other things that seemed unnecessary after going through line by line and trying to understand what each is doing. When it made sense to me and still had a problem, figured out that it probably isn't the code, it is the GPIO connection. Think I'm probably running too long of a wire (25'-35') to the other Teensys.
 
Yes that length could be problematic. You say "wire" and not "wires" which raises a red flag. You can't send a signal along one wire without it being an antenna. And there would be no common ground reference.
 
How close of a synchronization is required? To nanoseconds, microseconds, milliseconds or seconds? If roughly milliseconds, I'd use CAN, and twisted pair between Teensies. You need a 3.3V CAN transceiver. A differential signal is good for longer wire runs as it's more immune to noise. There's quite a few CAN examples to adapt. CAN needs a termination (120 ohm resistor at the end of the line, also called a bus). CAN is used in vehicles and in industrial automation.
 
Yes that length could be problematic. You say "wire" and not "wires" which raises a red flag. You can't send a signal along one wire without it being an antenna. And there would be no common ground reference.
Thank you. I had seen the "common ground" requirement before but I assumed since the power supplies were plugged into the same electrical circuit (same power strip, same house), that would be "common ground". But I see now that assumption was not correct.
 
How close of a synchronization is required? To nanoseconds, microseconds, milliseconds or seconds? If roughly milliseconds, I'd use CAN, and twisted pair between Teensies. You need a 3.3V CAN transceiver. A differential signal is good for longer wire runs as it's more immune to noise. There's quite a few CAN examples to adapt. CAN needs a termination (120 ohm resistor at the end of the line, also called a bus). CAN is used in vehicles and in industrial automation.
Thank you for the suggestion! I'm running out of time but let's explore this. I'm just trying to coordinate Hueshift between the Teensys to the naked eye so I'd think 20ms or less would be fine. I do have another coordination I will be doing, basically taking some flex sensors and sending High/Low data from one Teensy to another. I see 3.3V CAN Transceivers online, cheap and readily available. How does this work? Am I correct in reading the Teensy 4.1 pinout that there are three possible connections (CRX1, CRT1, CRX2, CRT2 and CRX3 and CRT3?). Then it looks I would connect a Transceiver via two conductors to another CAN Transceiver on another Teensy. If I have four Teensys, do I need three Transceivers connected to the Master Teensy or is it possible to connect them serially so I would only need a total of four Transceivers for the four Teensys? Does CAN work in 0's and 1's like GPIO ( high/low signal) or is it data like a Serial connection? Apologies for the remedial questions!
 
Last edited:
It's overkill for your application, but CAN is message based, so abstracting away the bus stuff, it's like Serial. You can have simple commands, like just a character. "F" for off, and "N" for on, for instance. Or whatever. CAN is far more sophisticated than that, but you don't need the extra features for what you are doing. So a simple message is all you need. You can address each unit individually, or broadcast to all devices from your master device.

Each Teensy that attached to the CAN bus needs a 3.3V transceiver. I'd recommend that each Teensy uses the same CAN bus pins, say CAN1. (Don't have the Teensy card in front of me.) If the Teensy's are all plugged into the same power strip the differential bus will take care of minor relative ground shifts between each Teensy.

The bus is a twisted pair of wires. Use two different colors for the wires, it just makes it easier for you, because you have to keep track of CANH and CANL. The bus topology is up to you, but I'd recommend one long "serial" connection, rather than a star connection to all the devices. The serial connection requires 120 ohms at the master, and 120 ohms at the end of the bus. The bus can extend past the last device, or end there. There's no need for additional resistors for the serial style bus.
 
Thank you. I had seen the "common ground" requirement before but I assumed since the power supplies were plugged into the same electrical circuit (same power strip, same house), that would be "common ground". But I see now that assumption was not correct.
Firstly some power supplies are floating and won't bring mains ground through. Secondly although the mains ground ought to be consistent in one building, you have to worry about induced noise in ground loops from transformers and other electrical equipment, and transients, Since we are using devices with only an allowable 3.3V dc range we don't want to trust that to the vaguaries of a mains power network.

Communication hardware designed for travelling around a large building such as RS485 allow for a ground offset of many volts to be present between ends of the cable. Or they are galvanically isolated (ethernet for instance).
 
It's overkill for your application, but CAN is message based, so abstracting away the bus stuff, it's like Serial. You can have simple commands, like just a character. "F" for off, and "N" for on, for instance. Or whatever. CAN is far more sophisticated than that, but you don't need the extra features for what you are doing. So a simple message is all you need. You can address each unit individually, or broadcast to all devices from your master device.

Each Teensy that attached to the CAN bus needs a 3.3V transceiver. I'd recommend that each Teensy uses the same CAN bus pins, say CAN1. (Don't have the Teensy card in front of me.) If the Teensy's are all plugged into the same power strip the differential bus will take care of minor relative ground shifts between each Teensy.

The bus is a twisted pair of wires. Use two different colors for the wires, it just makes it easier for you, because you have to keep track of CANH and CANL. The bus topology is up to you, but I'd recommend one long "serial" connection, rather than a star connection to all the devices. The serial connection requires 120 ohms at the master, and 120 ohms at the end of the bus. The bus can extend past the last device, or end there. There's no need for additional resistors for the serial style bus.
Firstly some power supplies are floating and won't bring mains ground through. Secondly although the mains ground ought to be consistent in one building, you have to worry about induced noise in ground loops from transformers and other electrical equipment, and transients, Since we are using devices with only an allowable 3.3V dc range we don't want to trust that to the vaguaries of a mains power network.

Communication hardware designed for travelling around a large building such as RS485 allow for a ground offset of many volts to be present between ends of the cable. Or they are galvanically isolated (ethernet for instance).
That all makes perfect sense, thank you for the information. I was able to get GPIO working with this help but I think in a future iteration, CAN will be the better option. Can't thank you all enough. May have some more questions upcoming :)
 
A little late to the party but one further refinement -

If I'm reading it correctly your code is basically
Code:
loop() {
  if (hue has looped) {
    set GPIO
    wait 50
    clear GPIO
  }

  set Leds
  wait 100
  increase hue
}

This means that your led update period when your hue value loops back to zero is 150 while the rest of the time your period is 100.

I'm guessing that your other end is only looking for the start of the pulse rather that looking at the length of the control pulse. Which means you could both simplify things and get more even timing by doing:

Code:
loop() {
  if (hue == 0) {
    set GPIO
  } else {
    clear GPIO
  }

  set Leds
  wait 100
  increase hue
}
 
Back
Top