Teensy 4.1 pulse counter - down to 20 ns !

khupys

Member
Hello?

I remember there was discussion for using Teensy 4.x as a fast pulse counter some years ago.
Pictured below is the pulse counter I built using FreqCount function, and all I need is counting 1 us wide pulses, I kept reducing the pulse width until Teensy became erratic. I confirmed that It can count quite reliably down to the pulse width of 20 ns. I suppose the Rigol pulse generator having rise/fall time of 3 ns, going further down would not give you reliable measurements.

The setup is relatively amateurish as I did not pay much attention to the termination, shielding, and impedance matching, etc.
That is why I had to apply 7.5 V pulse when the initial design was for 5 V pulse reduced to 3.3 V. I will fix it later :)

In any case, simple Teensy pulse counter works well down to 20 ns. This may not be surprising to some as the 600 Mhz clock of Teensy means 1.67 ns time resolution. But considering 10 - 20 ns counter board from NI, for example, costs 100s - 1000s of $, this is very nice.

Enjoy it.

Yongsup Park


Teensy-20ns.jpg
 
Last edited:
Nice!

Any chance you might share the code here or on github. I'm pretty sure other people will find it interesting, maybe even make their own pulse measurement.
 
Nice!

Any chance you might share the code here or on github. I'm pretty sure other people will find it interesting, maybe even make their own pulse measurement.

Hi !
I'v done it mostly by vibe coding with Gemini 3 CLI.

The code snippet relevant to the pulse counting is shown below. It is just a simple utilization of FreqCount function, no fancy logic.


Code:
  // Ensure the gate is set to the requested dwell in MICROSECONDS (Teensy 4.1 FreqCount uses usec)
  unsigned long startGate = millis();
  FreqCount.begin(dwell * 1000UL);

  while (!FreqCount.available()) {
    myusb.Task();
  }

  long counts = FreqCount.read();
  FreqCount.end();

  unsigned long endGate = millis();
  Serial.printf("Point: %.2fV, Dwell: %dms, Actual: %lums, Counts: %ld\n", v1, dwell, endGate - startGate, counts);

As I posted earlier in this forum the goal of this ongoing project is building a lab equipment control app that runs entirely on web browser with no dedicated computer for data acquisition. We have a single T4.1 that acts as a web server, USB device controller, and the pulse counter. The browser connects to T4.1 and downloads the React/TypeScript code from T4.1 and run it, after which the React app talks to T4.1 for pulse counting and USB device control.

Hope this helps.

Yongsup Park
 
Last edited:
Hello?

It turned out that the pulse counting limit of T4.1 is far better than I originally thought.

The figure below shows T4.1 can go as much as the Rigol DG 912 Pro waveform generator will go: 9 ns at 50 Mhz.

There is not-so-random counting error of ~ 250Hz/50MHz = 5 ppm.
This constant offset is probably due to time base error.
Radom fluctuation is ~3Hz/50Mhz which is better than typical NI boards, I suppose.

All this at the pulse width of 9 ns! Amazing.

Yongsup Park

 Teensy-9ns.jpg
 
Last edited:
One additional note:

Since the time base accuracy of Rigol 912 Pro itself is specified as ±2 ppm, the measured 5 ppm offset may not be entirely from T4.1.

YP
 
Would it be ok to show this on the public website?

Even if AI wrote some of the code, I think a lot of people would are interested in DIY building high precision measurement devices.
 
Any chance you might share the code, so anyone who wants to also build this can start from a working example

Well, the following is the whole .ino code.

The "webpage.h" contains the React/TypeScript browser front end code, about 1500 lines long, which has to be converted to hex byte array to be included as .h file. I did it using an AI coding assistant - Gemini CLI.

Even without the frontend code, the serial monitor displays the relevant values.

Code:
#include <QNEthernet.h>
#include <ArduinoJson.h>
#include <FreqCount.h>   
#include <USBHost_t36.h> 
#include "webpage.h"     

using namespace qindesign::network;

// ==========================================
// USB HOST (MiniSMU)
// ==========================================
USBHost myusb;
USBHub hub1(myusb);
USBSerial userial(myusb);

// ==========================================
// NETWORK CONFIGURATION
// ==========================================
IPAddress staticIP(192, 168, 0, 105);
IPAddress subnet(255, 255, 255, 0);
IPAddress gateway(192, 168, 0, 1);
IPAddress dns(192, 168, 0, 1);

EthernetServer server(80);

// ==========================================
// SYSTEM STATE
// ==========================================
float currentEnergy = 0.0;
long currentCounts = 0;
float currentCurrent = 0.0;
float currentMirrorCurrent = 0.0;
unsigned long lastIdleUpdate = 0;
bool lastHWStatus = false;

// Connection Debounce Globals
unsigned long disconnectStartTime = 0;
bool pendingDisconnect = false;

struct Config {
  float cathodeVoltage = -20.0;
  float mirrorVoltage = 0.0;
} sysConfig;

// ==========================================
// FUNCTION PROTOTYPES
// ==========================================
void handleRequest(EthernetClient &client);
void sendCommonHeaders(EthernetClient &client);
void updateIdleMonitoring();

// ==========================================
// SETUP
// ==========================================
void setup() {
  Serial.begin(115200);
  myusb.begin();
  userial.begin(115200);
 
  Serial.println("--- IPES Measurement Engine V0.8.7 (Slave Mode) ---");

  if (!Ethernet.begin(staticIP, subnet, gateway)) {
    Serial.println("Failed to start Ethernet!");
  }
  server.begin();
}

// Helper for active waiting
void wait(unsigned long ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    myusb.Task();
  }
}

void loop() {
  myusb.Task();

  bool currentHW = (bool)userial;
 
  if (currentHW && !lastHWStatus) {
    // === DEVICE CONNECTED (Stable) ===
    Serial.println("SMU Connected - Initializing...");
  
    // Robust Initialization Sequence
    wait(1000);           
    userial.begin(115200); 
    // Removed *RST to prevent USB detach/reset loops
    wait(200);
    userial.print("*CLS\n"); // Clear Status/Error Queue
    wait(50);
  
    // Init CH1 (Sample)
    Serial.println("Configuring CH1...");
    userial.println("SOUR1:FUNC VOLT");
    wait(50);
    userial.println("SENS1:FUNC \"CURR\"");
    wait(50);
    userial.println("SENS1:CURR:RANG:AUTO ON");
    wait(50);
    userial.println("OUTP1 ON");
    wait(100);

    // Init CH2 (Mirror)
    Serial.println("Configuring CH2...");
    userial.println("SOUR2:FUNC VOLT");
    wait(50);
    userial.println("SENS2:FUNC \"CURR\"");
    wait(50);
    userial.println("SENS2:CURR:RANG:AUTO ON");
    wait(50);
    userial.println("OUTP2 ON");
    wait(100);

    Serial.println("Setting Initial Bias...");
    userial.printf("SOUR1:VOLT %.2f\n", sysConfig.cathodeVoltage);
    userial.printf("SOUR2:VOLT %.2f\n", sysConfig.mirrorVoltage);
    Serial.println("SMU Init Complete.");
  
    lastHWStatus = true;
    pendingDisconnect = false;
  }
  else if (!currentHW && lastHWStatus) {
    // === POTENTIAL DISCONNECT ===
    if (!pendingDisconnect) {
      pendingDisconnect = true;
      disconnectStartTime = millis();
    } else {
      if (millis() - disconnectStartTime > 2000) { // 2000ms Debounce
         Serial.println("SMU Disconnected (Confirmed)");
         lastHWStatus = false;
         pendingDisconnect = false;
      }
    }
  }
  else if (currentHW && pendingDisconnect) {
    // === RECOVERED (Glitch detected) ===
    pendingDisconnect = false;
  }

  EthernetClient client = server.available();
  if (client) {
    handleRequest(client);
  }

  updateIdleMonitoring();
}

void updateIdleMonitoring() {
  if (millis() - lastIdleUpdate > 800) {
    lastIdleUpdate = millis();
    if (userial) {
      // Read CH1 Current
      while(userial.available()) userial.read();
      userial.print("MEAS1:CURR?\n");
      unsigned long start = millis();
      String buf = "";
      while (millis() - start < 100) {
        myusb.Task();
        if (userial.available()) {
          char c = userial.read();
          if ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == 'E' || c == 'e' || c == '+') buf += c;
          else if (buf.length() > 0 && (c == '\n' || c == '\r' || c == ',')) {
            currentCurrent = buf.toFloat() * -1000000.0;
            break;
          }
        }
      }
    
      // Read CH2 Current
      buf = "";
      userial.print("MEAS2:CURR?\n");
      start = millis();
      while (millis() - start < 100) {
        myusb.Task();
        if (userial.available()) {
          char c = userial.read();
          if ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == 'E' || c == 'e' || c == '+') buf += c;
          else if (buf.length() > 0 && (c == '\n' || c == '\r' || c == ',')) {
            currentMirrorCurrent = buf.toFloat() * -1000000.0;
            break;
          }
        }
      }
    }
  }
}

void doMeasure(float v1, float v2, int dwell, JsonDocument &res) {
  if (userial) {
    userial.printf("SOUR1:VOLT %.2f\n", v1);
    wait(10); // Ensure separation
    userial.printf("SOUR2:VOLT %.2f\n", v2);
  }
 
  delay(50); // Settle
 
  // Ensure the gate is set to the requested dwell in MICROSECONDS (Teensy 4.1 FreqCount uses usec)
  unsigned long startGate = millis();
  FreqCount.begin(dwell * 1000UL);

  while (!FreqCount.available()) {
    myusb.Task();
  }

  long counts = FreqCount.read();
  FreqCount.end();

 
  unsigned long endGate = millis();
  Serial.printf("Point: %.2fV, Dwell: %dms, Actual: %lums, Counts: %ld\n", v1, dwell, endGate - startGate, counts);

  float cur1 = 0.0;
  float cur2 = 0.0;
 
  if (userial) {
    // Read CH1
    while(userial.available()) userial.read();
    userial.print("MEAS1:CURR?\n");
    unsigned long t = millis();
    String b = "";
    while (millis() - t < 400) {
      myusb.Task();
      if (userial.available()) {
        char ch = userial.read();
        if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == 'E' || ch == 'e' || ch == '+') b += ch;
        else if (b.length() > 0 && (ch == '\n' || ch == '\r' || ch == ',')) {
          cur1 = b.toFloat() * -1000000.0;
          break;
        }
      }
    }
  
    // Read CH2
    b = "";
    userial.print("MEAS2:CURR?\n");
    t = millis();
    while (millis() - t < 400) {
      myusb.Task();
      if (userial.available()) {
        char ch = userial.read();
        if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == 'E' || ch == 'e' || ch == '+') b += ch;
        else if (b.length() > 0 && (ch == '\n' || ch == '\r' || ch == ',')) {
          cur2 = b.toFloat() * -1000000.0;
          break;
        }
      }
    }
  }

  res["v"] = v1;
  res["c"] = counts;
  res["i"] = cur1;
  res["im"] = cur2;
  res["hw"] = (bool)userial;
}

void handleRequest(EthernetClient &client) {
  String reqLine = client.readStringUntil('\n');
  reqLine.trim();
  if (reqLine.length() == 0) { client.stop(); return; }

  while (client.connected() && client.available()) {
    String line = client.readStringUntil('\n');
    if (line == "\r" || line == "") break;
  }

  if (reqLine.startsWith("OPTIONS")) {
    client.println("HTTP/1.1 204 No Content");
    client.println("Access-Control-Allow-Origin: *");
    client.println("Access-Control-Allow-Methods: POST, GET, OPTIONS");
    client.println("Access-Control-Allow-Headers: Content-Type");
    client.println("Connection: close\n");
    client.stop();
    return;
  }

  if (reqLine.startsWith("GET /api/status")) {
    client.println("HTTP/1.1 200 OK");
    sendCommonHeaders(client);
    JsonDocument doc;
    doc["v"] = currentEnergy;
    doc["c"] = currentCounts;
    doc["i"] = currentCurrent;
    doc["im"] = currentMirrorCurrent;
    doc["hw"] = (bool)userial;
    serializeJson(doc, client);
    client.println();
  }
  else if (reqLine.startsWith("POST /api/measure")) {
    JsonDocument req;
    if (!deserializeJson(req, client)) {
      JsonDocument res;
      doMeasure(req["v"] | 0.0, req["vm"] | sysConfig.mirrorVoltage, req["dwell"] | 500, res);
      client.println("HTTP/1.1 200 OK");
      sendCommonHeaders(client);
      serializeJson(res, client);
      client.println();
    }
  }
  else if (reqLine.startsWith("GET /api/stop")) {
    if (userial) {
        userial.printf("SOUR1:VOLT %.2f\n", sysConfig.cathodeVoltage);
        userial.printf("SOUR2:VOLT %.2f\n", sysConfig.mirrorVoltage);
    }
    client.println("HTTP/1.1 200 OK");
    sendCommonHeaders(client);
    client.println("{\"status\":\"stopped\"}");
  }
  else if (reqLine.startsWith("POST /api/config")) {
    JsonDocument doc;
    if (!deserializeJson(doc, client)) {
      sysConfig.cathodeVoltage = doc["cathode"] | sysConfig.cathodeVoltage;
      sysConfig.mirrorVoltage = doc["mirror"] | sysConfig.mirrorVoltage;
      if (userial) {
          userial.printf("SOUR1:VOLT %.2f\n", sysConfig.cathodeVoltage);
          userial.printf("SOUR2:VOLT %.2f\n", sysConfig.mirrorVoltage);
      }
      client.println("HTTP/1.1 200 OK");
      sendCommonHeaders(client);
      client.println("{\"status\":\"updated\"}");
    }
  }
  else {
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: text/html\nContent-Encoding: gzip");
    client.print("Content-Length: "); client.println(index_html_len);
    client.println("Connection: close\n");
    const unsigned long CHUNK_SIZE = 1460;
    unsigned long bytesSent = 0;
    while (bytesSent < index_html_len) {
      unsigned long remaining = index_html_len - bytesSent;
      unsigned long chunk = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE;
      size_t n = client.write(&index_html[bytesSent], chunk);
      if (n == 0) { delay(1); continue; }
      bytesSent += n;
    }
  }
  client.stop();
}

void sendCommonHeaders(EthernetClient &client) {
  client.println("Content-Type: application/json\nAccess-Control-Allow-Origin: *\nConnection: close\n");
}
 
Last edited:
Hi all !

Further investigation revealed that the 9ns/50Mhz performace with T4.1 was possible with 30ohm/20ohm voltage divider ONLY.
I mean 50ohm seen from the Rigol source and 30ohm from the T4.1 pin 9.
 
Back
Top