Skip to content

Bluetooth: Controller: Replace prepare pipeline with ordered list#79444

Open
cvinayak wants to merge 1 commit intozephyrproject-rtos:mainfrom
cvinayak:github_bt_ctlr_pipeline_fix
Open

Bluetooth: Controller: Replace prepare pipeline with ordered list#79444
cvinayak wants to merge 1 commit intozephyrproject-rtos:mainfrom
cvinayak:github_bt_ctlr_pipeline_fix

Conversation

@cvinayak
Copy link
Contributor

@cvinayak cvinayak commented Oct 5, 2024

Replace prepare pipeline with ordered linked list.

@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch 5 times, most recently from d3fd40c to d377960 Compare October 7, 2024 13:52
@cvinayak cvinayak closed this Oct 13, 2024
@cvinayak cvinayak reopened this Aug 9, 2025
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch 2 times, most recently from 01d6254 to 5224961 Compare August 9, 2025 06:11
@cvinayak cvinayak closed this Aug 21, 2025
@cvinayak cvinayak reopened this Sep 19, 2025
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch from 5224961 to d5aea11 Compare September 19, 2025 19:51
@cvinayak cvinayak closed this Sep 20, 2025
@cvinayak cvinayak reopened this Oct 17, 2025
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch 2 times, most recently from 37f9ac4 to 7ba61e4 Compare October 18, 2025 05:58
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch 3 times, most recently from 1f03b00 to 3308317 Compare October 22, 2025 16:17
@cvinayak
Copy link
Contributor Author

I can’t post comments to GitHub from here. Please paste the following into a PR comment:

data:
- url: "https://github.com/zephyrproject-rtos/zephyr/pull/79444"
  state: "open"
  draft: true
  title: "Bluetooth: Controller: Replace prepare pipeline with ordered list"
  number: 79444
  created_at: "05 October 2024"
  closed_at: ""
  merged_at: ""
  labels:
  author: "cvinayak"
  comments: 2
  assignees_avatar_urls:
- url: "https://github.com/zephyrproject-rtos/zephyr/pull/79444"
  state: "open"
  draft: true
  title: "Bluetooth: Controller: Replace prepare pipeline with ordered list"
  number: 79444
  created_at: "05 October 2024"
  closed_at: ""
  merged_at: ""
  labels:
  author: "cvinayak"
  comments: 2
  assignees_avatar_urls:

Summary of what changed

  • Replaced the MFIFO-based prepare pipeline with an explicitly ordered singly linked list in subsys/bluetooth/controller/ll_sw/ull.c.
    • New pipeline structure: pool-backed nodes storing a next pointer plus struct lll_event.
    • Enqueue is now order-aware by ticks_at_expire and treats resume events specially.
    • Dequeue/iteration now uses void** as an iterator cursor (NULL means start/end), replacing uint8_t indices.
  • API updates:
    • ull_prepare_dequeue_iter signature changed from uint8_t* to void** (see subsys/bluetooth/controller/ll_sw/lll.h). All call sites in Nordic and OpenISA LLL updated accordingly.
  • Nordic LLL changes (subsys/bluetooth/controller/ll_sw/nordic/lll/lll.c):
    • prepare_dequeue_iter_ready_get now filters out aborted, resume, and defer entries.
    • lll_disable, lll_prepare_resolve, and preempt reworked for the new iterator. Notably, the prior workaround that aborted earlier prepares to make room for “short” prepares has been removed; ordering is handled by the list.
  • OpenISA LLL changes (subsys/bluetooth/controller/ll_sw/openisa/lll/lll.c):
    • Equivalent iterator and control-flow updates to match the new pipeline semantics.
  • Scan updates:
    • lll_scan.h adds a persistent lll_prepare_param field when advanced scheduler is enabled.
    • lll_scan.c and lll_scan_aux.c copy timing fields into lll->prepare_param and pass that to mayfly, avoiding lifetime issues from passing a stack/local param.
  • Initialization:
    • The pipeline memory is initialized in init_reset via mem_init; MFIFO prep init removed.

Why this is a good change

  • Correctness and determinism: The prepare pipeline is now ordered by scheduling time instead of FIFO plus ad-hoc aborts. This removes the previous “abort to reorder” workaround and should reduce spurious aborts and reschedules, especially for short preemptors.
  • Simpler preemption logic: Preempt handling is cleaner; instead of scanning/aborting earlier entries to accommodate a short event, the ordered structure ensures correct sequencing.
  • Safer parameter passing in scan paths: Copying timing/param state into lll->prepare_param before scheduling mayfly avoids dangling references to transient stack data.
  • Clearer iteration semantics: Using a pointer cursor for ull_prepare_dequeue_iter is a better fit for list traversal than synthetic indices, reducing off-by-one/overflow risks.

Things to double-check

  • Concurrency/atomicity: MFIFO provided a well-tested queue; the new list relies on mem_acquire/mem_release and pointer manipulation. Ensure all accesses occur under the same context/locking assumptions as before and that no ISR/LLL context can mutate the list concurrently with ULL.
  • Ordering conditions: The insertion logic relies on ticker_ticks_diff_get and MSBIT checks. These are notoriously subtle with wrap-around; unit/soak tests around boundary conditions (near wrap and equal times) would be valuable.
  • Resume event handling: The enqueue code treats resume specially (often appended). The dequeue path enqueues at most two non-resume events before switching behavior; verify this preserves prior fairness/progress guarantees and doesn’t starve resumes or normal events under load.
  • Memory footprint: Each pipeline node adds a pointer over the previous structure. The increase is small but present; confirm this fits tight controller builds.
  • API ripple: All call sites appear updated for the new iterator type in Nordic and OpenISA; ensure there are no out-of-tree or less commonly built targets relying on the old signature.

Overall assessment

  • This is a solid improvement. It addresses a known limitation of the FIFO pipeline, removes complex workarounds, and makes scheduling behavior more predictable. With careful validation around concurrency and time-diff edge cases, it looks like a positive and maintainable change.
@zephyrbot zephyrbot requested review from Tronil and wopu-ot October 22, 2025 17:00
@cvinayak cvinayak added the Enhancement Changes/Updates/Additions to existing features label Oct 27, 2025
@cvinayak cvinayak added this to the v4.4.0 milestone Oct 27, 2025
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch from 3308317 to ed1da0f Compare October 28, 2025 12:18
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch from ed1da0f to 8c40860 Compare November 19, 2025 05:13
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR replaces the MFIFO (multi-producer, multi-consumer FIFO) prepare pipeline with an ordered linked list implementation in the Bluetooth controller. The new implementation maintains events sorted by their ticks_at_expire value, eliminating the need for O(n) searches through the pipeline.

Key Changes:

  • Replaced MFIFO-based prepare pipeline with a custom ordered linked list structure
  • Updated iterator functions to use void ** instead of uint8_t indices for traversal
  • Simplified preemption logic in nordic/lll/lll.c by removing complex short-prepare search code

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
subsys/bluetooth/controller/ll_sw/ull.c Implements new ordered linked list structure, enqueue/dequeue logic with time-based ordering, and memory management for pipeline events
subsys/bluetooth/controller/ll_sw/openisa/lll/lll.c Updates iterator usage from uint8_t index to void* pointer and adds list termination checks
subsys/bluetooth/controller/ll_sw/nordic/lll/lll.c Updates iterator usage, simplifies preemption logic by removing short-prepare search loops, and converts do-while to while loop in prepare_dequeue_iter_ready_get
subsys/bluetooth/controller/ll_sw/lll.h Updates function signature for ull_prepare_dequeue_iter to accept void** instead of uint8_t*

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

/* Initialize prepare pipeline list */
mem_init(pipeline.mem.pool, EVENT_PIPELINE_EVENT_SIZE,
sizeof(pipeline.mem.pool) / EVENT_PIPELINE_EVENT_SIZE,
&pipeline.mem.free);
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing initialization of pipeline.head and pipeline.tail to NULL. These pointers should be explicitly initialized to NULL during reset to ensure proper operation of the linked list, especially since ull_prepare_enqueue checks if (pipeline.tail) at line 2153.

Suggested change
&pipeline.mem.free);
&pipeline.mem.free);
pipeline.head = NULL;
pipeline.tail = NULL;
Copilot uses AI. Check for mistakes.
Comment on lines +2204 to +2206
if (!*next) {
pipeline.tail = next;
}
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with the insertion logic when placing a new event at the head of the list. At line 2204, the check if (!*next) is intended to handle the case where the list becomes empty after moving to head, but this condition can never be true here. The *next was just set to pipeline.head at line 2174, and if pipeline.head was NULL, the outer condition at line 2153 (if (pipeline.tail)) would have been false. Consider removing this unreachable condition or documenting why it exists.

Suggested change
if (!*next) {
pipeline.tail = next;
}
Copilot uses AI. Check for mistakes.
struct lll_event *next;
static memq_link_t link;
static struct mayfly mfy = {0, 0, &link, NULL, lll_resume};
uint8_t enqueued_loop = 2U;
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable enqueued_loop is initialized to 2U but its purpose and why it's set to 2 specifically is unclear. This magic number should be documented with a comment explaining why 2 iterations are allowed before setting is_enqueued flag, or defined as a named constant.

Copilot uses AI. Check for mistakes.
/* Trigger the LLL resume */
mfy.param = e;
ret = mayfly_enqueue(caller_id, TICKER_USER_ID_LLL, 0, &mfy);
LL_ASSERT(!ret);
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent assertion macro usage: LL_ASSERT is used here (line 2344) while LL_ASSERT_ERR is used elsewhere in the same function (line 2306). Consider using the same assertion macro consistently throughout the function, or document why different macros are needed in different locations.

Suggested change
LL_ASSERT(!ret);
LL_ASSERT_ERR(!ret);
Copilot uses AI. Check for mistakes.
}
/* Start the preempt timeout for ready event */
ret = preempt_ticker_start(ready, NULL, ready);
LL_ASSERT((ret == TICKER_STATUS_SUCCESS) ||
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent assertion macro usage: LL_ASSERT is used here (line 1232) while LL_ASSERT_ERR was used in the previous code (visible in the diff context). Consider using the same assertion macro consistently, or document why different macros are needed.

Suggested change
LL_ASSERT((ret == TICKER_STATUS_SUCCESS) ||
LL_ASSERT_ERR((ret == TICKER_STATUS_SUCCESS) ||
Copilot uses AI. Check for mistakes.
return MFIFO_DEQUEUE_GET(prep);
void *e;

/* peak lll_event */
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "peak" should be "peek". The function is inspecting/peeking at the head element, not finding a peak/maximum.

Copilot uses AI. Check for mistakes.
return NULL;
}

/* Peak lll_event */
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "Peak" should be "Peek". The function is inspecting/peeking at the event, not finding a peak/maximum.

Copilot uses AI. Check for mistakes.
/* Trigger the LLL resume */
mfy.param = e;
ret = mayfly_enqueue(caller_id, TICKER_USER_ID_LLL, 0, &mfy);
LL_ASSERT(!ret);
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent assertion macro usage: LL_ASSERT is used here (line 2375) while LL_ASSERT_ERR is used at line 2306. Consider using the same assertion macro consistently throughout the function, or document why different macros are needed in different locations.

Suggested change
LL_ASSERT(!ret);
LL_ASSERT_ERR(!ret);
Copilot uses AI. Check for mistakes.
preempt_abort_resume:
/* Abort any duplicate non-resume, that they get dequeued */
iter_idx = UINT8_MAX;
/* Abort any duplicates so that they get dequeued */
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Comment text update has a minor issue: "Abort any duplicate non-resume" (old) was changed to "Abort any duplicates" (new), but the old comment was grammatically incomplete. The new comment is better but could be more precise: "Abort any duplicate events so that they get dequeued" would be clearer.

Suggested change
/* Abort any duplicates so that they get dequeued */
/* Abort any duplicate events so that they get dequeued */
Copilot uses AI. Check for mistakes.
Comment on lines +2184 to +2186
if (!e_curr->is_aborted && !e_curr->is_resume) {
prev = curr;
}
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug in list insertion ordering: The prev pointer is only updated for non-aborted, non-resume events (lines 2184-2186), but the list should maintain all nodes including aborted ones until they are explicitly dequeued. If the list contains aborted events followed by a valid event that should come after the new event (e.g., [aborted1] -> [aborted2] -> [valid_event]), the loop will skip updating prev while traversing aborted events, leaving prev as NULL. When the loop exits upon finding valid_event, prev is still NULL, causing the new event to be incorrectly inserted at the head (lines 2202-2203) instead of before valid_event. The fix is to unconditionally update prev = curr before advancing curr at line 2188, regardless of the event's state.

Suggested change
if (!e_curr->is_aborted && !e_curr->is_resume) {
prev = curr;
}
prev = curr;
Copilot uses AI. Check for mistakes.
@github-actions
Copy link

This pull request has been marked as stale because it has been open (more than) 60 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 14 days. Note, that you can always re-open a closed pull request at any time.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* through the prepare pipeline.
*/
idx = UINT8_MAX;
idx = NULL;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside the iter_idx traversal loop, the code resets idx = NULL after abort_cb() to restart iteration, but the iterator variable being used is iter_idx. As written, the loop won't actually restart from the head after the pipeline is modified. This looks like it should reset iter_idx instead of idx.

Suggested change
idx = NULL;
iter_idx = NULL;
Copilot uses AI. Check for mistakes.
Comment on lines +349 to +355
#define EVENT_PIPELINE_EVENT_SIZE (sizeof(struct lll_event) + sizeof(void *))

static struct {
struct {
void *free;
uint8_t pool[EVENT_PIPELINE_MAX * EVENT_PIPELINE_EVENT_SIZE];
} mem;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EVENT_PIPELINE_EVENT_SIZE and the pipeline node layout don't account for the mem_* allocator's reserved header (next pointer + free-count stored at MROUND(sizeof(void *)), see util/mem.c). With the current layout (lll_event starts at + sizeof(void *)), mem_release() will overwrite the first bytes of struct lll_event, corrupting event contents if the block is released while still referenced (e.g., passed to mayfly). Consider placing struct lll_event after MEM_FREE_RESERVED_SIZE (and aligning as needed) and updating EVENT_PIPELINE_EVENT_SIZE/offset calculations accordingly.

Copilot uses AI. Check for mistakes.
return NULL;
}

e = (void *)((uint8_t *)next + sizeof(void *));
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e is derived as next + sizeof(void *), but the mem_* pool uses the bytes immediately after the next pointer to store the free-count (mem_release() writes it at MROUND(sizeof(void *))). If this block is released back to the pool before the consumer is done, e->prepare_param will be corrupted. Adjust the start offset of lll_event to be after the allocator's reserved header (e.g., MEM_FREE_RESERVED_SIZE, possibly rounded up for alignment).

Suggested change
e = (void *)((uint8_t *)next + sizeof(void *));
e = (void *)((uint8_t *)next + MEM_FREE_RESERVED_SIZE);
Copilot uses AI. Check for mistakes.
Comment on lines +2433 to +2447
/* Trigger the LLL resume */
mfy.param = e;
ret = mayfly_enqueue(caller_id, TICKER_USER_ID_LLL, 0, &mfy);
LL_ASSERT_ERR(!ret);

/* Pipeline could be modified, restart at head to remove all aborted
* prepare.
*/
e_prev = NULL;
e_curr = pipeline.head;
}

/* Free event memory */
mem_release(head, &pipeline.mem.free);

Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ull_prepare_dequeue() passes e to mayfly_enqueue() (which may defer execution when caller/callee priorities are not equal), but then immediately mem_release(head, ...) frees the backing storage. Since mem_release() overwrites the freed block's header (and free-count), lll_resume can observe corrupted struct lll_event data if the mayfly call is not executed inline. Keep the event storage valid until lll_resume has run (e.g., release in/after lll_resume, or only release when mayfly_enqueue executed inline / assert on inline-only usage).

Copilot uses AI. Check for mistakes.
Comment on lines +2476 to +2477
/* Release the memory to pool */
mem_release(head, &pipeline.mem.free);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same lifetime issue as above for resume prepares: the resume lll_event is passed to mayfly_enqueue() and then immediately mem_release(head, ...) frees/overwrites the memory. If the mayfly call is deferred, lll_resume may dereference invalid/corrupted data. Defer releasing the node until after lll_resume executes.

Suggested change
/* Release the memory to pool */
mem_release(head, &pipeline.mem.free);
/* Note: Do not release the resume event memory here.
* The node must remain valid until after lll_resume has executed.
* It will be released as part of the resume/done handling.
*/
Copilot uses AI. Check for mistakes.
Comment on lines +2315 to +2316
/* peak lll_event */
if (pipeline.head) {
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: peak lll_event should be peek lll_event.

Copilot uses AI. Check for mistakes.
Comment on lines +2339 to +2340
/* Peak lll_event */
e = (uint8_t *)*idx + sizeof(void *);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: Peak lll_event should be Peek lll_event.

Copilot uses AI. Check for mistakes.
@github-actions github-actions bot removed the Stale label Jan 26, 2026
Copilot AI added a commit to cvinayak/zephyr that referenced this pull request Jan 26, 2026
…os#79444

Co-authored-by: cvinayak <6350656+cvinayak@users.noreply.github.com>
Copilot AI added a commit to cvinayak/zephyr that referenced this pull request Jan 26, 2026
…ct-rtos#79444

Co-authored-by: cvinayak <6350656+cvinayak@users.noreply.github.com>
cvinayak added a commit to cvinayak/zephyr that referenced this pull request Jan 27, 2026
…os#79444

Co-authored-by: cvinayak <6350656+cvinayak@users.noreply.github.com>
cvinayak added a commit to cvinayak/zephyr that referenced this pull request Jan 27, 2026
…ct-rtos#79444

Co-authored-by: cvinayak <6350656+cvinayak@users.noreply.github.com>
Replace prepare pipeline with ordered linked list.

Signed-off-by: Vinayak Kariappa Chettimada <vich@nordicsemi.no>
@cvinayak cvinayak force-pushed the github_bt_ctlr_pipeline_fix branch from 92fb0f9 to 997a1e0 Compare February 14, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: Bluetooth Controller area: Bluetooth Enhancement Changes/Updates/Additions to existing features

3 participants