├── .gitattributes ├── DFC77_ESP32 ├── SHOW_TIME.ino ├── NTP.ino ├── WiFi.ino ├── SLEEP_CRON.ino └── DFC77_ESP32.ino └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /DFC77_ESP32/SHOW_TIME.ino: -------------------------------------------------------------------------------- 1 | void show_time() { 2 | Serial.print(&timeinfo, "Time now: %B %d %Y %H:%M:%S (%A) %Z "); 3 | if (timeinfo.tm_isdst == 0) { 4 | Serial.println("DST=OFF"); 5 | } else { 6 | Serial.println("DST=ON"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /DFC77_ESP32/NTP.ino: -------------------------------------------------------------------------------- 1 | 2 | void getNTP() { 3 | Serial.print("GetNTP "); 4 | int i = 0; 5 | do { 6 | i++; 7 | if (i > 40) { 8 | ESP.restart(); 9 | } 10 | configTime(0, 0, ntpServer); 11 | setenv("TZ", TZ_INFO, 1); 12 | delay(500); 13 | } while (!getLocalTime(&timeinfo)); 14 | Serial.println("Ok"); 15 | } 16 | -------------------------------------------------------------------------------- /DFC77_ESP32/WiFi.ino: -------------------------------------------------------------------------------- 1 | 2 | void WiFi_on() { 3 | Serial.print("Connecting WiFi..."); 4 | WiFi.begin(ssid, password); 5 | int counter = 0; 6 | while (WiFi.status() != WL_CONNECTED) { 7 | delay(500); 8 | if (counter > 20) ESP.restart(); 9 | Serial.print ( "." ); 10 | counter++; 11 | } 12 | Serial.println(); 13 | Serial.println("WiFi connected"); 14 | } 15 | 16 | void WiFi_off() { 17 | WiFi.disconnect(true); 18 | WiFi.mode(WIFI_OFF); 19 | Serial.println("WiFi disconnected"); 20 | Serial.flush(); 21 | } 22 | -------------------------------------------------------------------------------- /DFC77_ESP32/SLEEP_CRON.ino: -------------------------------------------------------------------------------- 1 | void sleepForMinutes(int minutes) { 2 | if (minutes < 2) return; 3 | uint64_t uSecToMinutes = 60000000; 4 | esp_sleep_enable_timer_wakeup(minutes * uSecToMinutes); // this timer works on uSecs, so 60M by minute 5 | //WiFi_off(); 6 | Serial.print("To sleep... "); 7 | Serial.print(minutes); 8 | Serial.println(" minutes"); 9 | Serial.flush(); 10 | esp_deep_sleep_start(); 11 | } 12 | 13 | void cronCheck() { 14 | // is this hour in the list? 15 | boolean work = false; 16 | for (int n = 0; n < sizeof(hoursToWakeUp); n++) { 17 | //Serial.println(sizeof(hoursToWakeUp)); Serial.print(work); Serial.print(" "); Serial.print(n); Serial.print(" "); Serial.print(actualHours); Serial.print(" "); Serial.println(hoursToWakeUp[n]); 18 | if ((actualHours == hoursToWakeUp[n]) or (actualHours == (hoursToWakeUp[n] + 1))){ 19 | work = true; 20 | // is this the minute to go to sleep? 21 | if ((actualMinutes > minuteToSleep) and (actualMinutes < minuteToWakeUp)) { 22 | // go to sleep (minuteToWakeUp - actualMinutes) 23 | Serial.print("."); 24 | sleepForMinutes(minuteToWakeUp - actualMinutes); 25 | } 26 | break; 27 | } 28 | } 29 | if (work == false) { // sleep until minuteToWakeUp (take into account the ESP32 can start for some reason between minuteToWakeUp and o'clock) 30 | if (actualMinutes >= minuteToWakeUp) { 31 | Serial.print(".."); 32 | sleepForMinutes(minuteToWakeUp + 60 - actualMinutes); 33 | } else { 34 | // goto sleep for (minuteToWakeUp - actualMinutes) minutes 35 | Serial.print("..."); 36 | sleepForMinutes(minuteToWakeUp - actualMinutes); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DCF77 Transmitter for ESP32 2 | 3 | [![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![GitHub stars](https://img.shields.io/github/stars/SensorsIot/DCF77-Transmitter-for-ESP32)](https://github.com/SensorsIot/DCF77-Transmitter-for-ESP32/stargazers) 5 | [![YouTube](https://img.shields.io/badge/YouTube-Video-red?logo=youtube)](https://youtu.be/r2UAmBLBBRM) 6 | 7 | ![ESP32](https://img.shields.io/badge/ESP32-Supported-green?logo=espressif) 8 | ![Arduino](https://img.shields.io/badge/Arduino-Compatible-00979D?logo=arduino) 9 | ![77.5kHz](https://img.shields.io/badge/DCF77-77.5kHz-blue) 10 | 11 | Simulate a DCF77 time signal transmitter using an ESP32. Sync your radio-controlled clocks without receiving the actual DCF77 signal. 12 | 13 | 📺 **Video Tutorial:** https://youtu.be/r2UAmBLBBRM 14 | 15 | ## 🔌 Wiring 16 | 17 | | Function | GPIO | 18 | |----------|------| 19 | | Antenna | GPIO 15 | 20 | | LED | GPIO 5 | 21 | 22 | > 💡 **Tip:** Connect antenna from GPIO 15 to ground with a 1kΩ resistor to limit transmitting power. A tuned ferrite antenna reaches ~3 meters. 23 | 24 | ## ⚙️ Configuration 25 | 26 | ### 📶 WiFi Credentials 27 | 28 | Create a file named `credentials.h` in the sketch folder: 29 | 30 | ```cpp 31 | #ifndef CREDENTIALS_H 32 | #define CREDENTIALS_H 33 | 34 | const char* ssid = "your-wifi-name"; 35 | const char* password = "your-wifi-password"; 36 | 37 | #endif 38 | ``` 39 | 40 | ### 🌍 Time Zone 41 | 42 | Edit the time zone in `DFC77_ESP32.ino`: 43 | 44 | ```cpp 45 | const char* ntpServer = "pool.ntp.org"; 46 | const char* TZ_INFO = "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"; 47 | ``` 48 | 49 | Find your time zone: https://remotemonitoringsystems.ca/time-zone-abbreviations.php 50 | 51 | ### ⏰ Continuous vs Scheduled Mode 52 | 53 | By default, the transmitter runs on a schedule to save power. To transmit continuously: 54 | 55 | ```cpp 56 | #define CONTINUOUSMODE // Uncomment to transmit all the time 57 | ``` 58 | 59 | ## 💡 Troubleshooting 60 | 61 | ### Deep Sleep NTP Sync Issue 62 | 63 | If time drifts after waking from deep sleep, add this to `setup()` to force NTP sync: 64 | 65 | ```cpp 66 | struct timeval tv; 67 | tv.tv_sec = -1; 68 | settimeofday(&tv, NULL); 69 | ``` 70 | 71 | ### ESP32-C3 Compatibility 72 | 73 | The ESP32-C3 requires amplitude modulation (15% signal level for "1" bits). Standard PWM won't work correctly. You'll need to use two pins with different attenuation levels to achieve proper AM modulation. 74 | 75 | ## 📦 Dependencies 76 | 77 | | Library | Description | 78 | |---------|-------------| 79 | | WiFi.h | Built-in ESP32 WiFi | 80 | | Ticker.h | Built-in timer library | 81 | | Time.h | Time functions | 82 | -------------------------------------------------------------------------------- /DFC77_ESP32/DFC77_ESP32.ino: -------------------------------------------------------------------------------- 1 | /* 2 | based on this sketch: https://github.com/aknik/ESP32/blob/master/DFC77/DFC77_esp32_Solo.ino 3 | 4 | Some functions are inspired by work of G6EJD ( https://www.youtube.com/channel/UCgtlqH_lkMdIa4jZLItcsTg ) 5 | 6 | Refactor by DeltaZero, converts to syncronous, added "cron" that you can bypass, see line 29 7 | The cron does not start until 10 minutes from reset (see constant onTimeAfterReset) 8 | Every clock I know starts to listen to the radio at aproximatelly the hour o'clock, so cron takes this into account 9 | 10 | Alarm clocks from Junghans: Every hour (innecesery) 11 | Weather Station from Brigmton: 2 and 3 AM 12 | Chinesse movements and derivatives: 1 o'clock AM 13 | */ 14 | 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | 22 | #include "credentials.h" // If you put this file in the same forlder that the rest of the tabs, then use "" to delimiter, 23 | // otherwise use <> or comment it and write your credentials directly on code 24 | // const char* ssid = "YourOwnSSID"; 25 | // const char* password = "YourSoSecretPassword"; 26 | 27 | #define LEDBUILTIN 5 // LED pin, LED flashes when antenna is transmitting 28 | #define ANTENNAPIN 15 // Antenna pin. Connect antenna from here to ground, use a 1k resistor to limit transmitting power. A slightly tuned ferrite antenna gets around 3 meters and a wire loop may work if close enough. 29 | #define CONTINUOUSMODE // Uncomment this line to bypass de cron and have the transmitter on all the time 30 | 31 | // cron (if you choose the correct values you can even run on batteries) 32 | // If you choose really bad this minutes, everything goes wrong, so minuteToWakeUp must be greater than minuteToSleep 33 | #define minuteToWakeUp 55 // Every hoursToWakeUp at this minute the ESP32 wakes up get time and star to transmit 34 | #define minuteToSleep 8 // If it is running at this minute then goes to sleep and waits until minuteToWakeUp 35 | 36 | 37 | byte hoursToWakeUp[] = {0,1,2,3}; // you can add more hours to adapt to your needs 38 | // When the ESP32 wakes up, check if the actual hour is in the list and 39 | // runs or goes to sleep until next minuteToWakeUp 40 | 41 | Ticker tickerDecisec; // TBD at 100ms 42 | 43 | //complete array of pulses for a minute 44 | //0 = no pulse, 1=100ms, 2=200ms 45 | int impulseArray[60]; 46 | int impulseCount = 0; 47 | int actualHours, actualMinutes, actualSecond, actualDay, actualMonth, actualYear, DayOfW; 48 | long dontGoToSleep = 0; 49 | const long onTimeAfterReset = 600000; // Ten minutes 50 | int timeRunningContinuous = 0; 51 | 52 | const char* ntpServer = "es.pool.ntp.org"; // enter your closer pool or pool.ntp.org 53 | const char* TZ_INFO = "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"; // enter your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php) 54 | 55 | struct tm timeinfo; 56 | 57 | void setup() { 58 | esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); 59 | 60 | Serial.begin(115200); 61 | Serial.println(); 62 | Serial.println("DCF77 transmitter"); 63 | 64 | /* 65 | can be added to save energy when battery-operated 66 | 67 | if(setCpuFrequencyMhz(80)){ 68 | Serial.print("CPU frequency set @"); 69 | Serial.print(getCpuFrequencyMhz()); 70 | Serial.println("Mhz"); 71 | } 72 | else 73 | Serial.println("Fail to set cpu frequency"); 74 | */ 75 | if (esp_sleep_get_wakeup_cause() == 0) dontGoToSleep = millis(); 76 | 77 | ledcAttach(ANTENNAPIN, 77500, 8); // Set pin PWM, 77500hz DCF freq, resolution of 8bit 78 | 79 | pinMode (LEDBUILTIN, OUTPUT); 80 | digitalWrite (LEDBUILTIN, LOW); // LOW if LEDBUILTIN is inverted like in Wemos boards 81 | 82 | WiFi_on(); 83 | getNTP(); 84 | WiFi_off(); 85 | show_time(); 86 | 87 | CodeTime(); // first conversion just for cronCheck 88 | #ifndef CONTINUOUSMODE 89 | if ((dontGoToSleep == 0) or ((dontGoToSleep + onTimeAfterReset) < millis())) cronCheck(); // first check before start anything 90 | #else 91 | Serial.println("CONTINUOUS MODE NO CRON!!!"); 92 | #endif 93 | 94 | // sync to the start of a second 95 | Serial.print("Syncing... "); 96 | int startSecond = timeinfo.tm_sec; 97 | long count = 0; 98 | do { 99 | count++; 100 | if(!getLocalTime(&timeinfo)){ 101 | Serial.println("Error obtaining time..."); 102 | delay(3000); 103 | ESP.restart(); 104 | } 105 | } while (startSecond == timeinfo.tm_sec); 106 | 107 | tickerDecisec.attach_ms(100, DcfOut); // from now on calls DcfOut every 100ms 108 | Serial.print("Ok "); 109 | Serial.println(count); 110 | } 111 | 112 | void loop() { 113 | // There is no code inside the loop. This is a syncronous program driven by the Ticker 114 | } 115 | 116 | void CodeTime() { 117 | DayOfW = timeinfo.tm_wday; 118 | if (DayOfW == 0) DayOfW = 7; 119 | actualDay = timeinfo.tm_mday; 120 | actualMonth = timeinfo.tm_mon + 1; 121 | actualYear = timeinfo.tm_year - 100; 122 | actualHours = timeinfo.tm_hour; 123 | actualMinutes = timeinfo.tm_min + 1; // DCF77 transmitts the next minute 124 | if (actualMinutes >= 60) { 125 | actualMinutes = 0; 126 | actualHours++; 127 | } 128 | actualSecond = timeinfo.tm_sec; 129 | if (actualSecond == 60) actualSecond = 0; 130 | 131 | int n, Tmp, TmpIn; 132 | int ParityCount = 0; 133 | 134 | //we put the first 20 bits of each minute at a logical zero value 135 | for (n = 0; n < 20; n++) impulseArray[n] = 1; 136 | 137 | // set DST bit 138 | if (timeinfo.tm_isdst == 0) { 139 | impulseArray[18] = 2; // CET or DST OFF 140 | } else { 141 | impulseArray[17] = 2; // CEST or DST ON 142 | } 143 | 144 | //bit 20 must be 1 to indicate active time 145 | impulseArray[20] = 2; 146 | 147 | //calculates the bits for the minutes 148 | TmpIn = Bin2Bcd(actualMinutes); 149 | for (n = 21; n < 28; n++) { 150 | Tmp = TmpIn & 1; 151 | impulseArray[n] = Tmp + 1; 152 | ParityCount += Tmp; 153 | TmpIn >>= 1; 154 | } 155 | if ((ParityCount & 1) == 0) 156 | impulseArray[28] = 1; 157 | else 158 | impulseArray[28] = 2; 159 | 160 | //calculates bits for the hours 161 | ParityCount = 0; 162 | TmpIn = Bin2Bcd(actualHours); 163 | for (n = 29; n < 35; n++) { 164 | Tmp = TmpIn & 1; 165 | impulseArray[n] = Tmp + 1; 166 | ParityCount += Tmp; 167 | TmpIn >>= 1; 168 | } 169 | if ((ParityCount & 1) == 0) 170 | impulseArray[35] = 1; 171 | else 172 | impulseArray[35] = 2; 173 | ParityCount = 0; 174 | 175 | //calculate the bits for the actual Day of Month 176 | TmpIn = Bin2Bcd(actualDay); 177 | for (n = 36; n < 42; n++) { 178 | Tmp = TmpIn & 1; 179 | impulseArray[n] = Tmp + 1; 180 | ParityCount += Tmp; 181 | TmpIn >>= 1; 182 | } 183 | TmpIn = Bin2Bcd(DayOfW); 184 | for (n = 42; n < 45; n++) { 185 | Tmp = TmpIn & 1; 186 | impulseArray[n] = Tmp + 1; 187 | ParityCount += Tmp; 188 | TmpIn >>= 1; 189 | } 190 | //calculates the bits for the actualMonth 191 | TmpIn = Bin2Bcd(actualMonth); 192 | for (n = 45; n < 50; n++) { 193 | Tmp = TmpIn & 1; 194 | impulseArray[n] = Tmp + 1; 195 | ParityCount += Tmp; 196 | TmpIn >>= 1; 197 | } 198 | //calculates the bits for actual year 199 | TmpIn = Bin2Bcd(actualYear); // 2 digit year 200 | for (n = 50; n < 58; n++) { 201 | Tmp = TmpIn & 1; 202 | impulseArray[n] = Tmp + 1; 203 | ParityCount += Tmp; 204 | TmpIn >>= 1; 205 | } 206 | //equal date 207 | if ((ParityCount & 1) == 0) 208 | impulseArray[58] = 1; 209 | else 210 | impulseArray[58] = 2; 211 | 212 | //last missing pulse 213 | impulseArray[59] = 0; // No pulse 214 | } 215 | 216 | int Bin2Bcd(int dato) { 217 | int msb, lsb; 218 | if (dato < 10) 219 | return dato; 220 | msb = (dato / 10) << 4; 221 | lsb = dato % 10; 222 | return msb + lsb; 223 | } 224 | 225 | void DcfOut() { 226 | switch (impulseCount++) { 227 | case 0: 228 | if (impulseArray[actualSecond] != 0) { 229 | digitalWrite(LEDBUILTIN, LOW); 230 | ledcWrite(ANTENNAPIN, 0); 231 | } 232 | break; 233 | case 1: 234 | if (impulseArray[actualSecond] == 1) { 235 | digitalWrite(LEDBUILTIN, HIGH); 236 | ledcWrite(ANTENNAPIN, 127); 237 | } 238 | break; 239 | case 2: 240 | digitalWrite(LEDBUILTIN, HIGH); 241 | ledcWrite(ANTENNAPIN, 127); 242 | break; 243 | case 9: 244 | impulseCount = 0; 245 | 246 | if (actualSecond == 1 || actualSecond == 15 || actualSecond == 21 || actualSecond == 29 ) Serial.print("-"); 247 | if (actualSecond == 36 || actualSecond == 42 || actualSecond == 45 || actualSecond == 50 ) Serial.print("-"); 248 | if (actualSecond == 28 || actualSecond == 35 || actualSecond == 58 ) Serial.print("P"); 249 | 250 | if (impulseArray[actualSecond] == 1) Serial.print("0"); 251 | if (impulseArray[actualSecond] == 2) Serial.print("1"); 252 | 253 | if (actualSecond == 59 ) { 254 | Serial.println(); 255 | show_time(); 256 | #ifndef CONTINUOUSMODE 257 | if ((dontGoToSleep == 0) or ((dontGoToSleep + onTimeAfterReset) < millis())) cronCheck(); 258 | #else 259 | Serial.println("CONTINUOUS MODE NO CRON!!!"); 260 | timeRunningContinuous++; 261 | if (timeRunningContinuous > 360) ESP.restart(); // 6 hours running, then restart all over 262 | #endif 263 | } 264 | break; 265 | } 266 | if(!getLocalTime(&timeinfo)){ 267 | Serial.println("Error obtaining time..."); 268 | delay(3000); 269 | ESP.restart(); 270 | } 271 | CodeTime(); 272 | } 273 | --------------------------------------------------------------------------------