RTC to POSIX time

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
Xeno
Member
Member
Posts: 56
Joined: Tue Oct 10, 2023 7:40 pm

RTC to POSIX time

Post by Xeno »

I am having trouble figuing out how to convert the time i get from the RTC to a Unix Timestamp.

Code: Select all

typedef struct {
    uint8_t sec;
    uint8_t min;
    uint8_t hour;
    uint8_t day;
    uint8_t month;
    uint64_t year;
} human_time_t;

//Needs to be converted to time_t
Does anyone know how to do this???
nullplan
Member
Member
Posts: 1789
Joined: Wed Aug 30, 2017 8:24 am

Re: RTC to POSIX time

Post by nullplan »

Ah, time. Yes, that gets quite complicated quite fast. The issue is mostly to do with leap days, actually. I am using an abstraction I call the GDN, or Gregorian Day Number (similar to Julian Day Number), which is the number of days since January 1, year 1 (which is a hypothetical date; it never happened; but it is a good abstraction).

In the Gregorian calendar, there are 365 days in a normal year, 366 days in a leap year. With the basis being year 1, the nice thing is that a leap year, if it occurs, will always be at the end of a four-year cycle. And a normal four-year cycle will have 1461 days. In the Gregorian calendar, of course, all centuries that are not divisible by 4 end in a non-leap cycle. But we can deal with that one layer further up. A normal century has 76 normal years and 24 leap years, making a total of 36,524 days. A leap century of course has 36,525 days. And a cycle, a group of four centuries, has 146,097 days.

With these definitions out of the way, converting a calendar year to a GDN is quite simple. I am assuming the normal C struct tm here, because it is the standard type for this. This means you may have to convert the RTC time into this standard type before hand. That likely means subtracting 1900 from the year and 1 from the month. I am further assuming that you have a signed 64-bit type for time_t, because otherwise you get into trouble in January of 2038.

Code: Select all

/* returns the GDN of January 1 of a given year */
int year_to_gdn(int year, int *is_leap) {
  /* year is the Gregorian calendar year */
  assert(year >= 1); /* C integer math with negative numbers is weird. And you don't really work with Roman times, right? */
  assert(year < INT_MAX/146097); /* otherwise this overflows the calculation down there */
  year--; /* makes the upcoming calculations easier */
  int cycle = year / 400;
  int year_in_cycle = year % 400;
  int century = year_in_cycle / 100;
  int year_in_century = year_in_cycle % 100;
  int group = year_in_century / 4;
  int year_in_group = year_in_century % 4;
  if (is_leap) *is_leap = year_in_group == 3 && (year_in_century != 99 || year_in_cycle == 399);
  return 146097 * cycle + 36524 * century + 1461 * group + 365* year_in_group;
}
Now you only have to calculate the number of days since January 1, and you have the GDN of the day you wanted.

Code: Select all

int tm_to_gdn(const struct tm *tm) {
  int is_leap;
  static const unsigned short month_to_gdn[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
  int gdn = year_to_gdn(tm->tm_year + 1900, &is_leap);
  gdn += month_to_gdn[tm->tm_mon] + (is_leap && tm->tm_mon > 1);
  gdn += tm->tm_mday - 1;
  return gdn;
}
And now with all of that out of the way, we do have to calculate the GDN of January 1, 1970. Well, 1970 is 4 cycles, 3 centuries, 17 groups and 1 year after the start of the calendar, so:

Code: Select all

#define GDN_EPOCH 719162
And with all of that out of the way, the remaining steps for converting the GDN to UNIX time and adding the time component as well are actually very simple:

Code: Select all

time_t tm_to_time(const struct tm *tm) {
  int gdn = tm_to_gdn(tm);
  return (gdn - GDN_EPOCH) * 86400LL + tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec;
}
Leap seconds you don't really need to care about. Your computer likely stores time as UTC (there are very very few machines out there storing time as TAI), which ignores leap seconds. Indeed, the standard UNIX time APIs cannot show a leap second at all. Time stamps are kept in UTC, which has no leap seconds, and converted to struct tm with no additional info.

As for time zones, I was assuming the struct tm was in UTC. If not, then you have to find the timezone offset somehow and subtract it from the result to get UTC, because time_t is supposed to contain the time as UTC.
Carpe diem!
Post Reply