So it was back to the drawing board for v2.0. My boss wants his traffic light by this week. Told him I can't since I need time to design the circuit and the pcb and write the software and think about how to mount the LED readouts and then finally put everything together. Working for 7-year olds is tough.
Well, writing the firmware was a breeze. The state machine structure lends itself to easy modification. And in the process bulbs went off in my head and I added other features including a new mode wherein the red and amber lights alternate, with the same on-time as the flashing red and flashing amber modes of 0.8sec. And since a seven segment LED display is going to be used anyway why not put it to use even in modes other than normal traffic light mode. So during the flashing red, flashing amber, and alternating red and amber modes the LED will be showing various non numeric characters just to add to the light show. And most importantly I've dumped the use of the green bulb as feedback for the number of seconds for go/stop on-time. The LED now displays "1", "2"... up to "6" to indicate 5sec, 10sec,... 30sec of go/stop on-time. I could easily show the actual number of seconds on the display but I didn't because I think it would be good multiplication practice for the kids. At least they get to know by heart the products of 1 x 5, 2 x 5, ... up to 6 x 5.
Parts list:
PS - AC to DC adapter 12VDC 1000mA output
D1 - 1N5822 Schottky diode
MCU - PIC16F1827 microcontroller
VR - 78L05 voltage regulator
7SEG1, 7SEG2 - seven segment LED display common anode
Q1, Q2 - S9012 PNP transistors
Q3, Q4 - 2N7000 n-channel enhancement MOSFET
Q5, Q6, Q7 - TIP102 NPN Darlington transistor
Q8 - ULN2003 transistor array
My initial design had the LED powered from the 5V rail. This required using an L7805 which has an output capacity of at least 1000mA instead of the 78L05 which can only put out 100mA. Computations showed that because of the large headroom (12V - 5V) and large current draw (around 150mA) the power dissipation of the voltage regulator would raise its temperature close to 100 Celsius and thus necessitate a heatsink. Shudder! A heatsink eats up so much space. So it occurred to me to just power the LED directly off the unregulated supply of 12V. Of course the power dissipation issue would move over to the LED current limiting resistors but that is easily taken care of by employing 1/4-W resistors instead of 1/8-W. Trivial. In the initial 5V design it was a simple matter of using PNP transistors to provide power to the common anode seven segment LEDs. But with the emitter at 12V the base of the PNP can no longer be directly hooked up to the 5-volt MCU. In order to switch the PNP I've opted to use the 2N7000 MOSFET. Using an NPN would've been just as effective but it would've required the addition of a base current limiting resistor. The less components on the board the better. In order for the MOSFET and PNP setup to work properly the base of the PNP must be pulled up to the emitter voltage when the 2N7000 is off, else the base will be left floating. Thus, the 51Kohm resistor. When the 2N7000 is turned off the pull-up resistor brings the base voltage to 12V thus switching off the PNP and cutting supply to the LED display.
I've decided to install two LED displays back-to-back both showing exactly the same readout. Each segment will be provided around 10mA. Because the current draw is double (two LED displays) and because of the 12VDC supply the MCU pins cannot be used to directly sink the current. Therefore a ULN2003 transistor array takes care of the power switching.
The tens and ones digits on the display are multiplexed and switched every 4ms giving a frequency of 1/ (4ms x 2) = 125Hz. That's more than fast enough to provide a flicker-free display.
Because of the additional function calls to update the LED display I've increased the the clock rate from the previous 500kHz to 2MHz. Timer2 tick is still 4ms. A check using the Saleae Logic shows that at 2MHz it usually takes only about half a millisecond to go through all the functions. However, when go/stop on-time is changed the time consumed jumps to over 3ms. Measurement shows the
ComputeRedGreenTime()
function takes 0.50ms. Multiplication does take time but I discovered the king of snails is the EEPROM write. Storing the one byte stop/go on-time takes a whopping 2.66ms. But this is actually better than the datasheet spec of 4.0ms typical and 5.0ms max. Given the half millisecond average (when the user is not selecting a stop/go time), I could actually have retained the 500kHz clock. The savings on power is, however, marginal and negligible compared to what the bulbs and LEDs consume.I'll post a vid and pics of the new and improved traffic light when I finally build the circuit and figure out how to mount the seven segment LED boards on top of the tower and still be able to have access to the screw of the tower's cap and protect the boards from the 7-, 6-, and 4-yr old demotion team!.
/* Kids' Traffic Light with Countdown Timer Using an Industrial Signal Tower Created: January 2012 Processor: PIC 16F1827 Compiler mikroC Pro 5.0.0 Configuration: power up timer, brownout reset (set to 2.5V), WDT, stack over/underflow reset -- all enabled, all others disabled * Uses a 12VDC wall wart as power supply * Tower light 12VDC red, amber, green incandescent lamps are switched by NPN Darlington transistors * pushbutton: * when momentarily pressed: cycles through different possible modes: 1. flashing amber 2. flashing red 3. alternating amber and red 4. normal traffic light: green -> amber -> red * when pressed and kept depressed for over a couple of seconds LED display shows 1,2,3,4,5,6 in sequence corresponding to following go/stop on-time: 1. 5sec 2. 10sec 3. 15sec 4. 20sec 5. 25sec 6. 30sec * although LED readout can be made to show number of real-time seconds when selecting go/stop time showing it in the manner above provides an opportunity to teach the children how to multiply--in this case multiply by 5 * go/stop on-time is stored in EEPROM * has 7-segment LED display * shows the go/stop on-time when selecting it * shows the remaining time before light changes * shows various non-numeric characters in other modes */ // ************************************************************************************************ // input / output // ************************************************************************************************ #define lred LATB.f4 // NPN darlington switches 12VDC incandescent lamp #define lamber LATA.f6 // NPN darlington switches 12VDC incandescent lamp #define lgreen LATB.f5 // NPN darlington switches 12VDC incandescent lamp #define pb PORTA.f5 // momentary contact push button #define tris_pb TRISA.f5 #define anode_ones LATA.f0 // S9012 PNP transistor #define anode_tens LATA.f1 // S9012 PNP transistor #define seg_a LATA.f2 // segment a of seven segment LED common anode #define seg_b LATA.f3 // segment b of seven segment LED common anode #define seg_c LATA.f4 // segment c of seven segment LED common anode #define seg_d LATB.f0 // segment d of seven segment LED common anode #define seg_e LATB.f1 // segment e of seven segment LED common anode #define seg_f LATB.f2 // segment f of seven segment LED common anode #define seg_g LATB.f3 // segment g of seven segment LED common anode // ************************************************************************************************ // traffic light time duration // ************************************************************************************************ #define count_ini 250 // number of timer2 ticks to make one second #define ambertime 500 // in normal mode -- amount of time for amber light to be on after green and before red -- time in terms of timer2 ticks #define flashambertime 200 // in flashing amber mode -- amount of time for amber light to be on and amount of time to be off -- time in terms of timer2 ticks #define flashredtime 200 // in flashing red mode -- amount of time for red light to be on and amount of time to be off -- time in terms of timer2 ticks #define flashredambtime 200 // in red/amber alternate flashing mode -- amount of time for red light to be on and amount of time amber light to be on -- time in terms of timer2 ticks #define ledflashtime 25 // in normal mode during yield (amber light) -- amount of time the character displayed on 7-seg is flashed on and off #define changergtime 375 // minimum amount of time for button to be kept pressed before cycling through the green light on time -- time in terms of timer2 ticks // given 4ms timer2 tick, changergtime of 250 = 1sec real time #define prechangergtime changergtime - 250 // amount of time after button is held down when all lights are turned off in preparation for possible change of green/red light on time #define rgtime_increm 1250 // amount of time for green/red light to be on, and multiples thereof upon continual button press -- value in terms of timer2 ticks. #define maxrgtime 6 // maximum allowed multiple of rgtime_increm int16 REDGREENTIME; // amount of time for green/red light to be on -- time in terms of timer2 ticks int8 RGTIME; // multiples of rgtime_increm such that RGTIME*rgtime_increm = REDGREENTIME // ************************************************************************************************ // for pushbutton // ************************************************************************************************ #define rising 1 // rising edge detected. used by PBedge #define released 1 // rising edge detected. used by PBedge #define falling 2 // falling edge detected. used by PBedge #define pressed 2 // falling edge detected. used by PBedge #define none 0 // no edge. used by PBedge int8 PBval; // last eight values of the switch upon reading it int8 PBedge; // edge detected?, 0 = no edge detect, 1 = rising edge, 2 = falling edge; other values = Not Used / Undefined for now bit PBlevel; // voltage level of switch when not bouncing (hi = 1, lo = 0) // ************************************************************************************************ // general defines and variables // ************************************************************************************************ #define addr_rgtime 0x10 // eeprom address for user selected green/red light on time #define disp_dash 111 // code to display a dash, for use with UpdateDisp() #define disp_3horiz 120 // code to display three horizontal segments, for use with UpdateDisp() #define disp_tophoriz 121 // code to display top horizontal segment, for use with SegmentAssign() #define disp_midhoriz 122 // code to display middle horizontal segment, for use with SegmentAssign() #define disp_botthoriz 123 // code to display bottom horizontal segment, for use with SegmentAssign() #define disp_brackets 130 // code to display brackets, for use with UpdateDisp() #define disp_left_bracket 131 // code to display left bracket, for use with SegmentAssign() #define disp_right_bracket 132 // code to display right bracket, for use with SegmentAssign() #define disp_topsquare 141 // code to display top square, for use with UpdateDisp() #define disp_bottsquare 142 // code to display bottom square, for use with UpdateDisp() #define disp_blank 255 // code to display nothing, for use with UpdateDisp() int16 TIME = 0; // records how long a light has been on -- in terms of timer2 ticks int8 VALUE = disp_blank; // number or character to be displayed on 7-seg LED int8 RGSECONDS; // user-selected red/green on-time in real-time seconds #define int1 bit #define int8 unsigned char #define int16 unsigned int #define int32 unsigned long #define on 1 #define off 0 #define _on 0 #define _off 1 #define yes 1 #define no 0 #define input 1 // for TRISx #define output 0 // for TRISx #define analog 1 // for ANSELx #define digital 0 // for ANSELx #define hi 1 // switch level high #define lo 0 // switch level low // ************************************************************************************************ // for state machines // ************************************************************************************************ // !! A C H T U N G !! // make sure all non user selectable modes such as _standby and _selectrgtime come AFTER user selectable // and that _normal" is the last item in the valid user selectable modes because it is used as the max value in ProcessKey() enum {_flashingamber, _flashingred, _flashingredamber, _normal, /* the modes that follow are non-user selectable --> */ _standby, _selectrgtime} STATEMODE = _flashingamber; enum {_stop, _yield_ini, _yield, _go} STATENORMAL = _stop; enum {_init, _flash} STATEFLASHGREEN = _init; // =========================================================================================== // LED 7-Segment Display // =========================================================================================== void SegmentAssign(int8 num) { seg_a = 0; seg_b = 0; seg_c = 0; seg_d = 0; seg_e = 0; seg_f = 0; seg_g = 0; switch (num) { case 1: seg_b = 1; seg_c = 1; break; case 2: seg_a = 1; seg_b = 1; seg_d = 1; seg_e = 1; seg_g = 1; break; case 3: seg_a = 1; seg_b = 1; seg_c = 1; seg_d = 1; seg_g = 1; break; case 4: seg_b = 1; seg_c = 1; seg_f = 1; seg_g = 1; break; case 5: seg_a = 1; seg_c = 1; seg_d = 1; seg_f = 1; seg_g = 1; break; case 6: seg_a = 1; seg_c = 1; seg_d = 1; seg_e = 1; seg_f = 1; seg_g = 1; break; case 7: seg_a = 1; seg_b = 1; seg_c = 1; break; case 8: seg_a = 1; seg_b = 1; seg_c = 1; seg_d = 1; seg_e = 1; seg_f = 1; seg_g = 1; break; case 9: seg_a = 1; seg_b = 1; seg_c = 1; seg_d = 1; seg_f = 1; seg_g = 1; break; case 0: seg_a = 1; seg_b = 1; seg_c = 1; seg_d = 1; seg_e = 1; seg_f = 1; break; case disp_dash: // displays a dash - middle horizontal segment seg_g = 1; break; case disp_3horiz: // displays three horizontal segments seg_a = 1; seg_d = 1; seg_g = 1; break; case disp_tophoriz: // displays top horizontal segment seg_a = 1; break; case disp_botthoriz: // displays bottom horizontal segment seg_d = 1; break; case disp_left_bracket: // displays opening bracket seg_a = 1; seg_d = 1; seg_e = 1; seg_f = 1; break; case disp_right_bracket: // displays closing bracket seg_a = 1; seg_b = 1; seg_c = 1; seg_d = 1; break; case disp_topsquare: // displays a square on upper half of LED seg_a = 1; seg_b = 1; seg_f = 1; seg_g = 1; break; case disp_bottsquare: // displays a square on the lower half of the LED seg_c = 1; seg_d = 1; seg_e = 1; seg_g = 1; break; default: // if invalid value then display blank case disp_blank: // segments have already been turned off at the start of this function so do nothing break; } // switch (num) } // void SegmentAssign(int8 num) void UpdateDisp() { static bit anode; // flag bit multiplexing display between tens and ones place int8 TENS, ONES; // contains the digit or code of the non-numeric character to be displayed in the tens and ones place // turn off power to 7-segment LED display. anode_ones = off; anode_tens = off; if (STATEMODE != _standby) // turn on LED display only when not in _standby mode { if (VALUE < 100) // values >= 100 are codes for non-numeric characters { TENS = VALUE/10; ONES = VALUE%10; } // if (VALUE < 100) else { switch (VALUE) { case disp_dash: TENS = disp_dash; ONES = disp_dash; break; case disp_3horiz: TENS = disp_3horiz; ONES = disp_3horiz; break; case disp_tophoriz: TENS = disp_tophoriz; ONES = disp_tophoriz; break; case disp_botthoriz: TENS = disp_botthoriz; ONES = disp_botthoriz; break; case disp_brackets: TENS = disp_left_bracket; ONES = disp_right_bracket; break; case disp_topsquare: TENS = disp_topsquare; ONES = disp_topsquare; break; case disp_bottsquare: TENS = disp_bottsquare; ONES = disp_bottsquare; break; default: // if invalid value then display blank case disp_blank: TENS = disp_blank; ONES = disp_blank; break; } // switch (VALUE) } // else if (VALUE >= 100) if (anode) { if (TENS != 0) // do not display tens place if it's zero { SegmentAssign(TENS); anode_tens = on; } anode = 0; } else { SegmentAssign(ONES); anode_ones = on; anode = 1; } } // if (display) } // void UpdateDisp() // =========================================================================================== // functions // =========================================================================================== void ComputeRedGreenTime() { REDGREENTIME = RGTIME*rgtime_increm; RGSECONDS = RGTIME*5; } void IniReg() { TRISA = output; TRISB = output; ANSELA = digital; ANSELB = digital; tris_pb = input; // enable weak pull up for pushbutton and unused pins OPTION_REG.NOT_WPUEN = 0; WPUA = 0xFF; // WPUB = wpullup; // set internal clock frequency to 2MHz OSCCON = 0b1100000; // Timer2 is used for state machine and switch debouncing timing tick // with clock = 2MHz, PR2 = 125, prescale = 1:16, postscale = 1:1 // TMR2 will count from zero to PR2 and timer2 interrupt occurs every 125*16 / (2MHz / 4) = 4ms = timer2 tick T2CON = 0b110; // postscaler = 1:1, prescaler = 1:16, timer2 on TMR2 = 0; PR2 = 125; WDTCON = 0b1000; // prescale = 1:512 (16ms typical) // WDTE in configuration word is configured so that WDT enabled when MCU awake and disabled when MCU asleep // retrieve stored green/red light on time value. // If eeprom-stored value is out of valid range then set it to minimum and store this value in eeprom // stored values are in terms of multiples of rgtime_increm such that stored value multiplied by rgtime_increm = time in terms of TMR2 ticks RGTIME = EEPROM_Read(addr_rgtime); if (RGTIME == 0 || RGTIME > maxrgtime) { RGTIME = 1; EEPROM_Write(addr_rgtime, RGTIME); } ComputeRedGreenTime(); // REDGREENTIME = RGTIME*rgtime_increm; // initialize push button PBval = 0xFF; PBlevel = hi; } // void InitRegisters() // amber flashes on and off at a rate determined by flashambertime // LED display shows the character stored in VALUE void FlashingAmber() { static bit flag; if (++TIME < flashambertime) { if (flag) { lamber = on; VALUE = disp_brackets; } else { lamber = off; VALUE = disp_blank; } } else { if (flag) flag = 0; else flag = 1; TIME = 0; } } // void FlashingAmber() // red flashes on and off at a rate determined by flashredtime // LED display shows the character stored in VALUE void FlashingRed() { static bit flag; if (++TIME < flashredtime) { if (flag) { lred = on; VALUE = disp_brackets; } else { lred = off; VALUE = disp_blank; } } else { if (flag) flag = 0; else flag = 1; TIME = 0; } } // void FlashingRed() // red and amber turn on alternately at a rate determined by flashredambertime // LED display shows the character stored in VALUE void FlashingRedAmber() { static bit flag; if (++TIME < flashredambtime) { if (flag) { lamber = off; lred = on; VALUE = disp_topsquare; } else { lred = off; lamber = on; VALUE = disp_bottsquare; } } else { if (flag) flag = 0; else flag = 1; TIME = 0; } } // void FlashingRedAmber() // normal traffic light operation: green --> amber --> red // red/green light on-time is user selectable // LED display shows the countdown time in seconds during red and green light // LED display shows a flashing "--" during amber light, blink rate determined by ledbflashtime void Normal() { static int8 COUNTER; static bit toggle; switch (STATENORMAL) { case _stop: if (++TIME < REDGREENTIME) { lred = on; if (--COUNTER == 0) { --VALUE; COUNTER = count_ini; } } else { STATENORMAL = _go; TIME = 0; COUNTER = count_ini; VALUE = RGSECONDS; lred = off; } break; case _yield_ini: toggle = 1; VALUE = disp_dash; COUNTER = ledflashtime; TIME = 0; STATENORMAL = _yield; break; case _yield: if (++TIME < ambertime) { lamber = on; if (--COUNTER == 0) { if (toggle) { VALUE = disp_blank; toggle = 0; } else { VALUE = disp_dash; toggle = 1; } COUNTER = ledflashtime; } } else { STATENORMAL = _stop; TIME = 0; COUNTER = count_ini; VALUE = RGSECONDS; lamber = off; } break; case _go: if (++TIME < REDGREENTIME) { lgreen = on; if (--COUNTER == 0) { --VALUE; COUNTER = count_ini; } } else { STATENORMAL = _yield_ini; lgreen = off; } break; default: TIME = 0; STATENORMAL = _yield; } // switch (STATENORMAL) } // void StateMachNormal() void StateMachMain() { switch (STATEMODE) { case _normal: Normal(); break; case _flashingamber: FlashingAmber(); break; case _flashingred: FlashingRed(); break; case _flashingredamber: FlashingRedAmber(); break; case _standby: // do nothing mode where all bulbs are off. used when changing red/green on-time break; case _selectrgtime: // er uhhh, I guess we do nothing as well break; default: STATEMODE = _normal; } } // void StateMachMain() void AllLightsOff() { lred = off; lamber = off; lgreen = off; } void DebounceSwitch() { // shift all bits to the left // if switch reading is hi then let pb_val bit 0 = 1 PBval <<= 1; if (pb) ++PBval; PBedge = none; // if level is lo and all bits of pb_val are now hi then // a rising edge has been detected // switch is considered just released when rising edge is detected // switch level is now hi if ((!PBlevel) && (PBval == 0xFF)) { PBlevel = hi; PBedge = rising; } // if level is hi and all bits of pb_val are now low then // a falling edge has been detected // switch is considered just pressed when falling edge is detected // switch level is now lo if ((PBlevel) && (!PBval)) { PBlevel = lo; PBedge = falling; } } // void DebounceSwitch() void ProcessKey() { int8 MODE; // temporary storage of current STATEMODE static int16 PBPRESSTIMETOTAL = 0; // Keeps track of how long PB is depressed in terms of timer2 ticks // Keeps track of total time from falling edge (switched pressed) to rising edge (switch released). // if less than changergtime then we know the keypress is momentary and user wants to change modes static int16 PBPRESSTIME = 0; // Keeps track of how long PB is depressed in terms of timer2 ticks // this variable is reset every time it exceeds changergtime // every time it exceeds changergtime red/green on-time is incremented until masrgtime and then it cycles back to one DebounceSwitch(); MODE = STATEMODE; if (PBlevel == lo) { if (++PBPRESSTIMETOTAL == prechangergtime) { AllLightsOff(); STATEMODE = _standby; } if (++PBPRESSTIME >= changergtime) { PBPRESSTIME = 0; if (++RGTIME > maxrgtime) RGTIME = 1; EEPROM_Write(addr_rgtime, RGTIME); ComputeRedGreenTime(); // REDGREENTIME = RGTIME*rgtime_increm; STATEMODE = _selectrgtime; VALUE = RGTIME; // RGTIME will be displayed on the LED readout } // if (++PBPRESSTIME >= changergtime) } // if (PBlevel == lo) if (PBedge == released) { if (PBPRESSTIMETOTAL < changergtime) // if pushbutton was only momentarily pressed then change to the next mode { STATEMODE = ++MODE; if (STATEMODE == _normal) STATENORMAL = _yield_ini; // this is necessary to start normal traffic light mode properly else if (STATEMODE > _normal) STATEMODE = 0; } // if (PBPRESSTIMETOTAL < changergtime) else // green/red light on time has been changed so go to normal traffic light mode and begin with amber light { STATEMODE = _normal; STATENORMAL = _yield_ini; } AllLightsOff(); // turn off all bulbs and let state machine take care of which ones to turn on TIME = 0; // reset timer so that whatever mode has been selected, light will go through full time allotted PBPRESSTIMETOTAL = 0; PBPRESSTIME = 0; } // if (PBedge == released) } // void ProcessKey() void main() { IniReg(); while(1) { if (PIR1.TMR2IF) { PIR1.TMR2IF = 0; asm{clrwdt} UpdateDisp(); ProcessKey(); StateMachMain(); } // if (PIR1.TMR2IF) } // while(1) } // void main()