Fix display freeze: move I2C work out of interrupt context #8

Open
claude-fable wants to merge 2 commits from claude-fable/metronom:fix/i2c-delay-in-isr-freeze into master
First-time contributor

Problem

Wyświetlacz potrafi się zawiesić (rotacja staje, odczyty przestają się aktualizować, pomaga tylko reset) — powtarzalnie przy aktualizacji temperatury w dół (np. 25.08), a nie losowo.

Przyczyna

Biblioteka grove_alphanumeric_display po każdym zapisie do wyświetlacza wywołuje delay(100) (pole _ms = 100 ustawiane w setTubeType), a updateTube() robi dwa takie zapisy z wnętrza przerwania TC3 (TasksHandler). Wewnątrz ISR przerwanie SysTick (napędzające millis()/micros()) nie może się wykonać, więc micros() przestaje normalnie rosnąć i oscyluje piłokształtnie w oknie ~1 ms.

Pętla delay() na SAMD:

while (ms > 0 && (micros() - start) >= 1000) { ms--; start += 1000; }

przez przekręcanie arytmetyki unsigned prawie zawsze kończy się natychmiast (dlatego urządzenie zwykle działa godzinami), ale gdy start zostanie spróbkowany dokładnie na dnie tej piły, warunek nie spełni się nigdy — pętla nieskończona w ISR, twardy zwis.

Dlaczego powtarzalnie przy tej samej wartości i kierunku zmiany: SysTick (1 ms), TC3 (20 ms — dokładna wielokrotność) i I2C chodzą z jednego zegara 48 MHz, więc faza wejścia w przerwanie jest zatrzaśnięta, a czas dojścia do delay() zależy co do mikrosekund od ścieżki kodu. Spadek i wzrost wartości to różne ścieżki (histereza 0.03 → inny skok wartości, strcmp kończy się na innym znaku), więc jedna konkretna ścieżka trafia w fatalne okno za każdym razem.

Ten sam problem dotyczy delay(100) w re-inicie Seeed_BME280 po nieudanej transmisji oraz updateTube() wołanego z przerwania przycisku.

Naprawa

Przerwania tylko ustawiają flagi (sensorsDue, displayNeedsUpdate, tubeRotated); odczyty BME280, aktualizacja wyświetlacza i przestawianie interwału rotacji wykonują się w loop(), gdzie delay() działa normalnie.

Skompilowane czysto dla Seeeduino Zero (arduino-cli, core Seeeduino:samd 1.8.6) — bez dostępu do sprzętu, do weryfikacji na urządzeniu.

🤖 Generated with Claude Code

## Problem Wyświetlacz potrafi się zawiesić (rotacja staje, odczyty przestają się aktualizować, pomaga tylko reset) — powtarzalnie przy aktualizacji temperatury **w dół** (np. 25.08), a nie losowo. ## Przyczyna Biblioteka `grove_alphanumeric_display` po każdym zapisie do wyświetlacza wywołuje `delay(100)` (pole `_ms = 100` ustawiane w `setTubeType`), a `updateTube()` robi dwa takie zapisy **z wnętrza przerwania TC3** (`TasksHandler`). Wewnątrz ISR przerwanie SysTick (napędzające `millis()`/`micros()`) nie może się wykonać, więc `micros()` przestaje normalnie rosnąć i oscyluje piłokształtnie w oknie ~1 ms. Pętla `delay()` na SAMD: ```c while (ms > 0 && (micros() - start) >= 1000) { ms--; start += 1000; } ``` przez przekręcanie arytmetyki unsigned **prawie zawsze** kończy się natychmiast (dlatego urządzenie zwykle działa godzinami), ale gdy `start` zostanie spróbkowany dokładnie na dnie tej piły, warunek nie spełni się nigdy — pętla nieskończona w ISR, twardy zwis. Dlaczego powtarzalnie przy tej samej wartości i kierunku zmiany: SysTick (1 ms), TC3 (20 ms — dokładna wielokrotność) i I2C chodzą z jednego zegara 48 MHz, więc faza wejścia w przerwanie jest zatrzaśnięta, a czas dojścia do `delay()` zależy co do mikrosekund od ścieżki kodu. Spadek i wzrost wartości to różne ścieżki (histereza 0.03 → inny skok wartości, `strcmp` kończy się na innym znaku), więc jedna konkretna ścieżka trafia w fatalne okno za każdym razem. Ten sam problem dotyczy `delay(100)` w re-inicie `Seeed_BME280` po nieudanej transmisji oraz `updateTube()` wołanego z przerwania przycisku. ## Naprawa Przerwania tylko ustawiają flagi (`sensorsDue`, `displayNeedsUpdate`, `tubeRotated`); odczyty BME280, aktualizacja wyświetlacza i przestawianie interwału rotacji wykonują się w `loop()`, gdzie `delay()` działa normalnie. Skompilowane czysto dla Seeeduino Zero (`arduino-cli`, core Seeeduino:samd 1.8.6) — bez dostępu do sprzętu, do weryfikacji na urządzeniu. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-fable added 1 commit 2026-06-10 03:53:54 +02:00
The tube library calls delay(100) after every display write and the
BME280 library calls delay(100) when re-initializing after a failed
transfer. Inside the TC3 timer ISR the SysTick interrupt is blocked,
so micros() stops advancing past ~1ms and delay() can spin forever
when its start sample lands exactly at the bottom of the micros()
sawtooth. All clocks derive from the same 48MHz source, so the ISR
phase is locked and one specific code path (display update on a
falling temperature reading) hits the fatal window reproducibly.

ISRs now only raise flags; sensor reads, display updates and timer
rescheduling run from loop(), where delay() works normally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude-fable added 1 commit 2026-06-10 04:13:58 +02:00
Piotr observed the freeze correlates with digit '8' on the last
(units) tube position, regardless of which sensor is shown
(25.08 C, 48 %, 1018 hPa). '8' lights the most 14-seg segments of
any digit, and the units digit changes most often, so the last
position is both peak-current and the most-written-to. At brightness
15 (max) that peak current likely dips the supply / glitches the I2C
bus, which the in-ISR delay()/endTransmission() then turns into a
permanent hang.

This commit drops brightness 15 -> 4 as a cheap, reversible probe.
If freezes stop or get much rarer, the root cause is current/brownout
(fix: decoupling cap on display VCC, or separate supply, plus an I2C
bus-recovery/timeout) rather than purely the ISR timing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
First-time contributor

Update — feedback od Piotra (właściciela repo) na sprzęcie z logowaniem:

na pewno się jebie jak jest wyświetlane 8 na ostatniej pozycji wyświetlacza, i nieważne czy to temperatura 25.08, wilgotność 48 czy ciśnienie 1018 — nie zawsze, ale tylko wtedy.

To częściowo koryguje pierwotną (czysto czasową) diagnozę: zawieszenie jest danio-zależne, nie tylko fazowe. Spójniejsza hipoteza łączona:

  • '8' w foncie 14-segmentowym (0x4778) zapala najwięcej segmentów ze wszystkich cyfr (8; dla porównania '1'=2, '0'=6, '7'=3).
  • HT16K33 multipleksuje cyfry — ostatnia (jedności) zmienia się najczęściej, więc generuje najwięcej zapisów I2C, a gdy pokazuje '8' przy setBrightness(15) (maks) → szczyt chwilowego poboru prądu.
  • Skok prądu podgryza zasilanie / zakłóca magistralę I2C → NACK lub zatrzaśnięta transmisja → Wire.endTransmission() (oraz delay(100) w re-inicie BME280) blokuje się, a w przerwaniu jest to trwały zawis. To tłumaczy: czemu nie losowo, czemu nie zawsze, i czemu pomaga tylko reset.

Wniosek: fix z tego PR (I2C poza ISR) prawdopodobnie nadal pomaga — zmienia trwały zawis w jedną zgubioną klatkę — ale może nie usunąć źródła, jeśli to brownout sprzętowy.

Dorzuciłem do PR osobny commit (6abd900) obniżający setBrightness(15) -> 4 jako tani, odwracalny test:

  • jeśli zawisy znikną/zrzedną → potwierdzony prąd/brownout → docelowo kondensator odsprzęgający na VCC wyświetlacza lub osobne zasilanie + Wire.setTimeout()/procedura odblokowania magistrali;
  • jeśli zawisy zostaną mimo niskiej jasności → źródłem jest timing w ISR i wystarczy główny fix.

Piotr — skoro masz już logowanie, log przed każdym writeBytes/endTransmission pokaże, na którym dokładnie wywołaniu staje (czy w endTransmission, czy w delay) i przygwoździ to ostatecznie.

Update — feedback od Piotra (właściciela repo) na sprzęcie z logowaniem: > na pewno się jebie jak jest wyświetlane **8 na ostatniej pozycji** wyświetlacza, i nieważne czy to temperatura 25.0**8**, wilgotność 4**8** czy ciśnienie 101**8** — nie zawsze, ale tylko wtedy. To częściowo koryguje pierwotną (czysto czasową) diagnozę: zawieszenie jest **danio-zależne**, nie tylko fazowe. Spójniejsza hipoteza łączona: - `'8'` w foncie 14-segmentowym (`0x4778`) zapala **najwięcej segmentów ze wszystkich cyfr** (8; dla porównania `'1'`=2, `'0'`=6, `'7'`=3). - HT16K33 multipleksuje cyfry — ostatnia (jedności) zmienia się najczęściej, więc generuje najwięcej zapisów I2C, a gdy pokazuje `'8'` przy `setBrightness(15)` (maks) → **szczyt chwilowego poboru prądu**. - Skok prądu podgryza zasilanie / zakłóca magistralę I2C → NACK lub zatrzaśnięta transmisja → `Wire.endTransmission()` (oraz `delay(100)` w re-inicie BME280) blokuje się, a w przerwaniu jest to trwały zawis. To tłumaczy: czemu **nie losowo**, czemu **nie zawsze**, i czemu **pomaga tylko reset**. Wniosek: fix z tego PR (I2C poza ISR) prawdopodobnie nadal pomaga — zmienia trwały zawis w jedną zgubioną klatkę — ale **może nie usunąć źródła**, jeśli to brownout sprzętowy. Dorzuciłem do PR osobny commit (6abd900) obniżający `setBrightness(15) -> 4` jako **tani, odwracalny test**: - jeśli zawisy znikną/zrzedną → potwierdzony prąd/brownout → docelowo kondensator odsprzęgający na VCC wyświetlacza lub osobne zasilanie + `Wire.setTimeout()`/procedura odblokowania magistrali; - jeśli zawisy zostaną mimo niskiej jasności → źródłem jest timing w ISR i wystarczy główny fix. Piotr — skoro masz już logowanie, log **przed** każdym `writeBytes`/`endTransmission` pokaże, na którym dokładnie wywołaniu staje (czy w `endTransmission`, czy w `delay`) i przygwoździ to ostatecznie.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u fix/i2c-delay-in-isr-freeze:claude-fable-fix/i2c-delay-in-isr-freeze
git checkout claude-fable-fix/i2c-delay-in-isr-freeze
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Arduino/metronom#8