MicroPython on Teensy 4.0, what max serial data transmit speed over USB?

PaulS

Well-known member
While playing with MicroPython on a Teensy 4.0, I wondered at what speed I could send serial data over USB from the Teensy to my PC.
Ran this MicroPython script as main.py on the device:
Python:
import sys
import time

# create a pre-allocated payload of 512 bytes
payload = bytes([i % 256 for i in range(512)])

while True:
    sys.stdout.buffer.write(payload)

On my Windows PC, I ran this Python script:
Python:
import serial
import time

# Open the device port (the baudrate parameter is ignored by USB CDC)
ser = serial.Serial('COM1', 115200, timeout=1)

print("Measuring USB data transfer rate for 5 seconds...")
total_bytes = 0
checksum = 0
start_time = time.time()

while time.time() - start_time < 5:
data = ser.read(512) # 64 bytes for USB 1.1 Full Speed, 512 bytes for USB 2.0 High Speed
total_bytes += len(data)

duration = time.time() - start_time
speed_kbs = (total_bytes / 1024) / duration
speed_mbps = (total_bytes * 8 / 1000000) / duration

print(f"Total bytes received: {total_bytes} bytes")
print(f"Average throughput: {speed_kbs:.2f} KB/s") # 1024 bytes/s
print(f"Data line speed: {speed_mbps:.2f} Mbps") # 1000000 bytes/s

ser.close()

Result: ~130 Mbps. Not too bad.

I also ran this C program on my PC to check whether Python was limiting here:
C:
#include <windows.h>
#include <stdio.h>
#include <time.h>

#define BUFFER_SIZE 512
#define TEST_DURATION_SEC 5.0

int main() {
    int port_number = 0;
    char port_name[64];

    printf("--- High-Speed Serial Benchmark Utility ---\n");
    printf("Enter the COM port number for your device (e.g., enter 3 for COM3): ");

    // Validate that the user entered a valid positive integer
    if (scanf_s("%d", &port_number) != 1 || port_number <= 0) {
        printf("Error: Invalid COM port number entered.\n");
        return 1;
    }

    // Automatically construct the required Win32 namespace path format: \\.\COMxx
    // This format is mandatory in Windows for accessing ports higher than COM9
    sprintf_s(port_name, sizeof(port_name), "\\\\.\\COM%d", port_number);
    printf("Attempting to connect to COM%d...\n\n", port_number);

    // Open the serial port with low-level Win32 file access
    HANDLE h_serial = CreateFileA(
        port_name,
        GENERIC_READ,
        0,              // Shared mode: 0 means exclusive access
        NULL,           // Security attributes
        OPEN_EXISTING,  // Port must already exist
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (h_serial == INVALID_HANDLE_VALUE) {
        printf("Error: Could not open %s. Ensure it is connected and not open in another app.\n", port_name + 4);
        return 1;
    }

    // Configure the serial port parameters
    DCB dcb_serial_params = { 0 };
    dcb_serial_params.DCBlength = sizeof(dcb_serial_params);

    if (!GetCommState(h_serial, &dcb_serial_params)) {
        printf("Error: Could not get serial port state.\n");
        CloseHandle(h_serial);
        return 1;
    }

    // Baudrate parameters are ignored by USB CDC firmware but required by the Win32 API
    dcb_serial_params.BaudRate = CBR_115200;
    dcb_serial_params.ByteSize = 8;
    dcb_serial_params.StopBits = ONESTOPBIT;
    dcb_serial_params.Parity = NOPARITY;

    // Assert DTR to notify the device hardware that the PC is actively streaming
    dcb_serial_params.fDtrControl = DTR_CONTROL_ENABLE;

    if (!SetCommState(h_serial, &dcb_serial_params)) {
        printf("Error: Could not set serial port parameters.\n");
        CloseHandle(h_serial);
        return 1;
    }

    // Set aggressive non-blocking execution timeouts for maximum polling speed
    COMMTIMEOUTS timeouts = { 0 };
    timeouts.ReadIntervalTimeout = MAXDWORD;        // Return instantly if bytes are available
    timeouts.ReadTotalTimeoutConstant = 0;          // No constant waiting delay
    timeouts.ReadTotalTimeoutMultiplier = 0;        // No multiplier delay

    if (!SetCommTimeouts(h_serial, &timeouts)) {
        printf("Error: Could not set serial port communication timeouts.\n");
        CloseHandle(h_serial);
        return 1;
    }

    printf("Successfully connected to %s.\n", port_name + 4);
    printf("Measuring raw native USB data transfer rate for %.1f seconds...\n", TEST_DURATION_SEC);

    unsigned char buffer[BUFFER_SIZE];
    unsigned long long total_bytes = 0;
    unsigned int checksum = 0;

    // Get precise tracking timestamps
    clock_t start_time = clock();
    clock_t current_time;
    double elapsed_time = 0.0;

    while (elapsed_time < TEST_DURATION_SEC) {
        DWORD bytes_read = 0;

        // Directly pull data out of the Windows Kernel buffer
        if (ReadFile(h_serial, buffer, BUFFER_SIZE, &bytes_read, NULL) && bytes_read > 0) {
            total_bytes += bytes_read;
        }

        current_time = clock();
        elapsed_time = (double)(current_time - start_time) / CLOCKS_PER_SEC;
    }

    // Calculate final metrics
    double speed_kbs = ((double)total_bytes / 1024.0) / elapsed_time;
    double speed_mbps = ((double)total_bytes * 8.0 / 1000000.0) / elapsed_time;

    printf("\n--- BENCHMARK RESULTS ---\n");
    printf("Total bytes received:  %llu bytes\n", total_bytes);
    printf("Average throughput:    %.2f KB/s\n", speed_kbs);    // 1024 bytes/s
    printf("Data line speed:       %.2f Mbps\n", speed_mbps);   // 1000000 bytes/s

    // Cleanly drop the DTR signal and release the OS port handle
    dcb_serial_params.fDtrControl = DTR_CONTROL_DISABLE;
    SetCommState(h_serial, &dcb_serial_params);
    CloseHandle(h_serial);
    return 0;
}

Result: again ~130 Mbps. Apparently Python was not limiting here (on an Intel i9-13900K, that is).

Next step was to compare the MicroPython script with this C++ program running on the Teensy:
C++:
uint8_t payload[512];

void setup() {
  // The baudrate parameter is ignored by the USB controller.
  // It automatically runs at the full 480 Mbps physical hardware link speed.
  Serial.begin(9600);
 
  // Fill the buffer with dummy test data
  for (int i = 0; i < 512; i++) {
    payload[i] = i % 256;
  }
}

void loop() {
  // Check if the PC is actively connected and reading the port
  if (Serial && Serial.dtr()) {
    // Push raw bytes directly to the USB DMA registers at hardware speed
    Serial.write(payload, 512);
  }
}

Now the results on the PC side (both the Python script and C program) were: ~260 Mbps.

So the take-away I guess is: MicroPython does pretty well on the Teensy 4, but for real speed move to C++.

Paul
 
Last edited:
If you're willing to write more python test code, I would be really interested to see a python version of the lines per second benchmark we used in the early days of Teensy 4.0.


This test tries to emulate common Serial.print() usage. Each line has 2 numbers that change, so the test measures how efficiently the print code can convert a 32 bit integer to formatted digits. Each line is smaller than a USB packet, so it also measures how efficiently the USB code handles packing output into USB packets. Even with C++ you're likely to see slower speed than sending a fixed message that neatly fits into a USB packet.
 
If you're willing to write more python test code, I would be really interested to see a python version of the lines per second benchmark we used in the early days of Teensy 4.0.
Sure, but that will be some time next week since I'm on vacation abroad now.
Paul
 
If you're willing to write more python test code, I would be really interested to see a python version of the lines per second benchmark we used in the early days of Teensy 4.0.


This test tries to emulate common Serial.print() usage. Each line has 2 numbers that change, so the test measures how efficiently the print code can convert a 32 bit integer to formatted digits. Each line is smaller than a USB packet, so it also measures how efficiently the USB code handles packing output into USB packets. Even with C++ you're likely to see slower speed than sending a fixed message that neatly fits into a USB packet.
Thanks, I will try.
 
If you're willing to write more python test code, I would be really interested to see a python version of the lines per second benchmark we used in the early days of Teensy 4.0.
Here you go:
Python:
import time

count = 10000000  # Starting with 8 digits gives consistent chars/line
prior_count = count
count_per_second = 0
prior_msec = time.ticks_ms()

while True:
    print("count=", count, ", lines/sec=", count_per_second, sep="")

    count += 1
    msec = time.ticks_ms()
    
    # Check if 1 second (1000 milliseconds) has elapsed
    if time.ticks_diff(msec, prior_msec) > 1000:
        prior_msec = time.ticks_add(prior_msec, 1000)
        count_per_second = count - prior_count
        prior_count = count

Ran this script as main.py on the Teensy 4.0.
Used Tycommander to show the data since the Thonny IDE's Shell is too slow to catch up:

1786809489163.png


Paul
 
Back
Top