Real-Time Si5351 Frequency Generator Control via Serial API

This project lets you control an Adafruit SI5351 clock generator directly from a web page running in your browser. You can:

  • Set output frequencies on channels CLK0, CLK1, and CLK2 (8 kHz–22 MHz).
  • See the actual frequency achieved (with PLL rounding error) displayed live.
  • Turn all outputs on or off with a single click.

It uses the Arduino’s USB-Serial interface together with the Web Serial API in Chrome/Edge to exchange simple text commands.

Hardware Requirements

Any Arduino compatible board with native USB-Serial (Uno, Nano 33 IoT, Leonardo, etc.)
• Adafruit SI5351 breakout (connected via I²C pins SDA/SCL)
• USB cable to your computer

Arduino Sketch

/*
  Si5351 “integer‐first” frequency setter, using only DIV = 4,6,8
  - Tries integer‐only multisynth divisors {4,6,8} for zero error
  - Falls back to a fractional‐N search if no perfect integer solution
  - Serial commands at 115200 baud:
      • "off\n" / "on\n"
      • "<freq>,<chan>\n"  (chan = 0,1,2)
*/

#include <Wire.h>
#include <Adafruit_SI5351.h>

Adafruit_SI5351 clockgen;

// Crystal reference and VCO limits
#define XTAL_FREQ 25000000UL
#define VCO_MIN   600000000UL
#define VCO_MAX   900000000UL

// R-divider tables
static const uint8_t R_DIV_ENUM[8]  = {
  SI5351_R_DIV_1, SI5351_R_DIV_2, SI5351_R_DIV_4, SI5351_R_DIV_8,
  SI5351_R_DIV_16,SI5351_R_DIV_32,SI5351_R_DIV_64,SI5351_R_DIV_128
};
static const uint8_t R_DIV_SHIFT[8] = {0,1,2,3,4,5,6,7};

// Only the three integer divisors supported:
static const uint8_t INT_DIVS[] = {
  SI5351_MULTISYNTH_DIV_4,
  SI5351_MULTISYNTH_DIV_6,
  SI5351_MULTISYNTH_DIV_8
};

// Greatest common divisor
unsigned long gcdul(unsigned long a, unsigned long b) {
  while (b) {
    unsigned long t = b;
    b = a % b;
    a = t;
  }
  return a;
}

// Try perfect integer solution (zero error)
bool tryIntegerOnly(unsigned long target, uint8_t chan) {
  for (auto DIV : INT_DIVS) {
    unsigned long vco = target * DIV;
    if (vco < VCO_MIN || vco > VCO_MAX) continue;
    if (vco % XTAL_FREQ) continue;        // not integer multiple
    unsigned long N = vco / XTAL_FREQ;
    if (N < 15 || N > 90) continue;       // PLL range
    clockgen.setupPLLInt(SI5351_PLL_A, N);
    clockgen.setupMultisynthInt(chan, SI5351_PLL_A, DIV);
    clockgen.enableOutputs(true);
    return true;
  }
  return false;
}

// Set channel to closest frequency; returns error in Hz
long set_freq(unsigned long target, uint8_t chan) {
  // 1) Integer‐only fast path
  if (tryIntegerOnly(target, chan)) {
    Serial.print("CLK"); Serial.print(chan);
    Serial.print(" SET → "); Serial.print(target);
    Serial.println(" Hz  ERR 0");
    return 0;
  }

  // 2) Fractional search
  long best_err = 0x7FFFFFFF;
  uint8_t best_N=0, best_r=0;
  unsigned long best_ip=0, best_num=0, best_den=1;

  for (uint8_t N = 15; N <= 90; N++) {
    unsigned long vco = XTAL_FREQ * N;
    if (vco < VCO_MIN || vco > VCO_MAX) continue;
    for (uint8_t i = 0; i < 8; i++) {
      double ms_target = double(target) * (1UL<<R_DIV_SHIFT[i]);
      double D = double(vco) / ms_target;
      if (D < 4.0 || D > 1800.0) continue;
      unsigned long ip  = floor(D);
      unsigned long den = 1000000UL;
      unsigned long num = round((D - ip) * den);
      unsigned long g   = gcdul(num, den);
      if (g>1) { num/=g; den/=g; }
      double fms  = double(vco) / (ip + double(num)/den);
      double fout = fms / (1UL<<R_DIV_SHIFT[i]);
      long err = lround(fout - double(target));
      if (labs(err) < labs(best_err)) {
        best_err = err;
        best_N   = N;
        best_r   = i;
        best_ip  = ip;
        best_num = num;
        best_den = den;
        if (best_err == 0) break;
      }
    }
    if (best_err == 0) break;
  }

  // Program fractional solution
  clockgen.setupPLLInt(SI5351_PLL_A, best_N);
  clockgen.setupMultisynth(chan,
                           SI5351_PLL_A,
                           best_ip,
                           best_num,
                           best_den);
  clockgen.setupRdiv(chan, R_DIV_ENUM[best_r]);
  clockgen.enableOutputs(true);

  unsigned long actual = (best_err >= 0)
                        ? target + best_err
                        : target - (unsigned long)(-best_err);

  Serial.print("CLK"); Serial.print(chan);
  Serial.print(" SET → "); Serial.print(actual);
  Serial.print(" Hz  ERR "); Serial.println(best_err);
  return best_err;
}

void setup() {
  Serial.begin(115200);
  while (!Serial);

  if (clockgen.begin() != ERROR_NONE) {
    Serial.println("INIT ERROR");
    while (1);
  }
  clockgen.enableOutputs(false);
  Serial.println("READY");
}

void loop() {
  static String cmd;
  while (Serial.available()) {
    char c = Serial.read();
    if (c=='\r') continue;
    if (c=='\n') {
      cmd.trim();
      if      (cmd.equalsIgnoreCase("off")) clockgen.enableOutputs(false), Serial.println("ALL OFF");
      else if (cmd.equalsIgnoreCase("on"))  clockgen.enableOutputs(true),  Serial.println("ALL ON");
      else {
        int comma = cmd.indexOf(',');
        if (comma>0) {
          unsigned long f = cmd.substring(0,comma).toInt();
          uint8_t ch = cmd.substring(comma+1).toInt();
          set_freq(f,ch);
        }
      }
      cmd="";
    } else cmd+=c;
  }
}

Key Points:

  • The sketch reads newline-terminated commands:
    • off / on toggles all outputs.
    • <frequency>,<channel> sets that channel’s freq (Hz).
  • After programming, it prints back a line like:CLK2 SET → 1 234 560 Hz ERR -7

HTML + JavaScript Interface

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title>Si5351 Real‐Freq Display</title>
  <style>
    * { box-sizing:border-box; margin:0; padding:0 }
    body {
      font-family:"Segoe UI",sans-serif;
      background:#f4f6f8; color:#333;
      padding:2rem;
      display:flex; flex-direction:column; align-items:center;
    }
    h1 { margin-bottom:1rem }
    #connect, #global-toggle {
      background:#0078d7; color:#fff; border:none; border-radius:4px;
      padding:.6rem 1rem; font-size:1rem; cursor:pointer;
      margin-bottom:1rem; width:220px; transition:background .2s;
    }
    #connect:disabled,
    #global-toggle.off { background:#aaa; cursor:default; }
    #connect:hover:not(:disabled),
    #global-toggle.on:hover { background:#005a9e; }

    .channels {
      display:flex; flex-direction:column; gap:1.5rem;
      width:100%; max-width:400px; margin:0 auto;
    }
    .channel {
      background:#fff; border-radius:8px;
      box-shadow:0 2px 6px rgba(0,0,0,0.1);
      padding:1rem; display:flex; flex-direction:column; gap:.5rem;
    }
    .channel h2 { text-align:center; font-size:1.2rem }
    .freq-display {
      min-height:1.2em; text-align:center;
      color:#28a745; font-size:1rem;
    }
    .controls {
      display:flex; align-items:center; gap:.5rem;
    }
    .controls input[type=range] {
      flex:1; height:6px; -webkit-appearance:none;
      background:#ddd; border-radius:3px;
    }
    .controls input[type=range]::-webkit-slider-thumb {
      width:18px; height:18px; border-radius:50%;
      background:#0078d7; cursor:pointer; transition:background .2s;
    }
    .controls input[type=range]::-webkit-slider-thumb:hover {
      background:#005a9e;
    }
    .controls input[type=text] {
      width:110px; padding:.4rem;
      border:1px solid #ccc; border-radius:4px;
      text-align:right;
    }
    .unit { margin-left:.2rem }
  </style>
</head>
<body>
  <h1>Si5351 Real‐Freq Control</h1>
  <button id="connect">Connect to Arduino</button>
  <button id="global-toggle" class="off" disabled>Turn All On</button>

  <div class="channels">
    <div class="channel" data-chan="0">
      <h2>CLK0</h2>
      <div class="freq-display">—</div>
      <div class="controls">
        <input type="range" class="rr" min="8000" max="22000000" step="1000">
        <input type="text"  class="nn">
        <span class="unit">Hz</span>
      </div>
    </div>
    <div class="channel" data-chan="1">
      <h2>CLK1</h2>
      <div class="freq-display">—</div>
      <div class="controls">
        <input type="range" class="rr" min="8000" max="22000000" step="1000">
        <input type="text"  class="nn">
        <span class="unit">Hz</span>
      </div>
    </div>
    <div class="channel" data-chan="2">
      <h2>CLK2</h2>
      <div class="freq-display">—</div>
      <div class="controls">
        <input type="range" class="rr" min="8000" max="22000000" step="1000">
        <input type="text"  class="nn">
        <span class="unit">Hz</span>
      </div>
    </div>
  </div>

  <script>
    let writer, reader;

    const fmt = n => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,' ');
    const throttle = (fn, wait=100) => {
      let last=0;
      return (...args) => {
        const now=Date.now();
        if(now-last>wait){ last=now; fn(...args); }
      };
    };

    function saveFreq(ch, hz){ localStorage.setItem('clk'+ch, hz); }
    function loadFreq(ch, def){
      const v = localStorage.getItem('clk'+ch);
      return v ? parseInt(v) : def;
    }

    document.getElementById('connect').onclick = async () => {
      const port = await navigator.serial.requestPort();
      await port.open({ baudRate:115200 });
      writer = port.writable.getWriter();
      reader = port.readable
        .pipeThrough(new TextDecoderStream())
        .getReader();

      document.getElementById('connect').disabled = true;
      document.getElementById('global-toggle').disabled = false;
      readLoop();
    };

    async function readLoop() {
      let buf='';
      while(true){
        const { value, done } = await reader.read();
        if(done) break;
        buf += value;
        let idx;
        while((idx = buf.indexOf('\n')) >= 0) {
          parseLine(buf.slice(0, idx).trim());
          buf = buf.slice(idx+1);
        }
      }
    }

    function parseLine(line) {
      const m = line.match(/^CLK(\d)\s+SET.*→\s*([\d ]+)\s*Hz/);
      if(!m) return;
      const ch = m[1], val = parseInt(m[2].replace(/ /g,''));
      const panel = document.querySelector(`.channel[data-chan="${ch}"]`);
      panel.querySelector('.freq-display').textContent = fmt(val)+' Hz';
      panel.querySelector('.rr').value = val;
      panel.querySelector('.nn').value = fmt(val);
      saveFreq(ch, val);
    }

    const gt = document.getElementById('global-toggle');
    gt.onclick = () => {
      const isOn = gt.classList.toggle('off');
      gt.classList.toggle('on', isOn);
      gt.textContent = isOn ? 'Turn All Off' : 'Turn All On';
      if(writer) {
        writer.write(new TextEncoder().encode((isOn?'on\n':'off\n')));
        if(isOn){
          document.querySelectorAll('.channel').forEach(ch => {
            const chan = +ch.dataset.chan;
            const v = +ch.querySelector('.rr').value;
            writer.write(new TextEncoder().encode(`${v},${chan}\n`));
          });
        }
      }
    };

    document.querySelectorAll('.channel').forEach(ch => {
      const chan = +ch.dataset.chan;
      const range = ch.querySelector('.rr');
      const input = ch.querySelector('.nn');
      const disp  = ch.querySelector('.freq-display');

      const mid = ((+range.min + +range.max) >> 1);
      const init = loadFreq(chan, mid);
      range.value = init;
      input.value = fmt(init);
      disp.textContent = fmt(init)+' Hz';

      const sendTh = throttle(v => {
        if(writer) writer.write(new TextEncoder().encode(`${v},${chan}\n`));
        saveFreq(chan, v);
      }, 100);

      range.oninput = () => {
        const v = +range.value;
        input.value = fmt(v);
        disp.textContent = fmt(v)+' Hz';
        sendTh(v);
      };
      input.onblur = () => {
        let raw = input.value.replace(/\D/g,'') || range.min;
        let v = +raw;
        v = Math.max(v, +range.min);
        v = Math.min(v, +range.max);
        range.value = v;
        input.value = fmt(v);
        disp.textContent = fmt(v)+' Hz';
        if(writer) writer.write(new TextEncoder().encode(`${v},${chan}\n`));
        saveFreq(chan, v);
      };
      input.onkeydown = e => { if(e.key==='Enter') input.blur(); };
    });
  </script>
</body>
</html>
</body>
</html>

How to Use

  1. Serve the HTML locally, over HTTPS or localhost.
  2. Upload the Arduino sketch.
    • Baud rate: 115 200
    • Open the Serial Monitor to confirm you see READY.
  3. Open the web page in Chrome/Edge.
    You’ll see three sliders and a “Connect to Arduino” button.
  4. Click “Connect to Arduino”.
    • Grant the site permission to access your Arduino’s serial port.
    • The “Connect” button grays out and “Turn All On” becomes enabled.
  5. Move any slider or edit the textbox.
    • The page sends "<freq>,<chan>\n" to set that channel.
    • When the Arduino replies, the “Actual” frequency is displayed.
  6. Global On/Off.
    • Click “Turn All On” to enable outputs on all three channels at their current values.
    • Click again (“Turn All Off”) to disable all outputs.

Leave a Reply