Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

When working with electronics, going from an idea to a functioning prototype usually weeks. Designing schematics, sourcing libraries, writing code, debugging, and finally getting something that works can feel like a long and often frustrating journey. But what if you could cut that down to just one hour?
That was the challenge I set for myself recently: build a complete digital delay generator with a touchscreen interface, starting from scratch and finishing with a working prototype in just 60 minutes. Impossible? Not with ChatGPT! A year ago, this kind of rapid development workflow would have been unthinkable. Today, it’s very much possible.
The goal was to create a digital delay generator, a device that can generate multiple output signals with precise timing relationships between them. I use such devices at work to trigger various scientific instruments (e.g. oscilloscopes, lasers, cameras etc) and often need the ability to vary the time delay between trigger signals on a nanosecond time scale despite the output waveforms being relatively low frequencies (kHz).
The design requirements were simple on paper:
The whole interface would be controlled via a cheap yellow display (CYD), with the ESP32 handling the logic and an external clock generator chip producing the signals.
The first 15 minutes of the project were spent wiring everything up. This was kept deliberately simple, just an I2C connection to the Si5351 (clock + data), 3.3 V power, and ground between the ESP32 and the clock generator. With no proper connectors at hand, I soldered wires directly to the socket. Messy, yes. Reliable, not really. But it worked.
Before diving into code, I tested each piece of hardware individually:
With both the display and the generator verified, it was time to bring in ChatGPT.
Here’s where things got interesting. Rather than manually piecing together code from scratch, I gave ChatGPT a detailed description of what I wanted: the hardware setup, links to the tutorials I used, and even a hand-drawn sketch of the planned user interface.
The first version of code ChatGPT produced was rough, the UI flickered badly, touch didn’t work, and no plots appeared. But the overall layout was correct: three input boxes, up/down buttons, and unit selectors.
From there, I went through several iterations:
What impressed me most was how ChatGPT handled hardware constraints. For example, it immediately flagged that the clock chip couldn’t generate a 1 kHz signal, explaining its real limits (7 kHz to hundreds of MHz). That alone saved me a lot of time.
Not everything was smooth. At one point, ChatGPT suggested switching to the LVGL graphics library. That led to a frustrating rabbit hole before I abandoned it and went back to simpler graphics. Lesson learned: sometimes “good enough” is better than “fancy.”
By the 45-minute mark, the basics were working: the touchscreen adjusted the master frequency, the oscilloscope confirmed the output, and delay settings shifted the waveforms as expected.
With a bit of time left, I pushed further:
Finally, I tidied up the interface with a dark mode theme. The result looked surprisingly sleek for something assembled so quickly.
At the one-hour mark, the ESP32-based digital delay generator was fully functional:
The final sketch was about 420 lines of code, something I could never have typed and debugged manually in that time frame! Below I’ve provided the final version of the code for you to have a play around with!
#include <SPI.h>
#include <Wire.h>
#include <TFT_eSPI.h> // https://github.com/Bodmer/TFT_eSPI
#include <XPT2046_Touchscreen.h> // https://github.com/PaulStoffregen/XPT2046_Touchscreen
#include <si5351.h> // Etherkit Si5351Arduino
/*
Digital Delay Generator v3
• CH0: 0.01–10.00 MHz, ◀▶ select digit, ▲▼ adjust
• CH1/CH2: 0–127 phase steps (¼ VCO cycle each), ▲▼ step ±1
Actual delay = phase/(4·Fvco) → displayed in ns
• Options → Frequency Control (M1/M2 multipliers), ◀▶ select digit, ▲▼ adjust
+ Toggle outputs On/Off (with dynamic label & color)
• Contrast-boosted value text for daylight visibility
*/
#define TS_CS 33
#define TS_IRQ 36
#define TOUCH_MOSI 32
#define TOUCH_MISO 39
#define TOUCH_CLK 25
#define I2C_SDA_PIN 22
#define I2C_SCL_PIN 27
#define TS_MINX 200
#define TS_MINY 200
#define TS_MAXX 3800
#define TS_MAXY 3800
#define Z_THRESH 8
SPIClass touchscreenSPI(VSPI);
TFT_eSPI tft = TFT_eSPI();
XPT2046_Touchscreen ts(TS_CS, TS_IRQ);
Si5351 si5351;
// VCO ~900 MHz → one phase LSB = T_VCO/4 ≈ 0.278 ns
static constexpr float Fvco = 25e6f * 36.0f;
// Layout
static const int SCREEN_W=320, SCREEN_H=240;
static const int TITLE_H=20;
static const int ROW_H=28, ROW_SP=8;
static const int BUTTON_W=24, BUTTON_H=ROW_H, BUTTON_SP=6;
static const int UNIT_W=34, X0=10;
static int VBW, rowY[3];
static const int PLOT_X=0, PLOT_Y=TITLE_H + (SCREEN_H-TITLE_H)/2;
static const int PLOT_W=SCREEN_W, PLOT_H=SCREEN_H-PLOT_Y;
// Colours
#define C_BG TFT_WHITE
#define C_TITLE TFT_BLACK
#define C_HILITE TFT_ORANGE
#define C_VALBOX TFT_DARKGREY
#define C_VALTXT TFT_WHITE // improved contrast
#define C_BTN TFT_LIGHTGREY
#define C_BTN_FG TFT_BLACK
#define C_AXIS TFT_BLACK
#define C_CH0 TFT_ORANGE
#define C_CH1 TFT_CYAN
#define C_CH2 TFT_MAGENTA
// State
static float freqMHz=1.00f;
static int cursorMain[3]={0,0,0};
static uint8_t phase1=0, phase2=0;
static int d1ns=0, d2ns=0;
static float m1=1.00f, m2=1.00f;
static int cursorSub[3]={0,1,2};
static bool outputsEnabled = true;
bool inSubMenu=false;
inline bool inRect(int x,int y,int rx,int ry,int rw,int rh){
return x>=rx && x<rx+rw && y>=ry && y<ry+rh;
}
inline float getHz0(){ return freqMHz*1e6f; }
// ── DRAW FUNCTIONS ────────────────────────────────────────────────────────────
void drawMainTitle(){
tft.fillRect(0,0,SCREEN_W,TITLE_H,C_BG);
tft.setTextFont(2); tft.setTextColor(C_TITLE);
tft.setTextDatum(ML_DATUM);
tft.drawString("Digital Delay Generator",5,TITLE_H/2,2);
int bx=SCREEN_W-75, by=1;
tft.fillRoundRect(bx,by,70,18,4,C_BTN);
tft.setTextColor(C_BTN_FG);
tft.setTextDatum(MC_DATUM);
tft.drawString("Options",bx+35,by+9,2);
}
void drawSubTitle(){
tft.fillRect(0,0,SCREEN_W,TITLE_H,C_BG);
tft.setTextFont(2); tft.setTextColor(C_TITLE);
tft.setTextDatum(MC_DATUM);
tft.drawString("Frequency Control",SCREEN_W/2,TITLE_H/2,2);
}
void drawMainRow(int idx){
int y=rowY[idx], vx;
const int aw=12, ah=6;
if(idx==0){
int x0=X0;
tft.fillRoundRect(x0,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x0+aw/2,y+14, x0+aw/2+aw/2,y+14-ah/2, x0+aw/2+aw/2,y+14+ah/2,C_BTN_FG);
int x1=x0+BUTTON_W+BUTTON_SP;
tft.fillRoundRect(x1,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x1+BUTTON_W-aw/2,y+14, x1+BUTTON_W-aw/2-aw/2,y+14-ah/2, x1+BUTTON_W-aw/2-aw/2,y+14+ah/2,C_BTN_FG);
vx=x1+BUTTON_W+BUTTON_SP;
} else vx=X0;
tft.fillRoundRect(vx,y,VBW,ROW_H,4,C_VALBOX);
tft.setTextFont(2);
tft.setTextColor(C_TITLE,C_VALBOX);
tft.setTextDatum(ML_DATUM);
tft.drawString(idx==0?"Ch0":idx==1?"Ch1":"Ch2",vx+5,y+14,2);
char buf[8];
if(idx==0) snprintf(buf,sizeof(buf),"%.2f",freqMHz);
else if(idx==1) snprintf(buf,sizeof(buf),"%03d",d1ns);
else snprintf(buf,sizeof(buf),"%03d",d2ns);
int len=strlen(buf);
tft.setTextFont(4);
int cw=tft.textWidth("0",4), th=tft.fontHeight(4);
int yC=y+(ROW_H-th)/2;
int mapIdx=(idx==0?(cursorMain[0]==0?0:cursorMain[0]==1?2:3):-1);
for(int i=0;i<len;i++){
int dx=vx+VBW-5-cw*len + i*cw + cw/2;
tft.setTextColor(i==mapIdx?C_HILITE:C_VALTXT,C_VALBOX);
tft.setTextDatum(MC_DATUM);
tft.drawChar(buf[i],dx,yC,4);
}
int x2=vx+VBW+BUTTON_SP;
tft.fillRoundRect(x2,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x2+BUTTON_W/2,y+6, x2+BUTTON_W/2-aw/2,y+6+ah, x2+BUTTON_W/2+aw/2,y+6+ah,C_BTN_FG);
int x3=x2+BUTTON_W+BUTTON_SP;
tft.fillRoundRect(x3,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x3+BUTTON_W/2,y+ROW_H-6, x3+BUTTON_W/2-aw/2,y+ROW_H-6-ah, x3+BUTTON_W/2+aw/2,y+ROW_H-6-ah,C_BTN_FG);
int ux=x3+BUTTON_W+BUTTON_SP;
tft.setTextFont(2);
tft.setTextColor(C_TITLE);
tft.drawString(idx==0?"MHz":"ns",ux+UNIT_W/2,y+14,2);
}
void updateControls(){
drawMainRow(0);
drawMainRow(1);
drawMainRow(2);
}
void drawSubRow(int idx){
int y=rowY[idx], vx=X0;
const int aw=12, ah=6;
tft.fillRoundRect(vx,y,VBW,ROW_H,4,C_VALBOX);
tft.setTextFont(2);
tft.setTextColor(C_TITLE,C_VALBOX);
tft.drawString(idx==0?"M1":"M2",vx+5,y+14,2);
char buf[8]; snprintf(buf,sizeof(buf),"%.2f", idx==0?m1:m2);
int len=strlen(buf);
tft.setTextFont(4);
int cw=tft.textWidth("0",4), th=tft.fontHeight(4);
int yC=y+(ROW_H-th)/2;
int mapIdx = idx==0?
(cursorSub[0]==0?0:cursorSub[0]==1?2:3)
:(cursorSub[1]==0?0:cursorSub[1]==1?2:3);
for(int i=0;i<len;i++){
int dx=vx+VBW-5-cw*len + i*cw + cw/2;
tft.setTextColor(i==mapIdx?C_HILITE:C_VALTXT,C_VALBOX);
tft.setTextDatum(MC_DATUM);
tft.drawChar(buf[i],dx,yC,4);
}
int x0=vx;
tft.fillRoundRect(x0,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x0+aw/2,y+14, x0+aw/2+aw/2,y+14-ah/2, x0+aw/2+aw/2,y+14+ah/2,C_BTN_FG);
int x1=x0+BUTTON_W+BUTTON_SP;
tft.fillRoundRect(x1,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x1+BUTTON_W-aw/2,y+14, x1+BUTTON_W-aw/2-aw/2,y+14-ah/2, x1+BUTTON_W-aw/2-aw/2,y+14+ah/2,C_BTN_FG);
int x2=vx+VBW+BUTTON_SP, x3=x2+BUTTON_W+BUTTON_SP;
tft.fillRoundRect(x2,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x2+BUTTON_W/2,y+6, x2+BUTTON_W/2-aw/2,y+6+ah, x2+BUTTON_W/2+aw/2,y+6+ah,C_BTN_FG);
tft.fillRoundRect(x3,y,BUTTON_W,BUTTON_H,4,C_BTN);
tft.fillTriangle(x3+BUTTON_W/2,y+ROW_H-6, x3+BUTTON_W/2-aw/2,y+ROW_H-6-ah, x3+BUTTON_W/2+aw/2,y+ROW_H-6-ah,C_BTN_FG);
}
void updateSubMenu(){
drawSubTitle();
drawSubRow(0);
drawSubRow(1);
int by=SCREEN_H-28;
// Back
tft.fillRoundRect(5,by,60,24,4,C_BTN);
tft.setTextFont(2);
tft.setTextColor(C_BTN_FG);
tft.drawString("⏎ Back",35,by+12,2);
// Outputs toggle
int bw=140, tx=SCREEN_W-bw-5;
uint16_t bg = outputsEnabled ? TFT_GREEN : TFT_RED;
tft.fillRoundRect(tx,by,bw,24,4,bg);
tft.setTextFont(2);
tft.setTextColor(TFT_WHITE);
tft.drawString(outputsEnabled?"Output Enabled":"Output Disabled", tx+bw/2, by+12,2);
}
void drawPlot(){
tft.fillRect(PLOT_X,PLOT_Y,PLOT_W,PLOT_H,C_BG);
tft.drawRect(PLOT_X,PLOT_Y,PLOT_W,PLOT_H,C_AXIS);
float f0=getHz0();
float freqs[3]={f0,f0*m1,f0*m2};
uint16_t cols[3]={C_CH0,C_CH1,C_CH2};
int bandY[3]={PLOT_Y+PLOT_H/6,PLOT_Y+PLOT_H/2,PLOT_Y+5*PLOT_H/6};
float delays[3]={0.0f,d1ns*1e-9f,d2ns*1e-9f};
for(int ch=0;ch<3;ch++){
float T=1.0f/freqs[ch], halfPx=(PLOT_W/2.0f)/(ch==0?1.0f:(ch==1?m1:m2));
float ofs=constrain((delays[ch]/T)*halfPx,0,halfPx);
int yC=bandY[ch], yH=yC-6, yL=yC+6;
for(int cyc=0;cyc<2;cyc++){
float bx=PLOT_X+cyc*2*halfPx, mx=bx+halfPx;
tft.drawLine(bx+ofs,yL,bx+ofs,yH,cols[ch]);
tft.drawLine(bx+ofs,yH,mx+ofs,yH,cols[ch]);
tft.drawLine(mx+ofs,yH,mx+ofs,yL,cols[ch]);
tft.drawLine(mx+ofs,yL,bx+2*halfPx+ofs,yL,cols[ch]);
}
}
tft.setTextFont(2); tft.setTextColor(C_TITLE);
tft.setTextDatum(MC_DATUM);
tft.drawString("Δ1:"+String(d1ns)+"ns",SCREEN_W/4,PLOT_Y+PLOT_H-10,2);
tft.drawString("Δ2:"+String(d2ns)+"ns",3*SCREEN_W/4,PLOT_Y+PLOT_H-10,2);
}
void applySi5351(){
uint64_t h0=(uint64_t)(getHz0()*SI5351_FREQ_MULT);
si5351.set_freq(h0,SI5351_CLK0);
si5351.set_freq(h0*m1,SI5351_CLK1);
si5351.set_freq(h0*m2,SI5351_CLK2);
si5351.set_phase(SI5351_CLK1,phase1);
si5351.set_phase(SI5351_CLK2,phase2);
si5351.pll_reset(SI5351_PLLA);
si5351.pll_reset(SI5351_PLLB);
d1ns = int(round(phase1 * 1e9f / (4.0f * Fvco)));
d2ns = int(round(phase2 * 1e9f / (4.0f * Fvco)));
si5351.output_enable(SI5351_CLK0, outputsEnabled);
si5351.output_enable(SI5351_CLK1, outputsEnabled);
si5351.output_enable(SI5351_CLK2, outputsEnabled);
}
void setup(){
Serial.begin(115200);
Wire.begin(I2C_SDA_PIN,I2C_SCL_PIN);
SPI.begin();
touchscreenSPI.begin(TOUCH_CLK,TOUCH_MISO,TOUCH_MOSI,TS_CS);
tft.begin(); tft.setRotation(1);
ts.begin(touchscreenSPI); ts.setRotation(1);
if(!si5351.init(SI5351_CRYSTAL_LOAD_8PF,0,0)){
Serial.println("SI5351 not detected!"); while(1) yield();
}
VBW=SCREEN_W-2*X0-3*(BUTTON_W+BUTTON_SP)-UNIT_W;
drawMainTitle();
rowY[0]=TITLE_H+ROW_SP;
rowY[1]=rowY[0]+ROW_H+ROW_SP;
rowY[2]=rowY[1]+ROW_H+ROW_SP;
updateControls();
drawPlot();
applySi5351();
}
void loop(){
if(!(ts.tirqTouched()&&ts.touched())) return;
TS_Point p=ts.getPoint(); if(p.z<Z_THRESH) return;
int x=map(p.x,TS_MINX,TS_MAXX,0,SCREEN_W),
y=map(p.y,TS_MINY,TS_MAXY,0,SCREEN_H);
bool redraw=false;
if(!inSubMenu){
int bx=SCREEN_W-75,by=1;
if(inRect(x,y,bx,by,70,18)){
inSubMenu=true;
tft.fillScreen(C_BG);
drawSubTitle();
updateSubMenu();
return;
}
for(int i=0;i<3;i++){
int y0=rowY[i];
int vx=(i==0?X0+2*(BUTTON_W+BUTTON_SP):X0);
int x2=vx+VBW+BUTTON_SP, x3=x2+BUTTON_W+BUTTON_SP;
if(inRect(x,y,x2,y0,BUTTON_W,BUTTON_H)){
if(i==0){
float st=(cursorMain[0]==0?1:cursorMain[0]==1?0.1:0.01);
freqMHz=constrain(freqMHz+st,0.01f,10.00f);
} else if(i==1){ if(phase1<127) phase1++; }
else{ if(phase2<127) phase2++; }
redraw=true; break;
}
if(inRect(x,y,x3,y0,BUTTON_W,BUTTON_H)){
if(i==0){
float st=(cursorMain[0]==0?1:cursorMain[0]==1?0.1:0.01);
freqMHz=constrain(freqMHz-st,0.01f,10.00f);
} else if(i==1){ if(phase1>0) phase1--; }
else{ if(phase2>0) phase2--; }
redraw=true; break;
}
if(i==0){
int x0=X0, x1=x0+BUTTON_W+BUTTON_SP;
if(inRect(x,y,x0,y0,BUTTON_W,BUTTON_H)){
cursorMain[0]=max(0,cursorMain[0]-1); redraw=true; break;
}
if(inRect(x,y,x1,y0,BUTTON_W,BUTTON_H)){
cursorMain[0]=min(2,cursorMain[0]+1); redraw=true; break;
}
}
}
if(redraw){
applySi5351();
updateControls();
drawPlot();
}
} else {
int by=SCREEN_H-28;
if(inRect(x,y,5,by,60,24)){
inSubMenu=false;
tft.fillScreen(C_BG);
drawMainTitle();
updateControls();
drawPlot();
applySi5351();
return;
}
int bw=140, tx=SCREEN_W-bw-5;
if(inRect(x,y,tx,by,bw,24)){
outputsEnabled = !outputsEnabled;
redraw=true;
}
for(int i=0;i<2;i++){
int y0=rowY[i];
int x0=X0, x1=x0+BUTTON_W+BUTTON_SP;
int vx=X0;
int x2=vx+VBW+BUTTON_SP, x3=x2+BUTTON_W+BUTTON_SP;
if(inRect(x,y,x0,y0,BUTTON_W,BUTTON_H)){
cursorSub[i]=max(0,cursorSub[i]-1); redraw=true; break;
}
if(inRect(x,y,x1,y0,BUTTON_W,BUTTON_H)){
cursorSub[i]=min(2,cursorSub[i]+1); redraw=true; break;
}
if(inRect(x,y,x2,y0,BUTTON_W,BUTTON_H)){
float st=(cursorSub[i]==0?1:cursorSub[i]==1?0.1:0.01);
if(i==0) m1=constrain(m1+st,0.01f,10.00f);
else m2=constrain(m2+st,0.01f,10.00f);
redraw=true; break;
}
if(inRect(x,y,x3,y0,BUTTON_W,BUTTON_H)){
float st=(cursorSub[i]==0?1:cursorSub[i]==1?0.1:0.01);
if(i==0) m1=constrain(m1-st,0.01f,10.00f);
else m2=constrain(m2-st,0.01f,10.00f);
redraw=true; break;
}
}
if(redraw){
tft.fillRect(0,TITLE_H,SCREEN_W,SCREEN_H-TITLE_H,C_BG);
drawSubTitle();
updateSubMenu();
applySi5351();
}
}
while(ts.touched()) delay(10);
}
This project wasn’t really about building a digital delay generator, it was about exploring a new workflow. The combination of ESP32 hardware, a simple touchscreen, and AI-assisted coding allowed me to go from an idea to a working prototype in just an hour.
The end result is close enough to a marketable product that, with a little more refinement, it could be commercialised. More importantly, it shows how dramatically prototyping has changed. If you’re an engineer, hobbyist, or student, I’d strongly encourage you to try this workflow yourself. It’s fast, fun, and incredibly versatile!