Skip to content

fix: prevent USB CDC TX stall on ring buffer wrap#5538

Open
nobonobo wants to merge 3 commits into
tinygo-org:devfrom
nobonobo:fix-cdc-large-packet
Open

fix: prevent USB CDC TX stall on ring buffer wrap#5538
nobonobo wants to merge 3 commits into
tinygo-org:devfrom
nobonobo:fix-cdc-large-packet

Conversation

@nobonobo

Copy link
Copy Markdown
Contributor

Fix USB CDC TX stall when TX ring buffer wraps

Fixes #5537

This patch fixes an occasional USB CDC TX stall when the TX ring buffer wraps.

When the ring buffer wrapped, the TX path could send a short packet before the remaining data.

Example:

Before:

7-byte packet
64-byte packet

Expected:

64-byte packet
7-byte packet

A short packet indicates the end of a USB bulk transfer. Although the packet sequence is valid at the USB protocol level, it can cause interoperability issues with some USB CDC host implementations.

The root cause was that the TX path only used the first contiguous segment returned by the ring buffer. When the buffer wrapped, the first segment could be smaller than the endpoint packet size, causing an unintended short packet.

This patch combines wrapped ring buffer segments to fill the USB endpoint packet size whenever possible.

The zero-copy path is preserved for contiguous data:

Ring buffer
|
v
USB DPRAM

Only wrapped data requires a temporary buffer:

Ring buffer
|
v
Temporary buffer
|
v
USB DPRAM

After this change, the TX stall could no longer be reproduced.

@deadprogram

Copy link
Copy Markdown
Member

Please note test failure:

--- FAIL: TestBinarySize (0.00s)
    --- FAIL: TestBinarySize/wioterminal/examples/pininterrupt (38.87s)
        sizes_test.go:66: Unexpected code size when compiling: -target=wioterminal examples/pininterrupt
        sizes_test.go:67:             code rodata   data    bss
        sizes_test.go:68: expected:   8013   1663    132   7488
        sizes_test.go:69: actual:     8095   1661    132   7488
FAIL
FAIL	github.com/tinygo-org/tinygo/builder	54.907s

@soypat

soypat commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

I'd suggest we reduce code size and simplify instead of adding more complexity on top of an already needlessly complex implementation, but oh well, such are the PRs we merge in tinygo

@nobonobo

nobonobo commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Ideally, we should read 64 bytes at a time from the ring buffer and write them directly to _usbDPSRAM.
However, it may be better to handle the optimization as a separate issue.

@nobonobo

nobonobo commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

テストの失敗にご注意ください:

--- FAIL: TestBinarySize (0.00s)
    --- FAIL: TestBinarySize/wioterminal/examples/pininterrupt (38.87s)
        sizes_test.go:66: Unexpected code size when compiling: -target=wioterminal examples/pininterrupt
        sizes_test.go:67:             code rodata   data    bss
        sizes_test.go:68: expected:   8013   1663    132   7488
        sizes_test.go:69: actual:     8095   1661    132   7488
FAIL
FAIL	github.com/tinygo-org/tinygo/builder	54.907s

The test in TestBinarySize seems to fail whenever there is any change in the code size.
Would it be okay if I updated this test?

@nobonobo

nobonobo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

code to reproduce the issue

server.go:

package main

import (
	"strings"
	"time"

	"machine"
)

func main() {
	for !machine.Serial.DTR() {
		time.Sleep(100 * time.Millisecond)
	}
	tick := time.NewTicker(100 * time.Microsecond)
	for range tick.C {
		println(strings.Repeat("*", 259))
	}
}

tinygo flash -target pico ./server.go

client.go:

package main

import (
	"bufio"

	"go.bug.st/serial"
)

func main() {
	port, err := serial.Open("COM3", &serial.Mode{})
	if err != nil {
		panic(err)
	}
	scanner := bufio.NewScanner(port)
	cnt := 0
	for scanner.Scan() {
		text := scanner.Text()
		if cnt > 0 && len(text) != 259 {
			println()
			panic("wrong length: " + text)
		} else {
			print("\rcount:", cnt)
		}
		cnt++
	}
}
$ go run ./client.go
count:164524
panic: wrong length: ********************************************************************************************************************************************************************************************************************************************************

goroutine 1 [running]:
main.main()
        C:/Users/nobo/Dropbox/Projects/diy-projects/tinygo-issue/client.go:22 +0x1a5
exit status 2
$ go run ./client.go
count:58939
panic: wrong length: ***********************************************************************************************************************************************************************************************************************************************************************************

goroutine 1 [running]:
main.main()
        C:/Users/nobo/Dropbox/Projects/diy-projects/tinygo-issue/client.go:22 +0x1a5
exit status 2

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.

USB CDC TX stall when ring buffer wraps

3 participants