USB HID device on ESP32 has a problem!
Port, board and/or hardware
ESP32S3
MicroPython version
I created an issue on USB-LIB but I think the problem must be solved here...
https://github.com/micropython/micropython-lib/issues/1044
Reproduction
The code for reproducing the problem has been tried on MicroPython 1.26.1 and also on the new 1.27 releases.
It is not ESP board related. I tried the examples on different boards with different MicroPython versions.
I can also share Wireshark logs if necessary, but the bug is easily reproducible with the unmodified USB HID device samples from the lib directory.
Expected behaviour
The samples should "just work" also for the ESP32S3 boards.
Observed behaviour
The LED out reports are arriving nicely in the code, but all keystrokes that are sent are arriving with all bytes "zero" at the host. (Confirmed by Wireshark )
Additional Information
See the debug code changes documented in the original lib-issue!
Code of Conduct
Yes, I agree
esp32/boards: Configure S2/S3 PSRAM boards to use USB in IRQ mode.
Summary
With MicroPython 1.26.0 and 1.26.1 on ESP32-S3 with PSRAM, machine.USBDevice is broken. Eg making a custom keyboard, keypresses sent to the host arrive as a packet of all zeros.
The root cause is commit https://github.com/espressif/esp-usb/commit/1b185586aad00e8eb99b15de8d16d604850e3070 in the espressif__esp_tinyusb managed component, which switched USB to use DMA by default. But DMA cannot read from PSRAM. So HID (and other USB) packets that live in PSRAM end up as all zeros when sent to the host.
To fix that, force USB to use IRQ mode when building with PSRAM enabled.
Fixes issue https://github.com/micropython/micropython-lib/issues/1044.
Testing
Tested with ESP32_GENERIC_S3 firmware, on a UM TinyS3, and the following code:
import time
import usb.device
from usb.device.keyboard import KeyboardInterface, KeyCode
kb = KeyboardInterface()
usb.device.get().init(kb, builtin_driver=True)
while not kb.is_open():
time.sleep(0.1)
for _ in range(10):
print("send key")
kb.send_keys([KeyCode.M])
time.sleep(0.15)
kb.send_keys([])
time.sleep(2)
Without the fix in this PR, that code doesn't send any keys. With the fix, it does.
Trade-offs and Alternatives
I'm not sure if this is technically a bug/regression in the IDF, because they did provide an easy option to disable DMA. So this seems the best way to workaround the issue for us.
Alternatively we could allocate a temporary buffer in internal SRAM, and copy the PSRAM buffer into that before passing it to the DMA engine. But it's hard to know when the DMA has finished with the buffer so it can be reused for the next transfer.