Skip to content

Commit 9e2288b

Browse files
committed
esp32c3: implement USBDevice using interrupts for more proper operation.
This improves the ESP32-C3 USBDevice implementation by using interrupts to properly handle data RX/TX. It also stubs out USB interfaces for HID functions, since those cannot be implemented on ESP32-C3 due to using a JTAG-USB interface. Signed-off-by: deadprogram <ron@hybridgroup.com>
1 parent 6424b09 commit 9e2288b

File tree

2 files changed

+242
-1
lines changed

2 files changed

+242
-1
lines changed

src/machine/machine_esp32c3_usb.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
//go:build esp32c3
2+
3+
package machine
4+
5+
import (
6+
"device/esp"
7+
"errors"
8+
"machine/usb"
9+
"machine/usb/descriptor"
10+
"runtime/interrupt"
11+
)
12+
13+
// USB Serial/JTAG Controller
14+
// See esp32-c3_technical_reference_manual_en.pdf pg. 736
15+
//
16+
// The ESP32-C3 has a built-in USB Serial/JTAG controller that provides a
17+
// CDC-ACM serial port. The USB protocol and enumeration are handled entirely
18+
// in hardware; software only reads/writes the EP1 FIFO.
19+
20+
const cpuInterruptFromUSB = 8
21+
22+
// flushTimeout is the maximum number of busy-wait iterations in flush().
23+
// Prevents hanging when no USB host is connected.
24+
const flushTimeout = 200000
25+
26+
type USB_DEVICE struct {
27+
Bus *esp.USB_DEVICE_Type
28+
Buffer *RingBuffer
29+
}
30+
31+
var (
32+
_USBCDC = &USB_DEVICE{
33+
Bus: esp.USB_DEVICE,
34+
Buffer: NewRingBuffer(),
35+
}
36+
37+
USBCDC Serialer = _USBCDC
38+
)
39+
40+
var (
41+
errUSBWrongSize = errors.New("USB: invalid write size")
42+
errUSBCouldNotWriteAllData = errors.New("USB: could not write all data")
43+
)
44+
45+
type Serialer interface {
46+
WriteByte(c byte) error
47+
Write(data []byte) (n int, err error)
48+
Configure(config UARTConfig) error
49+
Buffered() int
50+
ReadByte() (byte, error)
51+
DTR() bool
52+
RTS() bool
53+
}
54+
55+
var usbConfigured bool
56+
57+
// USBDevice provides a stub USB device for the ESP32-C3. The hardware
58+
// only supports a fixed-function CDC-ACM serial port, so the programmable
59+
// USB device features are no-ops.
60+
type USBDevice struct {
61+
initcomplete bool
62+
InitEndpointComplete bool
63+
}
64+
65+
var USBDev = &USBDevice{}
66+
67+
func (dev *USBDevice) SetStallEPIn(ep uint32) {}
68+
func (dev *USBDevice) SetStallEPOut(ep uint32) {}
69+
func (dev *USBDevice) ClearStallEPIn(ep uint32) {}
70+
func (dev *USBDevice) ClearStallEPOut(ep uint32) {}
71+
72+
// initUSB is intentionally empty — the interp phase evaluates init()
73+
// functions at compile time and cannot access hardware registers.
74+
// Actual hardware setup is deferred to the first Configure() call.
75+
func initUSB() {}
76+
77+
// Configure initialises the USB Serial/JTAG controller clock, pads, and
78+
// interrupt so that received data is buffered automatically.
79+
func (usbdev *USB_DEVICE) Configure(config UARTConfig) error {
80+
if usbConfigured {
81+
return nil
82+
}
83+
usbConfigured = true
84+
85+
// Enable the USB_DEVICE peripheral clock.
86+
// Do NOT reset the peripheral — the ROM bootloader has already
87+
// configured the USB Serial/JTAG controller and the host may
88+
// already be connected. Resetting would drop the USB link.
89+
esp.SYSTEM.SetPERIP_CLK_EN0_USB_DEVICE_CLK_EN(1)
90+
esp.SYSTEM.SetPERIP_RST_EN0_USB_DEVICE_RST(0)
91+
92+
// Ensure internal PHY is selected and USB pads are enabled.
93+
usbdev.Bus.SetCONF0_PHY_SEL(0)
94+
usbdev.Bus.SetCONF0_USB_PAD_ENABLE(1)
95+
usbdev.Bus.SetCONF0_DP_PULLUP(1)
96+
97+
// Clear any pending interrupts.
98+
usbdev.Bus.INT_CLR.Set(0xFFFFFFFF)
99+
100+
// Enable the RX-packet-received interrupt.
101+
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)
102+
103+
// Map the USB peripheral interrupt to CPU interrupt cpuInterruptFromUSB.
104+
esp.INTERRUPT_CORE0.SetUSB_INTR_MAP(cpuInterruptFromUSB)
105+
106+
_ = interrupt.New(cpuInterruptFromUSB, func(interrupt.Interrupt) {
107+
_USBCDC.handleInterrupt()
108+
}).Enable()
109+
110+
return nil
111+
}
112+
113+
// ensureConfigured triggers lazy initialization on first use.
114+
func (usbdev *USB_DEVICE) ensureConfigured() {
115+
if !usbConfigured {
116+
usbdev.Configure(UARTConfig{})
117+
}
118+
}
119+
120+
// handleInterrupt drains the hardware RX FIFO into the software ring buffer.
121+
func (usbdev *USB_DEVICE) handleInterrupt() {
122+
// Read INT_ST while INT_ENA is still set (INT_ST = INT_RAW & INT_ENA).
123+
intStatus := usbdev.Bus.INT_ST.Get()
124+
125+
// Disable the RX interrupt to prevent re-triggering while we drain.
126+
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0)
127+
128+
if intStatus&esp.USB_DEVICE_INT_ST_SERIAL_OUT_RECV_PKT_INT_ST != 0 {
129+
// Drain all available bytes from the EP1 OUT FIFO.
130+
// Use EP1.Get() directly — the generated GetEP1_RDWR_BYTE is
131+
// functionally identical, but a direct load makes the FIFO-pop
132+
// intent explicit.
133+
for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
134+
b := byte(usbdev.Bus.EP1.Get())
135+
usbdev.Buffer.Put(b)
136+
}
137+
// Clear the interrupt.
138+
usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1)
139+
}
140+
141+
// Re-enable the RX interrupt.
142+
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)
143+
}
144+
145+
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
146+
usbdev.ensureConfigured()
147+
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
148+
// FIFO full — try flushing first, then recheck.
149+
usbdev.flush()
150+
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
151+
return errUSBCouldNotWriteAllData
152+
}
153+
}
154+
155+
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
156+
// does a read-modify-write — the read side-effect pops a byte from
157+
// the RX FIFO.
158+
usbdev.Bus.EP1.Set(uint32(c))
159+
usbdev.flush()
160+
161+
return nil
162+
}
163+
164+
func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
165+
usbdev.ensureConfigured()
166+
if len(data) == 0 {
167+
return 0, nil
168+
}
169+
170+
for i, c := range data {
171+
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
172+
if i > 0 {
173+
usbdev.flush()
174+
}
175+
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
176+
return i, errUSBCouldNotWriteAllData
177+
}
178+
}
179+
usbdev.Bus.EP1.Set(uint32(c))
180+
}
181+
182+
usbdev.flush()
183+
return len(data), nil
184+
}
185+
186+
// Buffered returns the number of bytes waiting in the receive ring buffer.
187+
func (usbdev *USB_DEVICE) Buffered() int {
188+
usbdev.ensureConfigured()
189+
return int(usbdev.Buffer.Used())
190+
}
191+
192+
// ReadByte returns a byte from the receive ring buffer.
193+
func (usbdev *USB_DEVICE) ReadByte() (byte, error) {
194+
b, ok := usbdev.Buffer.Get()
195+
if !ok {
196+
return 0, nil
197+
}
198+
return b, nil
199+
}
200+
201+
func (usbdev *USB_DEVICE) DTR() bool {
202+
return false
203+
}
204+
205+
func (usbdev *USB_DEVICE) RTS() bool {
206+
return false
207+
}
208+
209+
// flush signals WR_DONE and waits (with timeout) for the hardware to
210+
// consume the data. A timeout prevents hanging when no USB host is present.
211+
func (usbdev *USB_DEVICE) flush() {
212+
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
213+
for i := 0; i < flushTimeout; i++ {
214+
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 {
215+
return
216+
}
217+
}
218+
}
219+
220+
// The ESP32-C3 USB Serial/JTAG controller is fixed-function hardware.
221+
// It only provides a CDC-ACM serial port; the USB protocol and endpoint
222+
// configuration are handled entirely in silicon. The functions below
223+
// are no-op stubs so that higher-level USB packages (HID, MIDI, …)
224+
// compile, but they cannot add real endpoints on this hardware.
225+
226+
// ConfigureUSBEndpoint is a no-op on ESP32-C3 — the hardware does not
227+
// support programmable USB endpoints.
228+
func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) {
229+
}
230+
231+
// SendZlp is a no-op on ESP32-C3 — the hardware handles control
232+
// transfers internally.
233+
func SendZlp() {
234+
}
235+
236+
// SendUSBInPacket is a no-op on ESP32-C3 — the hardware does not
237+
// support arbitrary IN endpoints. Returns false to indicate the
238+
// packet was not sent.
239+
func SendUSBInPacket(ep uint32, data []byte) bool {
240+
return false
241+
}

src/machine/machine_esp32xx_usb.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build esp32s3 || esp32c3
1+
//go:build esp32s3
22

33
package machine
44

0 commit comments

Comments
 (0)