Real-Time Clock Project

Thread Starter

Kittu20

Joined Oct 12, 2022
434
I'm planning to make a real-time clock project as a hobby that utilizes a 1ms timer interrupt using a PIC microcontroller. My goal is to accurately count interrupts to keep track of minutes, seconds, hours, days, months, and years.

I have attached the code I've written and a screenshot from the simulator to provide a clearer view of what I've done so far.

C:
//PIC18F45K80
#include <xc.h>
#include "config.h"

volatile int count = 0; // Declare count as a global variable

void initializeHardware() {
    // Set LATx registers to initial states (all low)
    LATA = 0x00;
    LATB = 0x00;
    LATC = 0x00;
    LATD = 0x00;
    LATE = 0x00;

    // Set TRISx registers to configure pins as outputs (all output)
    TRISA = 0x00;
    TRISB = 0x00;
    TRISC = 0x00;
    TRISD = 0x00;
    TRISE = 0x00;

    // Additional configuration for unused peripherals
    ANCON0 = 0x00; // Set all analog pins to digital mode
    ANCON1 = 0x00; // Set all analog pins to digital mode
    CM1CON = 0x00; // Turn off Comparator 1
    CM2CON = 0x00; // Turn off Comparator 2
    ADCON0 = 0x00; // Disable A/D conversion
    ADCON1 = 0x00; // Disable A/D conversion
    ADCON2 = 0x00; // Disable A/D conversion
}
    // Timer0 configuration for a 1ms interrupt
void configureTimer0() {
  
    T0CON = 0x88; // 16-bit mode, 1:256 prescaler

    // Preload Timer0 with the values 0xEC for the high byte and 0x78 for the low byte
    TMR0H = 0xEC;
    TMR0L = 0x78;

    // Enable global interrupt (GIE = 1) and Timer0 overflow interrupt (TMR0IE = 1)
    INTCON = 0xA0;
}

void main(void) {
    initializeHardware();
    configureTimer0();

    while (1) {
        //  main code here
    }
}

void __interrupt() isr() {
    if (TMR0IF == 1) {  // Timer0 overflow interrupt flag bit
        TMR0IF = 0;     // Clear the interrupt flag
        count++;        // Increment a counter

        // Reload Timer0 with the precise preload values for a 1ms interrupt
        TMR0H = 0xEC;
        TMR0L = 0x78;
    }
}
I'm interested in measuring the interrupt latency of my PIC microcontroller in this project. Could you please guide me on how to accurately measure interrupt latency time?

Attached Files:


1694004180795.png


I'm open to any feedback or guidance you can offer. This project is all about learning and having fun with microcontrollers, so I appreciate any help you can provide. @JohnInTX

Thank you in advance for your time and expertise!
 
Last edited:

John P

Joined Oct 14, 2008
2,013
The crystal attached to a microcontroller doesn't produce a very accurate timebase. If you want accuracy like a digital watch, you might do better with an add-on unit like this, available lots of places for a low cost. The designers have done all the work to make an accurate clock, it communicates with your processor via I2C and it has its own battery (so it keeps time while the processor isn't powered). The claim is that the battery lasts several years.
 

Attachments

MrChips

Joined Oct 2, 2009
29,802
Depending on your country and the electricity utility, the 50/60Hz mains frequency is more accurate than any crystal oscillator, provided that you do not experience frequent brownouts and blackouts.
 

dl324

Joined Mar 30, 2015
16,111
My goal is to accurately count interrupts to keep track of minutes, seconds, hours, days, months, and years.
What are you going to do with this time?

I made a scrolling LED clock (single digit) using a Raspberry Pi Zero W. I let the OS take care of updating time across power failures. Haven't bothered to see what the clock does during power outages. I have the Pi on a UPS, but power to the display isn't.
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
The crystal attached to a microcontroller doesn't produce a very accurate timebase. If you want accuracy like a digital watch, you might do better with an add-on unit like this, available lots of places for a low cost. The designers have done all the work to make an accurate clock, it communicates with your processor via I2C and it has its own battery (so it keeps time while the processor isn't powered). The claim is that the battery lasts several years.
Thank you for your advice. I completely understand that there are many RTC (Real-Time Clock) chips available in the market that offer high accuracy and reliability. However, in my current project, I have chosen to develop a software-based RTC system for a specific reason.

My primary goal is to explore and enhance my coding skills, and I believe that building a software-based RTC system provides a valuable learning experience. While I acknowledge that hardware RTC chips offer superior accuracy and stability, my focus at the moment is on honing my programming abilities.

I'm looking forward to the learning journey that comes with developing this software-based RTC system.
 

Ya’akov

Joined Jan 27, 2019
8,488
My primary goal is to explore and enhance my coding skills, and I believe that building a software-based RTC system provides a valuable learning experience. While I acknowledge that hardware RTC chips offer superior accuracy and stability, my focus at the moment is on honing my programming abilities.
It can be frustrating to spend time making something that “doesn’t work” so a suggestion for you to add on after you get the basic version done is to try using a GPS module that offers 1pps output to discipline your clock.

The GPS will be very accurate and you can use it as a standard to calibrate your timing loop. One of your biggest problems will be the sensitivity of the crystal to temperature so if this idea is attractive I would certainly recommend adding a homebuilt crystal oven made with a resistor. This gives some interesting hardware experience as well.

Bonus points for using the PIC as the temperature control.
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
The GPS will be very accurate and you can use it as a standard to calibrate your timing loop.
Absolutely! Adding a GSM (Global System for Mobile Communications) module along with GPS can add some exciting capabilities to project.

By combining GPS and GSM, we can create location-based triggers or alerts. For example, project could send an SMS when it reaches a specific geographic location or enters/exits a predefined area.
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
I've written some code and would greatly appreciate any suggestions or feedback, particularly regarding my Interrupt Service Routine (ISR). I would like to know if there are any optimizations or improvements I can make to this ISR

Here's my code :

C:
#include <xc.h>
#include "config.h"

volatile int count = 0; // Declare count as a global variable

// Timekeeping variables
volatile uint16_t milliseconds = 0;
volatile uint8_t seconds = 0;
volatile uint8_t minutes = 0;
volatile uint8_t hours = 0;
volatile uint8_t days = 1;
volatile uint8_t months = 1;
volatile uint16_t years = 2023;

void initializeHardware() {
    // Set LATx registers to initial states (all low)
    LATA = 0x00;
    LATB = 0x00;
    LATC = 0x00;
    LATD = 0x00;
    LATE = 0x00;

    // Set TRISx registers to configure pins as outputs (all output)
    TRISA = 0x00;
    TRISB = 0x00;
    TRISC = 0x00;
    TRISD = 0x00;
    TRISE = 0x00;

    // Additional configuration for unused peripherals
    ANCON0 = 0x00; // Set all analog pins to digital mode
    ANCON1 = 0x00; // Set all analog pins to digital mode
    CM1CON = 0x00; // Turn off Comparator 1
    CM2CON = 0x00; // Turn off Comparator 2
    ADCON0 = 0x00; // Disable A/D conversion
    ADCON1 = 0x00; // Disable A/D conversion
    ADCON2 = 0x00; // Disable A/D conversion
}
    // Timer0 configuration for a 1ms interrupt
void configureTimer0() {
    
    T0CON = 0x88; // 16-bit mode, 1:256 prescaler

    // Preload Timer0 with the values 0xEC for the high byte and 0x78 for the low byte
    TMR0H = 0xEC;
    TMR0L = 0x78;

    // Enable global interrupt (GIE = 1) and Timer0 overflow interrupt (TMR0IE = 1)
    INTCON = 0xA0;
}

void main(void) {
    initializeHardware();
    configureTimer0();

    while (1) {
        //  main code here
    }
}
void __interrupt() isr() {
    if (TMR0IF == 1) {
        TMR0IF = 0; // Clear Timer0 interrupt flag
        
        milliseconds++; // Increment milliseconds
        
        // Handle rollovers
        if (milliseconds >= 1000) { // If 1000 milliseconds have passed (1 second)
            milliseconds = 0; // Reset milliseconds
            seconds++; // Increment seconds
            
            if (seconds >= 60) { // If 60 seconds have passed (1 minute)
                seconds = 0; // Reset seconds
                minutes++; // Increment minutes
                
                if (minutes >= 60) { // If 60 minutes have passed (1 hour)
                    minutes = 0; // Reset minutes
                    hours++; // Increment hours
                    
                    if (hours >= 24) { // If 24 hours have passed (1 day)
                        hours = 0; // Reset hours
                        days++; // Increment days
                        
                        // Check for month and year changes (simplified, not accounting for leap years)
                        if (days > 30) { // If 30 days have passed (1 month, simplified)
                            days = 1; // Reset days
                            months++; // Increment months
                            
                            if (months > 12) { // If 12 months have passed (1 year, simplified)
                                months = 1; // Reset months
                                years++; // Increment years
                            }
                        }
                    }
                }
            }
        }
        
        // Reload Timer0 with the precise preload values for a 1ms interrupt
        TMR0H = 0xEC;
        TMR0L = 0x78;
        
        
    }
}
 

BobTPH

Joined Jun 5, 2013
8,069
Updating the clock count in the interrupt handler is not going to give you accurate, repeatable results. Particularly if placed where you placed it.

As already suggested, use a timer with a period register that resets automatically in hardware. That way, every period is identical in time, the way you are doing it, it is not.
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
Use Timer1 and CCP module to automatically reload the timer register.
This code should sets up Timer1 to automatically reload every 1ms using the CCP module in Compare mode. When 1ms elapses, it should triggers the CCP1 interrupt

C:
#include <xc.h>
#include "config.h"

volatile int count = 0; // Declare count as a global variable

void main() {
    // Configure Timer1
    T1CON = 0b00110000; // 16-bit Timer, Prescaler 16

    // Calculate Reload_Value for 1ms with a 20 MHz oscillator and prescaler 16
    uint16_t Reload_Value = 625; // (20,000,000 / 4 / 16) = 625

    // Configure CCP module in Compare mode
    CCP1CON = 0b00001000; // Compare mode
    CCPR1H = (Reload_Value >> 8); // Load high byte of Reload_Value
    CCPR1L = (Reload_Value & 0xFF); // Load low byte of Reload_Value

    // Enable Timer1 and CCP module
    TMR1ON = 1;
    CCP1IE = 1; // Enable CCP1 interrupt

    // Initialize INTCON register
    INTCON = 0; // Clear INTCON register

    // Enable global and peripheral interrupts
    INTCONbits.GIE = 1;  // Global Interrupt Enable
    INTCONbits.PEIE = 1; // Peripheral Interrupt Enable


    while (1) {
     
    }
}

void __interrupt() ISR() {
    if (CCP1IF) {
        // CCP1 interrupt occurred (1ms interval)
         count++;        // Increment a counter
        // Clear the CCP1 interrupt flag
        CCP1IF = 0;
    }
}
I am debugging code on simulator and I don't understand why I don't get 1ms time

clicked on Reset button

1694105574151.png

clicked on continue button

1694105676312.png

clicked on continue button
1694105795710.png

clicked on continue button

1694105884533.png
 

JohnInTX

Joined Jun 26, 2012
4,781
I am debugging code on simulator and I don't understand why I don't get 1ms time
You should clear TIMER 1 before starting it the first time.

Your code setting the timer interval looks OK but the comments are wrong. The prescaler is 8, not 16. GOOD JOB on showing your arithmetic. That made it easy to find the issue.

The timer needs to be reset after each capture. Otherwise, it interrupts at 625 counts then counts all around from 626->65535->626 again for the next capture interrupt. Consider using
CCP1CON = 0b00001011 // special event trigger which will cleanly reset TIMER1 to 0000h on each CCP interrupt.

On the first break after init, use a WATCH window and inspect the actual values in CCP1, TIMER1 etc. to be sure that the values you've specified actually are there. It is easy to make syntactical errors that compile OK but don't generate the desired values. That can save a lot of debugging time.

Set the stopwatch to 'Reset stopwatch on run' (button on the left edge of the stopwatch window) to get the elapsed time between the breaks. You need to run and break several times through the interrupts to get through the variable time it takes to get things up and running.

You should consider including the CONFIG setup in your code, too. The simulator is more forgiving than the actual hardware.

Have fun!
 
Last edited:

MrChips

Joined Oct 2, 2009
29,802
I would not reset the counter register. Allow the counter to keep counting up without stopping.
At each interrupt, add the reload value to the CCP register. You do not have to worry about 16-bit overflows. The math works out correctly.
 

BobTPH

Joined Jun 5, 2013
8,069
At each interrupt, add the reload value to the CCP register.
You don’t need to do that. You set the PR1 register once, the counter counts up to that preset value, then resets to zero and triggers an interrupt. Don’t remember what timer 1 mode bits you need to set to get that behavior.
 

JohnInTX

Joined Jun 26, 2012
4,781
You don’t need to do that. You set the PR1 register once, the counter counts up to that preset value, then resets to zero and triggers an interrupt. Don’t remember what timer 1 mode bits you need to set to get that behavior.
Mode 1011 as indicated in the post does the auto-reset of the timer when it reaches the value in CCP1 to make a period timer. Convenient!
 
Last edited:

Thread Starter

Kittu20

Joined Oct 12, 2022
434
You should clear TIMER 1 before starting it the first time.

Your code setting the timer interval looks OK but the comments are wrong. The prescaler is 8, not 16. GOOD JOB on showing your arithmetic. That made it easy to find the issue.

The timer needs to be reset after each capture. Otherwise, it interrupts at 625 counts then counts all around from 626->65535->626 again for the next capture interrupt. Consider using
CCP1CON = 0b00001011 // special event trigger which will cleanly reset TIMER1 to 0000h on each CCP interrupt.

On the first break after init, use a WATCH window and inspect the actual values in CCP1, TIMER1 etc. to be sure that the values you've specified actually are there. It is easy to make syntactical errors that compile OK but don't generate the desired values. That can save a lot of debugging time.

Set the stopwatch to 'Reset stopwatch on run' (button on the left edge of the stopwatch window) to get the elapsed time between the breaks. You need to run and break several times through the interrupts to get through the variable time it takes to get things up and running.

You should consider including the CONFIG setup in your code, too. The simulator is more forgiving than the actual hardware.

Have fun!
Thank you for pointing out the error in the comments regarding the prescaler value

If we have a 20 MHz oscillator and a prescaler of 8, the correct calculation for a 1ms timer interval would be:

Code:
// Calculate Reload_Value for 1ms with a 20 MHz oscillator and prescaler 8
uint16_t Reload_Value = 625; // (20,000,000 / 4 / 8) = 625
By using the special event trigger, we eliminate the need to manually reset Timer1 in interrupt service routine

C:
#include <xc.h>
#include "config.h"

volatile int count = 0; // Declare count as a global variable

void main() {

    // Configure Timer1 in 8-bit mode with a prescaler of 8
    T1CON = 0b00110000;

    // Calculate Reload_Value for 1ms with a 20 MHz oscillator and prescaler 8
    uint16_t Reload_Value = 625; // (20,000,000 / 4 / 8) = 625

    // Configure CCP module in Special Event Trigger mode
    CCP1CON = 0b00001011; // 0x0B

    // Load the high and low bytes of Reload_Value into CCPR1
    CCPR1H = (Reload_Value >> 8); // Load high byte of Reload_Value
    CCPR1L = (Reload_Value & 0xFF); // Load low byte of Reload_Value
     
    // Clear Timer1 registers
    TMR1H = 0;
    TMR1L = 0;

    // Clear Timer1 interrupt flag
    TMR1IF = 0;
   
    // Enable Timer1 and CCP module
    TMR1ON = 1;
    CCP1IE = 1; // Enable CCP1 interrupt

    // Initialize INTCON register
    INTCON = 0; // Clear INTCON register

    // Enable global and peripheral interrupts
    INTCONbits.GIE = 1;  // Global Interrupt Enable
    INTCONbits.PEIE = 1; // Peripheral Interrupt Enable

    while (1) {
     
    }
}

void __interrupt() ISR() {
    if (CCP1IF) {
        // CCP1 interrupt occurred (1ms interval)
         count++;        // Increment a counter
        // Clear the CCP1 interrupt flag
        CCP1IF = 0;
    }
}
Watch window

1694166629557.png

STOP Watch Window

1694166697243.png
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
I don't do the time update in the ISR. I set a flag in the ISR once every second. The time update is done in the main process loop. You have one second to complete the time and date update.
I'm curious about your thoughts.

I am updating the timekeeping variables in the main function rather than directly in the ISR.
C:
if (timeFlag) {
    // Increment seconds
    seconds++;

    // Handle rollovers for seconds
    if (seconds >= 60) {
        seconds = 0;
        minutes++;

        // Handle rollovers for minutes
        if (minutes >= 60) {
            minutes = 0;
            hours++;

            // Handle rollovers for hours
            if (hours >= 24) {
                hours = 0;
                days++;

                // Check for month and year changes (simplified, not accounting for leap years)
                if (days > 30) { // If 30 days have passed (1 month, simplified)
                    days = 1;
                    months++;

                    if (months > 12) { // If 12 months have passed (1 year, simplified)
                        months = 1;
                        years++;
                    }
                }
            }
        }
    }

    // Reset the flag
    timeFlag = 0;
}
 
Last edited:
Top