last updated: 2020-10-20

Old Oscilloscopes want to live forever. To do so, here is the oscilloscope clock. The code is written to get a clear image of a clock with no ghost lines. The hands are drawn beginning from the origin and back to the origin. Strokes are drawn during drawing the circle.
Teensy 3.6 with internal RTC is quick enough to use mathematical functions. With his two 12 Bit DAC, the picture is rock stable and sharp. The only hardware needed is an external battery for the RTC. The Teensy has no direct connection to the Internet and no DCF77, so the time may be getting imprecise over the year.
The new ESP32 has also two DAC with 8 Bit and the possibility to get the time over Internet, but it is not quick enough to use mathematical functions because of his operating system running in background (RTOS). It was even necessary to create tables for the circle and the hands and to reduce the points of the circle to 180 to get a stable non flickering image.
So the best solution is the Teensy with an ESP8266 to get NTP from Internet. The ESP8266 is sending once a day (4 AM) the time to the Teensy. If synchronising in the night was successful the text "NTP" is displayed until 18 hour.
Teensy 3.6 has an internal RTC (and crystal) and 2 DAC with 12 Bit. So no other hardware is needed then the Teensy-Board to create a nice oscilloscope clock.

All the magic is in the code. The Teensy is quick enough to calculate the needed points for the clock, so we can omit tables. The clock strokes are drawn from the circle line back and forth, so no ghost lines appear. Same thing for the second line and hands. The resolution can be changes from 8 Bit to 12 Bit. 12 Bit gets a real sharp, steady nice image of the clock.
The best solution is adding an ESP8266 to get NTP from Internet. The ESP8266 is sending once an hour the time to the Teensy.
Her is the Arduino code with ESP8266 (D4, TXD) connected to pin1 (RXD1) for the Teensy and the ESP8266.
The code (also code for Teensy without ESP) can be found under Downloads at the end of the page.
// oscilloscope clock with teensy 3.6
// weigu.lu v4 with NTP over serial
// 2020-10-20
#include <TimeLib.h> //https://www.pjrc.com/teensy/td_libs_Time.html
#define DEBUG
const byte DAC1 = A21;
const byte DAC2 = A22;
const float PI_8 = 3.14159265;
const byte DAC_RESOLUTION = 12; // dac DAC_RESOLUTIONolution in bit
int r = (pow(2,DAC_RESOLUTION)-2)/2; // max circle radius
int rs = r*0.80; // radius seconds
int rm = r*0.85; // radius minutes
int rh = r*0.65; // radius hours
int rst = r*0.95; // radius strokes
int rstm = r*0.90; // radius main strokes
int hands = 5; // fraction where to split the hand
int handam = 12; // angle (width) of hand (min)
int handah = 20; // angle (width) of hand (hour)
byte NTP_flag = 0;
unsigned long flag_time = millis();
void setup() {
#ifdef DEBUG
Serial.begin(115200);
delay(1000);
Serial.println("Debugging started!");
#endif
Serial1.begin(115200);
setSyncProvider(getTeensy3Time); // set the time library to use RTC
analogWriteResolution(DAC_RESOLUTION);
}
void loop() {
if (Serial1.available()) {
time_t t = processSyncMessage();
#ifdef DEBUG
Serial.print("Data available, epoch time: ");
Serial.println(t);
#endif
if (t != 0) {
Teensy3Clock.set(t); // set the RTC
setTime(t);
NTP_flag = 1;
flag_time = millis();
#ifdef DEBUG
Serial.print("NTP Flag: ");
Serial.println(NTP_flag);
#endif
}
}
circle(); // draw a circle
sec_line(second()); // draw the sec hand
min_line(minute()); // draw the minute hand
hour_line(hour(),minute(),second()); // draw the hour hand
display_weigu(r*15/10,r/32,r/64); // draw weigu.lu
if (NTP_flag) { // show that clock is syncing
display_NTP(r*18/10,r*19/10,r/64);
if ((millis()-flag_time) > 60000) { //clear after 1 minute
NTP_flag = 0;
#ifdef DEBUG
Serial.print("NTP Flag: ");
Serial.println(NTP_flag);
#endif
flag_time = millis();
}
}
}
time_t getTeensy3Time() {
return Teensy3Clock.get();
}
void point(int x,int y) {
analogWrite(DAC1, y);
analogWrite(DAC2, x);
}
void line (int x1,int y1,int x2,int y2) { // draw a line between 2 points
int d = sqrt(((x2-x1)*(x2-x1))+((y2-y1)*(y2-y1)));
int steps = d/4;
for(int i=0;i<steps;i++) {
point(x1+i*(x2-x1)/steps,y1+i*(y2-y1)/steps);
}
}
void dline (int x1,int y1,int x2,int y2) { // draw a line back and forth
int d = sqrt(((x2-x1)*(x2-x1))+((y2-y1)*(y2-y1)));
int steps = d/4;
for(int i=0;i<steps;i++) {
point(x1+i*(x2-x1)/steps,y1+i*(y2-y1)/steps);
}
for(int i=0;i<steps;i++) { //draw line backwards to avoid ghosts
point(x2+i*(x1-x2)/steps,y2+i*(y1-y2)/steps);
}
}
void hand (int x1,int y1,int x2,int y2, int angle) { // draw a hand between 2 points
int d = sqrt(((x2-x1)*(x2-x1))+((y2-y1)*(y2-y1)));
int ds = d/hands;
int ao = round( atan2 (y2-y1,x2-x1)*180/PI_8 );
int a1 = ao+angle;
int a2 = ao-angle;
int p1x = x1+ds*cos(a1*PI_8/180);
int p1y = y1+ds*sin(a1*PI_8/180);
int p2x = x1+ds*cos(a2*PI_8/180);
int p2y = y1+ds*sin(a2*PI_8/180);
line (x1,y1,p1x,p1y);
line (p1x,p1y,x2,y2);
line (x2,y2,p2x,p2y);
line (p2x,p2y,x1,y1);
}
void circle() { //function to draw a circle
for(int i=0;i<=360;i++) {
int circlex = r*cos(i*PI_8/180)+r;
int circley = r*sin(i*PI_8/180)+r;
point(circlex,circley);
if (i%6==0) { // min lines
if (i%30==0) { // 5 min lines
dline(circlex,circley,rstm*cos(i*PI_8/180)+r,rstm*sin(i*PI_8/180)+r);
}
else {
dline(circlex,circley,rst*cos(i*PI_8/180)+r,rst*sin(i*PI_8/180)+r);
}
}
}
}
void sec_line(int second) {
int angle = ((60-second)*6+90)%360;
dline(r,r,rs*cos(angle*PI_8/180)+r,rs*sin(angle*PI_8/180)+r);
}
void min_line(int minute) {
int angle = ((60-minute)*6+90)%360;
hand(r,r,rm*cos(angle*PI_8/180)+r,rm*sin(angle*PI_8/180)+r,handam);
}
void hour_line(int hour, int minute, int second) {
if (hour>12) {
hour = hour-12;
}
int hours = hour*3600 + minute*60 + second;
int angle = ((43200-hours)/120+90)%360;
hand(r,r,rh*cos(angle*PI_8/180)+r,rh*sin(angle*PI_8/180)+r,handah);
}
void display_weigu(int x1, int y1, int s) {
int h = 5*s; // height of chars
int w = 5*s; // width of chars
int d = 2*s; // distance between chars
line(x1,y1,x1,y1+h);
line(x1,y1,x1+w/2,y1+h/2);
line(x1+w/2,y1+h/2,x1+w,y1);
line(x1+w,y1,x1+w,y1+h);
x1 = x1+w+d;
w = w*3/5;
line(x1,y1,x1,y1+h);
line(x1,y1+h,x1+w,y1+h);
line(x1+w,y1+h,x1+w,y1+h/2);
line(x1+w,y1+h/2,x1,y1+h/2);
line(x1,y1,x1+w,y1);
x1 = x1+w+d;
line(x1,y1,x1,y1+h*3/4);
point(x1,y1+h);
point(x1+1,y1+h);
point(x1+1,y1+1+h);
point(x1,y1+1+h);
x1 = x1+d;
line(x1+w,y1,x1,y1);
line(x1,y1,x1,y1+h);
line(x1,y1+h,x1+w,y1+h);
line(x1+w,y1+h,x1+w,y1);
line(x1+w,y1,x1+w,y1-h/2);
line(x1+w,y1-h/2,x1,y1-h/2);
x1 = x1+w+d;
line(x1,y1,x1,y1+h);
line(x1,y1,x1+w,y1);
line(x1+w,y1,x1+w,y1+h);
x1 = x1+w+d;
point(x1,y1);
point(x1+1,y1);
point(x1+1,y1+1);
point(x1,y1+1);
x1 = x1+d;
line(x1,y1,x1,y1+1.5*h);
x1 = x1+d;
line(x1,y1,x1,y1+h);
line(x1,y1,x1+w,y1);
line(x1+w,y1,x1+w,y1+h);
}
void display_NTP(int x1, int y1, int s) {
int h = 5*s; // height of chars
int w = 3*s; // width of chars
int d = 2*s; // distance between chars
line(x1,y1,x1,y1+h);
line(x1,y1+h,x1+w,y1);
line(x1+w,y1+h,x1+w,y1);
x1 = x1+w+d;
//w = w*3/5;
line(x1+w/2,y1,x1+w/2,y1+h);
line(x1+w/2,y1+h,x1,y1+h);
line(x1,y1+h,x1+w,y1+h);
line(x1+w,y1+h,x1+w/2,y1+h);
line(x1+w/2,y1+h,x1+w/2,y1);
x1 = x1+w+d;
line(x1,y1,x1,y1+h);
line(x1,y1+h,x1+w,y1+h);
line(x1+w,y1+h,x1+w,y1+h/2);
line(x1+w,y1+h/2,x1,y1+h/2);
line(x1,y1+h/2,x1,y1);
}
/* code to process time sync messages from the serial port pjrc.com */
#define TIME_HEADER "T" // Header tag for serial time sync message
unsigned long processSyncMessage() {
unsigned long pctime = 0L;
const unsigned long DEFAULT_TIME = 1603037307; // 2020-10-20
if(Serial1.find(TIME_HEADER)) {
pctime = Serial1.parseInt();
serial1_in_flush(); // remove CR or LF
return pctime;
if( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than 2020-10-20)
pctime = 0L; // return 0 to indicate that the time is not valid
}
}
serial1_in_flush();
return pctime;
}
void serial1_in_flush() {
while(Serial1.available() > 0) {
Serial1.read();
}
}
Here the code for the ESP8266:
/* Send NTP Time (Wemos D1 mini pro, ESP8266) over serial (D4)
* 2020-10-20
* weigu.lu
*/
#include <NTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#define DEBUG1
//#define DEBUG2
// WiFi and network settings
const char *WIFI_SSID = ""; // SSID
const char *WIFI_PASSWORD = ""; // password
unsigned long utcOffsetInSeconds = 3600;
unsigned long epochTime;
struct tm;
int currentHour, currentMinute, currentSecond, currentYear, currentMonth, monthDay;
String currentDate, weekDay, currentMonthName, formattedTime;
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);
void setup() {
#ifdef DEBUG1
Serial.begin(115200);
delay(2000);
#endif
init_wifi();
timeClient.begin();
timeClient.update();
consider_daylight_saving();
Serial1.begin(115200);
}
void loop() {
timeClient.update();
consider_daylight_saving();
currentMinute = timeClient.getMinutes();
if (currentMinute == 0) {
formattedTime = "T" + String(epochTime);
#ifdef DEBUG1
Serial.println("Condition fulfilled!");
Serial.println(formattedTime);
#endif
Serial1.println(formattedTime);
delay(61000); //wait 1 minute
}
delay(1000);
}
void init_wifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(100);
#ifdef DEBUG1
Serial.print(".");
#endif
}
#ifdef DEBUG1
Serial.println();
Serial.println("Connected to WLAN!");
#endif
}
void consider_daylight_saving() {
epochTime = timeClient.getEpochTime();
tm *ptm = gmtime ((time_t *)&epochTime); // get a time structure to get information about the date.
int daylight_saving_time_flag = ptm->tm_isdst;
#ifdef DEBUG2
Serial.print("Flag: ");
Serial.println(daylight_saving_time_flag);
#endif
if (daylight_saving_time_flag == 0) {
timeClient.setTimeOffset(7200); // Set offset time in seconds to adjust for your timezone
}
if (daylight_saving_time_flag > 0) {
timeClient.setTimeOffset(3600); // Set offset time in seconds to adjust for your timezone
}
}
The ESP32 (Lolin32) don't need additional components.

Her is the Arduino code (see also Downloads at the end):
// oscilloscope clock with wemos lolin32 (esp32)
// weigu.lu
// (reduce upload speed if errors occur)
// for tables see libreoffice calc sheet
#include <WiFi.h>
#include "time.h"
#include <driver/dac.h>
const byte dac1 = 25;
const byte dac2 = 26;
const char* ssid = "myssid";
const char* password = "mypass";
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;
struct tm ti;
// sine and cosine table with 360 points
unsigned int circlex[]={
255,255,255,255,255,255,254,254,
...
};
unsigned int circley[]={
128,130,132,134,136,139,141,143,
...
};
// 5 min strokes
unsigned int min5linesxy[]={
243,128,227,185,185,227,128,243,
...
};
// seconds hand r = 102 (80%)
unsigned int secHandxy[]={
128,230,117,229,106,227,96,225,
...
};
// minute hand r = 108 (85%) fraction = 5 angle = 12
unsigned int minHand[]={
128,236,123,149,132,149,
...
};
// hour hand r = 83 (65%) fraction = 5 angle = 20
unsigned int hourHand[]={
128,211,122,143,133,143,
...
};
void setup() {
Serial.begin(115200);
//connect to WiFi
Serial.printf("Connecting to %s ", ssid);
WiFi.enableSTA(true);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" CONNECTED");
//init and get the time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
getLocalTime(&ti);
WiFi.disconnect(true); //disconnect WiFi as it's no longer needed
WiFi.mode(WIFI_OFF);
}
void loop() {
getLocalTime(&ti);
for(int i=0;i<5;i++) {
circle(); // function to make a circle
sec_hand(ti.tm_sec);
min_hand(ti.tm_min);
hour_hand(ti.tm_hour,ti.tm_min);
}
// Reset time at 4 in the morning
if ((ti.tm_hour == 4) and (ti.tm_min == 0) and (ti.tm_sec == 0)) {
ESP.restart();
}
}
void point(int x,int y) {
dac_out_voltage(DAC_CHANNEL_1, y);
dac_out_voltage(DAC_CHANNEL_2, x);
}
void line (int x1,int y1,int x2,int y2, int steps) { // draw a line between 2 points
for(int i=0;i<steps;i++) {
point(x1+i*(x2-x1)/steps,y1+i*(y2-y1)/steps);
}
}
void dline (int x1,int y1,int x2,int y2,int steps) { // draw a line back and forth
for(int i=0;i<steps;i++) {
point(x1+i*(x2-x1)/steps,y1+i*(y2-y1)/steps);
}
for(int i=0;i<steps;i++) { //draw line backwards to avoid ghosts
point(x2+i*(x1-x2)/steps,y2+i*(y1-y2)/steps);
}
}
void circle() { //function to draw a circle
for(int i=0;i<360;i=i+2) {
point(circlex[i],circley[i]);
if (i%30==0) { // 5 min lines
dline(circlex[i],circley[i],min5linesxy[i/30*2],min5linesxy[i/30*2+1],5);
}
}
}
void sec_hand(int second) {
int i = (60-second);
if (i == 60) i = 0;
i = i*2;
dline(127,127,secHandxy[i],secHandxy[i+1],30);
}
void min_hand(int minute) {
int i = (60-minute);
if (i == 60) i = 0;
i = i*6;
line (127,127,minHand[i+2],minHand[i+3],10);
line (minHand[i+2],minHand[i+3],minHand[i],minHand[i+1],40);
line (minHand[i],minHand[i+1],minHand[i+4],minHand[i+5],40);
line (minHand[i+4],minHand[i+5],127,127,10);
}
void hour_hand(int hour, int minute) {
if (hour>12) hour = hour-12;
int hours = (hour*5 + (minute/12));
int i = (60-hours);
if (i == 60) i = 0;
i = i*6;
line (127,127,hourHand[i+2],hourHand[i+3],7);
line (hourHand[i+2],hourHand[i+3],hourHand[i],hourHand[i+1],30);
line (hourHand[i],hourHand[i+1],hourHand[i+4],hourHand[i+5],30);
line (hourHand[i+4],hourHand[i+5],127,127,7);
}
Find the freecad file and the stl files under Downloads. I used 4 hole flange mount BNCs from my stock with 19 mm distance between the holes. Perhaps you have to adjust the distance in the freecad file.

The little piece of breadboard that holds the teensy is glued with hot glue to the bottom of the case.