Making a very fast Gamepad with the Teensy 4.0 with one big problem.

SylvesterFPS

New member
I am using Teensyduino version 1.62 with an XInputHID interface that I extended in usb_desc.c. Basically, everything works, but I'm encountering confusion regarding latency for an 8000Hz gamepad. When I test just my raw button latency using an external device, I get an average latency of around 108µs, which is correct based on the descriptor. However, if I activate both analog sticks, rotate them quickly in circles, and measure the button latency again, I get a significantly higher button latency. How can this problem be solved without sacrificing speed? I only send my data when the state changes, but with analog sticks, that becomes difficult since there are more than just two states. I don't know how to solve this programmatically—deadzone, averaging, and lower read resolution are not options.
Screenshot 2026-08-01 145251.png
Screenshot 2026-08-01 145101.png
 

Attachments

  • Gamepad.ino
    11.4 KB · Views: 30
  • usb.c
    34.4 KB · Views: 10
  • usb_desc.c
    171.8 KB · Views: 16
  • usb_desc.h
    44.4 KB · Views: 35
  • usb_inst.cpp
    2.6 KB · Views: 11
  • usb_serial.h
    18.3 KB · Views: 11
  • usb_xinputhid.c
    4.4 KB · Views: 11
  • usb_xinputhid.h
    4.3 KB · Views: 35
  • WProgram.h
    5.2 KB · Views: 13
  • boards.txt
    95.7 KB · Views: 12
You are aware that mechanical chatter (bouncing) in switches far exceeds 100us, aren't you? Typically mechanical switch bounce time is >1ms.

Expecting 100us latency from mechanical switch is unreasonable.

Having said that, the code that you provided seems to send analog reads as fast at it can, without any delays and without even detecting if there was any change to read values. This alone may cause flood of messages that your receiver (computer) might have trouble handling, resulting in increased latency. Suggested approach is:
a) add at least 100us delay inside while(true) loop, so it doesn't send data more often than your desired latency (limiting the rate to 10000 updates per second max).
b) store the value that you read in previous loop turn and send only values that actually change.

But then again, I don't think that expecting game controller to send updates at 8kHz rate is reasonable. No game will be able to react that fast. You would need to have game frame rate of 1000FPS to be able to use 1ms resolution, and what you are trying is 8x faster than that.
 
Last edited:
You are aware that mechanical chatter (bouncing) in switches far exceeds 100us, aren't you? Typically mechanical switch bounce time is >1ms.

Expecting 100us latency from mechanical switch is unreasonable.

Having said that, the code that you provided seems to send analog reads as fast at it can, without any delays and without even detecting if there was any change to read values. This alone may cause flood of messages that your receiver (computer) might have trouble handling, resulting in increased latency. Suggested approach is:
a) add at least 100us delay inside while(true) loop, so it doesn't send data more often than your desired latency (limiting the rate to 10000 updates per second max).
b) store the value that you read in previous loop turn and send only values that actually change.

But then again, I don't think that expecting game controller to send updates at 8kHz rate is reasonable. No game will be able to react that fast. You would need to have game frame rate of 1000FPS to be able to use 1ms resolution, and what you are trying is 8x faster than that.
Thanks for the feedback, but I had actually already tried both options you suggested, and in both cases, the latency was higher. Second, those tests did not use mechanical buttons; they measured the digital latency from the Teensy pin to the USB output. I wouldn't be asking if it weren't possible to reduce the latency further. Button latency isn't the problem either; rather, it's when the analog sticks output rapid value changes, thus clogging the USB transfer. If I add a delay, I end up delaying the button latency as well. I've been working on this for a long time, the solution isn't that simple, and there definitely is a way to do it.
 
Don't use delays, instead use the ARM_DWT_CYCCNT to keep track of the last time you sent the updates, and postpone updates of the same axis/button until enough time has passed.

Essentially, for each button and axis you have uint32_t sent_cyccnt cycle counter timestamp and sent and pending state values. When sent == pending, no update is needed. When they differ, you compare sent_cyccnt to the current cycle counter, and only send the HID report (and set sent = pending) if enough time has passed. At 600 MHz, 1 ms = 600,000 cycles. You could set the minimum cycle interval per-button/axis, but I suppose something like 1ms is very good.

(The additional trick is that when (uint32_t)(ARM_DWT_CYCCNT - sent_cyccnt) >= 1800000000U, it's been 3 seconds since that event, and you can reset the sent cycle counter to say one second ago, sent_cyccnt = ARM_DWT_CYCCNT - 600000000U, so that you don't need to worry about the cycle counter wrapping around. Instead of handling the wraparound, you advance the old timestamps so that they are still "long ago in the past", but not so long ago the wraparound might happen.)

This means that a change that happens "long" after any previous changes, are reported without delay. However, subsequent changes will be reported at the set interval, say once per millisecond. Thus, no added latency, just limiting update interval; good.

Use loops to read the pins/ADCs to the pending states first, then a separate loop to construct the HID event report. For each axis/button, check if the pending and sent states match. If they do, you check for the 3-second wraparound above, and only update the sent cycle counter if needed but don't do other changes. If the two differ, you check how many cycles since the last HID update, and if enough cycles have passed, you include the pending value in the HID update, set the last update cycle counter timestamp for this axis/button to match the current cycle counter (at the start of the loop), and set sent = pending. Only do the HID update if there were at least one axis/button in it. Simples!

I use a closely related approach — dead time after initial report — for software button debouncing without latency, and am very happy with it.
 
Last edited:
Don't use delays, instead use the ARM_DWT_CYCCNT to keep track of the last time you sent the updates, and postpone updates of the same axis/button until enough time has passed.

Essentially, for each button and axis you have uint32_t sent_cyccnt cycle counter timestamp and sent and pending state values. When sent == pending, no update is needed. When they differ, you compare sent_cyccnt to the current cycle counter, and only send the HID report (and set sent = pending) if enough time has passed. At 600 MHz, 1 ms = 600,000 cycles. You could set the minimum cycle interval per-button/axis, but I suppose something like 1ms is very good.

(The additional trick is that when (uint32_t)(ARM_DWT_CYCCNT - sent_cyccnt) >= 1800000000U, it's been 3 seconds since that event, and you can reset the sent cycle counter to say one second ago, sent_cyccnt = ARM_DWT_CYCCNT - 600000000U, so that you don't need to worry about the cycle counter wrapping around. Instead of handling the wraparound, you advance the old timestamps so that they are still "long ago in the past", but not so long ago the wraparound might happen.)

This means that a change that happens "long" after any previous changes, are reported without delay. However, subsequent changes will be reported at the set interval, say once per millisecond. Thus, no added latency, just limiting update interval; good.

Use loops to read the pins/ADCs to the pending states first, then a separate loop to construct the HID event report. For each axis/button, check if the pending and sent states match. If they do, you check for the 3-second wraparound above, and only update the sent cycle counter if needed but don't do other changes. If the two differ, you check how many cycles since the last HID update, and if enough cycles have passed, you include the pending value in the HID update, set the last update cycle counter timestamp for this axis/button to match the current cycle counter (at the start of the loop), and set sent = pending. Only do the HID update if there were at least one axis/button in it. Simples!

I use a closely related approach — dead time after initial report — for software button debouncing without latency, and am very happy with it.
Thank you for your help! I had already tried a similar approach, and by enabling the analog sticks, I was able to significantly reduce the latency
Screenshot 2026-08-07 105719.png
 
Back
Top