// ============================================================ // Laser Show Data Capture — Teensy 4.x // Protocol: PWM-encoded, ~1.176MHz bit rate (850ns bit period) // Frame: 66 bits = 12X + 12Y + 6R + 6G + 6B + 24 extra // Inter-frame gap: channel HIGH for ~9.3us (≥11 bits worth) // ============================================================ // // WIRING: // DATA → Teensy pin DATA_PIN (CH0 from analyzer) // GND → Teensy GND // // CLOCK NOTE: // The signal is self-clocked (PWM / pulse-width encoded). // You do NOT need the external 10MHz clock (CH1). The // Teensy reads pulse widths directly — no clock wire needed. // // ENCODING (confirmed from capture): // Each bit = one LOW pulse + one HIGH pulse: // HIGH pulse ≥ 400ns → bit = 1 // HIGH pulse < 400ns → bit = 0 // LOW pulse is approximately fixed at ~450ns. // Total bit period ≈ 850ns → ~1.176 Mbps effective data rate. // // FRAME STRUCTURE (66 bits, MSB first): // [11: 0] X position (12-bit, 0–4095) // [23:12] Y position (12-bit, 0–4095) // [29:24] Red (6-bit, 0–63) // [35:30] Green (6-bit, 0–63) // [41:36] Blue (6-bit, 0–63) // [47:42] Extra field (6-bit) // [53:48] Extra field (6-bit) // [59:54] Extra field (6-bit) // [65:60] Extra field (6-bit) // // INTER-FRAME GAP: // DATA line stays HIGH for ≥ 9.3us between frames. // The decoder resets whenever it sees HIGH > GAP_US. // ============================================================ // ---------- Pin & Timing Configuration ---------- static const int DATA_PIN = 2; // Connect CH0 laser data here static const int BAUD = 2000000; // USB-serial baud (2 Mbaud) // Pulse-width thresholds (microseconds) // LOW pulse is ~0.45us, HIGH pulse is 0.30us (bit=0) or 0.45us (bit=1) static const float LOW_MIN_US = 0.20f; // LOW must be at least this long static const float HIGH_THR_US = 0.38f; // HIGH >= this → bit 1, else bit 0 static const float GAP_US = 5.0f; // HIGH > this → inter-frame gap // ---------- Frame size ---------- static const int BITS_PER_FRAME = 66; // If you discover a different bit count, just change this. // The parser will still extract X,Y,RGB from fixed offsets. // ---------- Output format selection ---------- // Set to 1 for compact binary (faster, less CPU), 0 for ASCII CSV #define OUTPUT_BINARY 0 // ============================================================ // State // ============================================================ static uint8_t bit_buf[BITS_PER_FRAME]; static int bit_count = 0; static bool in_frame = false; // ============================================================ // Parse and emit one complete frame // ============================================================ static void emit_frame() { if (bit_count < 42) return; // need at least X+Y+RGB // Extract fields (MSB first) uint16_t X = 0, Y = 0; uint8_t R = 0, G = 0, B = 0; uint8_t extra[4] = {0, 0, 0, 0}; for (int i = 0; i < 12; i++) X = (X << 1) | bit_buf[i]; for (int i = 0; i < 12; i++) Y = (Y << 1) | bit_buf[12 + i]; for (int i = 0; i < 6; i++) R = (R << 1) | bit_buf[24 + i]; for (int i = 0; i < 6; i++) G = (G << 1) | bit_buf[30 + i]; for (int i = 0; i < 6; i++) B = (B << 1) | bit_buf[36 + i]; // Extra 6-bit fields — map to what your protocol spec says for (int f = 0; f < 4 && bit_count >= 42 + (f + 1) * 6; f++) for (int i = 0; i < 6; i++) extra[f] = (extra[f] << 1) | bit_buf[42 + f * 6 + i]; #if OUTPUT_BINARY // Binary packet: 0xAA sync, then packed fields // Total: 1 + 2 + 2 + 1 + 1 + 1 + 4 + 1 = 13 bytes uint8_t pkt[13]; pkt[0] = 0xAA; // sync byte pkt[1] = (X >> 8) & 0x0F; // X high nibble pkt[2] = X & 0xFF; // X low byte pkt[3] = (Y >> 8) & 0x0F; // Y high nibble pkt[4] = Y & 0xFF; // Y low byte pkt[5] = R; pkt[6] = G; pkt[7] = B; pkt[8] = extra[0]; pkt[9] = extra[1]; pkt[10] = extra[2]; pkt[11] = extra[3]; // Simple checksum uint8_t chk = 0; for (int i = 0; i < 12; i++) chk ^= pkt[i]; pkt[12] = chk; Serial.write(pkt, 13); #else // ASCII CSV: X,Y,R,G,B,E0,E1,E2,E3\n Serial.print(X); Serial.print(','); Serial.print(Y); Serial.print(','); Serial.print(R); Serial.print(','); Serial.print(G); Serial.print(','); Serial.print(B); Serial.print(','); Serial.print(extra[0]); Serial.print(','); Serial.print(extra[1]); Serial.print(','); Serial.print(extra[2]); Serial.print(','); Serial.print(extra[3]); Serial.print('\n'); #endif } // ============================================================ // Arduino setup / loop // ============================================================ void setup() { Serial.begin(BAUD); pinMode(DATA_PIN, INPUT); // Teensy 4.x: make sure we have enough resolution in pulseIn // pulseIn uses the ARM cycle counter — sub-microsecond resolution } void loop() { // ---- Wait for the next LOW pulse (start of a bit) ---- // The line sits HIGH during inter-frame gaps. // When it goes LOW we start timing. // Time the LOW pulse unsigned long low_us = pulseIn(DATA_PIN, LOW, 50); // 50us timeout if (low_us == 0) { // Timeout — line has been stable too long, reset bit_count = 0; in_frame = false; return; } if (low_us < (unsigned long)(LOW_MIN_US * 1000)) { // Glitch / noise — ignore return; } // Time the HIGH pulse that follows unsigned long high_us = pulseIn(DATA_PIN, HIGH, 50); if (high_us == 0) { bit_count = 0; in_frame = false; return; } // Inter-frame gap detection if (high_us >= (unsigned long)(GAP_US * 1000)) { // End of frame — emit if we collected enough bits if (bit_count == BITS_PER_FRAME || bit_count >= 42) { emit_frame(); } bit_count = 0; in_frame = true; return; } // Decode bit value from HIGH pulse width // pulseIn returns microseconds; our threshold is ~0.38us = 380ns // pulseIn resolution on Teensy 4.x ≈ 10–20ns, so this works fine if (!in_frame) return; // skip until we've seen a gap boundary if (bit_count < BITS_PER_FRAME) { bit_buf[bit_count++] = (high_us >= (unsigned long)(HIGH_THR_US * 1000)) ? 1 : 0; } } // ============================================================ // ADVANCED VERSION: Use interrupts for higher accuracy // Uncomment this block and remove the loop() above if you // find pulseIn() misses bits at high frame rates (~14.6 kHz). // ============================================================ /* #include volatile uint32_t rise_time = 0; volatile uint32_t fall_time = 0; volatile bool got_bit = false; volatile uint8_t pending_bit = 0; void data_isr() { uint32_t now = ARM_DWT_CYCCNT; // Teensy 4.x cycle counter (600MHz) if (digitalReadFast(DATA_PIN) == LOW) { // Falling edge — start of LOW pulse fall_time = now; } else { // Rising edge — end of LOW pulse, start of HIGH pulse // Measure HIGH will be done at the NEXT falling edge rise_time = now; } } void setup() { Serial.begin(BAUD); pinMode(DATA_PIN, INPUT); ARM_DEMCR |= ARM_DEMCR_TRCENA; ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; attachInterrupt(digitalPinToInterrupt(DATA_PIN), data_isr, CHANGE); } void loop() { // Processing happens in ISR context via a ring buffer in a // full implementation — see Teensy forum for examples } */