USB Reader/Writer with C++ (Embedded): CDC-ACM and libusb Bulk

Two pragmatic ways to exchange data with a USB device from C++ on Linux: 1) USB CDC-ACM (appears as /dev/ttyACM*//dev/ttyUSB*) using POSIX termios. 2) Raw USB via libusb for bulk endpoints (common for custom devices).

CDC-ACM is ideal for quick logs/commands. Use libusb when you control endpoints and need throughput or custom protocols.

1) CDC-ACM serial: read/write /dev/ttyACM0

#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <string>

int openSerial(const char* path, speed_t baud) {
    int fd = ::open(path, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) { std::perror("open"); return -1; }
    termios tio{};
    if (tcgetattr(fd, &tio) != 0) { std::perror("tcgetattr"); ::close(fd); return -1; }

    cfmakeraw(&tio);
    cfsetispeed(&tio, baud);
    cfsetospeed(&tio, baud);
    tio.c_cflag |= (CLOCAL | CREAD);
    tio.c_cflag &= ~CSTOPB;    // 1 stop bit
    tio.c_cflag &= ~PARENB;    // no parity
    tio.c_cflag &= ~CRTSCTS;   // no HW flow
    tio.c_cc[VMIN]  = 0;       // non-blocking read
    tio.c_cc[VTIME] = 1;       // read timeout = 0.1s

    if (tcsetattr(fd, TCSANOW, &tio) != 0) { std::perror("tcsetattr"); ::close(fd); return -1; }
    return fd;
}

int main() {
    const char* dev = "/dev/ttyACM0"; // or /dev/ttyUSB0
    int fd = openSerial(dev, B115200);
    if (fd < 0) return 1;

    // Write a command
    const char* cmd = "PING\n";
    if (::write(fd, cmd, std::strlen(cmd)) < 0) std::perror("write");

    // Read response (non-blocking, simple loop)
    char buf[256];
    std::string acc;
    for (int i = 0; i < 50; ++i) {
        int n = ::read(fd, buf, sizeof(buf));
        if (n > 0) acc.append(buf, buf + n);
        usleep(20'000);
    }
    std::printf("Response: %s\n", acc.c_str());

    ::close(fd);
    return 0;
}

Build:

g++ -std=c++17 serial_demo.cpp -o serial_demo

Notes

  • Ensure udev permissions (or run as root). For stable paths, create udev rules matching VID/PID.
  • Device firmware should set CDC line coding (115200 8N1) or ignore it.

2) libusb bulk transfer: custom endpoints

#include <libusb-1.0/libusb.h>
#include <cstdio>
#include <vector>

struct UsbDev { libusb_device_handle* h = nullptr; uint8_t inEp = 0x81; uint8_t outEp = 0x01; };

bool openDevice(uint16_t vid, uint16_t pid, UsbDev& dev) {
    if (libusb_init(nullptr) != 0) return false;
    dev.h = libusb_open_device_with_vid_pid(nullptr, vid, pid);
    if (!dev.h) return false;
    libusb_set_auto_detach_kernel_driver(dev.h, 1);
    if (libusb_claim_interface(dev.h, 0) != 0) return false;
    // Optionally scan descriptors to find bulk endpoints
    return true;
}

int main() {
    UsbDev d;
    uint16_t vid = 0x1234, pid = 0x5678; // replace with your device
    if (!openDevice(vid, pid, d)) { std::fprintf(stderr, "open failed\n"); return 1; }

    // Write bulk OUT
    const unsigned char tx[] = { 'H','e','l','l','o' };
    int wrote = 0;
    int rc = libusb_bulk_transfer(d.h, d.outEp, const_cast<unsigned char*>(tx), sizeof(tx), &wrote, 1000);
    if (rc != 0) std::fprintf(stderr, "bulk out err %d\n", rc);

    // Read bulk IN
    unsigned char rx[512]; int got = 0;
    rc = libusb_bulk_transfer(d.h, d.inEp, rx, sizeof(rx), &got, 1000);
    if (rc == 0) std::printf("got %d bytes\n", got);

    libusb_release_interface(d.h, 0);
    libusb_close(d.h);
    libusb_exit(nullptr);
    return 0;
}

Build:

g++ -std=c++17 bulk_demo.cpp -lusb-1.0 -o bulk_demo

Finding endpoints

  • Inspect with lsusb -v -d vid:pid and look for Bulk IN/OUT endpoint addresses (e.g., 0x81 IN, 0x01 OUT).
  • Alternatively, enumerate configurations/interfaces/endpoints at runtime and pick the first bulk pair.

Throughput tips

  • Use large transfers (up to 16–64 KB) and queue multiple in parallel (libusb async API) for higher throughput.
  • Avoid small packet sizes in hot loops; amortize syscalls.

Safety

  • If a kernel driver (e.g., cdc_acm) owns the interface, either use CDC-ACM path or auto-detach (libusb_set_auto_detach_kernel_driver). Don’t race the same interface with two drivers.