Skip to content

IR: TIM1 + DMA engine, external IR module and progress bar - #67

Open
bey0ne wants to merge 6 commits into
i12bp8:mainfrom
bey0ne:ir-dma-module-progress
Open

bey0ne wants to merge 6 commits into
i12bp8:mainfrom
bey0ne:ir-dma-module-progress

Conversation

@bey0ne

@bey0ne bey0ne commented Oct 27, 2026 •

Copy link
Copy Markdown

IR: TIM1 + DMA engine, external IR module and progress bar

1. Play PP4 frames with TIM1 + DMA instead of a busy loop

Problem

tagtinker_ir_transmit() bit-bangs each frame with a DWT->CYCCNT busy loop
inside FURI_CRITICAL_ENTER/EXIT. The comment says the critical section lasts
1-2 ms, but one PP4 symbol takes ~191 us on average (40 us burst + 151 us mean
gap), i.e. ~0.77 ms per byte:

Frame Interrupts masked
~30-byte data ~23 ms
96 bytes (max) ~74 ms

While masked, USB, BLE, the GUI and the expansion UART stall, and the TX thread
spins at the highest priority between frames.

Change

The stock furi_hal_infrared async TX cannot be reused: it caps the carrier at
INFRARED_MAX_FREQUENCY (1 MHz) and ESLs need ~1.25 MHz. So this PR drives TIM1
directly with the same technique the firmware uses:

  • TIM1 counts carrier periods (64 MHz / 51 = 1.255 MHz, unchanged).
  • The repetition counter makes TIM1 raise one update event per segment.
  • On each update, DMA2 ch1 writes the next output mode to CCMR2
    (PWM2 = carrier on, forced inactive = off) and DMA2 ch2 writes the next
    segment length to RCR.
  • The TX thread sleeps on a semaphore until the DMA transfer-complete IRQ.

Files:

  • ir/tagtinker_ir_seq.[ch] (new, pure C): builds the DMA tables from a frame.
    No Flipper headers, so it is unit-testable on a PC.
  • ir/tagtinker_ir.c: hardware driver. Public API unchanged, no caller changes.
  • tests/test_ir_seq.c + make test + a tests CI job (ASan + UBSan):
    replays the tables through a model of TIM1 + DMA and compares the waveform
    with the timing of the old driver (2000 random frames + edge cases).
  • README.md / CONTRIBUTING.md: document make test.
  • build_and_flash.bat: small Windows helper (ufbt update, build,
    ufbt launch) for contributors without make. Happy to drop it or move it
    to a separate PR if you prefer.

Behaviour notes

  • Each segment is rounded to whole carrier periods: every edge is within
    0.4 us of the old timing, every symbol (burst + gap) within 0.8 us. Bursts
    are 40.6 us instead of 40.3 us. PP4 is pulse-position coded, so this is far
    below the ~60 us spacing between symbol values.
  • Max frame length is now 96 bytes (TAGTINKER_MAX_FRAME_SIZE) instead of 255.
    All callers already use frames of at most 96 bytes.
  • tagtinker_ir_init() refuses to start if furi_hal_infrared_is_busy().
  • tagtinker_ir_stop() only signals; the TX thread halts the hardware itself.
  • tagtinker_ir_is_busy() was declared but never defined; it is now.
  • Uses ~2.3 KB of heap for the DMA tables while the transmit scene is open.

Tested

  • make test passes (host, ASan + UBSan); the test fails if two gap constants
    are swapped.
  • Cross-compiled for Cortex-M4 with -Wall -Wextra -Werror against the
    firmware headers; all external symbols are exported in api_symbols.csv.
  • On hardware: Flipper Zero, firmware mntm 12 (API 88.2), Pricer F14
    (type 1327, 208x112 red):
    • LED test
    • Text
    • Image, page 1
    • Back during a long send stops cleanly
    • USB stays connected during a send (CLI log streamed while sending)
    • Measured frame duration = computed duration within ~1.5 us

2. External IR module on GPIO A7 (#39, #45, #52)

Problem

The built-in IR LED has a short range; several users get partial or no
image updates (#39, #45, #52).

Change

Two settings, like the stock Infrared app:

  • IR Output: Internal / GPIO A7 / Auto (detects a module when sending);
  • 5V on GPIO: powers modules that take 5V from pin 1.

The driver uses TIM1 CH1N on PA7 instead of CH3N on PB9. Auto uses
furi_hal_infrared_detect_tx_output() and, like the stock app, powers a
detected module. The 5V rail is only switched on if it was off and is
restored afterwards. The transmit screen says "Point the IR module at tag".
Old settings files are still read.

Tested

Flipper Zero, firmware mntm 12, external IR module:
Internal / GPIO A7 / Auto, settings kept after restart, stock Infrared app
fine afterwards, image sent to a Pricer F14 through the module.

3. Progress bar while sending images (#45)

Problem

An image send takes up to ~30 s with only three animated dots (#45).

Change

Image and multi-frame sends draw a bar with a percentage. Each payload
covers a share of the send and each data frame moves the bar. Single
commands and Repeat mode keep the dots. This needs the DMA engine: the old
driver blocked the CPU during frames.

Tested

Flipper Zero, firmware mntm 12: bar from 0 to 100 %, Back during a
send stops cleanly and a new send works.

bey0ne and others added 6 commits October 27, 2026 14:02
The old driver bit-banged every frame with interrupts masked for up to
~74 ms (96-byte frame). TIM1 now counts carrier periods, its repetition
counter raises one update per segment, and DMA2 ch1/ch2 feed CCMR2 and RCR
on each update. The TX thread sleeps until DMA transfer-complete.

- ir/tagtinker_ir_seq.[ch]: pure-C DMA table builder (host testable)
- ir/tagtinker_ir.c: TIM1 + DMA driver, same public API
- tests/test_ir_seq.c, make test, CI job with ASan/UBSan
- README/CONTRIBUTING: document make test
- build_and_flash.bat: Windows helper (ufbt update, build, launch)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x
Settings gain "IR Output" (Internal / GPIO A7 / Auto) and "5V on GPIO",
mirroring the stock Infrared app. The DMA driver drives TIM1 CH1N on PA7
(CCMR1) instead of CH3N on PB9 (CCMR2); Auto uses
furi_hal_infrared_detect_tx_output() and, like the stock app, powers a
detected module. The 5V rail is only switched on if it was off and is
restored when the transmitter is released. Settings file keeps reading
the old 2- and 3-field formats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x
Image and multi-frame sends now draw a bar with a percentage instead of
the three dots: each payload covers a share of the whole send and every
data frame moves the bar inside it (text chunks by rows, BMP and Color 2.6
as one payload). Single commands and Repeat mode keep the dots.

The old comment said the GUI could not animate because the IR blaster
starved it; with TIM1 + DMA the TX thread sleeps during frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x
Same steps as the Windows helper: ufbt update, build, ufbt launch.
It uses .venv/bin/ufbt from `make setup` when present, else ufbt on
PATH, else `python3 -m ufbt`. .gitattributes keeps *.sh in LF so a
Windows checkout does not break it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYkmJ3RnCgGZEBKnfRJt5x

This branch has not been deployed

No deployments
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