├── clean.bat ├── Makefile ├── .gitattributes ├── .gitignore ├── README.md └── main.c /clean.bat: -------------------------------------------------------------------------------- 1 | del *.hex 2 | del *.o 3 | del main -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GCC=avr-gcc -mmcu=atmega328p -std=gnu99 2 | FLAGS=-Os -DF_CPU=16000000UL 3 | OBJCOPY=avr-objcopy -O ihex -R .eeprom 4 | 5 | AVRDUDE=avrdude -c arduino -b57600 -p ATMEGA328P -P COM5 -U flash:w: 6 | 7 | all: 8 | $(GCC) $(FLAGS) -c -o main.o main.c 9 | $(GCC) main.o -o main 10 | $(OBJCOPY) main main.hex 11 | 12 | flash: 13 | $(AVRDUDE)main.hex 14 | 15 | clean: 16 | rm *.hex 17 | rm *.o 18 | rm main -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fully Sik DRLs 2 | ## Description 3 | This repo is the control logic for an extemely cheap WS2812B strip of LED lights I've mounted as daytime running lights (DRLs) on the front grille of my car. The code is written for an ATMega328P (currently an Arduino Nano) in AVR C. 4 | Being fully addressable, the daytime running lights also serve the purpose of acting as sequential turn signals, similar to those on the new Audi A6. For legal (and courtesy) reasons the lights also have to dim when the car's park lights are activated. 5 | 6 | ### Operation Video 7 | [![Video of the lights in action.](http://i.imgur.com/K8r4bpJ.png)](https://youtu.be/KnyYZ7LNsUY) 8 | 9 | 10 | ## Details 11 | - As a challenge, the code doesn't store the state of the LEDs as a bitmap, but makes each frame on the fly. Wanted to practice avoiding RAM use. 12 | - The unclean 12V (fluctuating due to alternator) from the car had to serve as digital inputs to the Arduino. A simple zener diode was used to drop each input because it was an inexpensive method, and it didn't draw too much as it was driven unfused directly by the Body Control Module (expensive!). 13 | - The indicators had to be able to cancel their animation as soon as the main indicator turned off, otherwise an accidental indication could display on the strip after the main indicator had turned off. This would also eliminate any out of sync flashing issues. 14 | - For some reason, WS2812 lights emit a very bluish (sort of like 8000K) light when powered with 5.2V at RGB(255,255,255). This had to be tweaked to be natural white (about 6000K) during the day, and match the halogens (about 4300K) during the night. 15 | - The strip also works as hazard lights. 16 | - A startup animation was added just before installation of the lights. I have no idea about the legality of this. 17 | 18 | ## References 19 | Due to hearing about timing issues in communicating with WS2812Bs, this code uses a slightly modified version of [bigjosh2's](https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/) code for communicating with the LED strip. 20 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /* 9 | Actual driving logic, and assembler instructions forked as they worked very well on long strings 10 | More info at http://wp.josh.com/2014/05/11/ws2812-neopixels-made-easy/ 11 | */ 12 | 13 | #define PIXELS 56 // Number of pixels in the string 14 | 15 | #define PIXEL_PORT PORTD // Port of the pin the pixels are connected to 16 | #define PIXEL_DDR DDRD // Port of the pin the pixels are connected to 17 | #define PIXEL_BIT 2 // Bit of the pin the pixels are connected to 18 | 19 | // These are the timing constraints taken mostly from the WS2812 datasheets 20 | // These are chosen to be conservative and avoid problems rather than for maximum throughput 21 | 22 | #define T1H 900 // Width of a 1 bit in ns 23 | #define T1L 600 // Width of a 1 bit in ns 24 | 25 | #define T0H 400 // Width of a 0 bit in ns 26 | #define T0L 900 // Width of a 0 bit in ns 27 | 28 | #define RES 6000 // Width of the low gap between bits to cause a frame to latch 29 | 30 | // Here are some convience defines for using nanoseconds specs to generate actual CPU delays 31 | 32 | #define NS_PER_SEC (1000000000L) // Note that this has to be SIGNED since we want to be able to check for negative values of derivatives 33 | 34 | #define CYCLES_PER_SEC (F_CPU) 35 | 36 | #define NS_PER_CYCLE ( NS_PER_SEC / CYCLES_PER_SEC ) 37 | 38 | #define NS_TO_CYCLES(n) ( (n) / NS_PER_CYCLE ) 39 | 40 | #define STATE_NONE 0 41 | #define STATE_LEFT 1 42 | #define STATE_RIGHT 2 43 | #define STATE_HAZARD 3 44 | 45 | #define LEVEL_HIGH 0 46 | #define LEVEL_LOW 1 47 | 48 | #define COLOUR_SIGNAL (struct RGB) {gammaTable[255], gammaTable[120], gammaTable[0]} 49 | #define COLOUR_DAY (struct RGB) {gammaTable[255],gammaTable[200],gammaTable[180]} 50 | #define COLOUR_DAY_DIM (struct RGB) {gammaTable[127],gammaTable[100],gammaTable[90]} 51 | //#define COLOUR_NIGHT (struct RGB) {gammaTable[255],gammaTable[170],gammaTable[130]} 52 | #define COLOUR_NIGHT (struct RGB) {gammaTable[191],gammaTable[128],gammaTable[98]} 53 | #define COLOUR_NIGHT_DIM (struct RGB) {gammaTable[127],gammaTable[85],gammaTable[65]} 54 | #define COLOUR_BLACK (struct RGB) {gammaTable[0],gammaTable[0],gammaTable[0]} 55 | 56 | #define PIN_PORT PORTC 57 | #define PIN_PIN PINC 58 | #define PIN_DDR DDRC 59 | #define PIN_LEFT 0 60 | #define PIN_RIGHT 1 61 | #define PIN_LOW 2 62 | 63 | 64 | #define TIME_ANIMATION 15000 65 | #define TIME_STARTUP 60000 66 | 67 | struct RGB{ 68 | uint8_t r; 69 | uint8_t g; 70 | uint8_t b; 71 | }; 72 | 73 | 74 | const uint8_t gammaTable[] PROGMEM = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 75 | 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 76 | 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 77 | 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 78 | 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 79 | 19, 19, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 27, 28, 80 | 29, 29, 30, 31, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 81 | 40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 82 | 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 83 | 71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 88, 89, 84 | 90, 91, 93, 94, 95, 96, 98, 99,100,102,103,104,106,107,109,110, 85 | 111,113,114,116,117,119,120,121,123,124,126,128,129,131,132,134, 86 | 135,137,138,140,142,143,145,146,148,150,151,153,155,157,158,160, 87 | 162,163,165,167,169,170,172,174,176,178,179,181,183,185,187,189, 88 | 191,193,194,196,198,200,202,204,206,208,210,212,214,216,218,220, 89 | 222,224,227,229,231,233,235,237,239,241,244,246,248,250,252,255}; 90 | 91 | 92 | // Actually send a bit to the string. We must to drop to asm to enusre that the complier does 93 | // not reorder things and make it so the delay happens in the wrong place. 94 | 95 | uint8_t state = STATE_NONE; 96 | uint8_t level = LEVEL_HIGH; 97 | uint8_t overflows = 0; 98 | 99 | bool startup = true; 100 | bool overflowed = false; 101 | 102 | inline void sendBit(unsigned char bitVal) { 103 | cli(); 104 | 105 | if ( bitVal ) { // 0 bit 106 | 107 | asm volatile ( 108 | "sbi %[port], %[bit] \n\t" // Set the output bit 109 | ".rept %[onCycles] \n\t" // Execute NOPs to delay exactly the specified number of cycles 110 | "nop \n\t" 111 | ".endr \n\t" 112 | "cbi %[port], %[bit] \n\t" // Clear the output bit 113 | ".rept %[offCycles] \n\t" // Execute NOPs to delay exactly the specified number of cycles 114 | "nop \n\t" 115 | ".endr \n\t" 116 | :: 117 | [port] "I" (_SFR_IO_ADDR(PIXEL_PORT)), 118 | [bit] "I" (PIXEL_BIT), 119 | [onCycles] "I" (NS_TO_CYCLES(T1H) - 2), // 1-bit width less overhead for the actual bit setting, note that this delay could be longer and everything would still work 120 | [offCycles] "I" (NS_TO_CYCLES(T1L) - 2) // Minimum interbit delay. Note that we probably don't need this at all since the loop overhead will be enough, but here for correctness 121 | 122 | ); 123 | 124 | } else { // 1 bit 125 | 126 | // ************************************************************************** 127 | // This line is really the only tight goldilocks timing in the whole program! 128 | // ************************************************************************** 129 | 130 | 131 | asm volatile ( 132 | "sbi %[port], %[bit] \n\t" // Set the output bit 133 | ".rept %[onCycles] \n\t" // Now timing actually matters. The 0-bit must be long enough to be detected but not too long or it will be a 1-bit 134 | "nop \n\t" // Execute NOPs to delay exactly the specified number of cycles 135 | ".endr \n\t" 136 | "cbi %[port], %[bit] \n\t" // Clear the output bit 137 | ".rept %[offCycles] \n\t" // Execute NOPs to delay exactly the specified number of cycles 138 | "nop \n\t" 139 | ".endr \n\t" 140 | :: 141 | [port] "I" (_SFR_IO_ADDR(PIXEL_PORT)), 142 | [bit] "I" (PIXEL_BIT), 143 | [onCycles] "I" (NS_TO_CYCLES(T0H) - 2), 144 | [offCycles] "I" (NS_TO_CYCLES(T0L) - 2) 145 | 146 | ); 147 | 148 | } 149 | 150 | // Note that the inter-bit gap can be as long as you want as long as it doesn't exceed the 5us reset timeout (which is A long time) 151 | // Here I have been generous and not tried to squeeze the gap tight but instead erred on the side of lots of extra time. 152 | // This has thenice side effect of avoid glitches on very long strings becuase 153 | 154 | sei(); 155 | } 156 | 157 | 158 | inline void sendByte( unsigned char byte ) { 159 | for( unsigned char bit = 0 ; bit < 8 ; bit++ ) { 160 | sendBit((byte >> ( 7 - bit )) & 0x01); // Neopixel wants bit in highest-to-lowest order 161 | } 162 | } 163 | 164 | void setup() { 165 | 166 | TCCR1B |= _BV(CS12); 167 | TIMSK1 |= _BV(0); //TOIE Timer1 overflow interrupt enable 168 | 169 | 170 | PIXEL_DDR |= _BV(PIXEL_BIT); //enable output on 171 | 172 | PIN_DDR = 0x00; //all inputs on the indicator detecting io 173 | PIN_PORT = 0x00; //no pullup resistors, the circuit already pulls to ground if 12V not applied 174 | 175 | sei(); 176 | 177 | } 178 | 179 | inline void sendPixel(struct RGB colour) { 180 | // Just wait long enough without sending any bits to cause the pixels to latch and display the last sent frame 181 | 182 | sendByte(colour.g); // Neopixel wants colours in green then red then blue order 183 | sendByte(colour.r); 184 | sendByte(colour.b); 185 | 186 | } 187 | 188 | 189 | void show() { 190 | _delay_us( (RES / 1000UL) + 1); // Round up since the delay must be _at_least_ this long (too short might not work, too long not a problem) 191 | } 192 | 193 | void showColour(struct RGB colour) { 194 | for( int p=0; p> PIN_LEFT) & 0x01); 212 | bool rightPin = ((PIN_PIN >> PIN_RIGHT) & 0x01); 213 | 214 | if (((PIN_PIN >> PIN_LOW) & 0x01)) level = LEVEL_LOW; 215 | else level = LEVEL_HIGH; 216 | 217 | if (rightPin && leftPin) { 218 | state = STATE_HAZARD; 219 | } else if (leftPin) { 220 | state = STATE_LEFT; 221 | } else if (rightPin) { 222 | state = STATE_RIGHT; 223 | } else { 224 | state = STATE_NONE; 225 | } 226 | 227 | if (state != oldState) { //reset timer used for animations 228 | cli(); 229 | TCNT1 = 0; 230 | TIFR1 |= _BV(0); //TOV clear overflow flag 231 | overflowed = false; 232 | sei(); 233 | } 234 | 235 | 236 | 237 | struct RGB whiteColour = level == LEVEL_LOW ? COLOUR_NIGHT : COLOUR_DAY; 238 | unsigned int count = TCNT1; 239 | 240 | if (state == STATE_NONE) { //solid white display 241 | if (overflowed || count >= TIME_STARTUP) { 242 | startup = false; //disable the startup animation once it's completed 243 | } 244 | if (!startup) { 245 | showColour(whiteColour); 246 | } else { 247 | unsigned int animation = (count / (TIME_STARTUP/70)); //70 frames in the startup animation 248 | uint8_t p = 0; //pixel counter for loop 249 | struct RGB dimColour = level == LEVEL_LOW ? COLOUR_NIGHT_DIM : COLOUR_DAY_DIM; 250 | for (p=0; p<56; p++) { //56 pixel animation hardcoded, logic done on each pixel keeping animation frame in mind 251 | int symmP = p > 27 ? 28 - (p - 27) : p; //distance from the closest edge 252 | if (animation < 28) { //two single dots meeting at the middle 253 | if (symmP == animation) sendPixel(dimColour); 254 | else sendPixel(COLOUR_BLACK); 255 | } else { //once they've met 256 | int dist = 27 - symmP; //distance from the center 257 | if (animation >= 42 && dist <= (animation - 42)) { //second the dim trail starts growing to full brightness from center 258 | sendPixel(whiteColour); 259 | } else if (dist <= (animation - 28)) { // first the two dots meeting bounce and grow outward leaving a dim trail 260 | sendPixel(dimColour); 261 | } else { //blank pixel if the animation isn't upto stage yet 262 | sendPixel(COLOUR_BLACK); 263 | } 264 | 265 | } 266 | } 267 | 268 | } 269 | } else { 270 | startup = false; 271 | uint8_t animateLeft = (state == STATE_HAZARD) || (state == STATE_LEFT); 272 | uint8_t animateRight = (state == STATE_HAZARD) || (state == STATE_RIGHT); 273 | 274 | //animation for the datsun style growing indicator/turn signals 275 | unsigned int animation = 0; 276 | if (overflowed || count >= TIME_ANIMATION) animation = 9; 277 | else { 278 | animation = (count / (TIME_ANIMATION/10)); 279 | } 280 | 281 | uint8_t p = 0; 282 | 283 | for (p = 0; p < 3; p++) { 284 | sendPixel(whiteColour); 285 | } 286 | 287 | for (p = 0; p < 10; p++) { 288 | if (animateRight && (p >= (9 - animation))) sendPixel(COLOUR_SIGNAL); 289 | else sendPixel(whiteColour); 290 | } 291 | 292 | 293 | for (p = 0; p < 30; p++) { 294 | sendPixel(whiteColour); 295 | } 296 | 297 | 298 | for (p = 0; p < 10; p++) { 299 | if (animateLeft && (p <= animation)) sendPixel(COLOUR_SIGNAL); 300 | else sendPixel(whiteColour); 301 | } 302 | 303 | for (p = 0; p < 3; p++) { 304 | sendPixel(whiteColour); 305 | } 306 | 307 | 308 | 309 | } 310 | 311 | show(); 312 | } 313 | } --------------------------------------------------------------------------------