Halo Style Arduino Controlled LED Tail Lights Schematics and Source - My 1976 Mazda RX-5 Cosmo Restoration


Home > My 1976 Mazda RX-5 Cosmo Restoration > Halo Style Arduino Controlled LED Tail Lights Schematics and Source

Episode 41 of my 1976 Mazda RX-5 Cosmo restoration covers the conversion of the old style incandescent tail lights to more modern style LED tail lights. I patterned my lights after those of the 2012 Dodge Charger. Like the Charger lights, my design consists of an outline, the "halo", made of large 10MM LEDs, with the inner light section, the "field", made of 5MM LEDs. These two light sections are controlled by an Arduino ATTINY85 microcontroller providing independent PWM based brightness control of each LED array.

Part 41: LED Tail Light Conversion, Part 1

View In Popup

View On YouTube

Part 42: LED Tail Light Conversion, Part 2

View In Popup

View On YouTube

Because presenting circuit diagrams and source code is, well, rather boring and difficult on video I've made this page for those who are curious of the details not given on camera. I'll update this page with changes as the next episode is released as well.

The Arduino ATTINY85 based dual PWM controller was the first circuit presented in the video.

ATTINY85 Based Dual PWM LED Controller

This simple circuit is used to control both the brake and signal lights, triggering the field and halo LED arrays as well as controlling their brightness. The only difference between the brake light control and signal control is in the software with a switch set at compile time. There are two controllers used per tail light side; one for the brake lights, one for the signal light.

Schematic

ATTINY85 PWM taillight controller schematic

Most of the functionality of this controller is in the software, so only the components needed to drive the LEDs and accept inputs from the car are needed, along with some power supply stuff.

The power supply is based around a 78L05 low current linear regulator. 12V from the car first has some basic protections applied to control surges and spikes, as well as protect against reverse polarity. First, the ERZ-V20D220 MOV has a clamping voltage of 20V, shunting anything over 20V to ground. Incidentally this is the same MOV as used on the MegaSquirt V3 board. The two 1N4007 diodes provide clamping and reverse polarity protection. The reverse bias diode in parallel with the MOV allows low voltage reverse spikes (anything larger than 20V is clamped by the MOV) to pass through from ground to 12V. It also has the side effect of (hopefully) blowing the supply fuse if the circuit is connected backwards. The series 1N4007 guards against current flow in the reverse direction if, once again, the circuit is connected backwards. Capacitors on either side of the 78L05 provide filtering/stabilization and are exactly as suggested by the component datasheet (PDF), no magic here.

The circuit uses an input section consisting of two identical parts, and an output section once again of two identical parts. A 4N35 opto-isolator has its LED side connected to either the brake or running light wire through a 1K limiting resistor. 1K is a fairly large value but the LED inside the opto requires very little current to operate. When the vehicle supplies 12V to turn on the light, the LED inside the opto shines on the phototransistor, causing it to conduct. The input of the ATTINY85 is normally held to about 5V by the 4.7K resistor, keeping it high. When the transistor conducts this pulls current away from the input, causing it to drop to about 0V and thus go low. The firmware running on the ATTINY85 then decides what to do with the input.

Control of the LEDs is achieved by using IRLU3410 logic level MOSFETs. When the ATTINY85 decides to illuminate the LEDs, it sends out a PWM signal on the appropriate output. That signal drives the gate of the MOSFET, causing it to conduct. By varying the pulse width of the signal the Tiny controls the on-time of the MOSFET, thus the on-time of the LEDs, and therefore the LED brightness. The 1M resistor from gate to ground bleeds current from the very high impedance gate preventing the MOSFET from conducting due to stray/leakage voltage, and quickly shunting to ground its gate capacitance when the output of the ATTINY85 is brought low making sure the MOSFET switches off immediately. When the MOSFET conducts, it allows current to flow from the 12V source (switched 12V from the car), through the LEDs, to ground. The number and configuration of the LEDs depend on the light the controller is for, as does the value of R, the dropping resistor. The IRLU3410 is good for about 10A at 12V, more when PWM is used, so quite a lot of LEDs could be driven.

Note that "power" and "logic" grounds are separated. While there is obviously continuity between them, the power ground is kept at the bottom of the circuit board and anything on the 12V side grounds to it. Logic ground is only after the regulator and ran on the board directly to the components around the Arduino.

Source Code

Most of the work of the controller is done by the software. You can see above the circuit takes inputs from the vehicle headlight circuit and the running light circuit. Depending on the state of the two inputs, the software decides which light segment to illuminate (halo only, both halo and field) and how bright. With running lights on, the halo is illuminated. If the brake is then applied, the halo is brightened and the field LEDs are illuminated. If the running lights are off and the brake light activated, then both the halo and field are turned on. For added awesomeness, the software runs through a simple "opening celebration" when first booted which fades the halo up, the field up, then fades them down in reverse order. Obviously just a fancy way to verify the tail lights are functioning, right?

A software switch called IsSignalLight is set before the code is burned to the Arduino if the turn signal controller is being programmed. Setting this to true has the effect of altering the behavior of the controller to ignore the running light input and just switch the halo and field on in sequence when the field input is triggered. When the field trigger is removed, the LEDs will deluminate in opposite sequence.

The source code is presented below. I'd expect this preliminary code to change once the lights are installed on the Cosmo. Things like brightness will need to be adjusted, probably to avoid blinding those behind the car.

//Cosmo Marker/Brake Light Control Version 2.0
//Aaron Cake, Copyright © 2015
//http://www.aaroncake.net/
//http://www.aaroncake.net/76cosmo/taillights

//Rights to modify, distribute, re-use are granted as long as credit to original author remains

//Change log
// 1.0 - Initial version
// 1.1 - Moved trigger pin status read to once at beginning of loop, instead of inside each IF statement. 
// 2.0 - Added boolean condition IsSignalLight to determine if code is running signal lights. If true, changes main loop code to ignore halo trigger
//       and switch both arrays in a pre-determined pattern.


//Software runs on ATTINY85 and controls two groups of LEDs by PWM'ing MOSFETs on pins 0 and 1. Input triggers are pin A1 and A2, isloated
// via opto isolators. A1 and A2 are pulled low when active. LEDs form two groups, the "halo" which surrounds the main light area and the 
// "field" which makes up the brake light area inside the halo. Halo is used as a marker light active when headlights are turned on (triggerd
// by 12V headlight signal at optoisolator (A1) and kept at a nominal brightness by PWM. When 12V brake signal applied to optoisolator on
// pin A2, brake lights are triggered at a nominal PWM. Some PWM time is added to halo to increase brightness. Halo is always triggered with
// brake application at increased brightness regardless of whether market lights are on or off.
// If the variable IsSignalLight is set to true, there is a conditional statement in the main loop to ignore the brake light related code and
// run the signal light code. The signal code ignores the halo input and only switches based on the field input. When that input is triggered,
// the code just illuminates the halo, delays a configurable amount, then lights the field. Upon trigger removal, the opposite happens.


// first set up all the variables needed
const int OptoTriggerThreshold = 500; //Opto threshold. Pin is held high by pullup. Trigger drops to about 0V when optoisolator activated

// Pin assignments for ATTINY85. Uncomment to burn to ATTINY85 for production use
// const int HaloOutPin = 0; // LED halo output pin
// const int FieldOutPin = 1; // LED field output pin
// const int HaloTriggerPin = A1; //Opto pin input for halo
// const int FieldTriggerPin = A2; //Opto pin input for field


// Pin assignments for Arduino UNO R3. Uncomment for development
const int HaloOutPin = 3; // LED halo output pin
const int FieldOutPin = 9; // LED field output pin
const int HaloTriggerPin = A0; //Opto pin input for halo
const int FieldTriggerPin = A1; //Opto pin input for field


const int HaloOffBrightness = 0; // PWM brightness values for LEDs turned off, generally zero :-)
const int FieldOffBrightness = 0;
const int HaloOnBrightness = 50; // Activated PWM brightness values for LEDs, max 255
const int FieldOnBrightness = 50;
const int HaloBrakeBrightnessAdder = 50; // Value added to halo PWM brightness when brake activated
const int MaxCelebrationBrightness = 255; // Maximum brightness for LEDs used in opening celebration, max 255
const int LoopFadeDelay = 10; // Delay for each loop iteration in fade to slow down for human eye, mS
const int FadeStep = 5; // Increment step value for LED fades
const int SignalStageDelay = 250; //Delay between illuminating and deluminating the halo and field when in turn signal mode, in mS

//determines if the code is going to control brake lights or signal lights. Set to TRUE if being burned to signal controller. This changes the 
//behaviour of the outputs to ignore the halo input and instead illuminate and deluminate the two LED arrays in a specific pattern.
const boolean IsSignalLight = true;

int HaloTriggerPinStatus = 1000; // Status of the halo trigger pin. Set at 1000 as pin is pulled low on trigger, so this is "off"
int FieldTriggerPinStatus = 1000; // Same as HaloTriggerPin

boolean SignalLightOn = false; // To track whether the signal light is activated

// function to fade up LED on FadeOutputPin. Parameters fairly self explanitory. Max 255 on FadeEndValue, FadeDelayValue in mS
void FadeUpLED (int FadeOutputPin, int FadeStartValue, int FadeEndValue, int FadeStepValue, int FadeDelayValue) {
  for (int OutputPWM = FadeStartValue; OutputPWM <= FadeEndValue; OutputPWM += FadeStepValue) {
    analogWrite(FadeOutputPin, OutputPWM);
    delay(FadeDelayValue);
    // The fade on power up is cool, but all this fancy-pancy stuff needs to stop immediately if the brake or marker light
    // is turned on during to resume normal brake/marker light operation. Check halo and field trigger pins for low signal
    // and bomb out of for loop if detected
    if (analogRead(HaloTriggerPin) < OptoTriggerThreshold || analogRead(FieldTriggerPin) < OptoTriggerThreshold) {
      break;
    }
  }
}

// function to fade down LED on FadeOutputPin. Parameters fairly self explanitory. Max 255 on FadeEndValue, FadeDelayValue in mS
void FadeDownLED (int FadeOutputPin, int FadeStartValue, int FadeEndValue, int FadeStepValue, int FadeDelayValue) {
  for (int OutputPWM = FadeStartValue; OutputPWM >= FadeEndValue; OutputPWM -= FadeStepValue) {
    analogWrite(FadeOutputPin, OutputPWM);
    delay(FadeDelayValue);
    // Stop fade if brake light or marker light is activated, exiting loop
    if (analogRead(HaloTriggerPin) < OptoTriggerThreshold || analogRead(FieldTriggerPin) < OptoTriggerThreshold) {
      break;
    }
  }
}

void setup()
{
  pinMode(HaloOutPin, OUTPUT);
  pinMode(HaloTriggerPin, INPUT);
  pinMode(FieldOutPin, OUTPUT);
  pinMode(FieldTriggerPin, INPUT);

  //Debug: Give two blinks for OK to go
//  digitalWrite(HaloOutPin, HIGH);
//  delay (200);
//  digitalWrite(HaloOutPin, LOW);
//  delay (200);
//  digitalWrite(HaloOutPin, HIGH);
//  delay (200);
//  digitalWrite(HaloOutPin, LOW);
//  digitalWrite(FieldOutPin, HIGH);
//  delay (200);
//  digitalWrite(FieldOutPin, LOW);
//  delay (200);
//  digitalWrite(FieldOutPin, HIGH);
//  delay (200);
//  digitalWrite(FieldOutPin, LOW);
//  delay(2000);

   //Fade the halo on, then the field on for opening celebration. Looks cool.
  FadeUpLED(HaloOutPin,HaloOffBrightness,MaxCelebrationBrightness,FadeStep,LoopFadeDelay);
  FadeUpLED(FieldOutPin,FieldOffBrightness,MaxCelebrationBrightness,FadeStep,LoopFadeDelay);

  //Then fade them down in reverse order
  FadeDownLED(FieldOutPin,MaxCelebrationBrightness,FieldOffBrightness,FadeStep,LoopFadeDelay);
  FadeDownLED(HaloOutPin,MaxCelebrationBrightness,HaloOffBrightness,FadeStep,LoopFadeDelay);

}


void loop()
{

  //Read status of Halo and Field trigger pins and load into status variable. Best to do this once per loop and access
  // status via variable instead of a bunch of analogReads as analogRead is very CPU costly and introduces a noticable
  // flicker into the PWM loop
  HaloTriggerPinStatus = analogRead(HaloTriggerPin);
  FieldTriggerPinStatus = analogRead(FieldTriggerPin);

 //if IsSignalLight is false then go into brake light control mode
 if (IsSignalLight == false) 
 {
   // Control the halo depending on whether marker lights, brake lights, or marker & brake lights on

   // Brake lights and marker lights on. Halo is on and add brightness to it
   if (HaloTriggerPinStatus < OptoTriggerThreshold && FieldTriggerPinStatus < OptoTriggerThreshold)
  {
    analogWrite(HaloOutPin, HaloOnBrightness + HaloBrakeBrightnessAdder);
  }    //Marker lights on, brake lights off. Halo is on at normal brightness
  else if (HaloTriggerPinStatus < OptoTriggerThreshold && FieldTriggerPinStatus > OptoTriggerThreshold)
  {
    analogWrite(HaloOutPin, HaloOnBrightness);
  }  //Marker lights off, brake lights on. Halo is on and add brightness to it.
    else if (HaloTriggerPinStatus > OptoTriggerThreshold && FieldTriggerPinStatus < OptoTriggerThreshold)
  {
    analogWrite(HaloOutPin, HaloOnBrightness + HaloBrakeBrightnessAdder);
  }
  //Marker lights off, brake lights off. Halo is off.
  else if (HaloTriggerPinStatus > OptoTriggerThreshold && FieldTriggerPinStatus > OptoTriggerThreshold)
  {
    analogWrite(HaloOutPin, HaloOffBrightness);
  }


   // The brakes are a lot easier. Just turn them on or off depending on whether the trigger is active
   if (FieldTriggerPinStatus < OptoTriggerThreshold)
  {
    analogWrite(FieldOutPin, FieldOnBrightness);
  }

   if (FieldTriggerPinStatus > OptoTriggerThreshold)
  {
    analogWrite(FieldOutPin, FieldOffBrightness);
  }
}
 
// if IsSignalLight is set to true, then go into signal light mode. Ignore the halo trigger and just control based on field trigger
if (IsSignalLight == true)
{
  //Read the status of the field trigger pin.
  FieldTriggerPinStatus = analogRead(FieldTriggerPin);
  
  // not doing any debouncing here since because flags are used to indicate the status of the lights, and delay is used to form the sequence, any jitter
  // in the input signal is effectively ignored. Also when installed in the car, the lights are going to be controlled by another processor, so if there was
  // bounce in that signal, there are bigger problems!
  
  // if the trigger pin has been pulled low to activate the signal light and the signal lights are turned off (SignalLightOn = F) then go through
  // the turn on sequence
  if (FieldTriggerPinStatus < OptoTriggerThreshold && SignalLightOn == false)
  {
    //turn the halo on
    analogWrite(HaloOutPin, HaloOnBrightness);
    // delay for a little whiile. Yes, delay is evil but do I care the processor can't do anything else for a few hundred mS? Nope.
    delay (SignalStageDelay);
    //turn on the field
    analogWrite(FieldOutPin, FieldOnBrightness);
    // set the flag saying the signal light is on so next time through the loop it doesn't start the sequence again
    SignalLightOn = true;
  }
  
  // if the trigger has been deactivated (pulled high) and the lights are on, do the opposite to turn them off
  if (FieldTriggerPinStatus > OptoTriggerThreshold && SignalLightOn == true)
  {
    //turn the field off
    analogWrite(FieldOutPin, FieldOffBrightness);
    // delay for a little whiile. Yes, delay is evil but do I care the processor can't do anything else for a few hundred mS? Nope.
    delay (SignalStageDelay);
    //turn off the halo
    analogWrite(HaloOutPin, HaloOffBrightness);
    // set the flag saying the signal light is off
    SignalLightOn = false;
  }
  
  
}
 

}



I'd like to think the code is well enough commented that any explanation is already within it. I could be wrong here, but I'd assume the compiler will see the state of IsSignalLight and be smart enough to simply not compile in the side of the If statement which can never occur.

One thing which may be added in the future is a dimming feature which will limit the LED brightness if the marker lights are turned on. The controller for the signal lights still has the input for the marker lights but it is ignored in software. If it turns out that at night the LEDs are dazzling to other drivers then when the headlights are on and thus the marker light signal is present, the PWM can be decreased to run the LEDs a little dimmer. Another option would be to light the LEDs at full brightness then over a 30 second period, gradually dim them. That way they are nice and bright to signal the vehicle is stopping but not so bright to laser the eyes of the driver stopped behind. Still further, the emergency vehicles around here actually flash the brake lights twice when they are applied then light them solidly. Really grabs ones attention and something I am considering implementing.

Printed Circuit Board Patterns

For the most part, the LED boards are fairly straightforward. The reverse light is easiest as a simple arrangement of 12 LEDs, in series pairs, with a dropping resistor for each pair. The brake and signal light boards simply have two LED arrays; the 5MM LEDs making up the field and the 10MM LEDs making up the halo. Again these LEDs are in series groups with a dropping resistor, and each group arranged in parallel within their respective array. The boards are shaped to fit within the tail light housings.

I've created the boards using Copper Connection by Robot Room. It's a very powerful, yet straightforward to use PCB CAD program. Most helpful to me was the feature to print the board for toner transfer etching. It automatically reduces the board to black and white, then reverses it. There is a very popular bird based CAD package out there that requires modifying the printout in a photo editor to make it suitable for toner transfer. Shocking, considering the price of said package. Only disadvantage of Copper Connection is that the free version saves in a proprietary format. You will need Copper Connection if you wish to download, view, modify and/or print these boards. I don't remember why I added pad J1.

Reverse Light PCB Signal Light PCB
Reverse Light PCB Pattern Signal Light PCB Pattern
Download (.ZIP, 4K) Download (.ZIP, 5K)

Outer Brake Light PCB Inner Brake Light PCB
Outer Brake Light PCB Pattern Brake Light PCB Pattern
Download (.ZIP, 30K) Download (.ZIP, 26K)

Probably the only board you'd want to reproduce is the controller board below. Unless of course you too are making LED tail lights for your '76 Mazda Cosmo. But the LED controller can be applied to any tail light if you want control over dual LED arrays. Just keep in mind that the BOM (Bill Of Materials) in the PCB pattern file may not be correct. It's a simple circuit so I didn't put much time into adding all that info to the PCB. Check the above schematic for proper component values. Note during assembly the MOSFETs are mounted on the bottom of the PCB. Remove the center lead and solder the tab to the large copper pad.

Dual PWM LED Controller PCB
Dual PWM LED Controller PCB Pattern
Download (.ZIP, 30K)

Back To Cosmo Page | Mail Me | Search