Skip to content

Fix HAL_UART_ErrorCallback wedging the system when the Rx listener errors - #708

Open
jonwaterschoot wants to merge 1 commit into
electro-smith:masterfrom
Synthux-Academy:fix/uart-error-callback-null-deref
Open

Fix HAL_UART_ErrorCallback wedging the system when the Rx listener errors#708
jonwaterschoot wants to merge 1 commit into
electro-smith:masterfrom
Synthux-Academy:fix/uart-error-callback-null-deref

Conversation

@jonwaterschoot

Copy link
Copy Markdown

MapInstanceToHandle() explicitly returns NULL for any instance not in its known list of 9 UART/USART peripherals. f7c63ae added an unconditional write through that pointer on every UART error (handle->listener_mode_ = false), with no null check.

Found on a board (TouchPlaited) that runs UART MIDI continuously alongside I2C (OLED, capacitive touch) and USB: a floating/idle MIDI input generating occasional UART framing noise caused the OLED to stop updating, touch pads to stop responding, and USB enumeration to fail entirely, while audio and switch/knob input kept working normally. Bisected the regression to this exact commit against a known-good older libDaisy snapshot; reverting just these two lines resolves all three symptoms on the affected hardware.

I haven't fully isolated the mechanism at the register level, it could be the null write itself for an unmapped instance, or the added per-interrupt cost under a high error rate starving other peripherals' interrupt servicing, but the fix is verified against the observed failure, and the missing null check is a real gap regardless of which it is.

@jonwaterschoot

Copy link
Copy Markdown
Author

Same as my other PR before this: I was using an LLM to help me make TouchPlaited a firmware for Simple Touch using the mpr121 sensor and after updating libDaisy this issue surfaced. Traced it back to this, however it might be purposefully implemented for another bug.
It made my OLED freeze and the USB midi connection fail. Hence this was the fix. Since this might be a personal experience and is not guaranteed by my own knowledge this may be closed or remade etc.
Sorry for any inconvenience - won't submit PR's directly like this again.

@stephenhensley

Copy link
Copy Markdown
Collaborator

So the lines you remove here were added to solve a problem (ironically, the frame errors you describe causing issues elsewhere in your case). The error callback should flag listening mode as off so that users can query UartHandler::IsListening to properly restart the peripheral.

Does the failure in your case persist if you, instead have an error callback:

extern "C" void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart)
{
    auto* handle = MapInstanceToHandle(huart->Instance);
    if (handle) {
        handle->listener_mode_ = false;
    }
    UartHandler::Impl::DmaTransferFinished(huart, UartHandler::Result::ERR);
}

If that works for you, please update.
Then I can give this a quick regression test against the MIDI Failures this was initially added for and we can merge it.

@stephenhensley

Copy link
Copy Markdown
Collaborator

oh, actually I was just looking over my changes for a different project that happened to also touch this callback -- I can't remember exactly what required the change, but could have been a similar symptom that you experienced.

That said, in the case below, the DmaTransferFinished wouldn't be called after a frame error while in listening mode while in yours it is called regardless.

On that branch I have:

extern "C" void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart)
{
    auto* handle = MapInstanceToHandle(huart->Instance);
    if(handle->listener_mode_)
        handle->listener_mode_ = false;
    else
        UartHandler::Impl::DmaTransferFinished(huart, UartHandler::Result::ERR);
}

Still probably wise to a check on handle regardless, but figured worth sharing since it is the same callback function that was giving you issues.

MapInstanceToHandle() returns NULL for any instance outside its list of
9 UART/USART peripherals, and f7c63ae writes through that pointer with
no null check.

The clearing of listener_mode_ also has to undo what DmaListenStart()
armed. It enables the IDLE interrupt alongside setting the flag, and
UART_IRQHandler acknowledges IDLE only inside its listener_mode_ branch
-- ST's HAL does not touch the flag in HAL_UART_RECEPTION_STANDARD mode.
So clearing the flag while IDLE stays enabled leaves the flag set with
nobody left to clear it, and the UART interrupt re-asserts continuously.
DmaListenStop() already disables both together; do the same here.

Observed as a UART interrupt storm on hardware running TRS MIDI in
continuously: floating-input framing noise starved I2C (OLED, touch)
and USB while SAI-DMA audio kept running.
@jonwaterschoot jonwaterschoot changed the title Fix unguarded pointer write in HAL_UART_ErrorCallback Fix HAL_UART_ErrorCallback wedging the system when the Rx listener errors Aug 9, 2026
@jonwaterschoot
jonwaterschoot force-pushed the fix/uart-error-callback-null-deref branch from 73083e8 to 9df11f1 Compare August 9, 2026 01:38
@jonwaterschoot

Copy link
Copy Markdown
Author

Follow-up, and an answer to your question.

One bit of context that turned out to matter more than I expected: I have two modded hardware builds of the Simple Touch. They are complete original kits, Daisy with mpr121 touch sensor, but modded:

  • one unit one with the OLED and CV clock but no MIDI TRS input circuit;
  • and one with a TRS MIDI breakout but no screen.

I assumed the MIDI unit would be the one that could reproduce this. It's the other way round, and that ended up being most of the story. I ran the failing builds twice to confirm, and checked the decisive results on both units.

I've also retitled the PR. I filed it as a null deref, and the testing below shows that isn't what it is — the null check is still worth having, but it isn't what fixes the freeze, and the old title might have sent you looking in the wrong place.

The write-up below is Claude's. It guided the debugging and wrote the three candidate patches; I did the building, flashing and observing, so where it says "I", that's me reporting what I actually saw on hardware.


The answer

Your first snippet does not fix it. Your second one does, and so does a small variation on it that I'd like to propose.

Five builds, all from the same commit, differing only in the body of HAL_UART_ErrorCallback:

Callback body Result
master today freezes
this PR as filed (both lines removed) ok
your 1st snippet — null guard, DmaTransferFinished always runs freezes
your 2nd snippet + null guard — skip DmaTransferFinished while listening ok
clear the flag, disable the IDLE IRQ, DmaTransferFinished always runs ok

"Freezes" means: I2C dies (OLED stops updating, MPR121 touch pads stop responding) and USB stops enumerating, while SAI-DMA audio and the ADC keep running normally. I reproduced both the master failure and the first-snippet failure twice each.

The repro needs no MIDI hardware at all

It's a floating USART1 Rx pin.

My TRS MIDI unit never fails, even on unpatched master — the input optocoupler holds the line at a clean idle, so no framing errors are ever generated. My other unit has no MIDI circuit fitted, so D14 floats and picks up noise while the firmware starts the listener anyway. That one fails every time.

So on a bare Seed: start a UART Rx listener, leave the Rx pin unconnected.

Why it happens

The table above is measured. This part is inference from reading the code.

DmaListenStart sets listener_mode_ and enables the IDLE interrupt; DmaListenStop clears both. UART_IRQHandler acknowledges IDLE only inside its if(listener_mode_) branch, and the HAL doesn't touch that flag in HAL_UART_RECEPTION_STANDARD mode.

So clearing listener_mode_ on error while IDLE stays enabled leaves the flag set with nothing left to clear it, and the ISR keeps re-entering. The recovery you designed for — MidiHandler::Listen() checking IsListening() and restarting — runs in the main loop, which is exactly what gets starved.

Re-entry alone isn't sufficient, though: your second snippet leaves IDLE enabled and is fine. What tips it over is re-entry plus the cost of DmaTransferFinished on every entry — HAL_UART_Init under a ScopedIrqBlocker, ending in a polled UART_CheckIdleState. Removing either ingredient is enough, which is what the two single-variable comparisons in the table show: 1st→3rd differs only by the IDLE disable, 1st→2nd only by DmaTransferFinished. That may also be why the version on your other branch behaved for you.

What's pushed

extern "C" void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart)
{
    auto* handle = MapInstanceToHandle(huart->Instance);
    if(handle && handle->listener_mode_)
    {
        handle->listener_mode_ = false;
        __HAL_UART_DISABLE_IT(huart, UART_IT_IDLE);
    }
    UartHandler::Impl::DmaTransferFinished(huart, UartHandler::Result::ERR);
}

IsListening() still reports false so users can restart the peripheral, the error still reaches next_end_callback_ as Result::ERR, and the IDLE disable mirrors what DmaListenStop already does.

I went with this over the skip-DmaTransferFinished variant only because that one stops reporting the error at all while listening, which is a behaviour change the bug doesn't require. Both pass on hardware — happy to switch to yours if you'd rather.

Afterwards I flashed this build onto the TRS MIDI unit with a keyboard plugged in: normal MIDI input works, and a glitch no longer wedges anything, since Listen() re-arms it on the next pass.

@github-actions

Copy link
Copy Markdown

Test Results

167 tests  +167   167 ✅ +167   0s ⏱️ ±0s
  1 suites +  1     0 💤 ±  0 
  1 files   +  1     0 ❌ ±  0 

Results for commit 9df11f1. ± Comparison against base commit 3bd68f2.

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.

2 participants