How to show millis() last 3 numbers

Hello, made a datalogger to save some engine data and want to make the date stamp ISO8601 compliant and ran into not knowing how to format the millis() output to only show the last 3 digits and preferably with leading zero's in case it's a number with less then 3 digits, attached code gives me: 2026-05-16T19:47:27.42616 but want to have 2026-05-16T19:47:27.616

void createFile(){
  sprintf(folder, "20%02d/%02d-%02d", yy, mo, dd);
  sprintf(file, "20%02d/%02d-%02d/%02d-%02d.CSV", yy, mo, dd, hh, mi);
  SD.mkdir(folder);
  myFile=SD.open(file, FILE_WRITE);
  myFile.println("Time,EGT1,EGT2,EGT3,EGT4,Oil_Temp,IAT,Exhaust_Press,Boost,H2O_Press,Oil_Press,Fuel_Press,Volt,RPM,Pinion_RPM,Turbo_RPM");
  myFile.close();
}

void writeData() {
  if ((writeSD == 1) && (logBit == 0)) {
    logBit = 1;
    createFile();
    startTime = millis();
    myFile=SD.open(file, O_WRITE | O_APPEND );
  }
  if ((writeSD == 1) && (logBit == 1)) {
    snprintf(time, sizeof time, "20%02d-%02d-%02dT%02d:%02d:%02d.", yy, mo, dd, hh, mi, ss);
    String writedata = "";
    writedata += time;
    writedata += (millis() - startTime);
    writedata += ',';
    writedata += egt1;
    writedata += ',';
    writedata += egt2;
    writedata += ','; 
    writedata += egt3;
    writedata += ',';
    writedata += egt4;
    writedata += ',';
    writedata += clt;
    writedata += ',';
    writedata += iat;
    writedata += ',';
    writedata += pressure1;
    writedata += ',';
    writedata += pressure2;
    writedata += ',';
    writedata += pressure3;
    writedata += ',';
    writedata += pressure4;
    writedata += ',';
    writedata += pressure5;
    writedata += ',';
    writedata += volt;
    writedata += ',';
    writedata += rpm1;
    writedata += ',';
    writedata += rpm2;
    writedata += ',';
    writedata += rpm3;
    myFile.println(writedata);
    myFile.flush(); // save to card every line to prevent data loss
  }
  if ((writeSD == 0) && (logBit == 1)) {
    myFile.close();
    logBit = 0;
  }
}

Your current code appends the entire elapsed millisecond counter after the dot, which is why you get values like 2026-05-16T19:47:27.42616.

If you want ISO8601-style milliseconds, you need only the last 3 digits and zero padding. %03lu does that correctly ➜ % format specifier, 03 width 3 with leading zeros, lu unsigned long

unsigned long ms = (millis() - startTime) % 1000ul;
snprintf(time, sizeof time, "20%02d-%02d-%02dT%02d:%02d:%02d.%03lu",  yy, mo, dd, hh, mi, ss, ms);

Then remove this line entirely:

writedata += (millis() - startTime);

and replace:

writedata += time;

with:

writedata += String(time);

give it a try

millis() is "time alive", an unsigned 32-bit value. That means it rolls over back to zero after 49 days, 17 hours, 2 minutes, and 47.295 seconds. Just tacking it on the end, it will jump from .295 to .000 every seven weeks. Time could "go backward".

Make sure you're OK with that.

Unrelated, but it's memory-inefficient to build a String just to print it. You can "print as you go"

myFile.print(time);
myFile.print(millis() - startTime);  // will also roll over after seven weeks
myFile.print(',');
myFile.print(egt1);
myFile.print(',');
myFile.print(egt2);
// etc
myFile.println(rpm3);  // CRLF at the end
myFile.flush();

look this over

 1  0:00:00.000
36  9:50:47.296
36  9:50:47.297
36  9:50:47.297
36  9:50:47.301
36  9:50:47.318
char s [90];

const unsigned long OneDayMsec = 24L *3600L *1000L;
      unsigned long msec0      = 13.3 *OneDayMsec;

// -----------------------------------------------------------------------------
char *
fmtTime (
    unsigned long msec,
    char          *s )
{
    unsigned long secs  = msec  / 1000L;
    unsigned long mins  = secs  / 60L;
    unsigned long hours = mins  / 60L;
    unsigned long days  = hours / 24L; 

    msec  %= 1000L;
    secs  %= 60L;
    mins  %= 60L;
    hours %= 24L;          // optionally add 1, 1-24

    sprintf(s, "%2lu %2lu:%02lu:%02lu.%03lu", days, hours, mins, secs, msec);

    return s;
}

// -----------------------------------------------------------------------------
void loop ()
{
    unsigned long msec = millis ();

    Serial.println (fmtTime (msec -msec0, s));
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    Serial.println (fmtTime (OneDayMsec, s));  Serial.println (" day");
}

The last 3 digits will roll over from 999 to 1000 so you will see 000 again.

This won't work well. You need to synchronise with your RTC time, maybe like this:

void loop() {
  ...
  if (ss != prev_ss) startTime = millis();
  prev_ss = ss;
  ...

Also you can simplify your code by making better use of snprintf():

    char writedata[80];
    snprintf(
      writedata, sizeof writedata,
      "20%02d-%02d-%02dT%02d:%02d:%02d.%03l,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
      yy, mo, dd, hh, mi, ss, millis() - startTime,
      egt1, egt2, egt3, egt4,
      clt, iat,
      pressure1, pressure2, pressure3, pressure4, pressure5,
      volt, rpm1, rpm2, rpm3
    );
    myFile.println(writedata);

With some types of Arduino you can use printf(), making it even easier:

    myFile.printf(
      "20%02d-%02d-%02dT%02d:%02d:%02d.%03l,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d\n",
      yy, mo, dd, hh, mi, ss, millis() - startTime,
      egt1, egt2, egt3, egt4,
      clt, iat,
      pressure1, pressure2, pressure3, pressure4, pressure5,
      volt, rpm1, rpm2, rpm3
    );

I'm not sure what you are trying to achieve, without your full code (or at least the portions where you call those functions).
Since I assume yy, mo, dd, hh, mi, and ss come from an RTC, you only need the milliseconds not available from the RTC, right? Your code seems to simply "add" the last three digits to the value of the "millis()" function as milliseconds, but that doesn't make much sense to me because they're not the "real" milliseconds since the start of that specific second; you're just using the milliseconds since the last "startTime." Apparently, aside from the last three digits being drawn, it seems more like a "random" value than a real value to me...

But, if your samples are taken at intervals greater than or equal to a second, unless such precise record timing is absolutely necessary, I don't think you need to add anything, just add a fixed ".000" and you're done (and ISO compliant).

If you need to get the "real" milliseconds, you should store in "startTime" the value of millis() when the "ss" digit changes (you could do it inside the loop() to detect such changes, and hoping you don't have a delay() somewhere), and then when you write, with (millis() - startTime) you will have (more or less) the real milliseconds of that timestamp.

If you wanted that to be accurate, it would be better to use an RTC that had a 1-second pulse output to trigger an interrupt, and store the value of millis() in the interrupt.

Thanks guys, you really helped me a lot!!! :grin:

I like J-M-L approach, most important is that I get how that works and still understand it when I look a year later ;-)

Also like PaulRB simplifying with printf

The roll over isn't an issue, code is running for 5 minutes and then shutdow. The complete code reads a bunch of sensors 20x per second and graphing that without ms values isn't working very well but don't have to be accurate it's not used for calculations only to visualize the data.

Thanks again and have a good weekend!

ps complete project: https://drive.google.com/file/d/15u0WtDb8lJYBtUj_7R7o4aFxCqclEaXB/view?usp=drive_link