32 bits time_t and Y2038 issue

Mar 11, 2025 Last reply: 1 anno fa 53 Replies

I have an embedded project that is compiled in Atmel Studio 7.0. The target is and ARM MCU, so the toolchain is arm-gnu-toolchain. The installed toolchain version is 6.3.1.508. newlib version is 2.5.0.



In this build system the type time_t is defined as long, so 32 bits.



I'm using time_t mainly to show it on a display for the user (as a broken down time) and tag with a timestamp some events (that the user will see as broken down time).



The time can be received by Internet or by the user, if the device is not connected. In both cases, time_t is finally used.



As you know, my system will show the Y2038 issue. I don't know if some of my devices will be active in 2038, anyway I'd like to fix this potential issue now.



One possibility is to use a modern toolchain[1] that most probably uses a new version of newlib that manages 64 bits time_t. However I think I should address several warnings and other problems after upgrading the toolchain.



Another possibility is to rewrite my own my_mktime(), my_localtime() and so on that accepts and returns my_time_t variables, defined as 64 bits. However I'm not capable in writing such functions. Do you have some implementations? I don't need full functional time functions, for example the timezone can be fixed at build time, I don't need to set it at runtime.



Any suggestions?


[1]
formatting link

I /seriously/ dislike Microchip's way of handling toolchains. They work with old, outdated versions, rename and rebrand them and their documentation to make it look like they wrote them themselves, then add license checks and software locks so that optimisation is disabled unless you pay them vast amounts of money for the software other people wrote and gave away freely. To my knowledge, they do not break the letter of the license for GCC and other tools and libraries, but they most certainly break the spirit of the licenses in every way imaginable.

Prior to being bought by Microchip, Atmel was bad - but not as bad.

So if for some reason I have no choice but to use a device from Atmel / Microchip, I do so using tools from elsewhere.

As a general rule, the gcc-based toolchains from ARM are the industry standard, and are used as the base by most ARM microcontroller suppliers. Some include additional library options, others provide the package as-is. For anything other than a quick demo, my preferred setup is using makefiles for the build along with an ARM gcc toolchain. That way I can always build my software, from any system, and archive the toolchain. (One day, I will also try using clang with these packages, but I haven't done so yet.)

Any reasonably modern ARM gcc toolchain will have 64-bit time_t. I never like changing toolchains on an existing project, but you might make an exception here.

However, writing functions to support time conversions is not difficult. The trick is not to start at 01.01.1970, but start at a convenient date as early as you will need to handle - 01.01.2025 would seem a logical point. Use

formatting link
to get the time_t constant for the start of your epoch.

To turn the current time_t value into a human-readable time and date, first take the current time_t and subtract the epoch start. Divide by

365 * 24 * 60 * 60 to get the additional years. Divide the leftovers by 24 * 60 * 60 to get the additional days. Use a table of days in the months to figure out the month. Leap year handling is left as an exercise for the reader (hint - 2100, 2200 and 2300 are not leap years, while 2400 is). Use the website I linked to check your results.

Or you can get the sources for a modern version of newlib, and pull the routines from there.

David

Maybe you are thinking about Microchip IDE named MPLAB X or something similar. I read something about disabled optimizations in the free version of the toolchain.

However I'm using *Atmel Studio* IDE, that is an old IDE distributed by Atmel, before the Microchip purchase. The documentation speaks about some Atmel customization of ARM gcc toolchain, but it clearly specified the toolchain is an arm gcc.

Why do you think Atmel was bad? I think they had good products.

Yes, you're right, but now it's too late to change the toolchain.

I will check.

If I had to rewrite my own functions, I could define time64_t as uint64_t, keeping the Unix epoch as my epoch.

Regarding implementation, I don't know if it so simple. mktime() fix the members of struct tm passed as an argument (and this is useful to calculate the day of the week). Moreover I don't only need the conversion from time64_t to struct tm, but viceversa too.

It's a very complex code. time functions are written for whatever timezone is set at runtime (TZ env variable), so their complexity are higher.

Il 11/03/2025 17:32, David Brown ha scritto: [...]

[...]

Regading this point, it's what I want to do in new projects. What I don't know is...

Why many silicon vendors provide a *custom* arm gcc toolchain? Are those customizations important to build firmware for their MCUs? If not, why they invest money to make changes in a toolchain? It isn't a simple job.

Another point is visual debugging. I don't mean text editor with syntax hilighting, code completion, project management and so on. There are many tools around for this. I used to have a button in the IDE to launch a debugging session. The generation of a good debugging session configuration is simplified in IDE if you use main debuggin probe (for example, J-Link).

How do you debug your projects without a full-features and ready-to-use IDE from the silicon vendor?

I believe it applies to all of Microchip's toolchains - and that now includes those for the Atmel devices it acquired.

OK.

It is not the products that I am talking about. I've always like the AVR architecture (though it could have been massively better with a few small changes). Though I haven't used their ARM devices myself, I have heard nice things about them. I am talking about the toolchains.

They had a very mixed attitude to open source software. For a long time, they dismissed GCC completely, and gave no help or information for other parts of the ecosystem (debuggers, programmers, etc.). Eventually they realised that there was a substantial customer base who did not want to pay huge prices for IAR toolchains, or preferred open-source toolchains for other reasons, and they made various half-hearted efforts to support GCC for the AVR. Basically, they did enough to be able to have a working setup that they could provide it for free, but not enough to make it efficient. I think they spent more money on rebranding GCC for the AVR and ARM than they did on technically improving them. People looking for AVR GCC toolchains were left with no idea what version of GCC they can get from Atmel, or how those builds compare with mainline GCC versions, what devices they support, or how the various required extensions are handled. Their ARM toolchains were a bit more standard, and a bit less obfuscated in their branding and versioning.

So not as bad as Microchip, but still far from good.

Day of week calculations are peanuts - divide the seconds count by the number of seconds in a day, add a constant value for whatever day

01.01.1970 was, and reduce modulo 7.

Most of the effort for converting a struct tm into a time_t is checking that the values make sense.

For all of this, the big question is /why/ you are doing it. What are you doing with your times? Where are you getting them? Are you actually doing this in a sensible way because they suit your application, or are you just using these types and structures because they are part of the standard C library - which is not good enough for your needs here?

Maybe you are going about it all the wrong way. If you need to be able to display and set the current time and date, and to be able to conveniently measure time differences for alarms, repetitive tasks, etc., then you probably don't need any correlation between your monotonic seconds counter and your time/date tracker. All you need to do is add one second to each, every second. I don't know the details of your application (obviously), but often no conversion is needed either way.

So find a simpler standard C library implementation. Try the avrlibc, for example.

But I have no doubt at all that you can make all this yourself easily enough.

Some changes are reasonable. A prime example is support for newer devices - or workarounds for bugs in existing devices. The ideal way to handle this sort of thing, as done by a number of suppliers, is to figure out a fix and push it upstream.

Full upstream projects - primarily GCC and binutils - will generally add these in to their current development line. They will generally only add it in to previous lines if the changes are small, fairly clean patches, important for code correctness, and don't require changes in additional areas (such as command-line options or documentation). Even then, they only go a few releases back.

The main next-step upstream project for ARM GCC tools is ARM's GCC toolchain - they can be a bit more flexible, and maintain a list of patches that go on top of GCC and binutils releases so that such fixes can be back-ported to older sets. Their releases are always a bit behind upstream mainline, because they need time to do their testing and packaging.

Finally, some microcontroller manufacturers will have their own packed builds based on ARM's builds. (Other intermediaries used to be popular before, such as Code Sourcery, but I think ARM's builds are now ubiquitous.) They can react faster to adding fixes and patches to existing code - they don't need to think about conflicts or testing with other people's ARM cores, for example. But again, their releases will be even further behind mainline because they must be tested against all their SDK's and other code.

These sorts of things are all good for the user, good for the community as a whole, and good for the manufacturer - they can make quick fixes if they have to, but in the long term they keep aligned with mainline.

But some vendors - such as Microchip - put a great deal of effort into re-branding. They are looking for marketing, control, and forced tie-in

- they want it to be as hard as possible to move to other vendors. And they can have PHB's that think the development tool department of the company should make a profit on its own, rather than be there to support sales from the device production - thus they try to force developers to pay for licenses. IMHO this is all counter-productive - I am sure I am not the only developer who would not consider using microcontrollers from Microchip unless there was no other option. (I am happy to use other parts from Microchip - they make good devices.)

That varies from project to project, part to part. But I am quite happy to use an IDE from a vendor while using an external makefile and a standard GNU ARM toolchain for the build.

I suspect because of Arduino, that is open source and started with ATmega328.

I didn't follow the history of AVR MCUs and the available toolchains. I only used the AVR toolchain installed by Atmel Studio (I think by Atmel), but I know there's at leat winavr project. I don't know if there are real differeces between them.

Yes, you should be right.

When the user wants to set the current date and time, I fill a struct tm with user values. Next I call mktime() to calculate time_t that is been incrementing every second.

When I need to show the current date and time to the user, I call localtime() to convert time_t in struct tm. And I have day of the week too.

Consider that mktime() and localtime() take into account timezone, that is important for me. In Italy we have daylight savings time with not so simple rules. Standard time functions work well with timezones.

I'm talking about *wall* clock only. Internally I have a time_t variable that is incremented every second. But I need to show it to the user and I can't show the seconds from the epoch.

I think timezone rules are not so simple to implement.

The sane way to do this - the way it has been done for decades on small embedded systems - is to track both a human-legible date/time structure (ignore standard struct tm - make your own) /and/ to track a monotonic seconds counter (or milliseconds counter, or minutes counter - whatever you need). Increment both of them every second. Both operations are very simple - far easier than any conversions. Adding or subtracting an hour on occasion is also simple.

If your system is connected to the internet, then occasionally pick up the current wall-clock time (and unix epoch, if you like) from a server, along with the time of the next daylight savings change. If it is not connected, then the user is going to have to make adjustments to the time and date occasionally anyway, as there is always drift - they can do the daylight saving change at the same time as they change their analogue clocks, their cooker clock, and everything else that is not connected.

You don't need them. That makes them simple.

If I got your point, adding one second to struct mytm isn't reduced to a

++ on one of its member. I should write something similar to this:

if (mytm.tm_sec < 59) { mytm.tm_sec += 1; } else { mytm.tm_sec = 0; if (mytm.tm_min < 59) { mytm.tm_min += 1; } else { mytm.tm_min = 0; if (mytm.tm_hour < 23) { mytm.tm_hour += 1; } else { mytm.tm_hour = 0; if (mytm.tm_mday < days_in_month(mytm.tm_mon, mytm.tm_year)) { mytm.tm_mday += 1; } else { mytm.tm_mday = 1; if (mytm.tm_mon < 12) { mytm.tm_mon += 1; } else { mytm.tm_mon = 0; mytm.tm_year += 1; } } } } }

However taking into account dst is much more complex. The rule is the last sunday of March and last sunday of October (if I'm not wrong).

All can be coded manually from the scratch, but there are standard functions just to avoid reinventing the wheel.

Tomorrow I could install my device in another country in the world and it could be easy to change the timezone with standard function.

Yes, but the problem is *when*. You need to know the rules and you need to implement them. localtime() just works.

What do you mean with "next daylight savings change"? I'm using NTP (specifically SNTP from a public server) and I'm able to retrive the current UTC time in seconds from Unix epoch.

I just take this value and overwrite my internal counter.

In other application, I retrive the current time from incoming SMS. In this case I have a local broken down time.

Drifts? By using a 32.768kHz quartz to generate a 1 Hz clock that increases the internal counter avoid any drifts.

- they can

I think you can take into account dst even if the device is not connected.

I bet Windows is able to show the correct time (with dst changes) even if the PC is not connected.

Yes, that's about it.

No, it is not complex. Figure out the rule for your country (I'm sure Wikipedia well tell you if you are not sure) and then apply it. It's just a comparison to catch the right time and date, and then you add or subtract an extra hour.

You've just written the code! You have maybe 10-15 more lines to add to handle daylight saving.

How many countries are you targeting? Europe all uses the same system.

formatting link

You are getting ridiculous. This is not rocket science.

Besides, any fixed system is at risk from changes - and countries have in the past and will in the future change their systems for daylight saving. (Many have at least vague plans of scraping it.) So if a simple fixed system is not good enough for you, use the other method I suggested - handle it by regular checks from a server that you will need anyway for keeping an accurate time, or let the user fix it for unconnected systems.

There is no such thing as a 32.768 kHz crystal - there are only approximate crystals. If you don't update often enough from an accurate time source, you will have drift. (How much drift you have, and what effect it has, is another matter.)

You certainly can. But then you have to have a fixed algorithm known in advance.

I bet it can't, in cases where the date system for the daylight savings time has changed or been removed. Other than that, it will just use a table of date systems such as on the Wikipedia page. Or perhaps MS simply redefined what they think other people should use.

Older Windows needed manual changes for the date and time, even when it was connected - their support for NTP was late.

Ok, but I don't understand why you prefer to write your own code (yes, you're an exper programmer, but you can introduce some bugs, you have to write some tests), while there are standard functions that make the job for you.

I could rewrite memcpy, strcat, strcmp, they aren't rocket science, but why? IMHO there is no sense.

In my case standard functions aren't good (because of Y2038 issue) and rewriting them can be a valid solution. But if I had a 64 bits time_t, I would live with standard functions very well.

My users like the automatic dst changes on my connected and unconnected devices. The risk of a future changes in the dst rules doesn't seem to me a good reason to remove that feature.

Of course, the quartz has an accuracy that changes with life, temperature an so on. However the real accuracy doesn't allow the time drifting so much the user needs to reset the time.

Maybe Windows is not able, but I'm reading Linux is. It saves the time as UTC on the hw RTC and shows it to the user as localtime, of course applying dst and timezone rules from a database of rules.

So, as long as the timezone/dst info for my timezone is correct, I think Linux could manage dst changes automatically without user activity.

My approach is identical to what Linux does.

With STM devices I use Linux 'stlink' program and gdb. That is command line debugging. I can set breakpoints, single step, view and and modify device registers, those are main things that I need. I also use debugging UART. For debugging I normally load code into RAM which means that I can have unlimited number of breakpoints without writning to flash (I am not sure if that is really important, but at least makes me feel better).

I have also used stlink with some non-STM devices (IIRC LPC), but that required modification to 'stlink' code and IIUC use of of non-STM devices is blocked in new firmware for the debugging dongle.

'gdb' can be used with many other debugging dongles.

Visual tools may be nicer and automatically do some extra things. But I got used to gdb.

I always used visual debuggers. I know they generally uses gdb internally, but I don't know how.

I'm feeling better when I use mouse, point on a variable in the code and see its content. And similar things.

I'm sure it's possible to let a generic IDE (like Visual Studio Code) permforms visual debugging session with J-Link (or similar probes), but you have to install/configure many things to reach a functional system.

I'm going to try when I have free time... free time? What is it?

Sure, but the project is old. I will check if using a newer toolchain is a feasible solution for this project.

I agree with you. I thought you were suggesting to use custom made functions in any case, because my approach that uses time_t counter (seconds from epoch) and localtime()/mktime() isn't good.

Yes, if your functios are better than standard functions, it's the way to go for me too.

Yes.

Chaning the toolchain is a possibile solution. I have to check yet.

In the past I sometimes lurked in the newlib code and it seems too complicated for me. I will search for other simple implementations of localtime()/mktime().

I don't think.

It's enough.

I fully appreciate - and agree with - not wanting to change toolchains on an existing established project. It might be the best solution here, but it is certainly not one to be picked lightly.

No. I am merely saying that if you can't use the standard functions and have to get other ones from somewhere (or write them yourself), making them match standard function interfaces is of no benefit. There are many alternative formats that could be better for your use.

There are other C standard libraries around - maybe others are better than newlib for this purpose. (I don't know if newlib nano is mixed in with newlib here.) Newlib sources are, at least in parts, a monstrosity of conditional compilation to support vast numbers of targets, compilers, OS's, and options.

I really hope you missed a word in that sentence :-)

Same here. I just switched to ARM gcc + picolibc for all my ARM projects - this required some changes in the way my makefiles generate linker scripts and startup code, and now I am quite happy with that setup.

I have one project where I needed custom time functions: a nixie clock that has both a RTC (with seconds/minutes/... registers), and NTP to get current time. NTP time is seconds since 1.1.1900 and UTC.

The sane approach to handling timezones and DST is the unix way: keep everything in UTC internally and convert to localtime when displaying the time. To set the RTC, that requires a version of mktime that does *not* do timezone conversion - I simply pulled mktime from the newlib sources and removed the timezone stuff - done. You could write that stuff yourself, but getting all the corner cases right will take some time. The existing code is there and works fine.

cu Michael

Yep. IMO, that's definitely the "One True Answer" for embedded development.

I worked with a guy who wanted to use Eclipse for embedded development. After _months_ of f&*king around, he was finally able to build a binary that worked.

But trying to build that Eclipse "project" on another computer (same OS, same version of Eclips, same toolchain) was a complete failure.

I finally told him it was fine if he wanted to use Eclipse as his editor, gdb front-end, SVN gui, filesystem browser, office-cleaner and nose-wiper. But it was a non-negotiable requirement that it be possible to check the source tree and toolchain out of SVN, type "make", hit enter, and end up with a working binary.

Yes, we do that at work - build using makefiles, and some colleagues use eclipse as their editor/debugger. I prefer emacs / ddd.

Getting reproducable build results using eclipse (or some vendor-patched eclipse) is a PITA.

cu Michael

One day or another I will try to move from my actual build system (that depends on silicon vendor IDE, libraries, middleware, drivers, and so on) to a generic makefile and generic toolchain.

Sincerely I tried in the past with some issues. First of all, I use a Windows machine for development and writing makefiles that work on Windows is not simple. Maybe next time I will try with WSL, writing makefiles that work directly in Unix.

Another problem that I see is the complexity of actual projects: TCP/IP stack, cripto libraries, drivers, RTOS, and so on. Silicon vendors usually give you several example projects that just works with one click, using their IDE, libraries, debuggers, and so on. Moving from this complex build system to custom makefiles and toolchain isn't so simple.

Suppose you make the job to "transform" the example project into a makefile. You start working with your preferred IDE/text editor/toolchain, you are happy. After some months the requirements change and you need to add a driver for a new peripheral or a complex library. You know there are ready-to-use example projects in the original IDE from silicon vendor that use exactly what you need (mbedtls, DMA, ADC...), but you can't use them because you changed your build system.

Another problem is debugging: launch a debug sessions that means download the binary through a USB debugger/probe and SWD port, add some breakpoints, see the actual values of some variables and so on. All this works very well without big issues if using original IDE. Are you able to configure *your* custom development system to launch debug sessions?

Eventually another question. Silicon vendors usually provide custom toolchains that often are a customized version of arm-gcc toolchian (yes, here I'm talking about Cortex-M MCUs only, otherwise it would be much more complex). What happens if I move to the generic arm-gcc?

This is exactly what I do. I don't use RTC with registers (seconds, minutes...) anymore, only a 32.768kHz oscillator (present in many MCUs) that increments a counter.

Install msys2 (and the mingw-64 version of gcc, if you want a native compiler too). Make sure the relevant "bin" directory is on your path. Then gnu make will work perfectly, along with all the little *nix utilities such as touch, cp, mv, sed, etc., that makefiles sometimes use.

The only time I have seen problems with makefiles on Windows is when using ancient partial make implementations, such as from Borland, along with more advanced modern makefiles, or when someone mistakenly uses MS's not-make "nmake" program instead of "make".

Of course your builds will be slower on Windows than on Linux, since Windows is slow to start programs, slow to access files, and poor at doing it all in parallel, but there is nothing hindering makefiles in Windows. My builds regularly work identically under Linux and Windows, with the same makefiles.

That's why you still have a job. Putting together embedded systems is not like making a Lego kit. Running a pre-made demo can be easy - merging the right bits of different demos, samples and libraries into complete systems is hard work. It is not easy whether you use an IDE for project and build management, or by manual makefiles. Some aspects may be easier with one tool, other aspects will be harder.

Find the files you need from the SDK or libraries, copy them into your own project directories (keep them organised sensibly).

A good makefile picks up the new files automatically and handles all the dependencies, so often all you need is a new "make -j". But you might have to set up include directories, or even particular flags or settings for different files.

Build your elf file with debugging information, open the elf file in the debugger.

You probably have a bit of setup to specify things like the exact microcontroller target, but mostly it works fine.

This has already been covered. Most vendors now use standard toolchain builds from ARM.

What happens if the vendor has their own customized tool and you switch to a generic ARM tool depends on the customization and the tool versions. Usually it means you get a new toolchain with better warnings, better optimisation, and newer language standard support. But it might also mean vendor-supplied code with bugs no longer works as it did. (You don't have any bugs in your own code, I presume :-) )

Join the Discussion

Have something to add? Share your thoughts — no account required.

Didn't find your answer?

Ask the community — no account required