Repository navigation
Conversation
NAV-SIG (M9, M10) and NAV-SAT (M8) were requested at every navigation epoch. They feed only the satellite list in the CLI, yet they are most of what the receiver sends: NAV-SIG carries 16 bytes for each tracked signal, several hundred bytes per epoch on a multi-constellation receiver, against about 100 for the NAV-PVT that navigation runs on. They are now requested every (gps_ublox_nav_hz + 1) / 2 epochs, which is about twice a second at any navigation rate.
A receive buffer has to hold what arrives between two runs of the task that drains it. For the GPS task, 20 ms apart, that is up to 230 bytes at 115200 and 460 at 230400, and a reply to a poll has to fit whole: MON-VER alone can be 258 bytes. With the 256 bytes every port has, a NEO-F10N at 230400 was never identified, because its MON-VER reply was always cut. The GPS port now gets a 512 byte buffer of its own, in FASTRAM, where there is room: CCM on F405, RAM1 on AT32F43x. serialSetRxBuffer() swaps it in right after the port is opened. Every other port keeps its 256 bytes. The note that occupied sizes are returned as uint8_t no longer held: the driver functions and the ring indices are all 32 bit.
The satellite details also feed the OSD's GPS extra stats, not only gpssats, so the comment now says both. The comments are one line each, the buffer states the rate it covers (230400; at 460800 and above a NAV-PVT plus a large NAV-SIG can still overflow it), its size is asserted to be a power of two because softserial masks with size - 1, and it only exists where the u-blox provider does, the only one that opens a port. The note in serial_uart.h about sending a UBLOX SVINFO was stale.
A UART receiving through a DMA stream costs no interrupt per byte, and keeps the bytes that arrive while interrupts are held off. A target turns it on per port with UARTx_RX_DMA; a port without one, and a target that names none, build and behave exactly as before. The stream fills the port's own ring, whose head is read back from the transfer counter, so nothing above the driver changes. Ports whose owner takes each byte through rxCallback (the serial receivers, which frame by timing) stay on the interrupt even when a stream is named. F4 and F7 wire each receiver to fixed streams, so a tag naming another is a build error. H7 and AT32 route through the DMAMUX. On H7 the ring moves to D2 SRAM, out of the data cache. A stream any timer output is mapped to is left to the timers, since MSP ports open before the motors do. On F7, H7 and AT32 the interrupt handler tested the RX flag without the enable, so a port receiving through DMA could have lost a byte to the interrupt raised for what it sends; it now leaves the data register alone. Enabled on TBS_LUCID_H7_WING for UART2, on DMA2 stream 5.
serialSetRxBuffer() only swapped the ring's pointer and size, but a DMA stream keeps writing to the memory it was started on, so a port receiving through DMA would have gone on filling its old ring while the reader looked at the new one. The serial vtable gets an optional setRxBuffer, which the UARTs implement by swapping the ring and, if a stream is running, starting it again on the new one. The GPS ring must then be memory a stream can reach on targets that receive through DMA: D2 SRAM on H7 rather than DTCM, and on F4 and AT32F43x plain RAM rather than CCM or RAM1. Everywhere else it stays in FASTRAM, so a target without UARTx_RX_DMA builds exactly as before. On the TBS Lucid H7 Wing, GPS on UART2 through DMA2 stream 5, NEO-F10N: identified with no errors or timeouts at 115200 and after autobaud to 230400, NAV-PVT at the configured 10 Hz. Over ten reboots, 1 error and 2 timeouts at startup, against 1 and 1 over nine with the byte interrupt.
A UART sends one byte per interrupt. A target can now name a stream for a port's transmitter (UARTx_TX_DMA), as it can for the receiver: the stream empties the transmit ring and the port takes one interrupt per transfer. writeBuf() queues a message whole, so MSP and MAVLink send each in one transfer; what is queued while one runs goes in the next. On F4, F7, H7 and AT32. The transmit ring moves to D2 SRAM on H7, as the receive ring does. The stream check the receiver used now serves both, and F4/F7 targets naming a stream the transmitter is not wired to fail to build. The TBS Lucid H7 Wing sends on UART2 through DMA2 stream 7.
iNavFlight#12119 turns the F7 D-cache on and makes DMA_RAM an uncached SRAM2 region. A ring left in cached RAM could then hand the CPU a line it cached before the stream filled it. The rings go in DMA_RAM, as on H7; on F7 that is plain RAM until iNavFlight#12119, so on its own this changes nothing. Not seen to fail at 115200: FLYWOOF722PRO with iNavFlight#12119 and the GPS sending on the interrupt had 0-1 errors per boot with the rings in cached RAM and in DMA_RAM alike.
On F7 and H7, stopping the stream clears USART_CR3_DMAR and only uartReconfigure() set it again. After serialSetRxBuffer() the stream ran but the UART never fed it, and with the byte interrupt off for a DMA port the port stayed deaf until the next baud, mode or option change. The GPS got away with it because the u-blox driver changes the baud rate right after swapping its ring. F4 and AT32 already set the request when the stream starts. With a test patch that swaps a larger GPS ring in 10 s after boot, FLYWOOF722PRO and KAKUTEH7MINI had a timeout on every boot without this and none with it.
# Conflicts: # src/main/drivers/serial_uart_stm32f7xx.c
MSP DisplayPort and the serial gimbal swap their own TX buffer into the port after opening it. On H7 that buffer is in AXI SRAM, which the CPU caches, so the stream sends what is in RAM rather than what was just written. Kakute H7 Mini with the GPS port given such a buffer (a test patch): the u-blox never got its configuration (3 timeouts, no navigation messages); with this, a navigation message every 120 ms and 0 errors over 3 reboots. On F7 every ring is cached once iNavFlight#12119 turns its D-cache on: FLYWOOF722PRO with iNavFlight#12119 had 11-14 GPS errors and no fix on every boot, and 0 with this. The lines the transfer covers are written back first. Without a D-cache, or on the uncached D2 rings, that does nothing.
The stream check refused every stream any timer output is mapped to, so a target naming a UART stream on F4 or F7, where each UART has one or two fixed streams, would find most of them taken. Now it refuses a stream while DSHOT motors that may take it have not started, and the LED strip's stream when that feature is on. Started motors already own their streams, so a port opened after them gets the streams of outputs that ended up as servos or unused, and every stream but the LED strip's when the motors are not on DSHOT. The choice is made at each boot.
…get names none On F4, F7, H7 and AT32F43x a UART whose target names no stream now picks a free one when it opens: on F4 and F7 the streams the reference manual wires it to, on H7 and AT32 any stream the DMAMUX can route except the ADC's. It skips the streams any target names for a port, the SD card's, its own other direction's, and those a timer output will use; a port opened again takes back a stream it already holds first, since none is ever given back. Serial receivers keep receiving on the interrupt, and half-duplex ports get no stream unless the target names one. A target keeps a port on the interrupt by setting UARTx_RX_DMA or UARTx_TX_DMA to DMA_NONE. Tested on F722 (Flywoo F722 Pro, also with the D-cache of iNavFlight#12119) and H743 (Kakute H7 Mini, TBS Lucid H7 Wing) with GPS, MSP and DSHOT motors. F4 and AT32 are untested on hardware.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
PR Summary by QodoEnable runtime UART DMA selection and enlarge the GPS receive buffer
AI Description
Diagram
High-Level Assessment
Files changed (22)
|
Code Review by Qodo
1. DMA errors can stall serial output
|
| #define UART_TX_DMA_FLAGS (DMA_IT_TCIF | DMA_IT_HTIF | DMA_IT_TEIF | DMA_IT_DMEIF | DMA_IT_FEIF) | ||
| #define UART_TX_DMA_ENDED (DMA_IT_TCIF | DMA_IT_TEIF) |
There was a problem hiding this comment.
1. Dma errors can stall serial output 🐞 Bug ☼ Reliability
On STM32F4/F7/H7, uartTxDmaHandler() clears the direct-mode error flag but only resets txDmaCount for transfer-complete or transfer-error flags, while TX setup enables interrupts only for those latter flags. If a direct-mode error stops a transfer without either enabled completion event, the recorded count prevents another transfer from starting and a buffered write can wait indefinitely when the ring fills.
Agent Prompt
## Issue description
The F4, F7 and H7 TX DMA drivers clear direct-mode errors without enabling an interrupt or clearing the active transfer state for them. A stopped transfer can leave buffered serial output unable to progress.
## Fix Focus Areas
- src/main/drivers/serial_uart_stm32f4xx.c[340-341]
- src/main/drivers/serial_uart_stm32f7xx.c[434-435]
- src/main/drivers/serial_uart_stm32h7xx.c[492-493]
## Recommended Fix
Enable the direct-mode error interrupt in each driver and handle that flag as a failed transfer. Clear the active count and apply the existing error-path tail and restart policy so queued output can continue.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
A direct mode error doesn't end the transfer, only a transfer error does. The HAL in this tree handles it the same way: HAL_DMA_IRQHandler() disables the stream only on HAL_DMA_ERROR_TE (stm32f7xx_hal_dma.c:928-936) and just records a direct mode error, after which the transfer runs on to its transfer complete. uartTxDmaHandler() gets that interrupt and clears DMEIF along with the other flags, so the count is reset there. No change.
|
RAM / Flash usage vs. base commit
See RAM/flash optimization guide for techniques to reduce usage. |
|
Test firmware build ready — commit Download firmware for PR #12129 251 targets built. Find your board's
|
On F7 DMA_RAM is plain RAM until iNavFlight#12119 turns the D-cache on and makes it an uncached SRAM2 section, so until then each ring only duplicated the port's own, 256 B per UART with a receive stream. They are compiled only where build_config.h says DMA_RAM is uncached (DMA_RAM_UNCACHED, set together with the D-cache), so either PR can go in first.
# Conflicts: # src/main/drivers/serial_uart_stm32f7xx.c
A stopped stream was kept by its UART, so a port closed at runtime held streams another port could have used. Every stop now frees its stream and closing a port stops its streams; the pick no longer needs to look for a port's own streams first.
Since streams are given back only the F4/F7 candidate tables read it, and CI builds with warnings as errors.
Builds on #12128 (the stream rule), #12062 (UART TX DMA), #12033 (UART RX DMA) and #12001 (the GPS port's own receive buffer); only the commits after #12128's are this PR.
With #12033/#12062 a UART uses DMA only where its target names a stream, and today one target does (TBS_LUCID_H7_WING,
UART2). This PR turns it on for every UART on F4, F7, H7 and AT32F43x: a port whose target names no stream picks a
free one when it opens.
DMA2 0-2 (the ADCs). AT32: any channel except the ADC's.
first), the F4/F7 SDIO stream, the port's own other direction, and the streams the timer outputs will use (serial: leave a UART only the DMA streams a timer output will use #12128).
even with a stream named, as in serial: receive through DMA where the target names a stream #12033. Half-duplex ports (IBUS telemetry and an SRXL2 ESC always;
SmartPort, HoTT, VTX control and others when set to half-duplex) get no stream unless the target names one.
#define UARTx_RX_DMA DMA_NONE(or_TX_) keeps that port on the interrupt.handing its port to LTM when armed, a serial blackbox when disarmed) leaves them to the ports opened after it, and a
port opened again picks again (39bddd3, from a Qodo point on serial: CRSF and SBUS receivers on DMA, handed each frame when the line goes idle #12132).
closeSerialPort()stops the port's streamsthrough a new optional
releaseentry in the serial port table; what the port still had to send is dropped, asreopening it already did.
DMA_RAM, as serial: receive through DMA where the target names a stream #12033 does for named ports: D2 SRAM on H7 (with the transmit rings, +3 KB there on TBS_LUCID_H7_WING, out of 256 KB). On F7 a port gets a ring of its own only with F7: enable the D-cache, DMA buffers in a non-cacheable SRAM2 region #12119's D-cache (DMA_RAM_UNCACHED); without it the port receives into its own ring, as before (8ae0509, from a Qodo point on serial: leave a UART only the DMA streams a timer output will use #12128).The target guide (
timer-dma-conflicts.md) describes the runtime pick and the opt-out.Tested
FLYWOOF722PRO (F722), DSHOT300 on four motors, GPS (u-blox M10) on UART5:
took DMA2 S2 to receive and stayed on the interrupt to send (its only stream belongs to output S6);
maintenance-10.x);
it. This needs serial: send through DMA where the target names a stream #12062's cache clean before a transfer: without it the GPS had 11-14 errors and no fix on every
boot.
MAMBAF722_2022B (F722), DSHOT300 in burst on DMA2 S1/S4, a GPS port on UART3 (no module fitted, so it keeps
sending while it looks for one): the port took DMA1 S1/S3. Disarmed motor steps alternating this PR and the base
image, with the ESC powered throughout: the same current within 0.04 A at every step.
KAKUTEH7MINI (H743), GPS (SAM-M10Q) on UART2: on DMA1 S2/S3 with no motors, on DMA1 S4/S5 with four DSHOT motors on
S0-S3; UART1 on DMA2 S3/S4. No timeouts in 10 reboots, one checksum error at startup in one of them.
TBS_LUCID_H7_WING (H743, the target that names DMA2 S5/S7 for UART2), on two boards. A second Lucid with the
final code, GPS on UART2, MSP on UART1 and UART3, SD card fitted: the GPS on its named DMA2 S5/S7, MSP on DMA1 S0/S1
and S2/S3, the SD card mounted, no GPS timeout and a fix in each of 10 reboots. The bench Lucid, GPS (u-blox M10)
on UART2, MSP on UART1, SD card fitted, on the previous round of this branch (the changes since, #12033's restart fix, #12062's cache
clean and the streams given back, do not change which stream a port gets at boot):
and stayed on the interrupt to send, leaving S5/S7 to the GPS; the motor then took DMA1 S0;
Streams given back (39bddd3), on #12132's tree with bench commands that list each stream's owner and open, write
and close a spare UART:
opened again it took them back and sent again. Without this commit the closed port kept both. UART3 with MSP shared
with LTM, armed and disarmed 3 times: arming, MSP gave back S1/S3 and LTM (send only) took S3 again; disarming, the
reverse. LTM sent 278 transfers; the ELRS receiver on UART1 had no bad frame.
free (DMA2 S7): UART6 took it and, when it closed, UART7, opened next, took it; without this commit UART7 would have
stayed on the interrupt. UART3 with MSP shared with MAVLink and a serial blackbox on UART6, armed and disarmed 3
times: the blackbox took the stream MSP had just given back and MAVLink the next ones, in an order that changed
between cycles, and every port sent on the stream it got (blackbox 6425 transfers, MAVLink 27). After the last
disarm the streams were as at boot.
All 237 release targets build, none over its regions; 645 of 645 unit tests pass.
uart_dma_stream_unittestcoversthe pick (named streams) on plain data, with source checks that pin the live code it mirrors: the candidate tables,
the named list for every port and direction, the
common_post.hdefaults, each driver's two calls and each driver'sstops giving their stream back. Each check was seen to fail on a deliberate change to the code it pins.
Not tested on hardware: F4 and AT32F43x. The F4 and AT32 DMA paths of #12033/#12062 have not run on a board
either. Tests on an F405/F411/F722-class board with a GPS and MSP, and on an AT32F435, would be very welcome: what to
look at is GPS and MSP working as before, and
statusfree of errors.Size
Against the stream rule PR (#12128), on the final tree:
The RAM is the GPS ring moving out of the fast RAM to where #12033 puts a ring a stream fills, and the larger port structure. Until 8ae0509 the F7 rows also carried a duplicate receive ring for every port, 1280 to 1536 B, now built only with #12119's D-cache. KAKUTEF7's 40 B of ITCM are not serial code:
gyroFilterreaches its variables through other offsets once the GPS ring leaves DTCM.Related PRs
after the MSP ports have opened. FLYWOOF722PRO with gyro: data-ready reads on F4 and F7 #12065 and this PR, MSP on UART1, which takes DMA2 S2, SPI1's
first receive stream: the gyro took SPI1's other receive stream (DMA2 S0) and gyro: data-ready reads on F4 and F7 #12065 moved the ADC to S4, so gyro
and ADC stayed on DMA and the PID loop time did not change. With the receiver on UART1, as that board ships, nothing
moves (gyro on DMA2 S2/S3, GPS on DMA1 S0/S7). On H7 gyro: read the ICM426xx on its data-ready interrupt (H7) #12064 takes streams only for a read longer than the SPI
FIFO (an IMU on SPI4, or gyro: read the ICM426xx through its FIFO on data-ready (off by default) #12067's FIFO reads), and it searches down from DMA2 S7 while this PR picks up from
DMA1 S0. TBS_LUCID_H7_WING with gyro: read the ICM426xx on its data-ready interrupt (H7) #12064 and this PR: with the gyro on SPI1 (read through the FIFO) nothing
moved; with its second gyro on SPI4 (read through DMA) the gyro took DMA2 S6/S4 instead of S6/S7, S7 being the
GPS's named stream, so gyro, GPS (DMA2 S5/S7) and MSP (DMA1 S0-S3) all stayed on DMA, PID loop time unchanged.
Merge order and conflicts
uartSetTxPin()toserial_uart_stm32h7xx.cright afteruartGetPortPins(), where this PR adds its DMA code. That is the only conflict between them, and neither change undoes the other: whichever goes in second keeps both blocks. Both are mine, so I'll fix the one left.DMA_RAM_UNCACHED, and the F7 receive rings follow it.