Skip to content

fix(led-strip): migrate dmaInit() to ownership-checked dmaAllocate() - #1394

Merged
nerdCopter merged 2 commits into
emuflight:masterfrom
nerdCopter:fix/dmainit-ledstrip-migration
Aug 18, 2026
Merged

fix(led-strip): migrate dmaInit() to ownership-checked dmaAllocate()#1394
nerdCopter merged 2 commits into
emuflight:masterfrom
nerdCopter:fix/dmainit-ledstrip-migration

Conversation

@nerdCopter

@nerdCopter nerdCopter commented Aug 17, 2026

Copy link
Copy Markdown
Member

AI Generated pull-request

Summary

Stage 3 of 5 in IT#1363's fleet-wide dmaInit()dmaAllocate() migration (stage 1, ADC, merged as PR#1389; stage 2, transponder, merged as PR#1393). Converts ws2811LedStripHardwareInit() in both light_ws2811strip_stdperiph.c and light_ws2811strip_hal.c from unchecked dmaInit() to ownership-checked dmaAllocate() + dmaEnable().

Why this is safe

  • ws2811LedStripHardwareInit() is void in EF (BF's equivalent returns bool, checked by its one caller) — but EF already gates the DMA-enable path on a file-scope ws2811Initialised flag, set true only at the end of a fully successful init and checked by ws2811LedStripDMAEnable() before it touches any DMA register. An early return on allocation failure happens before that assignment, so the existing flag closes the same reachability gap BF closes via its bool return — no signature change or caller update needed.
  • Both variants already had a dmaRef == NULL early-return before this change; the dmaAllocate() check was combined into that same early-return, matching BF 4.5-maintenance's stm32/light_ws2811strip_{hal,stdperiph}.c structure exactly (allocate check first, before any GPIO/RCC/timer/DMA-clock setup).
  • No coupling risk of the kind found in PR#1389's H7 ADC fix: everything after the DMA claim is this one function's own timer/DMA setup, not a separate subsystem.
  • dmaInit() never fails (unconditional overwrite), so this claim cannot currently collide with anything on any existing target — boot-time, single call site per variant, defense-in-depth/BF-parity change, not a fix for an active bug.

Review history

Local coderabbit review --agent (round 1) found one real issue: in the stdperiph variant, TIM_Cmd(timer, ENABLE) ran before the dmaAllocate() check, so a claim failure would leave the timer running with the output pin driving the LED data line. Fixed in db2c44d0c by moving the dmaAllocate() call to combine with the existing dmaRef == NULL early-return, before any hardware setup, in both variants (matching BF's ordering exactly). Local coderabbit review --agent (round 2) and an opencode second-opinion pass both came back clean — opencode additionally confirmed no DMA register access occurs between the allocation check and dmaEnable() in either variant, and that the change's only owner-leak-on-later-failure characteristic is a pre-existing pattern (no dmaRelease() API exists in EF or BF), not introduced here.

Verification

  • Build: 7/7 targets, no warnings — HELIOSPRING (F4), FOXEERF722V4 (F7), STELLARH7DEV (H7) (all 3 real aircraft compile USE_LED_STRIP via the common_fc_pre.h fleet default), plus CRAZYBEEF4FS (F4 stdperiph), MATEKF722HD (F7 HAL), SPRACINGH7EF (H7 HAL, the only H7 target with USE_LED_STRIP enabled), plus SITL.
  • Host unit tests: make test — 33/33 test binaries pass (incl. test_ledstrip_unittest, test_ws2811_unittest), unchanged from the PR#1393 baseline.
  • No physical LED strip available for end-to-end verification. DMA-claim behavior itself is checkable via the dma/resource CLI on real hardware without a strip attached, but only after enabling feature LED_STRIP + save + reboot on the target — compiling USE_LED_STRIP in alone does not trigger the DMA-claiming ws2811LedStripHardwareInit() call (fc_init.c only calls it via ledStripEnable(), gated on the feature flag). Not yet performed — build-only this session.

Related

Summary by CodeRabbit

  • Bug Fixes
    • Improved WS2811 LED-strip DMA channel setup during hardware initialization.
    • Added handling for DMA allocation failures to prevent incomplete initialization.
    • Improved DMA activation reliability for LED-strip output.

light_ws2811strip_stdperiph.c and light_ws2811strip_hal.c called
dmaInit(), which silently overwrites any existing DMA owner. Replace
with dmaAllocate() + dmaEnable(), matching the pattern already used
by ADC and transponder (PR#1389, PR#1393).

Both driver variants already gate ws2811LedStripDMAEnable() on a
file-scope ws2811Initialised flag, set true only at the end of a
fully successful ws2811LedStripHardwareInit(). The new early return
on dmaAllocate() failure happens before that assignment, so no
signature change or caller update is needed.
Local CodeRabbit review flagged: TIM_Cmd(timer, ENABLE) in the
stdperiph variant ran before the dmaAllocate() check, so a claim
failure left the timer running with the output pin driving the LED
data line. The HAL variant had the same ordering risk relative to
its GPIO/timer-init calls, though its PWM start was already gated
after allocation.

Moved the dmaAllocate() call to combine with the existing dmaRef-NULL
early-return, before any GPIO/RCC/timer/DMA-clock setup, matching BF
4.5-maintenance's stm32/light_ws2811strip_{hal,stdperiph}.c structure
exactly. A claim failure now leaves no hardware touched at all.
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e2858118-9d39-498e-8c84-19ea4f011585

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37b7771f-83f1-4a18-9346-43c28d55f31e

📥 Commits

Reviewing files that changed from the base of the PR and between d80ff20 and db2c44d.

📒 Files selected for processing (2)
  • src/main/drivers/light_ws2811strip_hal.c
  • src/main/drivers/light_ws2811strip_stdperiph.c

Included review availability: Your plan includes up to 4 reviews per rolling hour; 3 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Codacy Static Code Analysis
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-15T14:49:12.754Z
Learnt from: nerdCopter
Repo: emuflight/EmuFlight PR: 1189
File: src/main/drivers/bus_spi.c:631-631
Timestamp: 2026-05-15T14:49:12.754Z
Learning: In the EmuFlight repo (emuflight/EmuFlight), the STM32F303 (F303) targets currently do not compile on the master branch due to a pre-existing hardware RAM budget overflow (unrelated to any specific PR). When reviewing driver changes in shared driver files (e.g., src/main/drivers/bus_spi.c), avoid adding or fixing F303-specific driver behavior as part of the current PR; instead, defer those F303-targeted fixes to a dedicated F3-targeted PR and only revisit once the RAM overflow/build issue is resolved.

Applied to files:

  • src/main/drivers/light_ws2811strip_stdperiph.c
  • src/main/drivers/light_ws2811strip_hal.c
🔇 Additional comments (2)
src/main/drivers/light_ws2811strip_hal.c (1)

56-56: LGTM!

Also applies to: 102-102

src/main/drivers/light_ws2811strip_stdperiph.c (1)

65-65: LGTM!

Also applies to: 109-109


📝 Walkthrough

Walkthrough

WS2811 hardware initialization now allocates the DMA channel and stops when allocation fails. Output setup enables the allocated channel with dmaEnable instead of calling dmaInit.

Changes

WS2811 DMA handling

Layer / File(s) Summary
DMA allocation and activation
src/main/drivers/light_ws2811strip_hal.c, src/main/drivers/light_ws2811strip_stdperiph.c
Both drivers validate DMA allocation during hardware initialization. Both drivers enable the allocated DMA channel during output setup instead of reinitializing it.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Merge Risk: ⚪ Minimal · up to db2c4

This localized change replaces unchecked DMA initialization with ownership-checked allocation for LED-strip support in two implementations. Reported builds and unit tests pass, and no actionable merge-blocking risk remains beyond normal checks.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the DMA ownership migration in the LED-strip code.
Description check ✅ Passed The description clearly explains the scope, safety rationale, review history, related issue, and verification results, including known test limitations.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nerdCopter

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nerdCopter
nerdCopter merged commit a4ca6ad into emuflight:master Aug 18, 2026
18 checks passed
@nerdCopter

Copy link
Copy Markdown
Member Author

@coderabbitai, AI generated comment

Requesting analysis on the DMA-claim ordering specifically (this PR is merged; asking as an independent second check, not a re-review).

light_ws2811strip_stdperiph.c and light_ws2811strip_hal.c: dmaAllocate() was moved to combine with the pre-existing dmaRef == NULL early-return at the top of ws2811LedStripHardwareInit(), before any GPIO/RCC/timer/DMA-clock setup in either file.

Please analyze:

  1. Is there any GPIO, RCC, or timer register access remaining in either file's ws2811LedStripHardwareInit() before the dmaAllocate() check succeeds?
  2. ws2811LedStripDMAEnable() gates on the file-scope ws2811Initialised flag (set true only at the very end of a successful init) -- confirm no path exists where ws2811Initialised could be true while the DMA claim actually failed, in either driver variant.
  3. Does the final placement match BF 4.5-maintenance's stm32/light_ws2811strip_{stdperiph,hal}.c structure exactly, not just in the presence of the check but in its position relative to every other setup step?

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

std_file="$(fd -t f '^light_ws2811strip_stdperiph\.c$' . | head -n1)"
hal_file="$(fd -t f '^light_ws2811strip_hal\.c$' . | head -n1)"

printf '%s\n' "=== Local file paths ==="
printf '%s\n' "$std_file" "$hal_file"

for file in "$std_file" "$hal_file"; do
  printf '\n=== Outline: %s ===\n' "$file"
  ast-grep outline "$file" --items all --match 'ws2811LedStripHardwareInit|ws2811LedStripDMAEnable|ws2811Initialised' || true

  printf '\n=== Relevant definitions and state writes: %s ===\n' "$file"
  rg -n -C 8 \
    'ws2811LedStripHardwareInit\s*\(|ws2811LedStripDMAEnable\s*\(|\bws2811Initialised\b|dmaAllocate\s*\(|dmaEnable\s*\(' \
    "$file"
done

mkdir -p /tmp/bf45
for name in light_ws2811strip_stdperiph.c light_ws2811strip_hal.c; do
  gh api \
    -H 'Accept: application/vnd.github.raw+json' \
    "repos/betaflight/betaflight/contents/src/main/drivers/${name}?ref=4.5-maintenance" \
    > "/tmp/bf45/${name}"
done

printf '\n=== Upstream function order and state writes ===\n'
for file in /tmp/bf45/*.c; do
  printf '\n--- %s ---\n' "$file"
  rg -n -C 8 \
    'ws2811LedStripHardwareInit\s*\(|ws2811LedStripDMAEnable\s*\(|\bws2811Initialised\b|dmaAllocate\s*\(|dmaEnable\s*\(' \
    "$file"
done

printf '\n=== Diff: local vs Betaflight 4.5-maintenance ===\n'
diff -u /tmp/bf45/light_ws2811strip_stdperiph.c "$std_file" || true
diff -u /tmp/bf45/light_ws2811strip_hal.c "$hal_file" || true

Length of output: 8481


🏁 Script executed:

#!/bin/bash
set -euo pipefail

std_file="$(fd -t f '^light_ws2811strip_stdperiph\.c$' . | head -n1)"
hal_file="$(fd -t f '^light_ws2811strip_hal\.c$' . | head -n1)"

printf '%s\n' '=== Complete local initialization functions ==='
for file in "$std_file" "$hal_file"; do
  printf '\n--- %s ---\n' "$file"
  sed -n '/void ws2811LedStripHardwareInit(/,/^}/p' "$file"
  sed -n '/void ws2811LedStripDMAEnable(/,/^}/p' "$file"
  printf '\nState occurrences:\n'
  rg -n -C 4 '\bws2811Initialised\b' "$file"
done

printf '%s\n' '\n=== Initialization callers and timer lookup definition ==='
rg -n -C 5 \
  '\bws2811LedStripHardwareInit\s*\(|\btimerGetByTag\s*\(' \
  src/main -g '*.[ch]' || true

printf '%s\n' '\n=== Candidate repositories and 4.5-maintenance refs ==='
for repo in emuflight/EmuFlight betaflight/betaflight; do
  printf '\n--- %s branches matching maintenance ---\n' "$repo"
  gh api --paginate "repos/${repo}/branches?per_page=100" \
    --jq '.[] | select(.name | test("4[._-]5|maintenance"; "i")) | .name' || true
done

printf '%s\n' '\n=== Locate both upstream files for candidate refs ==='
for repo_ref in \
  'emuflight/EmuFlight 4.5-maintenance' \
  'betaflight/betaflight 4.5-maintenance' \
  'betaflight/betaflight maintenance/4.5' \
  'betaflight/betaflight 4.5.0'; do
  repo="${repo_ref% *}"
  ref="${repo_ref##* }"
  for path in \
    src/main/drivers/light_ws2811strip_stdperiph.c \
    src/main/drivers/light_ws2811strip_hal.c \
    src/main/drivers/stm32/light_ws2811strip_stdperiph.c \
    src/main/drivers/stm32/light_ws2811strip_hal.c; do
    status="$(gh api -i "repos/${repo}/contents/${path}?ref=${ref}" 2>/dev/null | head -n1 || true)"
    if printf '%s' "$status" | grep -q '200'; then
      printf 'FOUND repo=%s ref=%s path=%s\n' "$repo" "$ref" "$path"
    fi
  done
done

Length of output: 28640


🏁 Script executed:

#!/bin/bash
set -euo pipefail

std_file="$(fd -t f '^light_ws2811strip_stdperiph\.c$' . | head -n1)"
hal_file="$(fd -t f '^light_ws2811strip_hal\.c$' . | head -n1)"
mkdir -p /tmp/bf45

for name in light_ws2811strip_stdperiph.c light_ws2811strip_hal.c; do
  gh api \
    -H 'Accept: application/vnd.github.raw+json' \
    "repos/betaflight/betaflight/contents/src/main/drivers/stm32/${name}?ref=4.5-maintenance" \
    > "/tmp/bf45/${name}"
done

printf '%s\n' '=== Upstream exact initialization functions ==='
for file in /tmp/bf45/*.c; do
  printf '\n--- %s ---\n' "$file"
  sed -n '/void ws2811LedStripHardwareInit(/,/^}/p' "$file"
  sed -n '/void ws2811LedStripDMAEnable(/,/^}/p' "$file"
done

printf '%s\n' '\n=== Normalized local vs upstream function diffs ==='
python3 - "$std_file" "$hal_file" <<'PY'
import re
import sys
from pathlib import Path

def function(text, name):
    start = re.search(rf'\bvoid\s+{re.escape(name)}\s*\([^)]*\)\s*\{{', text)
    if not start:
        raise SystemExit(f"Function {name} not found")
    i = start.end()
    level = 1
    while level and i < len(text):
        if text[i] == '{':
            level += 1
        elif text[i] == '}':
            level -= 1
        i += 1
    return text[start.start():i]

def normalize(text):
    text = re.sub(r'/\*.*?\*/', '', text, flags=re.S)
    text = re.sub(r'//.*', '', text)
    return '\n'.join(line.rstrip() for line in text.splitlines() if line.strip())

for local_path in map(Path, sys.argv[1:]):
    upstream_path = Path('/tmp/bf45') / local_path.name
    print(f'\n--- {local_path.name} ---')
    for name in ('ws2811LedStripHardwareInit', 'ws2811LedStripDMAEnable'):
        local = normalize(function(local_path.read_text(), name))
        upstream = normalize(function(upstream_path.read_text(), name))
        print(f'{name}: {"IDENTICAL" if local == upstream else "DIFFERENT"}')
        if local != upstream:
            import difflib
            print('\n'.join(difflib.unified_diff(
                upstream.splitlines(), local.splitlines(),
                fromfile='BF-4.5', tofile='EmuFlight', lineterm=''
            )))
PY

printf '%s\n' '\n=== dmaAllocate implementation and ownership behavior ==='
rg -n -C 12 '\bdmaAllocate\s*\(' src/main -g '*.[ch]'

printf '%s\n' '\n=== All LED-strip initialization call sites ==='
rg -n -C 6 '\bws2811LedStripInit\s*\(|\bws2811LedStripHardwareInit\s*\(' . \
  -g '*.[ch]' -g '!**/build/**' -g '!**/dist/**'

Length of output: 3679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

std_file="$(fd -t f '^light_ws2811strip_stdperiph\.c$' . | head -n1)"
hal_file="$(fd -t f '^light_ws2811strip_hal\.c$' . | head -n1)"

printf '%s\n' '=== Upstream symbols and DMA allocation contexts ==='
for file in /tmp/bf45/light_ws2811strip_stdperiph.c /tmp/bf45/light_ws2811strip_hal.c; do
  printf '\n--- %s: outline ---\n' "$file"
  ast-grep outline "$file" --items all || true
  printf '\n--- %s: dmaAllocate and setup context ---\n' "$file"
  rg -n -C 35 '\bdmaAllocate\s*\(|\bdmaInit\s*\(|\bws2811Initialised\b|\bIOInit\s*\(|\bRCC_ClockCmd\s*\(|__DMA1_CLK_ENABLE\s*\(|\bHAL_TIM_PWM_Init\s*\(' "$file" || true
done

printf '%s\n' '\n=== Local DMA allocation contexts ==='
for file in "$std_file" "$hal_file"; do
  printf '\n--- %s ---\n' "$file"
  rg -n -C 35 '\bdmaAllocate\s*\(|\bws2811Initialised\b|\bIOInit\s*\(|\bRCC_ClockCmd\s*\(|__DMA1_CLK_ENABLE\s*\(|\bHAL_TIM_PWM_Init\s*\(' "$file"
done

printf '%s\n' '\n=== dmaAllocate implementation and descriptor state ==='
rg -n -C 20 'bool\s+dmaAllocate\s*\(|dmaAllocate\s*\(' src/main/drivers -g '*.[ch]' || true

printf '%s\n' '\n=== All local LED-strip init entry-point call sites ==='
rg -n -C 8 '\bws2811LedStripInit\s*\(|\bws2811LedStripHardwareInit\s*\(' . \
  -g '*.[ch]' -g '!**/build/**' -g '!**/dist/**' || true

printf '%s\n' '\n=== Full timerGetByTag implementation ==='
timer_common="$(fd -t f '^timer_common\.c$' src/main | head -n1)"
sed -n '/const timerHardware_t \*timerGetByTag(/,/^}/p' "$timer_common"

Length of output: 85557


@nerdCopter, I completed the independent check.

  1. No GPIO, RCC, timer-register, or DMA-register access occurs before the DMA claim succeeds.

    • In src/main/drivers/light_ws2811strip_stdperiph.c, Line 63 calls timerGetByTag(), and Line 64 stores timerHardware->tim. timerGetByTag() only searches the timerHardware[] table. Line 65 checks dmaRef and calls dmaAllocate(). GPIO setup starts at Line 68. Timer RCC access starts at Line 71. Timer register access starts at Line 73.
    • In src/main/drivers/light_ws2811strip_hal.c, Lines 53–55 do the same lookup and local field assignments. Line 56 checks dmaRef and calls dmaAllocate(). Timer HAL setup starts at Line 59. GPIO setup starts at Line 74. DMA clock enable starts at Line 79.

    The pre-claim work only reads timer metadata and assigns local variables.

  2. For an initial initialization attempt, the gate is correct in both variants.

    ws2811Initialised starts as false. Each file only sets it to true at the final statement of a successful ws2811LedStripHardwareInit() call. A failed dmaAllocate() returns before that assignment. Therefore, a first-time DMA claim failure leaves ws2811Initialised == false, and ws2811LedStripDMAEnable() does not access DMA.

    There is one narrow repeat-initialization detail. Neither driver resets ws2811Initialised at function entry. Also, dmaAllocate() returns false when a descriptor already has an owner, including OWNER_LED_STRIP. Therefore, if initialization succeeded once and the function is called again, the second dmaAllocate() call fails while ws2811Initialised remains true.

    This does not create a state where DMA is unclaimed. The original successful LED-strip claim remains active. It only means that “the current allocation call failed” is not equivalent to “the LED-strip DMA resource is not owned.” The shown production call path invokes ws2811LedStripHardwareInit() through ws2811LedStripInit() from ledStripEnable().

  3. The DMA claim placement matches BF 4.5-maintenance relative to hardware setup, but not exactly in source structure.

    In BF 4.5-maintenance, the files are under src/main/drivers/stm32/. Both variants:

    1. validate the tag;
    2. allocate or resolve the timer;
    3. resolve the DMA resource;
    4. call dmaAllocate();
    5. only then configure GPIO, RCC, timer, DMA clock, DMA, and PWM.

    The local files preserve the important placement: the DMA claim is before every hardware setup operation.

    The structure is not byte-for-byte or function-for-function identical. BF uses bool ws2811LedStripHardwareInit() and timerAllocate(). The local files use void ws2811LedStripHardwareInit() and timerGetByTag(). BF also resolves DMA through dmaGetChannelSpecByTimer() when applicable. These differences occur before the claim and do not move any hardware setup before it.

    Conclusion: The PR correctly prevents hardware setup on an initial DMA ownership conflict. The only qualification is the existing repeat-call behavior, where the flag can remain true after a second dmaAllocate() failure because the first successful claim remains owned.

You are interacting with an AI system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant