├── ESP32 ├── ESP32-C3 │ ├── NMEASerial │ │ └── NMEASerial.ino │ ├── tinyNTPServerPPS │ │ └── tinyNTPServerPPS.ino │ └── tinyNTPServerTime │ │ └── tinyNTPServerTime.ino └── ESP32-Ethernet │ └── PPS_Newaproach_V3 │ └── PPS_Newaproach_V3.ino ├── README.md └── Raspberry Pi └── NTP Server with Chrony.pdf /ESP32/ESP32-C3/NMEASerial/NMEASerial.ino: -------------------------------------------------------------------------------- 1 | // use UART1 for GPS 2 | // GPS TX → ESP32 RX1 (GPIO 21) 3 | // GPS RX → ESP32 TX1 (GPIO 20) (only needed if you plan to send config to the GPS) 4 | #define GPS_RX_PIN 20 // connects to GPS module TX pin 5 | #define GPS_TX_PIN 21 // connects to GPS module RX pin (optional) 6 | 7 | void setup() { 8 | // USB serial 9 | Serial.begin(115200); 10 | while(!Serial) { delay(10); } // wait for Serial to be ready 11 | 12 | // UART1 for GPS @ 9600 baud, 8N1 13 | Serial1.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); 14 | Serial.println("GPS reader started"); 15 | } 16 | 17 | void loop() { 18 | // read any chars from GPS and write them to USB serial 19 | while (Serial1.available()) { 20 | char c = Serial1.read(); 21 | Serial.write(c); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ESP32/ESP32-C3/tinyNTPServerPPS/tinyNTPServerPPS.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "credentials.h" 11 | 12 | // Pin Definitions 13 | #define GPS_RX_PIN 20 14 | #define GPS_TX_PIN 21 15 | #define PPS_PIN 2 16 | 17 | // Global Variables 18 | TinyGPSPlus gps; 19 | HardwareSerial gpsSerial(1); 20 | WiFiUDP udp; 21 | SemaphoreHandle_t gpsMutex; 22 | 23 | volatile bool PPSsignal = false; 24 | volatile uint32_t last_pps_micros_isr = 0; 25 | 26 | volatile uint32_t last_sync_unix_sec = 0; 27 | volatile uint32_t last_sync_micros_at_pps = 0; 28 | 29 | volatile uint32_t latest_gps_unix_second = 0; 30 | volatile bool latest_gps_second_valid = false; 31 | 32 | volatile bool PPSavailable = false; 33 | 34 | #define GPSBaud 9600 35 | #define CORRECTION_FACTOR 0 36 | 37 | static const uint32_t NTP_OFFSET = 2208988800UL; 38 | 39 | struct PreciseTime { 40 | uint32_t seconds; 41 | uint32_t microseconds; 42 | }; 43 | 44 | struct PreciseDateTime { 45 | struct tm timeinfo; 46 | uint32_t microseconds; 47 | }; 48 | 49 | // OLED Setup 50 | U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2( 51 | /* Rotation */ U8G2_R0, 52 | /* CS */ U8X8_PIN_NONE, 53 | /* SCL=*/6, 54 | /* SDA=*/5); 55 | 56 | 57 | // Function Prototypes 58 | void initNetwork(); 59 | void printMacAddress(); 60 | void initPPS(); 61 | void IRAM_ATTR handlePPSRisingEdge(); 62 | void initGPS(); 63 | void processGPS(); 64 | void syncTimeWithGPS(); 65 | void handleNTPRequest(); 66 | uint32_t dateToEpoch(uint8_t d, uint8_t m, uint16_t y); 67 | void centerText(const char* text, int y); 68 | 69 | 70 | PreciseTime getPreciseTime(uint32_t current_micros, uint32_t last_sync_sec, uint32_t last_sync_micros_at_pps) { 71 | PreciseTime pt; 72 | 73 | // Utilize PPS-aligned calculation only if PPS is available and a synchronization point exists. 74 | if (PPSavailable && last_sync_micros_at_pps != 0) { 75 | uint64_t elapsed_micros_64; 76 | 77 | if (current_micros >= last_sync_micros_at_pps) { 78 | elapsed_micros_64 = (uint64_t)current_micros - last_sync_micros_at_pps; 79 | } else { 80 | // Handle micros() counter rollover. 81 | elapsed_micros_64 = (uint64_t)(0xFFFFFFFF - last_sync_micros_at_pps) + current_micros + 1; 82 | } 83 | 84 | pt.seconds = last_sync_sec + (uint32_t)(elapsed_micros_64 / 1000000); 85 | pt.microseconds = (uint32_t)(elapsed_micros_64 % 1000000); 86 | } else { 87 | // Fallback to coarse system time if PPS is not available or not yet synchronized. 88 | struct timeval tv_current; 89 | gettimeofday(&tv_current, NULL); 90 | pt.seconds = (uint32_t)tv_current.tv_sec; 91 | pt.microseconds = (uint32_t)tv_current.tv_usec; 92 | } 93 | return pt; 94 | } 95 | 96 | PreciseDateTime getPreciseDateTime() { 97 | PreciseDateTime pdt; 98 | pdt.microseconds = 0; 99 | memset(&pdt.timeinfo, 0, sizeof(pdt.timeinfo)); 100 | 101 | // Safely acquire mutex to read synchronized time values. 102 | if (xSemaphoreTake(gpsMutex, pdMS_TO_TICKS(50)) == pdTRUE) { 103 | PreciseTime pt = getPreciseTime(micros(), last_sync_unix_sec, last_sync_micros_at_pps); 104 | 105 | // Cast `pt.seconds` to `time_t` for `gmtime_r` to ensure correct interpretation. 106 | time_t current_unix_time = (time_t)pt.seconds; 107 | gmtime_r(¤t_unix_time, &pdt.timeinfo); 108 | 109 | pdt.microseconds = pt.microseconds; 110 | xSemaphoreGive(gpsMutex); 111 | } else { 112 | // If mutex acquisition fails, fall back to system's `gettimeofday()`. 113 | Serial.println("[getPreciseDateTime] Warning: Could not take GPS mutex. Using system clock fallback or stale values."); 114 | struct timeval tv_fallback; 115 | gettimeofday(&tv_fallback, NULL); 116 | time_t fallback_unix_time = (time_t)tv_fallback.tv_sec; 117 | gmtime_r(&fallback_unix_time, &pdt.timeinfo); 118 | pdt.microseconds = (uint32_t)tv_fallback.tv_usec; 119 | } 120 | return pdt; 121 | } 122 | 123 | // FreeRTOS Tasks 124 | 125 | void gpsTask(void *param) { 126 | Serial.println("[gpsTask] Starting GPS task..."); 127 | for (;;) { 128 | // Periodically process GPS data and synchronize time. 129 | if (xSemaphoreTake(gpsMutex, pdMS_TO_TICKS(100)) == pdTRUE) { 130 | processGPS(); 131 | syncTimeWithGPS(); 132 | xSemaphoreGive(gpsMutex); 133 | } else { 134 | Serial.println("[gpsTask] Warning: Could not take GPS mutex, skipping GPS/sync cycle."); 135 | } 136 | vTaskDelay(pdMS_TO_TICKS(5)); 137 | } 138 | } 139 | 140 | // Helper function to center text on the display 141 | // Helper function to center text on the display 142 | void centerText(const char* text, int y) { 143 | int textWidth = u8g2.getStrWidth(text); 144 | int xPos = (u8g2.getDisplayWidth() - textWidth) / 2; 145 | u8g2.setCursor(xPos, y); 146 | u8g2.print(text); 147 | } 148 | 149 | void displayTask(void *param) { 150 | Serial.println("[displayTask] Starting display task..."); 151 | for (;;) { 152 | PreciseDateTime current_pdt = getPreciseDateTime(); 153 | 154 | char time_str[10]; // Sufficient for HH:MM:SS 155 | char date_str[15]; // Sufficient for YY-MM-DD 156 | 157 | // Format time (HH:MM:SS) - removed "UTC" 158 | snprintf(time_str, sizeof(time_str), "%02d:%02d:%02d", 159 | current_pdt.timeinfo.tm_hour, current_pdt.timeinfo.tm_min, 160 | current_pdt.timeinfo.tm_sec); 161 | // Full time with microseconds for Serial output (still useful for debugging) 162 | Serial.printf("[displayTask] Current Time: %02d:%02d:%02d.%06lu UTC\n", 163 | current_pdt.timeinfo.tm_hour, current_pdt.timeinfo.tm_min, 164 | current_pdt.timeinfo.tm_sec, current_pdt.microseconds); 165 | 166 | // Format date (YY-MM-DD) 167 | snprintf(date_str, sizeof(date_str), "%02d-%02d-%02d", 168 | current_pdt.timeinfo.tm_year % 100, current_pdt.timeinfo.tm_mon + 1, 169 | current_pdt.timeinfo.tm_mday); 170 | Serial.printf("[displayTask] Current Date: %02d-%02d-%02d\n", 171 | current_pdt.timeinfo.tm_year % 100, current_pdt.timeinfo.tm_mon + 1, 172 | current_pdt.timeinfo.tm_mday); 173 | 174 | // Clear the display buffer 175 | u8g2.clearBuffer(); 176 | 177 | // Draw a rounded rectangle border around the entire display 178 | u8g2.drawRFrame(0, 0, u8g2.getDisplayWidth(), u8g2.getDisplayHeight(), 5); 179 | 180 | // Calculate vertical positions to center two lines of text within the border 181 | int max_line_height = u8g2.getMaxCharHeight(); 182 | int total_text_height = max_line_height * 2 + 2; // Two lines plus a small gap 183 | 184 | int yPos_time = (u8g2.getDisplayHeight() - total_text_height) / 2 + 2; 185 | int yPos_date = yPos_time + max_line_height + 2; 186 | 187 | // Draw the time and date, centered 188 | centerText(time_str, yPos_time); 189 | centerText(date_str, yPos_date); 190 | 191 | // Send buffer to the display 192 | u8g2.sendBuffer(); 193 | 194 | vTaskDelay(pdMS_TO_TICKS(500)); // Update display and serial every 500ms 195 | } 196 | } 197 | 198 | // NTP Request Handling 199 | void handleNTPRequest() { 200 | int size = udp.parsePacket(); 201 | if (size >= 48) { 202 | uint8_t req[48]; 203 | uint8_t resp[48]; 204 | 205 | udp.read(req, 48); 206 | uint32_t T2_raw_micros = micros(); 207 | 208 | memset(resp, 0, 48); 209 | 210 | resp[0] = (req[0] & 0x38) | 0x04; 211 | resp[1] = 1; // Stratum 1 212 | resp[2] = 6; // Poll Interval 213 | resp[3] = -20; // Precision 214 | 215 | resp[12] = 'G'; 216 | resp[13] = 'P'; 217 | resp[14] = 'S'; 218 | resp[15] = ' '; // Reference Identifier 219 | 220 | // Acquire mutex to get synchronized time for NTP timestamps. 221 | PreciseTime t0_precise, t2_precise, t3_precise; 222 | if (xSemaphoreTake(gpsMutex, pdMS_TO_TICKS(10)) == pdTRUE) { 223 | t0_precise = getPreciseTime(last_sync_micros_at_pps, last_sync_unix_sec, last_sync_micros_at_pps); 224 | t2_precise = getPreciseTime(T2_raw_micros, last_sync_unix_sec, last_sync_micros_at_pps); 225 | uint32_t T3_raw_micros_temp = micros(); 226 | t3_precise = getPreciseTime(T3_raw_micros_temp, last_sync_unix_sec, last_sync_micros_at_pps); 227 | xSemaphoreGive(gpsMutex); 228 | } else { 229 | Serial.println("[NTPTask] Warning: Could not get mutex for NTP timestamps. Using stale values or failing."); 230 | t0_precise.seconds = last_sync_unix_sec; 231 | t0_precise.microseconds = 0; 232 | t2_precise = getPreciseTime(T2_raw_micros, last_sync_unix_sec, last_sync_micros_at_pps); 233 | t3_precise = getPreciseTime(micros(), last_sync_unix_sec, last_sync_micros_at_pps); 234 | } 235 | 236 | // Populate NTP response packet with calculated timestamps. 237 | uint32_t T0_sec = t0_precise.seconds + NTP_OFFSET; 238 | uint32_t T0_frac = (uint32_t)((double)t0_precise.microseconds * (4294967296.0 / 1e6)); 239 | resp[16] = (T0_sec >> 24) & 0xFF; resp[17] = (T0_sec >> 16) & 0xFF; resp[18] = (T0_sec >> 8) & 0xFF; resp[19] = T0_sec & 0xFF; 240 | resp[20] = (T0_frac >> 24) & 0xFF; resp[21] = (T0_frac >> 16) & 0xFF; resp[22] = (T0_frac >> 8) & 0xFF; resp[23] = T0_frac & 0xFF; 241 | 242 | memcpy(&resp[24], &req[40], 8); // Copy client's T1 to Originate Timestamp 243 | 244 | uint32_t T2_sec = t2_precise.seconds + NTP_OFFSET; 245 | uint32_t T2_frac = (uint32_t)((double)t2_precise.microseconds * (4294967296.0 / 1e6)); 246 | resp[32] = (T2_sec >> 24) & 0xFF; resp[33] = (T2_sec >> 16) & 0xFF; resp[34] = (T2_sec >> 8) & 0xFF; resp[35] = T2_sec & 0xFF; 247 | resp[36] = (T2_frac >> 24) & 0xFF; resp[37] = (T2_frac >> 16) & 0xFF; resp[38] = (T2_frac >> 8) & 0xFF; resp[39] = T2_frac & 0xFF; 248 | 249 | uint32_t T3_sec = t3_precise.seconds + NTP_OFFSET; 250 | uint32_t T3_frac = (uint32_t)((double)t3_precise.microseconds * (4294967296.0 / 1e6)); 251 | resp[40] = (T3_sec >> 24) & 0xFF; resp[41] = (T3_sec >> 16) & 0xFF; resp[42] = (T3_sec >> 8) & 0xFF; resp[43] = T3_sec & 0xFF; 252 | resp[44] = (T3_frac >> 24) & 0xFF; resp[45] = (T3_frac >> 16) & 0xFF; resp[46] = (T3_frac >> 8) & 0xFF; resp[47] = T3_frac & 0xFF; 253 | 254 | udp.beginPacket(udp.remoteIP(), udp.remotePort()); 255 | udp.write(resp, 48); 256 | udp.endPacket(); 257 | 258 | char buf_t2[32], buf_t3[32]; 259 | time_t t2_unix = t2_precise.seconds; 260 | time_t t3_unix = t3_precise.seconds; 261 | strftime(buf_t2, sizeof(buf_t2), "%H:%M:%S", gmtime(&t2_unix)); 262 | strftime(buf_t3, sizeof(buf_t3), "%H:%M:%S", gmtime(&t3_unix)); 263 | Serial.printf("[NTPTask] NTP Req. T2: %s.%06lu UTC, T3: %s.%06lu UTC\n", buf_t2, (unsigned long)t2_precise.microseconds, buf_t3, (unsigned long)t3_precise.microseconds); 264 | } 265 | } 266 | 267 | void ntpTask(void *param) { 268 | Serial.println("[NTPTask] Starting NTP task..."); 269 | for (;;) { 270 | handleNTPRequest(); 271 | vTaskDelay(pdMS_TO_TICKS(10)); 272 | } 273 | } 274 | 275 | // Network Initialization 276 | void initNetwork() { 277 | Serial.println("[initNetwork] Initializing network..."); 278 | WiFi.mode(WIFI_STA); 279 | WiFi.begin(ssid, password); 280 | unsigned long t0 = millis(); 281 | Serial.println("[initNetwork] Connecting to WiFi..."); 282 | while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) { 283 | delay(500); 284 | u8g2.clearBuffer(); 285 | centerText("Connecting", u8g2.getDisplayHeight() / 2 - u8g2.getMaxCharHeight() - 2); 286 | centerText(ssid, u8g2.getDisplayHeight() / 2 + 2); 287 | u8g2.sendBuffer(); 288 | } 289 | if (WiFi.status() == WL_CONNECTED) { 290 | Serial.print("[initNetwork] WiFi IP: "); 291 | Serial.println(WiFi.localIP()); 292 | u8g2.clearBuffer(); 293 | centerText("Connected!", u8g2.getDisplayHeight() / 2 - u8g2.getMaxCharHeight() - 2); 294 | char ip_str[20]; 295 | WiFi.localIP().toString().toCharArray(ip_str, sizeof(ip_str)); 296 | centerText(ip_str, u8g2.getDisplayHeight() / 2 + 2); 297 | u8g2.sendBuffer(); 298 | delay(2000); 299 | Serial.println("[initNetwork] WiFi connected."); 300 | } else { 301 | Serial.println("[initNetwork] WiFi failed to connect!"); 302 | u8g2.clearBuffer(); 303 | centerText("WiFi Failed!", u8g2.getDisplayHeight() / 2 - u8g2.getMaxCharHeight() / 2); 304 | u8g2.sendBuffer(); 305 | } 306 | udp.begin(123); 307 | Serial.println("[initNetwork] NTP server started on port 123"); 308 | } 309 | 310 | void printMacAddress() { 311 | Serial.print("[printMacAddress] WiFi MAC: "); 312 | Serial.println(WiFi.macAddress()); 313 | } 314 | 315 | // PPS Detection & ISR 316 | void IRAM_ATTR handlePPSRisingEdge() { 317 | PPSsignal = true; 318 | last_pps_micros_isr = micros(); 319 | } 320 | 321 | void initPPS() { 322 | pinMode(PPS_PIN, INPUT_PULLDOWN); 323 | unsigned long t0 = millis(); 324 | bool detected = false; 325 | Serial.print("[initPPS] Checking for PPS signal on pin "); Serial.print(PPS_PIN); Serial.println("..."); 326 | while (millis() - t0 < 2000) { 327 | if (digitalRead(PPS_PIN) == HIGH) { 328 | detected = true; 329 | break; 330 | } 331 | delay(1); 332 | } 333 | if (detected) { 334 | attachInterrupt(digitalPinToInterrupt(PPS_PIN), handlePPSRisingEdge, RISING); 335 | PPSavailable = true; 336 | Serial.println("[initPPS] PPS signal detected. Rising edge interrupt attached."); 337 | } else { 338 | PPSavailable = false; 339 | Serial.println("[initPPS] No PPS signal detected or pin is not connected."); 340 | Serial.println("[initPPS] Will rely solely on GPS NMEA time (less accurate)."); 341 | } 342 | } 343 | 344 | // GPS Initialization & Time Sync 345 | void initGPS() { 346 | gpsSerial.begin(GPSBaud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); 347 | unsigned long t0 = millis(); 348 | Serial.println("[initGPS] Waiting for initial GPS fix..."); 349 | while (millis() - t0 < 60000) { 350 | while (gpsSerial.available()) { 351 | gps.encode(gpsSerial.read()); 352 | if (gps.time.isValid() && gps.date.isValid()) { 353 | Serial.printf("[initGPS] Initial GPS fix acquired. Time: %02d:%02d:%02d\n", gps.time.hour(), gps.time.minute(), gps.time.second()); 354 | Serial.printf("[initGPS] Date: %02d/%02d/%04d\n", gps.date.day(), gps.date.month(), gps.date.year()); 355 | 356 | uint8_t d = gps.date.day(); 357 | uint8_t m = gps.date.month(); 358 | uint16_t y = gps.date.year(); 359 | uint32_t days = dateToEpoch(d, m, y); 360 | latest_gps_unix_second = days * 86400UL + gps.time.hour() * 3600UL + gps.time.minute() * 60UL + gps.time.second(); 361 | latest_gps_second_valid = true; 362 | 363 | if (xSemaphoreTake(gpsMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { 364 | last_sync_unix_sec = latest_gps_unix_second; 365 | xSemaphoreGive(gpsMutex); 366 | } else { 367 | Serial.println("[initGPS] Warning: Failed to acquire mutex for initial time set after fix."); 368 | } 369 | 370 | struct timeval tv = { .tv_sec = (time_t)latest_gps_unix_second, .tv_usec = CORRECTION_FACTOR }; 371 | settimeofday(&tv, NULL); 372 | struct timeval tv_check; 373 | gettimeofday(&tv_check, NULL); 374 | Serial.printf("[initGPS] System time initialized via NMEA. Set to %lu.%06lu\n", (unsigned long)tv_check.tv_sec, (unsigned long)tv_check.tv_usec); 375 | return; 376 | } 377 | } 378 | delay(100); 379 | } 380 | Serial.println("[initGPS] Initial GPS time acquisition failed (no fix within timeout)."); 381 | } 382 | 383 | void processGPS() { 384 | while (gpsSerial.available()) { 385 | gps.encode(gpsSerial.read()); 386 | } 387 | 388 | if (gps.time.isValid() && gps.date.isValid()) { 389 | uint8_t d = gps.date.day(); 390 | uint8_t m = gps.date.month(); 391 | uint16_t y = gps.date.year(); 392 | uint32_t days = dateToEpoch(d, m, y); 393 | latest_gps_unix_second = days * 86400UL + gps.time.hour() * 3600UL + gps.time.minute() * 60UL + gps.time.second(); 394 | latest_gps_second_valid = true; 395 | } else { 396 | latest_gps_second_valid = false; 397 | } 398 | } 399 | 400 | void syncTimeWithGPS() { 401 | // This function is called within a mutex-protected section in `gpsTask`. 402 | // It handles synchronization logic based on PPS and GPS data. 403 | 404 | if (PPSsignal && PPSavailable && latest_gps_second_valid) { 405 | PPSsignal = false; 406 | 407 | // Update the precise synchronization point using GPS NMEA and PPS. 408 | last_sync_unix_sec = latest_gps_unix_second + 1; 409 | last_sync_micros_at_pps = last_pps_micros_isr; 410 | 411 | // Set the system's time for OS-level time functions. 412 | struct timeval tv = { .tv_sec = (time_t)last_sync_unix_sec, .tv_usec = 0 }; 413 | settimeofday(&tv, NULL); 414 | 415 | Serial.printf("[syncTimeWithGPS] GPS+PPS Sync: Unix Sec: %lu, Micros at PPS: %lu\n", last_sync_unix_sec, last_sync_micros_at_pps); 416 | } else if (PPSsignal && PPSavailable && !latest_gps_second_valid) { 417 | // If PPS is present but GPS NMEA is lost, increment Unix time based on PPS only. 418 | PPSsignal = false; 419 | Serial.println("[syncTimeWithGPS] PPS available but GPS fix lost. Cannot sync time precisely with NMEA."); 420 | if (last_sync_unix_sec != 0) { 421 | last_sync_unix_sec++; 422 | last_sync_micros_at_pps = last_pps_micros_isr; 423 | struct timeval tv = { .tv_sec = (time_t)last_sync_unix_sec, .tv_usec = 0 }; 424 | settimeofday(&tv, NULL); 425 | Serial.printf("[syncTimeWithGPS] PPS only (NMEA lost): Unix Sec: %lu, Micros at PPS: %lu\n", last_sync_unix_sec, last_sync_micros_at_pps); 426 | } 427 | } else if (!PPSavailable && latest_gps_second_valid) { 428 | // If PPS is unavailable, synchronize solely based on GPS NMEA data. 429 | struct timeval tv = { .tv_sec = (time_t)latest_gps_unix_second, .tv_usec = CORRECTION_FACTOR }; 430 | settimeofday(&tv, NULL); 431 | struct timeval tv_check; 432 | gettimeofday(&tv_check, NULL); 433 | 434 | // Update synchronization point using current micros() for NMEA-only sync. 435 | last_sync_unix_sec = tv_check.tv_sec; 436 | last_sync_micros_at_pps = micros(); 437 | Serial.printf("[syncTimeWithGPS] NMEA Only Sync: Unix Sec: %lu.%06lu\n", last_sync_unix_sec, tv_check.tv_usec); 438 | } 439 | } 440 | 441 | uint32_t dateToEpoch(uint8_t d, uint8_t m, uint16_t y) { 442 | int Y = y; 443 | int M = m; 444 | if (M < 3) { 445 | Y--; 446 | M += 12; 447 | } 448 | uint32_t era = Y / 400; 449 | uint32_t yoe = Y - era * 400; 450 | uint32_t doy = (153 * (M - 3) + 2) / 5 + d - 1; 451 | uint32_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; 452 | return era * 146097 + doe - 719468; 453 | } 454 | 455 | // Arduino Setup and Loop 456 | 457 | void setup() { 458 | Serial.begin(115200); 459 | Serial.println("\n[setup] Starting setup..."); 460 | Serial.printf("[DEBUG] micros() at start of setup: %lu\n", micros()); 461 | 462 | // Initialize FreeRTOS mutex for thread-safe access to GPS time data. 463 | gpsMutex = xSemaphoreCreateMutex(); 464 | if (gpsMutex != NULL) { 465 | Serial.println("[setup] GPS Mutex created successfully."); 466 | } else { 467 | Serial.println("[setup] ERROR: Failed to create GPS Mutex! This is critical and will cause crashes."); 468 | while (true); 469 | } 470 | 471 | // Initialize OLED display. 472 | u8g2.begin(); 473 | u8g2.setContrast(50); // Set brightness based on your example 474 | u8g2.setBusClock(400000); 475 | u8g2.setFont(u8g2_font_6x10_tr); 476 | u8g2.setFontPosTop(); 477 | Serial.println("[setup] OLED initialized."); 478 | 479 | // Display welcome message with border 480 | u8g2.clearBuffer(); 481 | u8g2.drawRFrame(0, 0, u8g2.getDisplayWidth(), u8g2.getDisplayHeight(), 5); // ADDED THIS LINE FOR THE WELCOME BORDER 482 | centerText("NTP", u8g2.getDisplayHeight() / 2 - u8g2.getMaxCharHeight() - 2); 483 | centerText("Server", u8g2.getDisplayHeight() / 2 + 2); 484 | u8g2.sendBuffer(); 485 | delay(1500); // Show welcome message for a bit 486 | 487 | // Configure and connect to Wi-Fi, and start UDP listener for NTP. 488 | initNetwork(); 489 | printMacAddress(); 490 | 491 | // Detect PPS signal presence and attach interrupt if found. 492 | initPPS(); 493 | 494 | Serial.printf("[DEBUG] micros() before initGPS loop: %lu\n", micros()); 495 | // Initialize GPS serial communication and obtain initial time fix. 496 | initGPS(); 497 | 498 | // Create FreeRTOS tasks for GPS processing, NTP serving, and display updates. 499 | xTaskCreatePinnedToCore( 500 | gpsTask, 501 | "GPSTask", 502 | 4096, 503 | NULL, 504 | configMAX_PRIORITIES - 1, // Highest priority for critical time sync 505 | NULL, 506 | 0); 507 | Serial.println("[setup] GPSTask created."); 508 | 509 | xTaskCreatePinnedToCore( 510 | ntpTask, 511 | "NTPTask", 512 | 4096, 513 | NULL, 514 | configMAX_PRIORITIES - 2, // High priority for responsive NTP service 515 | NULL, 516 | 0); 517 | Serial.println("[setup] NTPTask created."); 518 | 519 | xTaskCreatePinnedToCore( 520 | displayTask, 521 | "DisplayTask", 522 | 2048, 523 | NULL, 524 | configMAX_PRIORITIES - 3, // Lower priority for display updates 525 | NULL, 526 | 0); 527 | Serial.println("[setup] DisplayTask created."); 528 | 529 | Serial.println("[setup] Setup complete. Tasks are running."); 530 | } 531 | 532 | void loop() { 533 | // This loop is intentionally empty as all primary logic is handled by FreeRTOS tasks. 534 | // The default loop task is deleted to save resources. 535 | vTaskDelete(NULL); 536 | } -------------------------------------------------------------------------------- /ESP32/ESP32-C3/tinyNTPServerTime/tinyNTPServerTime.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "credentials.h" // defines ssid, password 12 | 13 | // —————— Debug Macros —————— 14 | #define DEBUG_STANDARD 1 15 | #define DEBUG_TIME_CRITICAL 0 16 | 17 | #if DEBUG_STANDARD 18 | #define DEBUG_PRINT(x) Serial.print(x) 19 | #define DEBUG_PRINTLN(x) Serial.println(x) 20 | #else 21 | #define DEBUG_PRINT(x) 22 | #define DEBUG_PRINTLN(x) 23 | #endif 24 | 25 | #if DEBUG_TIME_CRITICAL 26 | #define DEBUG_CRITICAL_PRINT(x) Serial.print(x) 27 | #define DEBUG_CRITICAL_PRINTLN(x) Serial.println(x) 28 | #else 29 | #define DEBUG_CRITICAL_PRINT(x) 30 | #define DEBUG_CRITICAL_PRINTLN(x) 31 | #endif 32 | 33 | // —————— Pin Definitions —————— 34 | #define GPS_RX_PIN 20 // ESP32-C3 RX1 ← GPS TX 35 | #define GPS_TX_PIN 21 // ESP32-C3 TX1 → GPS RX (optional) 36 | #define PPS_PIN 2 // Pulse-per-second input 37 | 38 | // —————— Globals —————— 39 | TinyGPSPlus gps; 40 | HardwareSerial gpsSerial(1); 41 | WiFiUDP udp; 42 | SemaphoreHandle_t gpsMutex; 43 | 44 | volatile bool PPSsignal = false; 45 | volatile bool PPSavailable = false; 46 | 47 | #define GPSBaud 9600 48 | #define CORRECTION_FACTOR 0 49 | 50 | // —————— OLED Setup —————— 51 | const int xOffset = 30; // center 72px window horizontally 52 | const int yOffset = 25; // center 40px window vertically 53 | U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2( 54 | U8G2_R0, U8X8_PIN_NONE, 55 | /* SCL=*/ 6, /* SDA=*/ 5 56 | ); 57 | 58 | // Draw current time and date on the OLED 59 | void displayTime() { 60 | struct timeval tv; 61 | gettimeofday(&tv, NULL); 62 | time_t now = tv.tv_sec; 63 | struct tm timeinfo; 64 | localtime_r(&now, &timeinfo); 65 | 66 | char line1[16], line2[16]; 67 | snprintf(line1, sizeof(line1), "%02d:%02d:%02d", 68 | timeinfo.tm_hour, 69 | timeinfo.tm_min, 70 | timeinfo.tm_sec); 71 | snprintf(line2, sizeof(line2), "%02d-%02d-%02d", 72 | timeinfo.tm_year % 100, 73 | timeinfo.tm_mon + 1, 74 | timeinfo.tm_mday); 75 | 76 | uint8_t fh = u8g2.getMaxCharHeight(); // ~10px 77 | uint8_t spacing = 2; 78 | uint8_t lineH = fh + spacing; 79 | int x = xOffset + 2; 80 | 81 | u8g2.clearBuffer(); 82 | u8g2.setCursor(x, yOffset + 0 * lineH); 83 | u8g2.print(line1); 84 | u8g2.setCursor(x, yOffset + 1 * lineH); 85 | u8g2.print(line2); 86 | u8g2.sendBuffer(); 87 | } 88 | 89 | // —————— Wi-Fi + NTP Init —————— 90 | void initNetwork() { 91 | WiFi.mode(WIFI_STA); 92 | WiFi.begin(ssid, password); 93 | unsigned long t0 = millis(); 94 | while (WiFi.status() != WL_CONNECTED && millis() - t0 < 10000) { 95 | DEBUG_PRINTLN("Connecting to WiFi..."); 96 | delay(500); 97 | } 98 | if (WiFi.status() == WL_CONNECTED) { 99 | DEBUG_PRINT("WiFi IP: "); DEBUG_PRINTLN(WiFi.localIP()); 100 | } else { 101 | DEBUG_PRINTLN("WiFi failed"); 102 | } 103 | udp.begin(123); // NTP port 104 | } 105 | 106 | void printMacAddress() { 107 | String mac = WiFi.macAddress(); 108 | DEBUG_PRINT("WiFi MAC: "); DEBUG_PRINTLN(mac); 109 | } 110 | 111 | // —————— PPS Setup —————— 112 | void IRAM_ATTR PPS_ISR() { PPSsignal = true; } 113 | 114 | void initPPS() { 115 | pinMode(PPS_PIN, INPUT); 116 | unsigned long t0 = millis(); 117 | while (millis() - t0 < 1500) { 118 | if (digitalRead(PPS_PIN) == HIGH) { 119 | PPSavailable = true; 120 | break; 121 | } 122 | delay(1); 123 | } 124 | if (PPSavailable) { 125 | DEBUG_PRINTLN("PPS detected"); 126 | attachInterrupt(digitalPinToInterrupt(PPS_PIN), PPS_ISR, RISING); 127 | } else { 128 | DEBUG_PRINTLN("No PPS"); 129 | } 130 | } 131 | 132 | // —————— GPS Init & Time Sync —————— 133 | void initGPS() { 134 | gpsSerial.begin(GPSBaud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); 135 | unsigned long t0 = millis(); 136 | while (millis() - t0 < 30000) { 137 | while (gpsSerial.available()) { 138 | gps.encode(gpsSerial.read()); 139 | if (gps.time.isValid() && gps.date.isValid()) { 140 | DEBUG_PRINT("GPS time acquired: "); 141 | if (gps.time.hour() < 10) DEBUG_PRINT('0'); 142 | DEBUG_PRINT(gps.time.hour()); DEBUG_PRINT(':'); 143 | if (gps.time.minute() < 10) DEBUG_PRINT('0'); 144 | DEBUG_PRINT(gps.time.minute()); DEBUG_PRINT(':'); 145 | if (gps.time.second() < 10) DEBUG_PRINT('0'); 146 | DEBUG_PRINTLN(gps.time.second()); 147 | return; 148 | } 149 | } 150 | delay(100); 151 | } 152 | DEBUG_PRINTLN("GPS time fail"); 153 | } 154 | 155 | void readGPSTime() { 156 | while (gpsSerial.available()) { 157 | gps.encode(gpsSerial.read()); 158 | } 159 | if (gps.time.isValid() && gps.date.isValid()) { 160 | struct tm ti = {0}; 161 | ti.tm_year = gps.date.year() - 1900; 162 | ti.tm_mon = gps.date.month() - 1; 163 | ti.tm_mday = gps.date.day(); 164 | ti.tm_hour = gps.time.hour(); 165 | ti.tm_min = gps.time.minute(); 166 | ti.tm_sec = gps.time.second(); 167 | time_t unixTime = mktime(&ti) + 1; 168 | if (PPSavailable) { 169 | while (!PPSsignal) {} 170 | PPSsignal = false; 171 | } 172 | struct timeval tv = { .tv_sec = unixTime, .tv_usec = CORRECTION_FACTOR }; 173 | settimeofday(&tv, NULL); 174 | DEBUG_CRITICAL_PRINTLN("GPS time updated"); 175 | } 176 | } 177 | 178 | // —————— NTP Handler —————— 179 | void handleNTPRequest() { 180 | int size = udp.parsePacket(); 181 | if (size >= 48) { 182 | uint8_t req[48], resp[48]; 183 | udp.read(req, 48); 184 | 185 | // 1) Decode the client’s Transmit Timestamp (T₁) from bytes 40..47 186 | uint32_t T1_sec = (uint32_t)req[40]<<24 | (uint32_t)req[41]<<16 | (uint32_t)req[42]<<8 | (uint32_t)req[43]; 187 | uint32_t T1_frac = (uint32_t)req[44]<<24 | (uint32_t)req[45]<<16 | (uint32_t)req[46]<<8 | (uint32_t)req[47]; 188 | 189 | // 2) Record server receipt time (T₂) 190 | struct timeval tv2; 191 | gettimeofday(&tv2, NULL); 192 | uint32_t T2_sec = tv2.tv_sec + 2208988800UL; 193 | uint32_t T2_frac = (uint32_t)( (double)tv2.tv_usec * (4294967296.0/1e6) ); 194 | 195 | // Build the response header (unchanged)… 196 | resp[0] = 0x24; resp[1] = 1; resp[2] = req[2]; resp[3] = (uint8_t)-6; 197 | memset(&resp[4], 0, 8); 198 | resp[12]='L'; resp[13]='O'; resp[14]='C'; resp[15]='L'; 199 | 200 | // Reference Timestamp = T₂ 201 | for (int i = 0; i < 4; i++) resp[16+i] = (T2_sec >> (24-8*i)) & 0xFF; 202 | for (int i = 0; i < 4; i++) resp[20+i] = (T2_frac >> (24-8*i)) & 0xFF; 203 | 204 | // Originate Timestamp = client’s T₁ 205 | memcpy(&resp[24], &req[40], 8); 206 | 207 | // 3) Record server transmit time (T₃) immediately before sending 208 | struct timeval tv3; 209 | gettimeofday(&tv3, NULL); 210 | uint32_t T3_sec = tv3.tv_sec + 2208988800UL; 211 | uint32_t T3_frac = (uint32_t)( (double)tv3.tv_usec * (4294967296.0/1e6) ); 212 | for (int i = 0; i < 4; i++) resp[32+i] = (T3_sec >> (24-8*i)) & 0xFF; 213 | for (int i = 0; i < 4; i++) resp[36+i] = (T3_frac >> (24-8*i)) & 0xFF; 214 | for (int i = 0; i < 4; i++) resp[40+i] = (T3_sec >> (24-8*i)) & 0xFF; 215 | for (int i = 0; i < 4; i++) resp[44+i] = (T3_frac >> (24-8*i)) & 0xFF; 216 | 217 | // 4) Send the packet 218 | udp.beginPacket(udp.remoteIP(), udp.remotePort()); 219 | udp.write(resp, 48); 220 | udp.endPacket(); 221 | 222 | // 5) Log all three timestamps to Serial 223 | Serial.printf("T1(client orig): %10u.%08u\n", T1_sec, T1_frac); 224 | Serial.printf("T2(server recv): %10u.%08u\n", T2_sec, T2_frac); 225 | Serial.printf("T3(server send): %10u.%08u\n\n", T3_sec, T3_frac); 226 | } 227 | } 228 | 229 | 230 | // —————— Tasks —————— 231 | void gpsTask(void *param) { 232 | // Initialize GPS UART & OLED once 233 | gpsSerial.begin(GPSBaud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); 234 | u8g2.begin(); 235 | u8g2.setContrast(255); 236 | u8g2.setBusClock(400000); 237 | u8g2.setFont(u8g2_font_6x10_tr); 238 | u8g2.setFontPosTop(); 239 | 240 | for (;;) { 241 | readGPSTime(); 242 | displayTime(); // show synced time instead of lat/lon 243 | vTaskDelay(pdMS_TO_TICKS(500)); 244 | } 245 | } 246 | 247 | void ntpTask(void *param) { 248 | for (;;) { 249 | handleNTPRequest(); 250 | vTaskDelay(pdMS_TO_TICKS(100)); 251 | } 252 | } 253 | 254 | void setup() { 255 | Serial.begin(115200); 256 | initNetwork(); 257 | printMacAddress(); 258 | initPPS(); 259 | initGPS(); 260 | 261 | gpsMutex = xSemaphoreCreateMutex(); 262 | xTaskCreatePinnedToCore(gpsTask, "GPSTask", 4096, NULL, 1, NULL, 0); 263 | xTaskCreatePinnedToCore(ntpTask, "NTPTask", 4096, NULL, 1, NULL, 0); 264 | } 265 | 266 | void loop() { 267 | // everything handled in tasks 268 | } 269 | -------------------------------------------------------------------------------- /ESP32/ESP32-Ethernet/PPS_Newaproach_V3/PPS_Newaproach_V3.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include // For WiFi fallback and control 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "credentials.h" // Contains ssid and password definitions 11 | 12 | // Debugging levels 13 | #define DEBUG_STANDARD 1 14 | #define DEBUG_TIME_CRITICAL 0 15 | 16 | // Debugging Macros 17 | #if DEBUG_STANDARD 18 | #define DEBUG_PRINT(x) Serial.print(x) 19 | #define DEBUG_PRINTLN(x) Serial.println(x) 20 | #else 21 | #define DEBUG_PRINT(x) 22 | #define DEBUG_PRINTLN(x) 23 | #endif 24 | 25 | #if DEBUG_TIME_CRITICAL 26 | #define DEBUG_CRITICAL_PRINT(x) Serial.print(x) 27 | #define DEBUG_CRITICAL_PRINTLN(x) Serial.println(x) 28 | #else 29 | #define DEBUG_CRITICAL_PRINT(x) 30 | #define DEBUG_CRITICAL_PRINTLN(x) 31 | #endif 32 | 33 | // Global Variables 34 | TinyGPSPlus gps; 35 | SemaphoreHandle_t gpsMutex; 36 | WiFiUDP udp; 37 | 38 | volatile bool PPSsignal = false; 39 | volatile bool PPSavailable = false; 40 | 41 | HardwareSerial gpsSerial(1); 42 | 43 | // Baud rate for GPS and correction factor for time update 44 | #define GPSBaud 9600 45 | #define CORRECTION_FACTOR 0 46 | 47 | #define EEPROM_SIZE 1 48 | 49 | // Board configuration structure including PPS and GPS pins. 50 | struct BoardConfig { 51 | const char *boardName; 52 | eth_phy_type_t phyType; 53 | int ethAddr; 54 | int powerPin; // Use -1 if no Ethernet power control is needed. 55 | int mdcPin; 56 | int mdioPin; 57 | eth_clock_mode_t clkMode; 58 | int rxPin; // GPS RX pin 59 | int txPin; // GPS TX pin 60 | int ppsPin; // PPS signal pin 61 | }; 62 | 63 | // Define board configurations for different boards. 64 | BoardConfig boardConfigs[] = { 65 | // Board 0: LilyGo PoE configuration (Ethernet present). 66 | { "LilyGo PoE", ETH_PHY_LAN8720, 0, 5, 23, 18, ETH_CLOCK_GPIO17_OUT, 15, 4, 14 }, 67 | // Board 1: WT32-eth01 configuration (Ethernet present). 68 | { "WT32-eth01", ETH_PHY_LAN8720, 1, 16, 23, 18, ETH_CLOCK_GPIO0_IN, 15, 4, 2 }, 69 | // Board 2: WiFi Only configuration (no Ethernet hardware). 70 | { "WiFi Only", (eth_phy_type_t)0, 0, -1, -1, -1, ETH_CLOCK_GPIO17_OUT, 15, 4, 2 } 71 | }; 72 | const int numBoards = sizeof(boardConfigs) / sizeof(boardConfigs[0]); 73 | 74 | // Global variable to store the selected board configuration. 75 | BoardConfig currentBoardConfig; 76 | 77 | // Global flag indicating if WiFi is used (fallback or WiFi-only configuration). 78 | bool useWiFi = false; 79 | 80 | // Decide which configuration to use (here using EEPROM to cycle through boards). 81 | void selectBoardConfig() { 82 | if (!EEPROM.begin(EEPROM_SIZE)) { 83 | DEBUG_PRINTLN("Failed to initialize EEPROM."); 84 | } 85 | int lastTested = EEPROM.read(0); 86 | int currentBoard = (lastTested + 1) % numBoards; 87 | currentBoardConfig = boardConfigs[currentBoard]; 88 | DEBUG_PRINT("Selected board: "); 89 | DEBUG_PRINTLN(currentBoardConfig.boardName); 90 | 91 | // Print the actual pins from the configuration. 92 | DEBUG_PRINT("RX pin: "); 93 | DEBUG_PRINTLN(currentBoardConfig.rxPin); 94 | DEBUG_PRINT("TX pin: "); 95 | DEBUG_PRINTLN(currentBoardConfig.txPin); 96 | DEBUG_PRINT("PPS pin: "); 97 | DEBUG_PRINTLN(currentBoardConfig.ppsPin); 98 | 99 | // Save the configuration index back to EEPROM. 100 | EEPROM.write(0, currentBoard); 101 | EEPROM.commit(); 102 | } 103 | 104 | // Network initialization: if powerPin is valid, attempt Ethernet initialization; 105 | // otherwise (or if Ethernet fails), use WiFi. 106 | void initNetwork() { 107 | selectBoardConfig(); 108 | 109 | // If the board is WiFi-only (powerPin == -1), skip Ethernet initialization. 110 | if (currentBoardConfig.powerPin == -1) { 111 | DEBUG_PRINTLN("WiFi Only configuration selected. Skipping Ethernet."); 112 | useWiFi = true; 113 | WiFi.mode(WIFI_STA); 114 | WiFi.begin(ssid, password); 115 | unsigned long wifiStartTime = millis(); 116 | const unsigned long wifiTimeout = 10000; // 10 second timeout 117 | while (WiFi.status() != WL_CONNECTED && (millis() - wifiStartTime < wifiTimeout)) { 118 | DEBUG_PRINTLN("Connecting to WiFi..."); 119 | delay(500); 120 | } 121 | if (WiFi.status() == WL_CONNECTED) { 122 | DEBUG_PRINT("WiFi connected! IP address: "); 123 | DEBUG_PRINTLN(WiFi.localIP()); 124 | } else { 125 | DEBUG_PRINTLN("WiFi connection failed."); 126 | } 127 | } else { 128 | // For Ethernet-enabled boards, power-cycle the Ethernet chip if needed. 129 | pinMode(currentBoardConfig.powerPin, OUTPUT); 130 | digitalWrite(currentBoardConfig.powerPin, LOW); 131 | delay(100); 132 | digitalWrite(currentBoardConfig.powerPin, HIGH); 133 | delay(100); 134 | 135 | DEBUG_PRINT("Initializing Ethernet for "); 136 | DEBUG_PRINTLN(currentBoardConfig.boardName); 137 | bool ethStarted = ETH.begin(currentBoardConfig.phyType, 138 | currentBoardConfig.ethAddr, 139 | currentBoardConfig.mdcPin, 140 | currentBoardConfig.mdioPin, 141 | currentBoardConfig.powerPin, 142 | currentBoardConfig.clkMode); 143 | bool ethInitialized = false; 144 | if (ethStarted) { 145 | DEBUG_PRINT("Ethernet initialized successfully for "); 146 | DEBUG_PRINTLN(currentBoardConfig.boardName); 147 | unsigned long startTime = millis(); 148 | const unsigned long ethTimeout = 10000; // 10 second timeout 149 | while (ETH.localIP() == IPAddress(0, 0, 0, 0) && (millis() - startTime < ethTimeout)) { 150 | DEBUG_PRINTLN("Waiting for Ethernet IP address..."); 151 | delay(500); 152 | } 153 | if (ETH.localIP() != IPAddress(0, 0, 0, 0)) { 154 | DEBUG_PRINT("Ethernet connected! IP Address: "); 155 | DEBUG_PRINTLN(ETH.localIP().toString().c_str()); 156 | ethInitialized = true; 157 | } else { 158 | DEBUG_PRINTLN("Ethernet IP not obtained. Falling back to WiFi."); 159 | } 160 | } else { 161 | DEBUG_PRINT("Ethernet initialization failed for "); 162 | DEBUG_PRINTLN(currentBoardConfig.boardName); 163 | } 164 | if (ethInitialized) { 165 | DEBUG_PRINTLN("Disabling WiFi radio."); 166 | WiFi.disconnect(true); 167 | WiFi.mode(WIFI_OFF); 168 | } else { 169 | // Fallback to WiFi if Ethernet fails. 170 | useWiFi = true; 171 | DEBUG_PRINTLN("Initializing WiFi as fallback..."); 172 | WiFi.mode(WIFI_STA); 173 | WiFi.begin(ssid, password); 174 | unsigned long wifiStartTime = millis(); 175 | const unsigned long wifiTimeout = 10000; // 10 second timeout 176 | while (WiFi.status() != WL_CONNECTED && (millis() - wifiStartTime < wifiTimeout)) { 177 | DEBUG_PRINTLN("Connecting to WiFi..."); 178 | delay(500); 179 | } 180 | if (WiFi.status() == WL_CONNECTED) { 181 | DEBUG_PRINT("WiFi connected! IP address: "); 182 | DEBUG_PRINTLN(WiFi.localIP()); 183 | } else { 184 | DEBUG_PRINTLN("WiFi connection failed."); 185 | } 186 | } 187 | } 188 | udp.begin(123); // Start UDP on port 123 (used for NTP) 189 | } 190 | 191 | void printMacAddress() { 192 | char macStr[18]; // 17 characters + null terminator 193 | if (!useWiFi) { 194 | // Ethernet is enabled: get MAC from ETH 195 | uint8_t mac[6]; 196 | ETH.macAddress(mac); 197 | sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", 198 | mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); 199 | DEBUG_PRINT("Ethernet MAC Address: "); 200 | DEBUG_PRINTLN(macStr); 201 | } else { 202 | // WiFi fallback: get MAC from WiFi 203 | String mac = WiFi.macAddress(); 204 | DEBUG_PRINT("WiFi MAC Address: "); 205 | DEBUG_PRINTLN(mac); 206 | } 207 | } 208 | 209 | 210 | // PPS Interrupt Service Routine 211 | void IRAM_ATTR PPS_ISR() { 212 | PPSsignal = true; 213 | } 214 | 215 | void initPPS() { 216 | // Set the PPS pin as a normal input. 217 | pinMode(currentBoardConfig.ppsPin, INPUT); 218 | // Print the actual PPS pin used. 219 | DEBUG_PRINT("Using PPS pin: "); 220 | DEBUG_PRINTLN(currentBoardConfig.ppsPin); 221 | 222 | DEBUG_PRINTLN("Checking PPS signal..."); 223 | 224 | unsigned long startTime = millis(); 225 | PPSavailable = false; 226 | 227 | // Increase detection window to 1500 ms, sampling every 1 ms. 228 | while (millis() - startTime < 1500) { 229 | if (digitalRead(currentBoardConfig.ppsPin) == HIGH) { 230 | PPSavailable = true; 231 | break; 232 | } 233 | delay(1); 234 | } 235 | 236 | if (PPSavailable) { 237 | DEBUG_PRINTLN("PPS signal detected."); 238 | attachInterrupt(digitalPinToInterrupt(currentBoardConfig.ppsPin), PPS_ISR, RISING); 239 | DEBUG_PRINTLN("PPS initialized. Waiting for signal..."); 240 | } else { 241 | DEBUG_PRINTLN("PPS signal not detected! Continuing without PPS synchronization."); 242 | } 243 | } 244 | 245 | void initGPS() { 246 | DEBUG_PRINTLN("Initializing GPS..."); 247 | // Use the RX/TX pins from the current board configuration. 248 | DEBUG_PRINT("Using GPS RX pin: "); 249 | DEBUG_PRINTLN(currentBoardConfig.rxPin); 250 | DEBUG_PRINT("Using GPS TX pin: "); 251 | DEBUG_PRINTLN(currentBoardConfig.txPin); 252 | 253 | gpsSerial.begin(GPSBaud, SERIAL_8N1, currentBoardConfig.rxPin, currentBoardConfig.txPin); 254 | unsigned long startTime = millis(); 255 | while (millis() - startTime < 30000) { 256 | while (gpsSerial.available()) { 257 | gps.encode(gpsSerial.read()); 258 | if (gps.time.isValid()) { 259 | DEBUG_PRINT("GPS time acquired: "); 260 | if (gps.time.hour() < 10) DEBUG_PRINT("0"); 261 | DEBUG_PRINT(gps.time.hour()); 262 | DEBUG_PRINT(":"); 263 | if (gps.time.minute() < 10) DEBUG_PRINT("0"); 264 | DEBUG_PRINT(gps.time.minute()); 265 | DEBUG_PRINT(":"); 266 | if (gps.time.second() < 10) DEBUG_PRINT("0"); 267 | DEBUG_PRINTLN(gps.time.second()); 268 | return; 269 | } 270 | } 271 | delay(100); 272 | } 273 | DEBUG_PRINTLN("Failed to acquire valid GPS time!"); 274 | } 275 | 276 | void readGPSTime() { 277 | while (gpsSerial.available()) { 278 | gps.encode(gpsSerial.read()); 279 | } 280 | if (gps.time.isValid() && gps.date.isValid()) { 281 | struct tm timeinfo = { 0 }; 282 | timeinfo.tm_year = gps.date.year() - 1900; 283 | timeinfo.tm_mon = gps.date.month() - 1; 284 | timeinfo.tm_mday = gps.date.day(); 285 | timeinfo.tm_hour = gps.time.hour(); 286 | timeinfo.tm_min = gps.time.minute(); 287 | timeinfo.tm_sec = gps.time.second(); 288 | time_t unixTime = mktime(&timeinfo) + 1; 289 | // If PPS is available, wait for the next PPS signal for precise synchronization. 290 | if (PPSavailable) { 291 | while (!PPSsignal) {} 292 | PPSsignal = false; 293 | } 294 | struct timeval tv; 295 | tv.tv_sec = unixTime; 296 | tv.tv_usec = CORRECTION_FACTOR; 297 | settimeofday(&tv, NULL); 298 | DEBUG_CRITICAL_PRINTLN("GPS Time Updated"); 299 | } 300 | } 301 | 302 | void handleNTPRequest() { 303 | int packetSize = udp.parsePacket(); 304 | if (packetSize >= 48) { // NTP packets are 48 bytes long 305 | uint8_t requestBuffer[48]; 306 | uint8_t responseBuffer[48]; 307 | udp.read(requestBuffer, 48); 308 | struct timeval tv; 309 | gettimeofday(&tv, NULL); 310 | uint32_t recvSec = tv.tv_sec + 2208988800UL; 311 | uint32_t recvFrac = (uint32_t)((double)tv.tv_usec * (4294967296.0 / 1000000.0)); 312 | responseBuffer[0] = 0x24; // LI=0, VN=4, Mode=4 (server) 313 | responseBuffer[1] = 1; // Stratum 1 314 | responseBuffer[2] = requestBuffer[2]; // Poll interval from request 315 | responseBuffer[3] = -6; // Precision (example) 316 | // Clear Root Delay & Root Dispersion 317 | for (int i = 4; i < 12; i++) { 318 | responseBuffer[i] = 0; 319 | } 320 | // Reference Identifier ("LOCL") 321 | responseBuffer[12] = 'L'; 322 | responseBuffer[13] = 'O'; 323 | responseBuffer[14] = 'C'; 324 | responseBuffer[15] = 'L'; 325 | // Reference Timestamp 326 | responseBuffer[16] = (recvSec >> 24) & 0xFF; 327 | responseBuffer[17] = (recvSec >> 16) & 0xFF; 328 | responseBuffer[18] = (recvSec >> 8) & 0xFF; 329 | responseBuffer[19] = recvSec & 0xFF; 330 | responseBuffer[20] = (recvFrac >> 24) & 0xFF; 331 | responseBuffer[21] = (recvFrac >> 16) & 0xFF; 332 | responseBuffer[22] = (recvFrac >> 8) & 0xFF; 333 | responseBuffer[23] = recvFrac & 0xFF; 334 | // Originate Timestamp: copy client's Transmit Timestamp (offset 40) 335 | for (int i = 0; i < 8; i++) { 336 | responseBuffer[24 + i] = requestBuffer[40 + i]; 337 | } 338 | // Receive Timestamp 339 | responseBuffer[32] = (recvSec >> 24) & 0xFF; 340 | responseBuffer[33] = (recvSec >> 16) & 0xFF; 341 | responseBuffer[34] = (recvSec >> 8) & 0xFF; 342 | responseBuffer[35] = recvSec & 0xFF; 343 | responseBuffer[36] = (recvFrac >> 24) & 0xFF; 344 | responseBuffer[37] = (recvFrac >> 16) & 0xFF; 345 | responseBuffer[38] = (recvFrac >> 8) & 0xFF; 346 | responseBuffer[39] = recvFrac & 0xFF; 347 | // Transmit Timestamp (recorded just before sending) 348 | gettimeofday(&tv, NULL); 349 | uint32_t txSec = tv.tv_sec + 2208988800UL; 350 | uint32_t txFrac = (uint32_t)((double)tv.tv_usec * (4294967296.0 / 1000000.0)); 351 | responseBuffer[40] = (txSec >> 24) & 0xFF; 352 | responseBuffer[41] = (txSec >> 16) & 0xFF; 353 | responseBuffer[42] = (txSec >> 8) & 0xFF; 354 | responseBuffer[43] = txSec & 0xFF; 355 | responseBuffer[44] = (txFrac >> 24) & 0xFF; 356 | responseBuffer[45] = (txFrac >> 16) & 0xFF; 357 | responseBuffer[46] = (txFrac >> 8) & 0xFF; 358 | responseBuffer[47] = txFrac & 0xFF; 359 | udp.beginPacket(udp.remoteIP(), udp.remotePort()); 360 | udp.write(responseBuffer, 48); 361 | udp.endPacket(); 362 | DEBUG_PRINTLN("NTP response sent."); 363 | } 364 | } 365 | 366 | void gpsTask(void *parameter) { 367 | while (1) { 368 | readGPSTime(); 369 | vTaskDelay(pdMS_TO_TICKS(10)); 370 | } 371 | } 372 | 373 | void ntpTask(void *parameter) { 374 | while (1) { 375 | handleNTPRequest(); 376 | vTaskDelay(pdMS_TO_TICKS(100)); 377 | } 378 | } 379 | 380 | void setup() { 381 | Serial.begin(115200); 382 | initNetwork(); // Use Ethernet if available; otherwise, fall back to WiFi. 383 | printMacAddress(); // Print the actual MAC address used. 384 | initPPS(); 385 | initGPS(); 386 | gpsMutex = xSemaphoreCreateMutex(); 387 | xTaskCreatePinnedToCore(gpsTask, "GPSTask", 4096, NULL, 1, NULL, 0); 388 | xTaskCreatePinnedToCore(ntpTask, "NTPTask", 4096, NULL, 1, NULL, 0); 389 | } 390 | 391 | void loop() { 392 | // The tasks handle GPS and NTP functionality. 393 | } 394 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the writeup for the YouTube Video (Raspberry Pi): https://youtu.be/RKRN4p0gobk 2 | 3 | And this video (ESP32): https://youtu.be/BGb2t5FT-zw 4 | -------------------------------------------------------------------------------- /Raspberry Pi/NTP Server with Chrony.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/NTP-Server-with-GPS/20af20b43ffd5aa03f6ab6cb83868d1f64ad0485/Raspberry Pi/NTP Server with Chrony.pdf --------------------------------------------------------------------------------