Skip to content

feat: Add machine/usb package with USB device class support#29

Merged
xushiwei merged 2 commits into
goplus:mainfrom
luoliwoshang:feat/machine-usb
Nov 28, 2025
Merged

feat: Add machine/usb package with USB device class support#29
xushiwei merged 2 commits into
goplus:mainfrom
luoliwoshang:feat/machine-usb

Conversation

@luoliwoshang
Copy link
Copy Markdown
Member

Summary

Add machine/usb package synced from TinyGo v0.39.0 to provide USB device support for LLGO embedded applications.

TinyGo source reference: https://github.com/tinygo-org/tinygo/tree/v0.39.0/src/machine/usb

This PR extracts the USB package from #26 as an independent component for easier review and merging.

Synced Content

Directory Description
machine/usb/ Core USB device interface and configuration
machine/usb/adc/midi/ USB MIDI Audio Device Class support
machine/usb/cdc/ USB CDC (Communications Device Class) support
machine/usb/descriptor/ USB descriptor builders and definitions
machine/usb/hid/ USB HID (Human Interface Device) support
machine/usb/hid/joystick/ USB HID Joystick implementation
machine/usb/hid/keyboard/ USB HID Keyboard implementation
machine/usb/hid/mouse/ USB HID Mouse implementation
machine/usb/msc/ USB MSC (Mass Storage Class) support

Plus platform-specific USB implementations:

  • machine/machine_atsamd21_usb.go - Atmel SAMD21 USB
  • machine/machine_atsamd51_usb.go - Atmel SAMD51 USB
  • machine/machine_nrf52840_usb.go - Nordic nRF52840 USB
  • machine/machine_rp2040_usb.go - Raspberry Pi RP2040 USB
  • machine/machine_rp2350_usb.go - Raspberry Pi RP2350 USB
  • machine/machine_rp2_usb.go - Common RP2xxx USB code
  • machine/machine_rp2040_usb_fix_usb_device_enumeration.go - RP2040 USB enumeration fix

Package Details

USB Device Classes

1. CDC (Communications Device Class)

  • Use case: USB serial port emulation
  • Features: Virtual COM port, serial communication over USB
  • Devices: USB-to-Serial adapters

2. HID (Human Interface Device)

  • Use case: USB input devices
  • Supported devices:
    • Keyboard - Full keyboard with modifier keys
    • Mouse - 3-button mouse with wheel support
    • Joystick - Game controller with multiple axes and buttons
  • Features: Low-latency input, no drivers required on most OSes

3. MIDI (Musical Instrument Digital Interface)

  • Use case: USB MIDI devices
  • Features: Send/receive MIDI messages over USB
  • Applications: MIDI controllers, synthesizers

4. MSC (Mass Storage Class)

  • Use case: USB flash drives, storage devices
  • Features:
    • SCSI command support
    • Block device interface
    • Read/write operations
    • Unmap (TRIM) support

USB Descriptors

Comprehensive descriptor builders for:

  • Device descriptors
  • Configuration descriptors
  • Interface descriptors
  • Endpoint descriptors
  • HID report descriptors
  • Class-specific descriptors (CDC, HID, MIDI, MSC)

Implementation Approach

This PR uses a two-step migration approach to ensure clarity in the git history:

Step 1: Import Path Adaptation (Commit 1)

First, we adapt all import paths for the machine/_usb package:

  • "machine""github.com/goplus/emb/machine"
  • "machine/usb""github.com/goplus/emb/machine/_usb"
  • "runtime/interrupt""github.com/goplus/emb/runtime/interrupt"

This creates a temporary intermediate state with _usb naming.

Rationale: Using _usb as an intermediate step makes the migration traceable and allows reviewers to clearly see the import path changes separate from the directory rename.

Step 2: Directory Rename (Commit 2)

Then, we rename the directory and update imports:

  • Rename machine/_usb/machine/usb/
  • Update all "github.com/goplus/emb/machine/_usb""github.com/goplus/emb/machine/usb"

This restores the standard package naming convention while maintaining LLGO-compatible full import paths.

Result: Clean separation of concerns:

  1. Commit 1 shows pure import path adaptation
  2. Commit 2 shows pure directory rename + reference updates

LLGO Adaptations

Import Path Adaptation

All imports updated to use full module paths:

TinyGo goplus/emb
"machine" "github.com/goplus/emb/machine"
"machine/usb" "github.com/goplus/emb/machine/usb"
"machine/usb/cdc" "github.com/goplus/emb/machine/usb/cdc"
"machine/usb/hid" "github.com/goplus/emb/machine/usb/hid"
"machine/usb/msc" "github.com/goplus/emb/machine/usb/msc"
"runtime/interrupt" "github.com/goplus/emb/runtime/interrupt"

No Other Changes Required

The USB packages are pure Go implementations and work directly with LLGO without additional modifications. They do not use:

  • CGO
  • Compiler intrinsics (USB functionality is implemented by platform-specific machine files)
  • TinyGo-specific runtime features

Files Changed

Two commits with clear separation:

Commit 1: Import path adaptation

  • 31 files modified
  • 70 insertions(+), 70 deletions(-)
  • Pure import path updates for _usb intermediate state

Commit 2: Directory rename and final updates

  • 51 files changed (44 renames + 7 modifications)
  • 36 insertions(+), 36 deletions(-)
  • Rename _usbusb and update all references

Total files in USB package:

  • 52 Go files across USB subpackages
  • 7 platform-specific USB implementation files
  • Complete USB device class support (CDC, HID, MIDI, MSC)

Platform Support

The USB package supports multiple microcontroller platforms:

  • Atmel SAMD: SAMD21, SAMD51 (Arduino Zero, Adafruit M0/M4 boards)
  • Nordic nRF: nRF52840 (Adafruit nRF52840, Particle Xenon)
  • Raspberry Pi: RP2040, RP2350 (Pico, Pico W, Pico 2)

Each platform has dedicated USB controller integration with:

  • Device enumeration
  • Endpoint management
  • Interrupt handling
  • Platform-specific quirks and optimizations

Dependencies

This PR is extracted from #26 to allow independent review.

Dependency chain:

  1. ✅ PR feat: Add device generation tools and validation workflow for TinyGo v0.39.0 #25 (device package) - Already merged
  2. PR feat: Add internal utility packages (binary, bytealg, itoa) #28 (internal packages) - Required for binary/bytealg/itoa utilities
  3. PR feat: Add runtime/interrupt package and update volatile package #27 (runtime packages) - Required for interrupt handling
  4. → This PR (usb package) - USB device support
  5. PR feat: Add machine package validation workflow for TinyGo v0.39.0 #26 (machine package) - Main machine package depending on USB

Test Plan

  • Verify compilation without errors
  • Check that USB subpackages can be imported correctly
  • Validate USB descriptor generation
  • Test CDC virtual serial port functionality
  • Test HID device implementations (keyboard, mouse, joystick)
  • Verify platform-specific USB code compiles for each target

Migration Strategy

The two-commit approach provides:

  1. Clear audit trail: Import changes visible separately from renames
  2. Easier review: Reviewers can see each transformation independently
  3. Safer rollback: Each step can be reverted independently if needed
  4. Better understanding: Future developers can see why/how paths changed

This follows the principle of "separate concerns" in version control history.

🤖 Generated with Claude Code

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @luoliwoshang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request integrates a new machine/usb package into the LLGO project, enabling comprehensive USB device functionality for embedded applications. Sourced from TinyGo v0.39.0, the package supports various USB device classes such as CDC, HID, MIDI, and MSC, and is compatible with several popular microcontroller platforms including Atmel SAMD, Nordic nRF, and Raspberry Pi RP2xxx. The changes were meticulously applied through a two-step migration process to ensure clear version history and maintain full LLGO compatibility.

Highlights

  • New USB Package: Introduction of the machine/usb package, providing comprehensive USB device support for LLGO embedded applications.
  • TinyGo Code Sync: The core USB codebase is synced from TinyGo v0.39.0, ensuring a robust and well-tested foundation for USB functionalities.
  • Comprehensive Device Class Support: The package includes support for various USB device classes: CDC (serial communication), HID (Human Interface Devices like keyboard, mouse, joystick), MIDI (Musical Instrument Digital Interface), and MSC (Mass Storage Class).
  • Multi-Platform Compatibility: USB implementations are provided for several popular microcontroller platforms, including Atmel SAMD (SAMD21, SAMD51), Nordic nRF52840, and Raspberry Pi RP2xxx (RP2040, RP2350).
  • LLGO Integration: All import paths have been adapted to use full module paths (e.g., github.com/goplus/emb/...), and the USB logic is pure Go, not relying on CGO, compiler intrinsics, or TinyGo-specific runtime features for compatibility.
  • Structured Migration Approach: The changes were implemented using a two-commit strategy to clearly separate import path adaptations from directory renames, which enhances reviewability and historical traceability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds comprehensive USB device support by porting the machine/usb package from TinyGo. The changes mainly consist of adapting import paths to the project's structure. The port is well-structured and follows the original TinyGo implementation closely.

I've reviewed the newly added code and found two potential issues that should be addressed:

  • A potential data corruption bug in the CDC txRingBuffer when handling large writes.
  • An incorrect calculation in the MSC driver for the SCSI UNMAP command, which could lead to not all requested blocks being unmapped.

Details and suggestions for fixes are in the comments below.

Adapt all import paths in machine/_usb directory to use full module paths for LLGO compatibility.

Changes:
- Update "machine" imports to "github.com/goplus/emb/machine"
- Update "machine/usb" imports to "github.com/goplus/emb/machine/_usb"
- Update "runtime/interrupt" imports to "github.com/goplus/emb/runtime/interrupt"

This is step 1 of 2: Import path adaptation with _usb intermediate naming.

Affected files:
- machine/_usb/* - All USB subpackages (adc/midi, cdc, descriptor, hid, msc)

Total: 23 files modified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread machine/machine_atsamd21_usb.go Outdated
"runtime/interrupt"
"github.com/goplus/emb/device/sam"
"github.com/goplus/emb/machine/usb"
"github.com/goplus/emb/runtime/interrupt"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMPORTANT: Unbounded Busy-Wait During USB Reset

This loop has no timeout and will hang indefinitely if hardware fails to acknowledge the reset.

Issue: The nRF52840 implementation includes a 300,000 iteration timeout for similar operations (machine_nrf52840_usb.go:86-92), but ATSAMD implementations lack this safety mechanism.

Recommendation:
Add a timeout counter similar to the nRF implementation:

timeout := 300000
for (sam.USB_DEVICE.SYNCBUSY.HasBits(sam.USB_DEVICE_SYNCBUSY_SWRST) ||
    sam.USB_DEVICE.SYNCBUSY.HasBits(sam.USB_DEVICE_SYNCBUSY_ENABLE)) && timeout > 0 {
    timeout--
}
if timeout == 0 {
    return errors.New("USB reset timeout")
}

Comment thread machine/usb/cdc/usbcdc.go
"machine"
"machine/usb"
"runtime/interrupt"
"github.com/goplus/emb/machine"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMPORTANT: Silent Error Suppression

Errors from ReadByte() are silently ignored, potentially reading zero values instead of actual data.

Recommendation:
Either propagate the error or document why it can be safely ignored:

for i := 0; i < size; i++ {
    v, err := usbcdc.ReadByte()
    if err != nil {
        return i, err
    }
    data[i] = v
}

"errors"
"internal/binary"
"internal/bytealg"
"github.com/goplus/emb/internal/binary"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Redundant Volatile Register Reads

Each call to rb.head.Get() is an expensive volatile register read. The value is fetched twice here (lines 31-32 in buffer implementations).

Optimization for embedded systems:

newHead := rb.head.Get() + 1
rb.head.Set(newHead)
rb.buffer[newHead%rxRingBufferSize].Set(val)

Additionally, since buffer sizes are powers of 2, use bitwise AND instead of modulo:

const rxRingBufferMask = rxRingBufferSize - 1
rb.buffer[newHead&rxRingBufferMask].Set(val)  // Much faster on ARM Cortex-M

Complete the USB package migration by:
1. Renaming machine/_usb directory to machine/usb
2. Updating all import paths from "github.com/goplus/emb/machine/_usb" to "github.com/goplus/emb/machine/usb"

This is step 2 of 2: Directory rename and final import path updates.

This restores the standard package naming convention while maintaining LLGO-compatible import paths.

Affected:
- 44 files renamed from machine/_usb/* to machine/usb/*
- 14 files with updated import paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
"machine"
"machine/usb"
"machine/usb/descriptor"
"github.com/goplus/emb/machine"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Global Singleton Without Thread Safety

The global Midi variable is initialized in init() without synchronization. While this may work in single-threaded embedded contexts, it's not future-proof for concurrent access.

Recommendation:
Either:

  1. Use sync.Once for lazy initialization if concurrent access is possible
  2. Document clearly that this package assumes single-threaded execution
  3. Add build constraints if this is architecture-specific

@fennoai
Copy link
Copy Markdown
Contributor

fennoai Bot commented Nov 26, 2025

Code Review Summary

Comprehensive review completed using specialized agents for code quality, performance, security, and documentation.

Critical Issues (Must Address)

Race Conditions in Ring Buffers: CDC/HID/MIDI buffer operations lack atomic guarantees and proper synchronization, creating data corruption risks in interrupt contexts. See inline comments on machine/usb/cdc/buffer.go:30.

Acknowledged Data Corruption: TODO comment in TX buffer indicates known issue when buffer is full (machine/usb/cdc/buffer.go:93).

Unbounded Busy-Wait: ATSAMD21/51 USB reset loops lack timeouts, risking system hangs if hardware malfunctions.

Important Issues

  • Silent error suppression in CDC Read() method
  • Global singletons without thread-safety documentation
  • Performance inefficiencies in hot paths (redundant volatile reads, modulo operations on ARM Cortex-M)

Positive Aspects

  • Clean separation between USB device classes and platform code
  • Comprehensive USB protocol implementation (CDC, HID, MIDI, MSC)
  • Consistent import path migration to full module paths
  • Good interrupt-driven architecture

Recommendation

Address critical race conditions and data corruption risks before merging. Other issues can be tracked as follow-up technical debt.

Review completed with code-quality, performance, security, and documentation analysis agents.

Comment thread machine/usb/cdc/buffer.go

import (
"runtime/volatile"
"github.com/goplus/emb/runtime/volatile"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Race Condition in Ring Buffer

The Put() operation is not atomic and lacks proper synchronization. Between checking Used() and updating the buffer, an interrupt could modify the buffer state, leading to data corruption.

Issues:

  1. Used() result can become stale between check at line 30 and buffer update at line 32
  2. No interrupt masking around the critical section
  3. Similar pattern exists in HID and MIDI ring buffers

Recommendation:
Use interrupt.Disable()/Restore() around critical sections (CDC code already uses this pattern elsewhere in usbcdc.go):

func (rb *rxRingBuffer) Put(val byte) bool {
    state := interrupt.Disable()
    defer interrupt.Restore(state)
    
    if rb.Used() != rxRingBufferSize {
        rb.head.Set(rb.head.Get() + 1)
        rb.buffer[rb.head.Get()%rxRingBufferSize].Set(val)
        return true
    }
    return false
}

Comment thread machine/usb/cdc/buffer.go

import (
"runtime/volatile"
"github.com/goplus/emb/runtime/volatile"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Redundant Volatile Register Reads

Each rb.head.Get() is an expensive volatile register read. Line 31-32 performs this twice.

Optimization for embedded systems:

newHead := rb.head.Get() + 1
rb.head.Set(newHead)
rb.buffer[newHead%rxRingBufferSize].Set(val)

Further optimization: Since rxRingBufferSize = 128 is a power of 2, use bitwise AND instead of modulo (much faster on ARM Cortex-M without hardware divider):

const rxRingBufferMask = rxRingBufferSize - 1
rb.buffer[newHead&rxRingBufferMask].Set(val)

Comment thread machine/usb/msc/msc.go
"machine/usb/msc/csw"
"machine/usb/msc/scsi"
"github.com/goplus/emb/machine"
"github.com/goplus/emb/machine/usb"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Inefficient Busy-Wait Goroutine

This goroutine wakes 10,000 times per second to check a single boolean flag.

Issues:

  1. Continuous wakeup prevents deep sleep states (critical for battery-powered devices)
  2. Adds scheduler pressure in resource-constrained embedded systems
  3. Goroutine stack (~2KB minimum) permanently allocated

Recommendation:
Use channel-based signaling for event notification:

type msc struct {
    // ... existing fields ...
    taskChan chan struct{}
}

func (m *msc) processTasks() {
    for range m.taskChan {
        cmd := m.cbw.SCSICmd()
        // ... process task ...
        m.queuedBytes = 0
        m.taskQueued = false
        machine.AckUsbOutTransfer(usb.MSC_ENDPOINT_OUT)
    }
}

// In rxHandler, send signal: select { case m.taskChan <- struct{}{}: default: }

@xushiwei xushiwei merged commit cbbf358 into goplus:main Nov 28, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants