SevIQ
Labs / IR XOR Halo

IR XOR Halo Lab

Break a virtual beam, flip alternating LEDs with a 12-bit XOR mask, then compare direct AVR register code with workshop-friendly Arduino sketches.

12LED outputs
0b1010mask rhythm
2 pathsAVR + Arduino

IR detector -> XOR LED halo

Trigger the simulated sensor to apply mask ^= 0b101010101010. The ring holds for the selected dwell time, then clears by applying the same XOR pattern again.

Duration: 2.00s
Status: Idle
mask ^= pattern -> 0b000000000000

Bare-metal AVR

Direct register writes on ATmega-class MCUs keep the control loop explicit: port direction, pull-ups, input state, mask flips, and dwell timing all stay visible.

1 - 12-bit halo register

XOR the even LEDs into a ring mask, map straight into PORTB/PORTA, and keep JTAG off the pins.

#include <avr/io.h>
#include <util/delay.h>

#define HALO_PATTERN 0b101010101010

static inline void drive_ring(uint16_t mask) {
    PORTB = mask & 0xFF;          // PB0..PB7
    PORTA = (mask >> 8) & 0x0F;   // PA0..PA3
}

int main(void) {
    MCUSR |= (1 << JTD);
    MCUSR |= (1 << JTD);
    DDRB = 0xFF;
    DDRA = 0x0F;
    DDRD = 0xFF;

    uint16_t ring_mask = 0;
    for (;;) {
        ring_mask ^= HALO_PATTERN;
        drive_ring(ring_mask);
        _delay_ms(300);
    }
}

2 - IR latch

A beam break stores the XOR pattern, holds it for N cycles, then drops it without interrupts or framework state.

#include <avr/io.h>
#include <util/delay.h>

#define HALO_PATTERN 0b101010101010
#define HOLD_TICKS   12

static inline void setup_ports(void) {
  MCUSR |= (1 << JTD);
  MCUSR |= (1 << JTD);
  DDRB = 0xFF;
  DDRA = 0x0F;
  DDRC &= ~(1 << PC0);
  PORTC |= (1 << PC0);
}

static inline uint8_t ir_triggered(void) {
  return !(PINC & (1 << PC0));
}

static inline void pulse(uint16_t *mask) {
  *mask ^= HALO_PATTERN;
  PORTB = *mask & 0xFF;
  PORTA = (*mask >> 8) & 0x0F;
}

int main(void) {
  uint16_t ring_mask = 0;
  uint8_t hold = 0;
  setup_ports();

  for (;;) {
    if (ir_triggered()) {
      pulse(&ring_mask);
      hold = HOLD_TICKS;
    }
    if (hold) {
      _delay_ms(50);
      if (--hold == 0) pulse(&ring_mask);
    }
  }
}

3 - Minimal XOR pulse

Strip the idea to one port and one moving bit so the toggle operation is impossible to miss.

int main(void) {
    DDRB = 0xFF;
    uint8_t idx = 0;

    for (;;) {
        PORTB ^= (1 << idx);
        _delay_ms(200);
        if (++idx > 7) idx = 0;
    }
}

Arduino translations

The same mask logic in setup() and loop(), useful for workshops and fast bench testing without hiding the control flow.

1 - Halo mask with pins

Map 12 GPIOs, flip alternating bits with XOR, and write the mask with plain digitalWrite().

const uint8_t ringPins[] = {2,3,4,5,6,7,8,9,10,11,12,13};
const uint8_t RING_COUNT = sizeof(ringPins) / sizeof(ringPins[0]);
const uint16_t HALO_PATTERN = 0b101010101010;

uint16_t ringMask = 0;

void setup() {
  for (uint8_t i = 0; i < RING_COUNT; ++i) {
    pinMode(ringPins[i], OUTPUT);
    digitalWrite(ringPins[i], LOW);
  }
}

void writeRing(uint16_t mask) {
  for (uint8_t i = 0; i < RING_COUNT; ++i) {
    digitalWrite(ringPins[i], (mask >> i) & 0x01);
  }
}

void loop() {
  ringMask ^= HALO_PATTERN;
  writeRing(ringMask);
  delay(300);
}

2 - IR hold with millis()

Debounce the detector, latch the pattern for a fixed dwell, and drop back when time expires.

const uint8_t ringPins[] = {2,3,4,5,6,7,8,9,10,11,12,13};
const uint8_t RING_COUNT = sizeof(ringPins) / sizeof(ringPins[0]);
const uint8_t irPin = A0;
const uint16_t HALO_PATTERN = 0b101010101010;
const uint16_t HOLD_MS = 600;

uint16_t ringMask = 0;
unsigned long holdUntil = 0;

void setup() {
  pinMode(irPin, INPUT_PULLUP);
  for (uint8_t i = 0; i < RING_COUNT; ++i) {
    pinMode(ringPins[i], OUTPUT);
    digitalWrite(ringPins[i], LOW);
  }
}

void writeRing(uint16_t mask) {
  for (uint8_t i = 0; i < RING_COUNT; ++i) {
    digitalWrite(ringPins[i], (mask >> i) & 0x01);
  }
}

bool irTriggered() {
  return digitalRead(irPin) == LOW;
}

void pulse() {
  ringMask ^= HALO_PATTERN;
  writeRing(ringMask);
}

void loop() {
  const unsigned long now = millis();

  if (irTriggered()) {
    pulse();
    holdUntil = now + HOLD_MS;
    delay(30);
  }
  if (holdUntil && now > holdUntil) {
    pulse();
    holdUntil = 0;
  }
}

3 - Basic blink

A single LED proving ground for the smallest possible toggle loop.

const uint8_t ledPin = LED_BUILTIN;
bool state = false;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  state = !state;
  digitalWrite(ledPin, state);
  delay(200);
}