I thought Jon Chandler’s Clock for Geeks was a great project and his idea to make a clock with two dials in a VU meter type setup was brilliant. This is my take on that idea.
This is supposed to be a geeky clock. I reckon the following should be features of any truly geeky product:
With the above in mind, here are the features of my clock.
+12H and ALM Indicators: These show AM/PM and Alarm On/Off respectively. In keeping with the old-fashioned meter look I didn’t want to have LEDs sticking out of the front panel. Instead, each indicator is made from a small brass tube with an RGB LED behind it. When the LED is off there’s just a black disk with a brass surround on the panel, when it’s lit it looks like the tube is filled with colour. There are 16 different brightness settings for each of the red, green and blue LEDs, giving over 4,000 possible colours.
H and M Dials: Hopefully these are self-explanatory! The time shown in the photo is 7:20PM. The pointers are made from a 1mm diameter rod which I found in a hobby shop. I’m not exactly sure what it’s made of but I think it might be some sort of carbon rod used in RC aircraft and such.
Light Sensor: This is in the centre of the black stripe at the bottom of the face and is used to control when the front panel lamps are turned on and off. The light sensor sits inside a small plastic tunnel which blocks the light from the lamps, meaning it senses only the ambient lighting level. The sensor used is an LLS05-A which I got from Futurlec.
Front Panel Illumination: This is provided by four 6v tungsten filament lamps. The lamps have 16 brightness settings and can be set to operate in one of six modes:
Most of the clock’s settings are adjusted from a PC via the USB port. However, three buttons on top of the clock perform the following functions:
Button 0 (left)
Button 1 (middle)
In Alarm Set Mode
Button 2 (right)
In Normal Mode
In Alarm Set Mode
The buttons are backlit with RGB LEDs which, same as the front panel indicators, have 16 different brightness settings for each of the red, green and blue LEDs. The backlighting LEDs can be set to operate in one of three modes:

As well as the Reset button, the rear panel houses the sockets for the 6V DC input, the USB connection and the ICSP connection.
USB Port
All of the clock settings (LED settings, alarm time, lamp on / off thresholds, mode settings etc.) are adjusted from a PC connected via USB. The USB connection is also used to synchronise the clock time to the PC clock which keeps it nice and accurate.
This is the first time I’ve ever tried making something like this and I’m no expert. I’m not saying this is the best way to it - this is the method I used and it worked okay for me. I’ve added a few “Lessons Learned” points which detail things I would have done differently if I was doing it again.
The case was made by laminating 0.75mm layers of wood around a Former. As for the choice of wood, the shop I went to had sheets of balsa wood and sheets of something else. I tried the balsa but found it didn’t take tight curves as well as the “something else”. Not sure what the mystery wood is but I think it might be spruce.
The process starts with a plastic Former which has the same external dimensions as the interior of the case. I made mine by building bulkheads out of 3mm acrylic which I then wrapped a few layers of thin plastic sheet around. Note that when you come to laminate the wood around the Former you need to clamp the wood quite tightly so the Former has to be sturdy enough to take the pressure.
Lesson Learned: Time spent on the Former will pay off later. I didn’t take as much care with mine as I should have and it ended up with a few imperfections which not only made it harder to work with but also carried through to the final case.
Laminate
At its tightest, the curve in the case has a 4mm radius. In order to get the wood to curve that tight I had to soak it in water for at least 24 hours. I then strapped the wet wood around the Former and let it dry completely – usually for another 12 to 24 hours. The dry (and now curved) piece was then taken off, glued-up, and strapped back into place until the glue dried. This process is repeated for each layer.
Lesson Learned: I used a bandage to strap the wood in place whilst it dried. This worked okay around the curves but didn’t work very well on the straight sides of the case. The wood tended to bow away from the Former and really I should have used some planks of wood and clamps to hold it tightly in place along the sides.
For strength, it’s best if the laminate is arranged so that alternate layers have the wood-grain running perpendicular to each other. For this case I didn’t want lots of thin stripes showing in the cross section at the front so, rather then doing every other layer, I put two layers with the grain going one way and then two layers with the grain going the other way and so on. There are 6 layers for the main body of the case and a further 4 layers in a thin strip at the front to produce the “lip” which holds the face in place – 10 layers in total.
As you can see from the photo, each layer of wood is larger than the Former and the edges are far from neat and uniform! I found it was easier to do that and then trim it back when everything was finished than to try and get it all to line up from the start.
Finishing
Once everything is dry, the wooden case can be trimmed to the right size and sanded to finish. This photo shows the case removed from the Former and with the additional “lip” at the front added.
The final finish was to give it a few coats of matte polyurethane varnish with a light sanding between coats.
In exactly the same way as Jon’s clock, the hour and minute pointers are driven by RC style servos. As the clock is pretty small I had to use micro- rather than standard-sized servos. The servos can swing through about 175° whilst the pointers only need to cover a little over 90°. I therefore decided to gear-down the servos’ output which brought a number of benefits:
- Any error in the servo’s position is reduced by the amount of the gear ratio. i.e. if the output is geared-down 2:1 then an error of 1° in the servo translates to an error of only 0.5° at the pointer. However, this improved accuracy does have to be offset against any “play” in the gear-train so the benefit may not always be realised.
- I could position the pointers closer to the edges of the case because I didn’t have to worry about leaving enough clearance to fit the servo in the same place as where the pointer’s axle was.
- Damage Control! During coding and testing there were a few occasions when the servos swung wildly from one end of their travel to the other. If the output hadn’t been geared-down then the pointers would’ve smacked into the sides of the case and probably broken off.
Lesson Learned: When it comes to servos it seems there can be quite a difference in quality between brands. The first ones I was using (pictured above) had pretty poor accuracy which caused me quite a few headaches. During testing, one of them totally failed and mechanically locked-up meaning it had to be binned. I had much better results with a TowerPro SG50 which is what is now installed in the clock. TowerPro aren’t a high-end brand but I’m pretty pleased with the performance.
The face was drawn in DesignCAD, printed onto matt photo-paper and mounted onto a 3mm piece of acrylic. The white plastic surround hides the wooden inside of the case and holds the front “glass”, which is actually a 1mm piece of acrylic. The holes along the bottom are for the leads from the lamps and light sensor which are eventually covered-up by the black stripe on the bottom of the glass. The above photo also gives a good view of the +12H and ALM indicators before the RGB LEDs were put behind them.
To cut out the holes for the buttons I made a hole-saw by roughing up one end of piece of 5mm diameter brass tubing with a needle file to create some crude “teeth”. I used a hole-saw rather than a drill so that I could save the pieces of wood which came out of the centre of the holes and use them as the button tops – this meant the wood-grain on the buttons matched the surrounding case.
Nothing very special about the switches themselves - three regular pushbuttons mounted on stripboard with RGB LEDs between them. I used a double-decker board because there wasn’t enough height clearance to mount the LEDs onto the same board as the switches. Once the board was built, I put a plastic enclosure around the LEDs to direct as much of the light as possible upwards and also to diffuse and blend the red, green, and blue LEDs into one smooth colour.![]()
The clock runs on a PIC18F2550. I chose this because it had USB support, enough IO pins and (most importantly) I had one. A 20MHz crystal is used and the PIC’s PLL pre- and post-scalers set to give a 32MHz clock.
A piezo buzzer is used for the alarm. The PIC drives the buzzer directly but it can’t handle all the LEDs and lamps so I used a couple of ULN2803A drivers. The ULN2803 is an 8 channel version of the ULN2003 which Graham’s article does a great job of explaining. Each channel of the ULN2803A can handle 500mA continuous – more than enough for the LEDs but the front panel lamps are rated at 200mA so I had to parallel two channels to drive all four of them.
Power comes from a 6V DC wall-wart capable of providing up to 3A. The 6V supply is used to power the servos, LEDs and lamps directly. An LM2940 low-dropout regulator provides the 5V supply for the PIC. This particular regulator was chosen because I had one – anything with a dropout voltage of less than 1V would have done.
The board was assembled onto stripboard using my favoured “stick-piece-of-paper-with-layout-to-board-and-then-populate” method.

The photo below shows the rear panel, circuit board, switches, servos and face assembled and ready to be fixed inside the case. The silver box on the back of the face houses the RGB LEDs used for the +12H and ALM indicators. The box is lined with aluminium foil to prevent light from the LEDs leaking out around the edges of the face.![]()
I used my Servo.bas module to handle controlling the servos. The module is interrupt-driven so the main code must be written in such a way that it can handle being interrupted at any time. For best results the module needs to use the high-priority interrupt.
Graham has already written a great description of how servos work so I won’t repeat it here. Using the Servo.bas module, you move a servo by setting the variable ServoPosition(x) to the pulse length you want in uS, where x is the servo number from 0 to 7.
In this project only two servos are connected so I set the module option as such:
#option Servo_NumberOfServos = 2
To improve readability I also aliased the ServoPosition variables so they had meaningful names:
Dim ServoHours As Servo.ServoPosition(0) '}Aliases to Servo Module servo Dim ServoMinutes As Servo.ServoPosition(1) '}position variables
The pulse lengths required for each pointer position on the dials are held in two constant arrays “Hours” and “Minutes”. The Hours array has 48 values – one for every 15 minutes from 00:00 to 11:45. The Minutes array has 60 values – one for each minute. During testing I was changing these arrays quite a bit so I found it easier to keep them in a separate module – “ServoLookup.bas”
Module ServoLookup Public Const Hours(48) As Word = (708, 735, 768, 801, 830, 861, 892, 920, 952, 986, 1018, 1050, 1081, 1108, 1135, 1166, 1196, 1224, 1258, 1290, 1316, 1349, 1382, 1414, 1452, 1486, 1518, 1553, 1583, 1612, 1644, 1676, 1704, 1731, 1760, 1792, 1823, 1850, 1880, 1918, 1961, 2000, 2032, 2067, 2102, 2133, 2167, 2207) Public Const Minutes(60) As Word = (2150, 2124, 2089, 2054, 2029, 2011, 1981, 1957, 1919, 1890, 1873, 1845, 1817, 1788, 1760, 1732, 1703, 1674, 1644, 1615, 1586, 1557, 1528, 1500, 1471, 1442, 1417, 1392, 1366, 1341, 1316, 1289, 1262, 1235, 1208, 1181, 1156, 1132, 1107, 1083, 1058, 1034, 1009, 985, 960, 936, 912, 888, 865, 841, 817, 789, 766, 747, 726, 700, 676, 655, 630, 611)
I used the Swordfish USBHID.bas module to handle the USB comms. The EasyHID plugin was used to generate the necessary HID descriptor module.
To maintain the USB connection with the PC, it needs to be serviced every 1mS or so. By default, the USBHID.bas module will handle this using interrupts. As noted above, the Servo.bas module uses the high-priority interrupt so that leaves the low-priority interrupt for the USB. I tested the code with this dual-interrupt set-up and didn’t experience any problems. However, in the end I decided that less interrupts = less chance of things going wrong. I therefore disabled the USB interrupt servicing by setting the module option:
#option USB_SERVICE = False
The 18F2550 has 256 bytes of USB dual port RAM which can be used by the USBHID.bas module for sending and receiving data. I used this area to store almost all the system variables – i.e. current time, alarm time, lamp brightness, etc. etc. Using this area has the advantage that no processing has to be done on the incoming USB stream – simply reading in the USB report overwrites the variables with the new clock settings from the PC. This also has the disadvantage that no processing is done on the incoming USB stream and simply reading in the USB report overwrites the variables with whatever was read in! If the PC starts sending garbage then the clock will most likely lose its tiny mind. So far, I’ve not had any problems but time will tell how reliable this method is – I probably wouldn’t use it for anything mission-critical but it seems okay for a desktop clock!
In order to use the USB RAM and still have meaningful variable names, I declared a structure for the format of the data:
Structure tTime Hours As Byte Minutes As Byte Seconds As Byte End Structure Structure tUSBData 'Used to map variables onto USB RAM: Time As tTime 'Current Time (hours, minutes, seconds) Alarm As tTime 'Alarm Time (hours, minutes, seconds) AlarmSet As Bit '=1 if alarm is set AlarmOn As Bit '=1 if alarm is sounding Mode As Byte 'Which mode clock is operating in …etc …etc End Structure
Dim Data As tUSBData Absolute BufferRAM
This makes Swordfish overlay the structure tUSBData onto the USB RAM location. The data can then be accssed like any other variable:
'Set alarm time to 12:30:00 and turn on alarm Data.Alarm.Hours = 12 Data.Alarm.Minutes = 30 Data.Alarm.Seconds = 00 Data.AlarmSet = 1
The Swordfish Help file has a lot of useful information about using the USB RAM.

On the PC side, Graham’s iHID came to the rescue once again. I used his code to put together a simple PC app which allows me to sync the clock to the PC’s clock and monitor and adjust all the settings – it’s not pretty but it’s functional. In progress is a small app which will be scheduled to run once a day and which will sync the clock automatically, keeping it always at the right time.
The PC app really doesn't have to do anything clever - every 100mS the clock sends a USB report containing the values of all of the system variables. If any settings need to be changed the PC sends a report back to the clock with the new values.
Main Program Loop
The flowchart shows the high-level tasks carried out in the main program loop (click to see the full picture). All the tasks the are divided according to how frequently they need to occur – every 1mS, 10mS, 100mS or 1000mS.
Timer2 is set-up to rollover every 1mS. For most of the time the PIC sits in a loop waiting for the rollover to occur. When it does, the PIC carries out the tasks which need to happen every 1mS, namely:
- Service the USB connection;
- Update the software PWM signal being fed to the LEDs and lamps;
- Increase the event counters for 10mS, 100mS and 1000mS events.
Once the 10mS event counter equals 10, 10mS have passed. The PIC checks the state of the buttons and, if any have been pressed, carries out the appropriate action. Checking the buttons every 10mS may seem excessive but sampling them at that rate is necessary for the software de-bouncing and button auto-repeat routines to work well (see “Buttons” below).
Every 100mS all the outputs are updated – servo positions, lamps, LEDs and buzzer. This is also when the USB report is sent to the PC and any data from the PC is read.
Every 1000mS the time is updated and checked against the alarm time to see if the alarm should sound. The output from the light sensor is also read and compared against the on / off thresholds.
The brightness of the front panel lamps and LEDs are controlled by a software PWM signal output from the PIC. The subroutine UpdateLightPWM generates the signals and is called every 1mS. This flowchart shows what happens inside UpdateLightPWM but simplified for just one LED.
With 16 brightness levels and the routine called every 1mS, the frequency of the PWM output is just over 60Hz which means there is no noticeable flicker.
The subroutine CheckButtons is responsible for checking the state of the buttons and doing software debouncing. The routine sets or clears two flags for each button - “Pressed” and “Held”. “Pressed” is set when a button is pressed for longer than the Debounce_Delay period and then released. “Held” is set if a button is held down for longer than the Hold_Delay period. The flowchart below shows the process for one button.
{ ******************************************************************************** * Name : Meter Clock * * Author : AndyO * * Notice : Copyright (c) 2010 AndyO * * : All Rights Reserved * * Date : 02 December 2010 * * Version : 1.0 * * Notes : Servo module uses Timer1, Timer2 used for timekeeping and USB * * : service. * * : * * : Use 20MHz crystal * * : * * : Servos connected to PortB.0 and PortB.1 * * : * ******************************************************************************** } '=============================================================================== 'Device, Clock and Config directives '------------------------------------------------------------------------------- Device = 18F2550 Clock = 32 Config PLLDIV = 5, '} CPUDIV = OSC2_PLL3, '}Set oscillator options to give 32MHz USBDIV = 2, '}internal clock from 20MHz crystal FOSC = HSPLL_HS, '} VREGEN = On 'Enable internal 3.3V regulator for USB '=============================================================================== 'Module Options & Includes '------------------------------------------------------------------------------- '---Options--------------------------------------------------------------------- #option Debug_Mode = False 'TRUE turns on debugging parts of code #option USB_DESCRIPTOR = "MeterClockUSBDesc.bas" '}Descriptor generated '}by EasyHID #option USB_SERVICE = False 'USB not automatically serviced - call 'HID.Service every 1mS #option Servo_NumberOfServos = 2 'Servo module drives PortB.0 & PortB.1 #option Servo_Priority = ipHigh 'Servo module on high priority interrupt '---Includes-------------------------------------------------------------------- Include "usbhid.bas" Include "Servo.bas" Include "ServoLookup.bas" '=============================================================================== 'Variable and Constant Declarations '------------------------------------------------------------------------------- '---Constants------------------------------------------------------------------- Const ipHigh = 2 '}Interrupt priority constants Const ipLow = 1 '} Const DebounceDelay = 2 'Time in 10mS increments button must be down for Const SwitchHoldDelay = 35 'Time in 10mS increments button must be held for Const SwitchRepeatDelay = 10 'Time in 10mS increments between auto-repeats Const LampOnOffDelay = 60 'Number of seconds light level must be below / 'above the lamp on / off threshold levels for 'the front panel lamp to turn on / off Const LampFlashTime = 3 'Number of seconds front panel lamp is on for 'when button 2 is pushed Const MaxAlarmSoundTime = 600 'Maximum number of seconds alarm can sound for Const Mode_Normal = 0 '} Const Mode_AlarmSet = 1 '}Clock Mode constants Const Mode_LightTest = 2 '} Const SwitchLEDMode_AlwaysOn = 0 '} Const SwitchLEDMode_AlwaysOff = 1 '}Switch LED Mode constants Const SwitchLEDMode_AsLamps = 2 '} Const LampMode_AlwaysOn = 0 '} Const LampMode_AlwaysOff = 1 '} Const LampMode_Timed = 2 '}Front panel lamp Mode constants Const LampMode_Level = 3 '} Const LampMode_TimedAndLevel = 4 '} Const LampMode_WithUSB = 5 '} '---Structures------------------------------------------------------------------ Structure tTime Hours As Byte Minutes As Byte Seconds As Byte End Structure Structure tRGBLed Red As Byte Green As Byte Blue As Byte End Structure Structure tUSBData 'Used to map variables onto USB RAM: Time As tTime 'Current Time (hours, minutes, seconds) Alarm As tTime 'Alarm Time (hours, minutes, seconds) AlarmSet As Bit '=1 if alarm is set AlarmOn As Bit '=1 if alarm is sounding Mode As Byte 'Which mode clock is operating in SwitchLed As tRGBLed 'RGB brightness values for Switch LEDs SwitchLedOn As Bit '=1 if Switch LEDs are on SwitchLedMode As Byte 'Which mode Switch LEDs are operating in PMLed As tRGBLed 'RGB brightness values for AM/PM LED PMLedOn As Bit '=1 if AM/PM LED is on AlarmLed As tRGBLed 'RGB brightness values for Alarm LED AlarmLedOn As Bit '=1 if Alarm LED is on LampBrightness As Byte 'Brightness value for front panel lamp LampMode As Byte 'Which mode lamp is operating in LampOnThreshold As Byte 'Lamp On light level LampOffThreshold As Byte 'Lamp Off light level LampOnTime As tTime 'Lamp On time (Hours, Minutes, Seconds) LampOffTime As tTime 'Lamp Off time (Hours, Minutes, Seconds) LampOn As Bit '=1 if Lamp is on LightLevel As Byte 'Reading from light sensor End Structure '---Variables------------------------------------------------------------------- Dim Data As tUSBData Absolute BufferRAM 'Overlay above tUSBData structure onto 'the USB RAM buffer Dim LedPWMCounter As Byte 'Used for LED and Lamp PWM Dim Switch0DebounceCount As Byte '} Dim Switch1DebounceCount As Byte '}Counts whether button is pressed for Dim Switch2DebounceCount As Byte '}the debounce delay period Dim Switch0DownCount As Byte '} Dim Switch1DownCount As Byte '}Counts whether button is pushed down Dim Switch2DownCount As Byte '}for the hold delay period Dim Switch0Pressed As Boolean '} Dim Switch1Pressed As Boolean '}=TRUE when a switch has been pressed Dim Switch2Pressed As Boolean '} Dim Switch0Held As Boolean '} Dim Switch1Held As Boolean '}=TRUE when a switch is being held down Dim Switch2Held As Boolean '} Dim Switch1RepeatCount As Byte '}Counts whether switch has been held Dim Switch2RepeatCount As Byte '}down for the auto-repeat delay period Dim LampOnLevelCount As Byte '}Counts how long light level is below Dim LampOffLevelCount As Byte '}or above the lamp on & off thresholds Dim LampOverride As Boolean '=TRUE when Switch 2 turns on lamp Dim LampFlashCount As Byte 'Counter for momentarily lighting lamp Dim Buzzer100mSToggle As Bit 'Used to toggle alarm buzzer every 100mS Dim Buzzer1SecToggle As Bit 'Used to toggle alarm buzzer every 1 Sec Dim AlarmSoundCount As Word 'Number seconds alarm has been sounding Dim Timer2On As T2CON.Booleans(2) '} Dim Timer2InterruptFlag As PIR1.Booleans(1) '}Aliases to Timer 2 registers Dim Timer2InterruptEnable As PIE1.Booleans(1) '} Dim StartADConversion As ADCON0.Booleans(1) 'Alias to ADC Go/Done bit. 'Set TRUE to start conversion, '= FALSE when conversion done Dim Tick As Timer2InterruptFlag '=TRUE when Timer2 rolls over Dim mS10Count As Byte '}Period counters for 10mS, Dim ms100Count As Byte '}100mS and 1 Second events Dim mS1000Count As Word '} Dim ServoHours As Servo.ServoPosition(0) '}Aliases to Servo Module servo Dim ServoMinutes As Servo.ServoPosition(1) '}position variables '---Input / Output Aliases------------------------------------------------------ #if Debug_Mode = True Then Dim DebugPin0 As PORTB.7 '}PGC & PGD pins used for Dim DebugPin1 As PORTB.6 '}debugging #endif Dim LightSensor As PORTA.0 'Analog input from light sensor Dim SwitchLedRedPin As PORTA.1 '} Dim SwitchLedGreenPin As PORTA.2 '}RGB LEDs illuminating switches Dim SwitchLedBluePin As PORTA.3 '} Dim LampPin As PORTA.4 'Front-panel illumination lamps Dim BuzzerPin As PORTA.5 'Alarm buzzer Dim PMLedRedPin As PORTB.5 '} Dim PMLedGreenPin As PORTB.3 '}AM/PM RGB LED Dim PMLedBluePin As PORTB.4 '} Dim AlarmLedRedPin As PORTB.2 '} Dim AlarmLedGreenPin As PORTC.6 '}Alarm RGB LED Dim AlarmLedBluePin As PORTC.7 '} Dim Switch0 As PORTC.0 '} Dim Switch1 As PORTC.1 '}Switches Dim Switch2 As PORTC.2 '} '=============================================================================== 'Subs and Functions '------------------------------------------------------------------------------- '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'Name: UpdateTime() 'Purpose: Updates current time by 1 second - advancing seconds, minutes and ' hours as necesary ' 'Notes: Inline sub ' Called every 1 Second '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Private Inline Sub UpdateTime() Inc(Data.Time.Seconds) If Data.Time.Seconds = 60 Then Data.Time.Seconds = 0 Inc(Data.Time.Minutes) If Data.Time.Minutes = 60 Then Data.Time.Minutes = 0 Inc(Data.Time.Hours) If Data.Time.Hours = 24 Then Data.Time.Hours = 0 EndIf EndIf EndIf End Sub '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'Name: UpdateLightPWM() 'Purpose: Updates LED and Lamp PWM output according to brightness values ' 'Notes: Inline sub ' Called every 1mS '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Private Inline Sub UpdateLightPWM() Inc(LedPWMCounter) 'Increment the PWM counter If LedPWMCounter.Bits(4) = 1 Then 'When PWM counter reaches 16: LedPWMCounter = 0 'Reset counter SwitchLedRedPin = Data.SwitchLedOn '} SwitchLedGreenPin = Data.SwitchLedOn '} SwitchLedBluePin = Data.SwitchLedOn '}Set the output pin High '}for each LED or lamp which PMLedRedPin = Data.PMLedOn '}is set to be On PMLedGreenPin = Data.PMLedOn '} PMLedBluePin = Data.PMLedOn '} '} AlarmLedRedPin = Data.AlarmLedOn '} AlarmLedGreenPin = Data.AlarmLedOn '} AlarmLedBluePin = Data.AlarmLedOn '} '} LampPin = Data.LampOn '} EndIf '---Switch RGB LEDs: If LedPWMCounter = Data.SwitchLed.Red Then '} SwitchLedRedPin = 0 '} EndIf '}When PWM counter = Brightness '}setting for the LED, set If LedPWMCounter = Data.SwitchLed.Green Then'}output pin Low for that LED SwitchLedGreenPin = 0 '} EndIf '} '} If LedPWMCounter = Data.SwitchLed.Blue Then '} SwitchLedBluePin = 0 '} EndIf '} '---AM/PM RGB LEDs: If LedPWMCounter = Data.PMLed.Red Then '} PMLedRedPin = 0 '} EndIf '}When PWM counter = Brightness '}setting for the LED, set If LedPWMCounter = Data.PMLed.Green Then '}output pin Low for that LED PMLedGreenPin = 0 '} EndIf '} '} If LedPWMCounter = Data.PMLed.Blue Then '} PMLedBluePin = 0 '} EndIf '} '---Alarm RGB LEDs: If LedPWMCounter = Data.AlarmLed.Red Then '} AlarmLedRedPin = 0 '} EndIf '}When PWM counter = Brightness '}setting for the LED, set If LedPWMCounter = Data.AlarmLed.Green Then '}output pin Low for that LED AlarmLedGreenPin = 0 '} EndIf '} '} If LedPWMCounter = Data.AlarmLed.Blue Then '} AlarmLedBluePin = 0 '} EndIf '} '---Front Panel Lamp: If LedPWMCounter = Data.LampBrightness Then '}When PWM counter = Brightness LampPin = 0 '}setting, set output pin Low EndIf '} End Sub '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'Name: CheckButtons() 'Purpose: Checks buttons, debounces, and sets flags indicating if pressed or ' held ' 'Notes: Inline sub. ' Each button has two flags - "Pressed" and "Held". Pressed is set ' when the button is pushed once and released. Held is set when the ' button is pressed and remains down for longer than "SwitchHoldDelay" ' ' ---Routine for each switch:----------------------------------------- ' ' If pin reads High and it hasn't been down for the Debounce Delay, ' increase the Debounce Count; ' ' If pin reads High and Debounce Count = Debounce Delay then it is a ' valid press so increase the Switch Down Count. If the switch has ' been down for Hold Delay then set the Held flag; ' ' If pin goes Low after Debounce Count = Debounce Delay but before ' Hold Delay is reached, set switch's "Pressed" flag and reset all ' counters; ' ' If pin goes Low before Debounce Count = Debounce Delay then reset ' all counters. '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Private Inline Sub CheckButtons() '---Switch 0---------------------------------------------------------------- If Switch0 = 1 And Switch0DebounceCount < DebounceDelay Then Inc(Switch0DebounceCount) ElseIf Switch0 = 1 And Switch0DebounceCount = DebounceDelay And Switch0Held = False Then Inc(Switch0DownCount) If Switch0DownCount = SwitchHoldDelay Then Switch0Held = True EndIf ElseIf Switch0 = 0 And Switch0DebounceCount = DebounceDelay And Switch0Held = False Then Switch0Pressed = True Switch0DownCount = 0 Switch0DebounceCount = 0 ElseIf Switch0 = 0 Then Switch0Pressed = False Switch0Held = False Switch0DownCount = 0 Switch0DebounceCount = 0 EndIf '---Switch1----------------------------------------------------------------- If Switch1 = 1 And Switch1DebounceCount < DebounceDelay Then Inc(Switch1DebounceCount) ElseIf Switch1 = 1 And Switch1DebounceCount = DebounceDelay And Switch1Held = False Then Inc(Switch1DownCount) If Switch1DownCount = SwitchHoldDelay Then Switch1Held = True EndIf ElseIf Switch1 = 0 And Switch1DebounceCount = DebounceDelay And Switch1Held = False Then Switch1Pressed = True Switch1DownCount = 0 Switch1DebounceCount = 0 ElseIf Switch1 = 0 Then Switch1Pressed = False Switch1Held = False Switch1DownCount = 0 Switch1DebounceCount = 0 EndIf '---Switch2----------------------------------------------------------------- If Switch2 = 1 And Switch2DebounceCount < DebounceDelay Then Inc(Switch2DebounceCount) ElseIf Switch2 = 1 And Switch2DebounceCount = DebounceDelay And Switch2Held = False Then Inc(Switch2DownCount) If Switch2DownCount = SwitchHoldDelay Then Switch2Held = True EndIf ElseIf Switch2 = 0 And Switch2DebounceCount = DebounceDelay And Switch2Held = False Then Switch2Pressed = True Switch2DownCount = 0 Switch2DebounceCount = 0 ElseIf Switch2 = 0 Then Switch2Pressed = False Switch2Held = False Switch2DownCount = 0 Switch2DebounceCount = 0 EndIf End Sub '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'Name: ActionButtons() 'Purpose: Carries out appropriate action, depending on Mode, for any buttons ' which are held down or have been pressed ' 'Notes: Inline sub ' Called every 10mS '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Private Inline Sub ActionButtons() '---Switch 1 Behaviour------------------------------------------------------ ' 'In Normal Mode or Alarm_Set Mode: ' ' "Pressed" - if alarm is sounding, silence alarm. ' - if alarm is not sounding, toggle Alarm On / Off. ' ' "Held" - enter Alarm-Set Mode '--------------------------------------------------------------------------- If Data.Mode <> Mode_LightTest Then If Switch0Pressed = True Then If Data.AlarmOn = 1 Then Data.AlarmOn = 0 Else Data.AlarmSet = Not(Data.AlarmSet) EndIf EndIf If Switch0Held = True Then Data.Mode = Mode_AlarmSet Else Data.Mode = Mode_Normal EndIf EndIf '---Switch 1 Behaviour------------------------------------------------------ ' 'In Alarm-Set Mode: ' ' "Pressed" - advances Alarm-Time Hours by 1 ' ' "Held" - auto-repeats, advancing Alarm-Time Hours by 1 each time '--------------------------------------------------------------------------- If Data.Mode = Mode_AlarmSet Then If Switch1Pressed = True Then Data.Alarm.Hours = Data.Alarm.Hours + 1 If Data.Alarm.Hours = 24 Then Data.Alarm.Hours = 0 EndIf EndIf If Switch1Held = True Then Inc(Switch1RepeatCount) If Switch1RepeatCount = SwitchRepeatDelay Then Switch1RepeatCount = 0 Data.Alarm.Hours = Data.Alarm.Hours + 1 If Data.Alarm.Hours = 24 Then Data.Alarm.Hours = 0 EndIf EndIf Else Switch1RepeatCount = 0 EndIf EndIf '---Switch 2 Behaviour------------------------------------------------------ ' 'In Normal Mode: ' ' "Pressed" - turns on Front Panel Lamp for "LampFlashTime" seconds ' ' "Held" - turns on Front Panel Lamp for duration button is held ' ' 'In Alarm-Set Mode: ' ' "Pressed" - advances Alarm-Time Minutes by 1 ' ' "Held" - auto-repeats, advancing Alarm-Time Minutes by 1 each time '--------------------------------------------------------------------------- Select Data.Mode Case Mode_Normal If Switch2Pressed = True Then LampFlashCount = LampFlashTime LampOverride = True ElseIf Switch2Held = True Then LampOverride = True EndIf Case Mode_AlarmSet If Switch2Pressed = True Then Data.Alarm.Minutes = Data.Alarm.Minutes + 1 If Data.Alarm.Minutes = 60 Then Data.Alarm.Minutes = 0 EndIf ElseIf Switch2Held = True Then Inc(Switch2RepeatCount) If Switch2RepeatCount = SwitchRepeatDelay Then Switch2RepeatCount = 0 Data.Alarm.Minutes = Data.Alarm.Minutes + 1 If Data.Alarm.Minutes = 60 Then Data.Alarm.Minutes = 0 EndIf EndIf Else Switch2RepeatCount = 0 EndIf EndSelect End Sub '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'Name: UpdateDisplay() 'Purpose: Updates servo positions, LEDs and front panel lamp depending on mode ' 'Notes: Inline sub ' Called every 100mS '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Private Inline Sub UpdateDisplay() '---Servos------------------------------------------------------------------ ' 'Servo position values for each pointer position are stored in two constant 'arrays in ServoLookup.bas module: ' 'ServoLookup.Minutes has 60 values (0-59 mins) 'ServoLookup.Hours has 48 values (0-11 hrs, 4 x 15min slots for each hour) '--------------------------------------------------------------------------- If Data.Mode = Mode_Normal Or Data.Mode = Mode_LightTest Then If Data.Time.Hours > 11 Then ServoHours = ServoLookup.Hours(((Data.Time.Hours - 12) * 4) + (Data.Time.Minutes / 15)) Else ServoHours = ServoLookup.Hours((Data.Time.Hours * 4) + (Data.Time.Minutes / 15)) EndIf ServoMinutes = ServoLookup.Minutes(Data.Time.Minutes) ElseIf Data.Mode = Mode_AlarmSet Then If Data.Alarm.Hours > 11 Then ServoHours = ServoLookup.Hours(((Data.Alarm.Hours - 12) * 4) + (Data.Alarm.Minutes / 15)) Else ServoHours = ServoLookup.Hours((Data.Alarm.Hours * 4) + (Data.Alarm.Minutes / 15)) EndIf ServoMinutes = ServoLookup.Minutes(Data.Alarm.Minutes) EndIf '---Front Panel Lamp-------------------------------------------------------- ' 'Turn on or off front panel lamp depending on Lamp Mode: ' 'LampMode_Always On: ON ' 'LampMode_AlwaysOff: OFF ' 'LampMode_Timed: If time is after "LampOnTime" - ON ' If time is after "LampOffTime" - OFF ' ' 'LampMode_Level: If light level has been above "LampOnThreshold" for ' longer than "LampOnOffDelay" - ON ' ' If light level has been below "LampOffThreshold" ' for longer than "LampOnOffDelay" - OFF ' ' 'LampMode_TimedAndLevel: If light level has been above "LampOnThreshold" for ' longer than "LampOnOffDelay" AND time is after ' "LampOnTime" - ON ' ' If time is after "LampOffTime" - OFF ' ' 'LampMode_WithUSB: If USB connected - ON ' If USB not connected - OFF ' ' 'If Switch 2 has been pressed to turn on lamp then "LampOverride" = TRUE '--------------------------------------------------------------------------- If Data.Mode = Mode_LightTest Then Data.LampOn = 1 Else Select Data.LampMode Case LampMode_AlwaysOn Data.LampOn = 1 Case LampMode_AlwaysOff Data.LampOn = 0 Case LampMode_Timed If Data.Time.Hours >= Data.LampOnTime.Hours And Data.Time.Minutes >= Data.LampOnTime.Minutes And Data.Time.Seconds >= Data.LampOnTime.Seconds Then Data.LampOn = 1 EndIf If Data.Time.Hours >= Data.LampOffTime.Hours And Data.Time.Minutes >= Data.LampOffTime.Minutes And Data.Time.Seconds >= Data.LampOffTime.Seconds Then Data.LampOn = 0 EndIf Case LampMode_Level If LampOnLevelCount = LampOnOffDelay Then Data.LampOn = 1 ElseIf LampOffLevelCount = LampOnOffDelay Then Data.LampOn = 0 EndIf Case LampMode_TimedAndLevel If Data.Time.Hours >= Data.LampOnTime.Hours And Data.Time.Minutes >= Data.LampOnTime.Minutes And Data.Time.Seconds >= Data.LampOnTime.Seconds Then If LampOnLevelCount = LampOnOffDelay Then Data.LampOn = 1 ElseIf LampOffLevelCount = LampOnOffDelay Then Data.LampOn = 0 EndIf EndIf If Data.Time.Hours >= Data.LampOffTime.Hours And Data.Time.Minutes >= Data.LampOffTime.Minutes And Data.Time.Seconds >= Data.LampOffTime.Seconds Then Data.LampOn = 0 EndIf Case LampMode_WithUSB If HID.Attached Then Data.LampOn = 1 Else Data.LampOn = 0 EndIf EndSelect If LampOverride = True Then Data.LampOn = 1 EndIf EndIf '---Switch LED-------------------------------------------------------------- ' 'Turn on or off switch backlight LED depending on SwitchLED Mode: ' 'SwitchLEDMode_Always On: ON ' 'SwitchLEDMode_AlwaysOff: OFF ' 'SwitchLEDMode_AsLamps: Same state as front panel lamp '--------------------------------------------------------------------------- If Data.Mode = Mode_LightTest Then Data.SwitchLedOn = 1 Else Select Data.SwitchLedMode Case SwitchLEDMode_AlwaysOn Data.SwitchLedOn = 1 Case SwitchLEDMode_AlwaysOff Data.SwitchLedOn = 0 Case SwitchLEDMode_AsLamps Data.SwitchLedOn = Data.LampOn EndSelect EndIf '---Alarm LED--------------------------------------------------------------- ' 'LED ON when Alarm is set, OFF otherwise '--------------------------------------------------------------------------- If Data.Mode = Mode_LightTest Then Data.AlarmLedOn = 1 Else Data.AlarmLedOn = Data.AlarmSet EndIf '---PM LED------------------------------------------------------------------ ' 'In Normal mode, LED ON if Time is after 1159hrs, OFF otherwise ' 'In Alarm Set mode, LED ON if Alarm Time is after 1159hrs, OFF otherwise '--------------------------------------------------------------------------- Select Data.Mode Case Mode_LightTest Data.PMLedOn = 1 Case Mode_Normal If Data.Time.Hours > 11 Then Data.PMLedOn = 1 Else Data.PMLedOn = 0 EndIf Case Mode_AlarmSet If Data.Alarm.Hours > 11 Then Data.PMLedOn = 1 Else Data.PMLedOn = 0 EndIf EndSelect End Sub '=============================================================================== 'Main Program '------------------------------------------------------------------------------- '---Set-up Input / Output for all pins #if Debug_Mode = True Then Output(DebugPin0) Output(DebugPin1) #endif Output(SwitchLedRedPin) Output(SwitchLedGreenPin) Output(SwitchLedBluePin) Output(PMLedRedPin) Output(PMLedGreenPin) Output(PMLedBluePin) Output(AlarmLedRedPin) Output(AlarmLedGreenPin) Output(AlarmLedBluePin) Output(LampPin) Output(BuzzerPin) Input(Switch0) Input(Switch1) Input(Switch2) Input(LightSensor) '---Set all outputs low PORTA = %00000000 PORTB = %00000000 PORTC = %00000000 '---Set-up A/D module: ADCON1 = %00001110 '}Vref set to Vdd and Vss; '}AN0 is analogue input '}Result left justified; ADCON2 = %00000010 '}Aquisition time set to manual; '}Conversion clock set to Fosc/32. ADCON0 = %00000001 'Channel AD0 selected, Module turned on '---Set-up Timer2 to give 1mS roll-over: T2CON = %01111001 'Pre 1:4, Post 1:16 PR2 = 124 '}125 = 1mS period (take off 1 cycle for '}timer reload) Timer2InterruptFlag = False Timer2InterruptEnable = False '---Initialise all variables mS10Count = 0 ms100Count = 0 mS1000Count = 0 LedPWMCounter = 0 Switch0DebounceCount = 0 Switch1DebounceCount = 0 Switch2DebounceCount = 0 Switch0DownCount = 0 Switch1DownCount = 0 Switch2DownCount = 0 Switch1RepeatCount = 0 Switch2RepeatCount = 0 Switch0Pressed = False Switch1Pressed = False Switch2Pressed = False Switch0Held = False Switch1Held = False Switch2Held = False Buzzer100mSToggle = 0 Buzzer1SecToggle = 0 AlarmSoundCount = 0 LampOnLevelCount = 0 LampOffLevelCount = 0 LampOverride = False LampFlashCount = 0 Clear(HID.Buffer) '---Load default values for settings Data.SwitchLed.Red = 4 Data.SwitchLed.Green = 4 Data.SwitchLed.Blue = 12 Data.SwitchLedMode = SwitchLEDMode_AlwaysOn Data.PMLed.Red = 0 Data.PMLed.Green = 12 Data.PMLed.Blue = 8 Data.AlarmLed.Red = 9 Data.AlarmLed.Green = 0 Data.AlarmLed.Blue = 3 Data.LampBrightness = 7 Data.LampMode = LampMode_WithUSB Data.LampOnThreshold = 70 Data.LampOffThreshold = 150 '---Set servos to their upper limit ServoHours = ServoLookup.Hours(Bound(ServoLookup.Hours)) ServoMinutes = ServoLookup.Minutes(Bound(ServoLookup.Minutes)) Servo.On 'Start servicing servos DelayMS(1000) 'Delay allows servos to move to limit UpdateDisplay 'Set servos to correct position StartADConversion = True 'Get first A/D reading Timer2On = True 'Turn on Timer2 '===Main Program While-Wend Loop Begins Here==================================== While True Repeat '}Wait for 1mS to occur by Until Tick = True '}checking Timer2 interrupt flag Tick = False 'Reset interrupt flag #if Debug_Mode = True Then High(DebugPin0) #endif '---1mS - Everything in this section is done every 1mS-------------------------- Inc(mS10Count) '} Inc(ms100Count) '}Increase all period counters Inc(mS1000Count) '} HID.Service 'Service USB connection UpdateLightPWM '---10mS - Everything in this section is done every 10mS------------------------ If mS10Count = 10 Then #if Debug_Mode = True Then High(DebugPin1) #endif mS10Count = 0 CheckButtons ActionButtons #if Debug_Mode = True Then Low(DebugPin1) #endif EndIf '---100mS - Everything in this section is done every 100mS---------------------- If ms100Count = 100 Then ms100Count = 0 UpdateDisplay 'When alarm is sounding, buzzer makes 5 beeps per second and is on for 'one second, off for one second, on for one, off for one etc. 'Buzzer on/off is controlled by two bits - Buzzer100msToggle and 'Buzzer1SecToggle. BuzzerPin = Data.AlarmOn And Buzzer100mSToggle And Buzzer1SecToggle Buzzer100mSToggle = Not(Buzzer100mSToggle) If HID.Attached Then '} '}If USB connected, send Data HID.WriteArray(HID.Buffer, 32) '} If HID.DataAvailable Then '} '}If USB data available from HID.ReadArray(HID.Buffer, 32) '}the PC, read it in '} EndIf '} EndIf EndIf '---1000mS - Everything in this section is done every 1000mS-------------------- If mS1000Count = 1000 Then mS1000Count = 0 UpdateTime Buzzer1SecToggle = Not(Buzzer1SecToggle) If Data.Time.Hours = Data.Alarm.Hours And '} Data.Time.Minutes = Data.Alarm.Minutes And '} Data.Time.Seconds = Data.Alarm.Seconds And '}If Alarm Time reached Data.AlarmSet = 1 Then '}and alarm is set, '}sound alarm Data.AlarmOn = 1 '} Buzzer1SecToggle = 1 '} Buzzer100mSToggle = 1 '} AlarmSoundCount = 0 EndIf If Data.AlarmOn = 1 Then '}Keep track of how long alarm '}has been sounding Inc(AlarmSoundCount) '} If AlarmSoundCount = MaxAlarmSoundTime Then '}If alarm has been '}sounding for max time, Data.AlarmOn = 0 '}turn it off '} EndIf '} EndIf 'Get light sensor reading: Data.LightLevel = ADRESH '}Get result of A/D conversion StartADConversion = True '}and start next conversion If Data.LightLevel <= Data.LampOnThreshold Then '}If light level '}is below ON LampOffLevelCount = 0 '}threshold, Inc(LampOnLevelCount) '}increase the ON If LampOnLevelCount > LampOnOffDelay Then '}counter LampOnLevelCount = LampOnOffDelay '} EndIf '} ElseIf Data.LightLevel >= Data.LampOffThreshold Then '}If light level '}is above OFF LampOnLevelCount = 0 '}threshold, Inc(LampOffLevelCount) '}increase the OFF If LampOffLevelCount > LampOnOffDelay Then '}counter LampOffLevelCount = LampOnOffDelay '} EndIf '} Else '}Light level is '}not above or LampOnLevelCount = 0 '}below ON / OFF LampOffLevelCount = 0 '}thresholds, reset '}both counters EndIf '} If LampFlashCount > 0 Then '}If switch 2 was pressed to flash '}the front panel lamp then decrease Dec(LampFlashCount) '}the Flash counter. If LampFlashCount = 0 Then '} '}When the Flash counter reaches LampOverride = False '}zero, clear the Lamp Override '}flag EndIf '} EndIf EndIf #if Debug_Mode = True Then Low(DebugPin0) #endif Wend
Congratulations for reading this far! In case you're wondering, whilst she's too nice to say so to my face, I suspect my wife does think it's a little bit pointless...