BERGSONNE

USB

Plug a Core into a computer and it shows up as a serial port — no drivers, no crystal. core_usb is a USB CDC device with printf and buffered RX, plus a vendor HID path for raw 64-byte reports. It sits on hal_usb_cdc. Use the Core / HAL / LL toggle at the top of the sidebar to switch.

Overview

USB device is available on the Cores with a USB peripheral — Core.ST.L4 and Core.ST.H5. It runs crystal-less: the HSI48 oscillator plus the Clock Recovery System (synced to USB SOF) keeps the clock USB-accurate, so no external part is needed.

CDC appears as a COM port
On the host it’s a virtual serial device (/dev/tty.usbmodem*, a COM port on Windows) — no driver install on modern OSes. The same port is what make flash-dfu touches at 1200 baud to enter the bootloader — and, on Core.ST.L4 and Core.ST.H5, what make flash-serial opens at 2400 baud to update the firmware without one (serial update).

CDC serial

core_init() already starts USB on these Cores, so core_usb_init() is a harmless no-op you can keep for clarity. Then write like any serial port. Writes queue and return at once; with a terminal that stops reading they wait at most 50 ms, then drop. core_usb_connected() tells you the host has actually opened the port:

#include "core.h"
  #include "core_usb.h"        // Core.ST.L4 / H5; core.h includes it when "usb" is enabled
  #include "core_watchdog.h"

  core_usb_init();

  while (1) {
      core_watchdog_feed();
      if (core_usb_connected()) {
          core_usb_printf("adc=%u ticks=%lu\r\n", raw, core_millis());
      }
      core_delay_ms(500);
  }

Text printed before a terminal opens the port isn’t lost: it waits in the TX queue (up to its size) and goes out when the port opens. To wait for a terminal at start-up, core_usb_wait_host(ms) returns 1 once one is connected, or 0 on timeout, feeding the watchdog while it waits.

Receiving

Two RX modes. Poll a background ring buffer, or register a callback that fires from the USB interrupt as bytes arrive (when set, data isn’t buffered for polling):

// Polling:
  while (core_usb_available()) {
      uint8_t b = core_usb_getc();
  }

  // Or callback-driven:
  static void on_rx(const uint8_t *data, uint16_t len, void *ctx) { /* ... */ }
  core_usb_on_receive(on_rx, NULL);

For text commands, the line assembler collects bytes into lines. core_usb_poll_line() reads from the ring buffer (so not with a receive callback) and returns a finished line, without its ending, or NULL. Lines end at \n (\r\n works too), up to 127 characters:

#include <string.h>

  static core_usb_line_t lr;            // zero-initialized

  // in the loop:
  const char *line = core_usb_poll_line(&lr);
  if (line && strcmp(line, "led on") == 0) core_led_on();

core_usb_line_feed() does the same one byte at a time, for bytes you already have (from a callback, say). In Studio, on Core.USB.line(text) delivers each line.

HID reports

Alongside CDC, the device exposes a vendor HID interface for raw 64-byte reports — handy for a custom host tool (via hidapi / pyusb / Web HID) with no OS driver. Reports go both ways:

#include "core_usb_hid.h"

  uint8_t report[8] = { 0x01, 0x02, 0x03, 0x04 };
  core_usb_hid_send(report, sizeof(report));   // zero-padded to 64 bytes

  // host -> device: one report per call, from the USB interrupt
  static void on_report(const uint8_t *data, uint16_t len, void *ctx) { /* ... */ }
  core_usb_hid_set_rx_callback(on_report, NULL);

Cross-architecture support

CDC serial is the verified path on the USB-capable Cores; HID rides the same stack. MSC and USB host are not implemented. (The ROM DFU bootloader is covered in bootloading.)

·L0M0+●L4M4·W5M33●H5M33WCH (RISC-V) · Nordic (nRF54) — in development
·L0M0+●L4M4·W5M33●H5M33WCH (RISC-V) · Nordic (nRF54) — in development

See the implementation status for the full matrix.

Known gaps

What the SDK itself lists as missing here, straight from the headers:

USB
mediumDSLNo DSL surface for receive (byte read / available)

The Core.USB.receive event handles inbound bytes one at a time, and Core.USB.line delivers newline-terminated text commands, but DSL programs can't poll core_usb_available() / core_usb_read(). That makes binary message-framed protocols (read N bytes after a header) awkward — the workaround is to accumulate bytes in a DSL array inside the receive handler.

lowDSLNo DSL printf-with-formatting

print_int / float / bool emit one value per call, each on its own line. For multi-field log lines DSL programs concatenate with string ops (or call print multiple times). A `print_fmt(template, ...args)` host call would be the next step.

mediumC APIUSB MSC (mass storage)

SDK roadmap Tier 3: drag-and-drop firmware / datalog volume needs SCSI + FAT. A feature gap, not a workflow blocker — projects that need persistent storage today reach for NVM or SD via SPI.

mediumC APIUSB Host mode

Core.ST.H5 has DRD silicon but the wrapper is device-only. Most projects don't need host mode; the few that do (USB-keyboard / thumb-drive readers) drop into the lower-layer USB stack.

mediumC APINo CDC line-coding callback

Host-side baud changes (1200-touch bootloader trigger included) land in the lower stack. The header doesn't expose a hook for programs that want to react to baud / DTR changes.

USB HID
mediumDSLNo DSL surface for HID

Sending a HID report needs a buffer pointer + length. The DSL's array-host ABI (used for tile reads) could carry it, but no default-instance wrapper is wired up.

lowC APIVendor descriptor only

The HID descriptor is hard-coded as a 64-byte vendor report with usage page 0xFF00. No way to register custom reports (keyboard, mouse, gamepad) — that needs the full HID descriptor tooling.

From the @studio unsupported notes in core_usb.h, core_usb_hid.h — tiles@217a38f.

API reference

Default-instance · Tier 2
void core_usb_print(const char * s);
Print a string followed by a newline. Thin wrapper for DSL-style callers.
void core_usb_print_int(int v);
Print a signed integer followed by a newline.
void core_usb_print_float(double v);
Print a double with a newline. %g trims trailing zeros for readability.
void core_usb_print_bool(int v);
Print "true" / "false" followed by a newline.
Lower-level · Tier 1
void core_usb_init(void);
Initialize USB CDC. Device appears as /dev/tty.usbmodem* on the host.
int core_usb_connected(void);
Returns 1 if a host terminal is connected (DTR set).
int core_usb_wait_host(uint32_t timeout_ms);
Wait up to `timeout_ms` for a host terminal to open the port (DTR set). Feeds the watchdog while it waits. Not needed just to see early output: text printed before the port opens is kept (up to HAL_USB_CDC_TX_QUEUE_SIZE bytes) and sent when it does.
Returns: 1 if a terminal is connected, 0 on timeout.
int core_usb_write(const uint8_t * buf, uint16_t len);
Transmit data: queued, never waits long (see hal_usb_cdc_write); returns bytes queued, -1 before init.
int core_usb_try_write(const uint8_t * buf, uint16_t len);
Transmit without waiting: queues the whole buffer or none of it. Returns len if queued, 0 if there is no room right now (offer it again later), -1 if no terminal is connected. Main loop only.
void core_usb_on_receive(hal_usb_cdc_rx_cb_t cb, void * ctx);
Set a callback for received data. Called from USB ISR. When set, data is NOT buffered for polling reads.
uint16_t core_usb_available(void);
Returns the number of bytes available to read (ring buffer mode).
uint8_t core_usb_getc(void);
Read a single byte (blocking — waits for data).
uint16_t core_usb_read(uint8_t * buf, uint16_t max);
Read available bytes into buf (non-blocking). Returns bytes read.
int core_usb_try_read(uint8_t * byte);
Non-blocking single byte read. Returns 1 if a byte was read, 0 if empty.
char * core_usb_line_feed(core_usb_line_t * lr, uint8_t byte);
Feed one received byte to a line assembler. A line ends at '\n'. '\r' and NUL bytes are dropped, so "\r\n" and "\n" endings both work. Empty lines are ignored. A line longer than CORE_USB_LINE_MAX characters is dropped whole: the assembler discards everything up to the next '\n' and starts clean after it.
Returns: The completed line, NUL-terminated and without its ending, or NULL if this byte did not complete one. The text stays valid until the next call with the same `lr`.
char * core_usb_poll_line(core_usb_line_t * lr);
Read received bytes until a line completes (non-blocking). Takes bytes from the receive ring with core_usb_try_read(), at most 64 per call so a flood can't stall the main loop, and stops at the first complete line; later bytes stay in the ring for the next call. Call it from the main loop. Not for use with core_usb_on_receive(), which bypasses the ring, and not alongside other ring reads, which would steal bytes from the line. Studio programs don't call this: `on Core.USB.line(text)` gets each line from coregen's dispatcher, which feeds core_usb_line_feed() directly.
Returns: The completed line, NUL-terminated and without its ending, or NULL if no line is complete yet. Valid until the next call with `lr`.
int core_usb_hid_send(const uint8_t * buf, uint16_t len);
Send a HID report (up to 64 bytes, zero-padded automatically). Blocking — waits for the previous report to be read by the host. Returns bytes of user data sent, or -1 if USB not configured.
void core_usb_hid_set_rx_callback(hal_usb_hid_rx_cb_t cb, void * ctx);
Register a callback for inbound HID reports (host -> device). Fires from the USB ISR with one report per call (up to 64 bytes), delivered via the EP3 OUT interrupt endpoint or HID SET_REPORT. Pass NULL to disable; unhandled reports are dropped.
Types
TypeKindDescription
core_usb_line_tstructLine-assembler state for core_usb_line_feed() / core_usb_poll_line(). Zero-initialize it (a static or global is).
  • char[CORE_USB_LINE_MAX + 1] buf · Line so far, then the NUL-terminated line.
  • uint8_t len · Characters collected.
  • uint8_t overflow · Set while discarding an over-long line.

Generated from core_usb.h, core_usb_hid.h — tiles@77fd3f3.