Improving HID throughput consistency on Pico W

33 views
Skip to first unread message

Mitch Cairns

unread,
Aug 9, 2025, 11:49:31 PMAug 9
to btstack-dev
I’m working on a Bluetooth Classic HID device on the Raspberry Pi Pico W (RP2040 + CYW43), using BTstack from the Pico SDK. I’m trying to achieve a consistent reporting interval for gamepad input.

At a 4 ms target: ~95% of reports arrive on time, ~5% deviate (up to 20 ms delay)
At 2 ms target: ~60% on time, ~40% deviate (same jitter pattern)

Reports are only sent when BTstack’s HID_SUBEVENT_CAN_SEND_NOW fires (no busy-looping or direct sending without first having this fired).

I understand the model where the callback event can be fired to indicate when the RP2040 is allowed to fire another input report, however it would be nice to have some sort of flag, for unreliable packets especially to just 'send' a packet without a need to worry about its status etc. This seems to add unwanted latency for these sorts of use-cases, where we are simply interested in sending a constant stream of data at a fixed interval. 

Is there some way that this is already implemented that I'm missing? 

Thanks for your time!

Matthias Ringwald

unread,
Aug 10, 2025, 8:50:30 AMAug 10
to btsta...@googlegroups.com
Hi Mitch

First a quick overview: the Bluetooth Controller (in your case the CYW43439) has a number of ACL buffers and tries to send them on an active HCI connection as fast as possible.
On the Pico W, there's an intermediate buffer between RP2040 and CY43 - which isn't relevant for this discussion.
If you call hid_device_request_can_send_now_event, BTstack will emit HID_SUBEVENT_CAN_SEND_NOW ASAP, if the outgoing buffer / the intermediate buffer is free AND if there are free ACL buffers on the CYW43.
You cannot ignore either of them, as it would cause data loss (overwriting outgoing buffer) or trigger the Controller to abort (buffer overrun).

Back to your task: if you want to have a consistent reporting interval, I'd suggest to use a timer, e.g. every 5 ms, and call hid_device_request_can_send_now_event + send the current HID report.
If you call hid_device_request_can_send_now_event more often, this will only lead to HID reports queue up in the Bluetooth Controller.

A while ago, we've also looked into this with the guys from the open-source Alpakka game controller. You can have a look at branch develop-alpakka, which uses the "send hid report every 5 ms",
and the interval was quite consistent, but still had outliers. As far as I can tell, there's nothing we can do about it as that's deep in the CYW43xxx (a rather old Controller) firmware.

There are tricks that could be done if you fully control the Link Layer, e.g. on a nRF5x with Zephyr's implementation, e.g. sending one HID report per LE Connection Interval, but updating the HID report just before the packets goes over the air, which
would give you a consistent 7.5 ms interval with < 1ms latency for that packet resp, roughly 8 ms total latency, while some HID Reports might get dropped.

With LE Isochronous Connections, you could use a 5 ms ISO Interval with low retransmission, so no tricks are needed to either have the data transmitted in 5 ms or have it dropped.
The Bluetooth SIG was working on a HID Low Latency profile which uses LE Isochronous Connections as far as I know, but I don't know if it's complete, or if any OS implements it yet.

Cheers
Matthias
> --
> You received this message because you are subscribed to the Google Groups "btstack-dev" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to btstack-dev...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/btstack-dev/5ae36f15-7f9d-4394-be8b-253022f01286n%40googlegroups.com.

Mitch Cairns

unread,
Aug 11, 2025, 2:38:30 AMAug 11
to btstack-dev
Matthias,

Thank you for those suggestions and pointers. I tried to find that branch but it doesn't seem to exist anymore. If you have a link I would very much appreciate it.

I have now attempted to put together a basic example with a btstack timer that re-starts itself, and calls the hid 'can send' request at a 4ms interval. This only seems to result in reporting at an 8ms interval. I've tried constructing/sending the report both immediately after the 'can send' request, as well as constructing/sending the report in the 'can send' callback.

Is there something I'm missing with this? This was something I was butting into earlier as well, where I had to basically send reports at a 2ms interval to get an effective 4ms interval. 

Mitch Cairns

unread,
Aug 11, 2025, 2:39:05 AMAug 11
to btstack-dev
I should also mention, for this use-case BTLE is off the table and I'm only focusing on using BT Classic for some device compatibility reasons. 
Reply all
Reply to author
Forward
0 new messages