
OK, so this project might not be a killer robot or auto-guided missile, though for what ever reason(s) it has turned out to be a decently sized program! The scenario that brought this project to life would most likely be quite unique and rarely encountered, though I thought it would still be worth sharing as the program has a few handy little features and nifty tricks.
I fix/maintain a lot of gear at work that revolves around what is known as "Scheduled Servicing". Nothing new here - if an item has a 60 day Bay Service or Calibration, then every 60 days that servicing is carried out. Some are 30 days, others are 120 days and so forth. There's a problem here though - my work environment complies with Aeronautical AMO regulations (Approved Maintenance Organisation), a very thorough set of regulations/practices to keep a safe and accountable work environment.
I've recently discovered a deficiency with day type calculates for scheduled servicing. Consider the date is 25-Jul-2010, and you've just completed a calibration on a bit of gear that is due again in 60 days. When completing the paperwork, one would normally assume "60 days is two months" and write up the paperwork accordingly (next CAL due 25-Sep-2010). Heaven-behold anyone who uses the item from 23rd - 25th of September is actually in violation of a couple of regulations.
Why? Well the answer has most likely already hit home to most people by now - not every month has 30 days in it. Not only does this highlight possibility to unwillingly violate AMO regs, though there is the opportunity to slightly increase productivity (365 days/12 months = 30.42 days, not 30).
First option is a gimme - Just learn how many days in every month and accommodate for leap years etc... Easy enough, though I would forget it in about 32 minutes.
Second option - use an Excel spreadsheet and calculate the date. For example, the above scenario would be easily solved with the following expression "=DATE(2010,7,25)+60". Excel would display "23/09/2010" in the cell. Given we only have one PC at the end of our work area, it seems a mundane task to stop you in your tracks just to calculate a date.
Third option - make a box that does it for me and can be setup in-situ for easy use. Seems a whole lot of work for such a small deal, though I had nothing on during a Sunday arvo - and got to it.

I'm using a DS1307 RTC (Real Time Clock) as the backbone of time/date keeping. There are a handful of benefits with using the device over a dedicated PIC program - the biggest one is probably a lithium battery with 48mAh or greater will back up the DS1307 for more than 10 years in the absence of power. Not that I plan on using the device once every 10 years - though keeping the cost of consumables down is definitely on the agenda. I'm using two AA lithium batteries as a backup power source on the DS1307 - purely to prevent the loss of data in-case of power interruption. They also allow the unit to be unplugged, transported and reconnected without re-configuring settings (even stored in a draw for a week or two).
The first revision of the project used two switches. One for changing the Mode (projected date forecast), and the other to initiate settings configuration (to set the time/date). It soon became apparent that hitting away on a single button to increment variables was quite annoying - I did want to make something usable by the lowest common denominator at the end of the day. What I trialed and settled on was one button and a potentiometer. I ventured into an interesting method of interaction that will probably form the basis of my future designs.
Before I get to in-depth, consider the normal mode of operation - date projection. I want a box that will tell me what date it is in 30, 60, 120 days time (there's a few other ones, though I'll keep it easy). So, in normal operation, Mode One could display the date in 30 days time, Mode Two in 120 days time and so on. What if I put together a program that automatically calculates how many modes there are, and turns a 10-bit ADC result from the potentiometer into segments for different modes.
For example, I have three modes (30, 60, 120 days) - now simply break the 10-bit potentiometer result into 3 segments (1023/3=341). Therefore, a reading from 0-341 would imply Mode One, 342 to 682 is Mode Two and 683 to 1023 would be Mode Three. I know I'm referring to the processes as "Modes", its more so because the same approach can be transposed into different scenarios quite easily.
"Jittering??" I here some people yell out. For those that didn't yell out, perhaps this would trigger a thought - what happens if the potentiometer is biased at a point of change, such as hovering between 341 and 342. This would make the program jump between Mode One and Mode Two rapidly.... There are a number of methods that can be used to completely remove jitter, and in all honesty I did not experience any "jumpiness" even without the methods employed. First of all, build a stable voltage regulator with all the normal trimmings. Decent input/output caps, a decoupling cap as-close-as-possible to the PIC's Vdd/Vss. The additional method I took was taking many ADC readings and calculating the Median.
Finding the Median from a pool of samples is different to averaging. Consider some ADC samples; 512, 512, 511, 512, 1023. There are 5 readings, they appear to be sitting at about 512, though there is a noise spike which returned 1023. Calculating the average would include the undesirable noise (512+512+511+512+1023) / 5 = 614.
The Median can be found by arranging the results from smallest to largest, and then picking the sample in the middle. In this case, 511, 512, 512, 512, 1023. Our result, 512. This is a much more accurate method of sampling analogue signals, though it takes a little more time. I did have my own function to do this very job, though Swordfish already has such a function as part of the ADC.bas library; ADC.ReadMedian(ChannelNumber). It will take 64 samples and return the Median.
With the above considerations in place, your left with a blissfully easy-to-use and creative method for end users to interact. Here's a video of the unit in use;
How about I break things up a little bit and share the whole program (more tips below the code)
// Program: 0028 Projected Date // Author: Graham admin[at]digital-diy[dot]com // Written: 31/1/2010 // Purpose: To project the date in X days time. // Notes: Edit the constant PresetDays(x) to configure preset date ranges. // While the DS1307 offers leap year compensation as an RTC, this program // accommodates leap years with date forecasting as required. // // There are interlocks to ensure safe operation (when setting time/date for example). // // Edit the constant PresetDays(x) with careful attention to setting the array index // size. By doing so, the program will automatically use the preset date forecast // ranges. Device = 18F2620 Clock = 32 Config OSC = INTIO67 #option LCD_DATA = PORTB.0 // define custom LCD configuration #option LCD_EN = PORTA.1 // #option LCD_RW = PORTA.2 // #option LCD_RS = PORTA.3 // Include "INTOSC8PLL.bas" // this is a User Library that configures the INT OSC to 8Mhz on powerup Include "Convert.bas" // other system libraries used Include "LCD.bas" // Include "I2C.bas" // Include "ADC.bas" // // this constant is really the only one that needs to be changed. by default, // Const PresetDays(7) as Word = (15,30,45,60,90,120,365) defines date projection // with 15, 30, 45... days. Perhaps the end user does not require 365 days projection, // simply change the constant to: Const PresetDays(6) as Word = (15,30,45,60,90,120) // The program will handle the rest automatically. Nifty. Const PresetDays(8) As Word = (15,30,45,60,90,120,180,365) // program constants Const DaysOfMonth(12) As Byte = (31,28,31,30,31,30,31,31,30,31,30,31), DaysOfWeek(7) As String = ("Mon","Tue","Wed","Thu","Fri","Sat","Sun"), ModeChannel = 0 // program variables/defines Dim Day_Poll As Byte, ReCalc As Boolean, Mode As Word, SetDateTime As PORTB.4 // program structured variables Structure TTime Second As Byte // Second (0..59) Minute As Byte // Minute (0..59) Hour As Byte // Hour (0..11 or 0..23) End Structure Dim Time As TTime Structure TProjectedDate Day As Byte // Date (0..31) Month As Byte // Month (1..12) Year As Byte // Year (0..99) DayOfWeek As Byte // day of the week (1..7) End Structure Dim ProjectedDate As TProjectedDate Public Structure TDate Day As Byte // Date (0..31) Month As Byte // Month (1..12) Year As Byte // Year (0..99) DayOfWeek As Byte // day of the week (1..7) End Structure Dim Date As TDate // this function checks if the passed variable Year is in fact a leap year or not. Function LeapYear(ByVal Year As Byte) As Boolean Year = Year + 2000 // scale to correct date Result = False // reset the function result If Year Mod 4 = 0 Then // Mod returns the remainder of a division Result = True If Year Mod 100 = 0 Then If Year Mod 400 <> 0 Then Result = False EndIf EndIf EndIf End Function // increment by one day, handling month/year rollovers at the same time Sub IncrementDay(ByRef Day, Month, Year, MaxDays As Byte) If Day = MaxDays Then Day = 1 If Month = 12 Then Month = 1 Inc(Year) Else Inc(Month) EndIf Else Inc(Day) EndIf End Sub // increment the day of week Sub IncrementDayOfWeek(ByRef DayOfWeek As Byte) Inc(DayOfWeek) If DayOfWeek = 8 Then DayOfWeek = 1 EndIf End Sub // this is the higher sub routine for incrementing days. // it utilizes the above subs to perform functions and processes Sub CalculateProjectionDate(ByVal ProjectedDays As Word) Dim LastDay As Byte ProjectedDate.Day = Date.Day ProjectedDate.Month = Date.Month ProjectedDate.Year = Date.Year ProjectedDate.DayOfWeek = Date.DayOfWeek While ProjectedDays > 0 If LeapYear(ProjectedDate.Year) Then If ProjectedDate.Month = 2 Then LastDay = 29 Else LastDay = DaysOfMonth(ProjectedDate.Month-1) EndIf Else If ProjectedDate.Month = 2 Then LastDay = 28 Else LastDay = DaysOfMonth(ProjectedDate.Month-1) EndIf EndIf IncrementDay(ProjectedDate.Day,ProjectedDate.Month,ProjectedDate.Year,LastDay) IncrementDayOfWeek(ProjectedDate.DayOfWeek) Dec(ProjectedDays) Wend End Sub // DS1307 set time sub routine Sub SetTime(ByRef Hour, Minute, Second, DayOfWeek, Day, Month, Year As Byte) I2C.Start I2C.WriteByte(%11010000) // Send the RTC address, and put it in write mode I2C.WriteByte($00) // Move the pointer to first register I2C.WriteByte(DecToBCD(Second)) // Write each byte I2C.WriteByte(DecToBCD(Minute)) // I2C.WriteByte(DecToBCD(Hour)) // I2C.WriteByte(DecToBCD(DayOfWeek)) // I2C.WriteByte(DecToBCD(Day)) // I2C.WriteByte(DecToBCD(Month)) // I2C.WriteByte(DecToBCD(Year)) // I2C.WriteByte(0) // I2C.Stop End Sub // DS1307 GetTime sub routine Sub GetTime() I2C.Start I2C.WriteByte(%11010000) I2C.WriteByte($00) I2C.Restart I2C.WriteByte(%11010001) Time.Second = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE)) Time.Minute = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE)) Time.Hour = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE)) Date.DayOfWeek = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE)) Date.Day = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE)) Date.Month = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE)) Date.Year = BCDToDec(I2C.ReadByte(I2C_NOT_ACKNOWLEDGE)) I2C.Stop End Sub // return a three letter month abbreviation Function MonthToMMM(ByRef Month As Byte) As String(4) Select Month Case 1 Result = "Jan" Case 2 Result = "Feb" Case 3 Result = "Mar" Case 4 Result = "Apr" Case 5 Result = "May" Case 6 Result = "Jun" Case 7 Result = "Jul" Case 8 Result = "Aug" Case 9 Result = "Sep" Case 10 Result = "Oct" Case 11 Result = "Nov" Case 12 Result = "Dec" End Select End Function // debounce routine // this routine was edited to provide greater switch debounce control Sub Debounce(ByVal Period As Byte) DelayMS(Period) While SetDateTime = 0 While SetDateTime = 0 Wend DelayMS(Period) Wend End Sub // function that samples the Mode potentiometer and returns the desired decoded value Function GetModePosition(ByVal Min, Max As Word) As Word Dim Segments, i As Byte, Range As Word Result = ADC.ReadMedian(ModeChannel) Range = Max - Min // split the 10 bit ADC resolution into known segments for the array Segments = 1023 / (Range + 1) // determine where the ADC result lies throughout the segments & return the desired array value i = 0 Repeat If Result <= ((i+1) * Segments) Then Break EndIf Inc(i) Until i = Range Result = i + Min End Function // multi-purpose function that handles data collection from the Mode potentiometer. // there are special considerations for DaysOfWeek and Year modes, to make the data // more presentable to the end user. Function GetNewVal(ByVal DisplayString As String, ByVal OldValue As Word, ByVal Min, Max As Word, ByVal Length As Byte = 2) As Word Dim Update As Boolean, tmpPosition As Word Result = OldValue If Result > Max Then Result = Max ElseIf Result < Min Then Result = Min EndIf tmpPosition = GetModePosition(Min,Max) Result = tmpPosition Update = True Debounce(10) LCD.WriteAt(2,1," ") Repeat If Update = True Then If DisplayString = "DOW" Then LCD.WriteAt(2,1,DisplayString,": ", DaysOfWeek(Result-1)) ElseIf DisplayString = "Yr " Then LCD.WriteAt(2,1,DisplayString,": ", DecToStr(Result+2000,4)) Else LCD.WriteAt(2,1,DisplayString,": ", DecToStr(Result,Length)) EndIf Update = False EndIf tmpPosition = GetModePosition(Min,Max) If Result <> tmpPosition Then Result = tmpPosition Update = True EndIf Until SetDateTime = 0 End Function // sub routine that walks a user through setting the time. // it emplaces interlocks to insure the user can not set the date/time // beyond valid settings. Sub SetTheTime() LCD.Cls LCD.WriteAt(1,1," Set Date ") Debounce(10) Date.Year = GetNewVal("Yr ",Date.Year,10,99) Date.Month = GetNewVal("Mth",Date.Month,1,12) If Date.Month <> 2 Then Date.Day = GetNewVal("Day",Date.Day,1,DaysOfMonth(Date.Month-1)) Else If LeapYear(Date.Year) Then Date.Day = GetNewVal("Day",Date.Day,1,29) Else Date.Day = GetNewVal("Day",Date.Day,1,28) EndIf EndIf Date.DayOfWeek = GetNewVal("DOW",Date.DayOfWeek,1,7,1) LCD.WriteAt(1,1," Set Time ") Time.Hour = GetNewVal("Hr ",Time.Hour,0,23) Time.Minute = GetNewVal("Min",Time.Minute,0,59) Time.Second = GetNewVal("Sec",Time.Second,0,59) SetTime(Time.Hour, Time.Minute,Time.Second,Date.DayOfWeek,Date.Day,Date.Month,Date.Year) LCD.Cls LCD.WriteAt(1,1,"Saved") DelayMS(2000) End Sub // monitors for a change in the date, if one occurs a recalculation is requested Sub CheckForRefresh() GetTime() If Date.Day <> Day_Poll Then ReCalc = True Day_Poll = Date.Day EndIf End Sub // handles the Mode potentiometer settings while in date projection mode. // makes use of the preloaded constant array, and performs calculations to break the 10-bit ADC // result into segments for desired settings. Function GetDayMode() As Word Dim Segments, ArrayBound, i As Byte Result = ADC.ReadMedian(ModeChannel) ArrayBound = Bound(PresetDays) // split the 10 bit ADC resolution into known segments for the array Segments = 1023 / (ArrayBound + 1) // determine where the ADC result lies throughout the segments & return the desired array value i = 0 Repeat Inc(i) If Result <= (i * Segments) Then Break EndIf Until i = (ArrayBound + 1) Result = PresetDays(i-1) End Function // monitor for a change with the Mode potentiometer Sub CheckForModeChange() Dim tmpWord As Word tmpWord = GetDayMode If tmpWord <> Mode Then Mode = tmpWord ReCalc = True EndIf End Sub // monitor for a recalculation request, and handle if necessary Sub CheckForRecalculate() If ReCalc = True Then CalculateProjectionDate(Mode) LCD.Cls LCD.WriteAt(1,1,"Date: ", DecToStr(Date.Day,2), "-", MonthToMMM(Date.Month), "-", DecToStr(Date.Year,2)) LCD.WriteAt(2,1,"+", DecToStr(Mode,3),": ",DecToStr(ProjectedDate.Day,2),"-",MonthToMMM(ProjectedDate.Month),"-",DecToStr(ProjectedDate.Year)) ReCalc = False Debounce(10) EndIf End Sub // monitor for a press of the Set Time switch. // the user must hold the switch for ~2 seconds to enter the mode. Sub CheckForSetTime() Dim tmpByte, CurPos As Byte If SetDateTime = 0 Then LCD.Cls LCD.WriteAt(1,1,"Set Time?") LCD.WriteAt(2,1,"Hold button") CurPos = 12 // gradually display "....." while the button is pressed. // should the user depress the button before 2 seconds, the // routine will break and request a recalculation For tmpByte = 1 To 40 DelayMS(40) If SetDateTime = 1 Then Break EndIf If tmpByte Mod 5 = 0 Then LCD.WriteAt(2,CurPos,".") Inc(CurPos) EndIf Next If tmpByte = 41 Then SetTheTime() EndIf // request recalculation purely for LCD refresh ReCalc = True EndIf End Sub // start of program OSCTUNE.6 = 1 // enables PLL ADCON1 = %00001110 // channel 0 Analogue, everything else digital SetConvTime(FRC) // using a 10K Potentiometer - FRC offers good accuracy at high impedance I2C.Initialize() // set up the hardware I2C module Input(SetDateTime) // SetDateTime - button used to enter set date/time mode INTCON2.7 = 0 // enable PORTB weakpullups (for use with the above input) DelayMS(150) // allow the circuit to power up and stabilise LCD.Cls // splash screen LCD.WriteAt(1,1," Projected Date ") // LCD.WriteAt(2,1," By Mitchy 2010 ") // DelayMS(2000) // LCD.Cls // GetTime() // get the current date/time from DS1307 Day_Poll = 0 // reset the day poll register (used to restrict LCD updates) Mode = GetDayMode // initialize the program with current status of controls ReCalc = True // force the program to recalculate algorithms CheckForRecalculate() // While True // main program loop CheckForSetTime() // check to see if the time set button has been pushed CheckForRefresh() // handles polling of seconds and days to control the refresh rate of the LCD CheckForModeChange() // check if user has requested mode change CheckForRecalculate() // check if any recalculations are ready, could occur from Date.Day change, or Mode change Wend
OK, so thats a pretty big program to do something that appears to be simple enough. Another little trick I've picked up along the way with this project is calculating if its a leap year or not.
I always thought it was as easy as "if a year is divisable by 4 then its a leap year". Clearly I've been misslead at somestage! Here's the method;
1. If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5.
2. If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4.
3. If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5.
4. The year is a leap year (it has 366 days).
5. The year is not a leap year (it has 365 days).
In terms of program the above method into Swordfish;
FunctionLeapYear(ByVal Year As Byte) As Boolean
Year = Year + 2000 // scale to correct date
Result = False // reset the function result
If Year Mod 4 = 0 Then // Mod returns the remainder of a division
Result = True
If Year Mod 100 = 0 Then
If Year Mod 400 <> 0 Then
Result = False
endif
EndIf
EndIf
End Function
I scale the year up by 2000 as the DS1307 works with a scale of -2000. For example, the year 2010 would be read as 10.
Thoughts and comments are more then welcome!
Comments
Nice work Graham! Love using a pot as the user interface - a simple solution using available parts.
Is the trim piece around the LCD a standard part? Looks very nice.
The video link appears to be broken by the way.
Thanks Jon, should be working now - had some issues with the upload earlier.
The trim around the LCD is the standard metal frame around the display. This project turned out alright with the cut-out, if it didn't then I normally use some black goop/silicon to form a bridge from the case to the LCD screen. (random tip of the day)