HCI Command Credits: Prevent a Host from Overrunning Its Bluetooth Controller
Keep command-send allowance separate from the lifecycle of the operation a command starts. Command Status can permit another HCI command while the first procedure remains pending. Conversely, a later procedure-completion event does not automatically grant command credit.
Applicability: Bluetooth Core 6.1 HCI command flow control, with Linux v6.12 source used as an explicitly narrower implementation example. The replay below describes a proposed protocol-level host model, not Linux’s exact scheduling behavior. No controller commands or reset operation were executed.

1. Maintain two ledgers
The first ledger records how many ordinary HCI commands may currently be sent. The second records each submitted command’s expected response and any subsequent procedure event. Store controller generation, opcode, relevant connection or configuration context, submission time and terminal outcome.
Under Core command flow control, startup and reset initially permit at most one outstanding command until Command Complete or Command Status is received. Thereafter Num_HCI_Command_Packets reports the current allowance. Treat it as a reported quantity, not an increment to add to a leftover count.
Consume one allowance when an ordinary command is committed to the transport. Serialize that decision with queue removal so two producers cannot both spend the same slot. Define the transport ownership boundary carefully: a failed local enqueue and an uncertain partial transmission require different recovery.
Bound the pending queue even when allowance is zero. Credit flow control protects the controller; it does not prevent host memory exhaustion if callers can queue unlimited commands.
2. Classify the response pattern per opcode
Some commands finish with Command Complete. Others first return Command Status, then a command-specific completion event. A successful Status means the command is pending; a failed Status means it did not start and no later completion is expected for that attempt.
Record the pattern from the exact command specification. Do not infer it from whether an API function sounds synchronous. A host wrapper may block until initial acceptance while the radio procedure continues afterward.
Match responses by opcode and the documented procedure context rather than blindly completing the oldest queued operation. Core permits commands to complete out of their start order. Avoid concurrent ambiguous instances of the same opcode unless the command and implementation provide sufficient correlation.
Opcode 0x0000 in Command Complete or Command Status is a flow-control update without an associated host command. It must update allowance without consuming an application transaction or reporting a fictitious operation result.
3. Replay an allowance change while a procedure runs
Use the following hypothetical trace after normal initialization has established allowance two. Command A uses the Status-plus-later-event pattern. Command B uses Command Complete. Their concrete opcodes must be chosen from supported independent commands in the actual harness.
| Trace step | Allowance afterward | Operation state |
|---|---|---|
| Send A | 1 | A awaiting initial response |
| A Status success, reported allowance 2 | 2, not 3 | A procedure pending |
| Send B | 1 | A pending; B awaiting completion |
| B Complete, reported allowance 0 | 0 | B terminal; A still pending |
| A’s procedure event arrives | 0 | A terminal; no credit field here |
| Opcode-zero allowance update reports 1 | 1 | No operation completed |
The crucial assertions are that A’s accepted procedure does not block every unrelated command, the first allowance update does not add two to the remaining one, and A’s terminal event does not invent credit while the controller has advertised zero.
Also replay failed Status with nonzero allowance. The operation fails, but the allowance still follows the event’s flow-control field. Error handling must not accidentally discard the permission update or wait forever for a completion that will not arrive.
4. Inspect what the shipping host actually implements
Linux v6.12 deliberately does not expose the full illustrative multi-command window in the inspected path. Its handle_cmd_cnt_and_timer helper, outside reset handling, sets cmd_cnt to one when ncmd is nonzero. It also maintains separate response and zero-credit timer behavior.
The matching command worker and send helper gate transmission on cmd_cnt and decrement it after successful frame submission. This is an implementation policy within the protocol allowance, not evidence that every controller reports only one slot.
When debugging a trace, distinguish controller-advertised capacity from the host’s chosen concurrency. A queue that sends one command at a time may be working as designed. Do not patch a kernel counter to a larger value without reviewing response correlation, request ownership and transport behavior.
5. Time out evidence, not merely a caller
Use separate deadlines for initial HCI response and the command-specific procedure. Base the first on the selected transport and queueing behavior, and the second on the actual procedure contract. A single universal timeout obscures whether command delivery or peer interaction stalled.
If an initial response is missing, do not manufacture credit because an application timer expired. The controller may have accepted the command. Mark the result uncertain and invoke the owning host’s supported recovery path.
A caller cancellation similarly does not retract bytes already sent or make a non-idempotent operation safe to repeat. Where a protocol cancel command exists, treat it as its own command with its own response and race handling. Otherwise preserve the uncertain outcome until resolved or the controller lifecycle is reset.
Do not place the special Host Number Of Completed Packets command behind ordinary command credits: Core excludes it from command flow control. Keep that documented exception explicit rather than creating a blanket rule that every HCI command consumes the same allowance.
6. Reset the ledger at a verified lifecycle boundary
On supported recovery, stop ordinary dispatch, retire old pending requests and establish the transport/controller reset boundary before reopening the queue. Reset the bootstrap allowance under the protocol rule rather than carrying a previous large count into the new controller instance.
An application generation tag helps reject already queued host callbacks. It cannot identify arbitrary stale bytes on the wire, because ordinary HCI command responses do not carry that tag. The transport drain and reset sequence must make the new event stream trustworthy.
Proposed acceptance cases include delayed responses, zero-credit pauses, opcode-zero updates, failed Status, out-of-order procedure endings, malformed events and reset with pending work. Retain before/after allowance and operation state for every step.
Pass only when every send has valid allowance and every operation has a separately justified outcome. Command capacity and procedure completion are related observations, but neither can safely stand in for the other.